@effect/ai-openai-compat 4.0.0-beta.5 → 4.0.0-beta.51

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@effect/ai-openai-compat",
3
- "version": "4.0.0-beta.5",
3
+ "version": "4.0.0-beta.51",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "description": "An OpenAI compat integration for Effect",
@@ -45,10 +45,10 @@
45
45
  "provenance": true
46
46
  },
47
47
  "devDependencies": {
48
- "effect": "^4.0.0-beta.5"
48
+ "effect": "^4.0.0-beta.51"
49
49
  },
50
50
  "peerDependencies": {
51
- "effect": "^4.0.0-beta.5"
51
+ "effect": "^4.0.0-beta.51"
52
52
  },
53
53
  "scripts": {
54
54
  "codegen": "effect-utils codegen",
@@ -3,12 +3,12 @@
3
3
  */
4
4
  import * as Array from "effect/Array"
5
5
  import type * as Config from "effect/Config"
6
+ import * as Context from "effect/Context"
6
7
  import * as Effect from "effect/Effect"
7
8
  import { identity, pipe } from "effect/Function"
8
9
  import * as Layer from "effect/Layer"
9
10
  import * as Redacted from "effect/Redacted"
10
11
  import * as Schema from "effect/Schema"
11
- import * as ServiceMap from "effect/ServiceMap"
12
12
  import * as Stream from "effect/Stream"
13
13
  import type * as AiError from "effect/unstable/ai/AiError"
14
14
  import * as Sse from "effect/unstable/encoding/Sse"
@@ -49,7 +49,7 @@ export interface Service {
49
49
  * @since 1.0.0
50
50
  * @category service
51
51
  */
52
- export class OpenAiClient extends ServiceMap.Service<OpenAiClient, Service>()(
52
+ export class OpenAiClient extends Context.Service<OpenAiClient, Service>()(
53
53
  "@effect/ai-openai-compat/OpenAiClient"
54
54
  ) {}
55
55
 
@@ -223,10 +223,10 @@ export const layer = (options: Options): Layer.Layer<OpenAiClient, never, HttpCl
223
223
  * @category layers
224
224
  */
225
225
  export const layerConfig = (options?: {
226
- readonly apiKey?: Config.Config<Redacted.Redacted<string>> | undefined
226
+ readonly apiKey?: Config.Config<Redacted.Redacted<string> | undefined> | undefined
227
227
  readonly apiUrl?: Config.Config<string> | undefined
228
- readonly organizationId?: Config.Config<Redacted.Redacted<string>> | undefined
229
- readonly projectId?: Config.Config<Redacted.Redacted<string>> | undefined
228
+ readonly organizationId?: Config.Config<Redacted.Redacted<string> | undefined> | undefined
229
+ readonly projectId?: Config.Config<Redacted.Redacted<string> | undefined> | undefined
230
230
  readonly transformClient?: ((client: HttpClient.HttpClient) => HttpClient.HttpClient) | undefined
231
231
  }): Layer.Layer<OpenAiClient, Config.ConfigError, HttpClient.HttpClient> =>
232
232
  Layer.effect(
@@ -855,10 +855,12 @@ export type ChatCompletionRequest = {
855
855
  readonly tools?: ReadonlyArray<ChatCompletionTool> | undefined
856
856
  readonly tool_choice?: ChatCompletionToolChoice | undefined
857
857
  readonly service_tier?: string | undefined
858
+ readonly reasoning?: unknown
858
859
  readonly stream?: boolean | undefined
859
860
  readonly stream_options?: {
860
861
  readonly include_usage?: boolean | undefined
861
862
  } | undefined
863
+ readonly [x: string]: unknown
862
864
  }
863
865
  /**
864
866
  * @since 1.0.0
@@ -894,6 +896,11 @@ const ChatCompletionToolFunction = Schema.Struct({
894
896
  arguments: Schema.optionalKey(Schema.String)
895
897
  })
896
898
 
899
+ const ChatCompletionToolFunctionDelta = Schema.Struct({
900
+ name: Schema.optionalKey(Schema.String),
901
+ arguments: Schema.optionalKey(Schema.String)
902
+ })
903
+
897
904
  const ChatCompletionToolCall = Schema.Struct({
898
905
  id: Schema.optionalKey(Schema.String),
899
906
  index: Schema.optionalKey(Schema.Number),
@@ -901,6 +908,13 @@ const ChatCompletionToolCall = Schema.Struct({
901
908
  function: Schema.optionalKey(ChatCompletionToolFunction)
902
909
  })
903
910
 
911
+ const ChatCompletionToolCallDelta = Schema.Struct({
912
+ id: Schema.optionalKey(Schema.String),
913
+ index: Schema.optionalKey(Schema.Number),
914
+ type: Schema.optionalKey(Schema.String),
915
+ function: Schema.optionalKey(ChatCompletionToolFunctionDelta)
916
+ })
917
+
904
918
  const ChatCompletionMessage = Schema.Struct({
905
919
  role: Schema.optionalKey(Schema.String),
906
920
  content: Schema.optionalKey(Schema.NullOr(Schema.String)),
@@ -910,7 +924,7 @@ const ChatCompletionMessage = Schema.Struct({
910
924
  const ChatCompletionDelta = Schema.Struct({
911
925
  role: Schema.optionalKey(Schema.String),
912
926
  content: Schema.optionalKey(Schema.NullOr(Schema.String)),
913
- tool_calls: Schema.optionalKey(Schema.Array(ChatCompletionToolCall))
927
+ tool_calls: Schema.optionalKey(Schema.Array(ChatCompletionToolCallDelta))
914
928
  })
915
929
 
916
930
  const ChatCompletionChoice = Schema.Struct({
@@ -1,16 +1,16 @@
1
1
  /**
2
2
  * @since 1.0.0
3
3
  */
4
+ import * as Context from "effect/Context"
4
5
  import * as Effect from "effect/Effect"
5
6
  import { dual } from "effect/Function"
6
- import * as ServiceMap from "effect/ServiceMap"
7
7
  import type { HttpClient } from "effect/unstable/http/HttpClient"
8
8
 
9
9
  /**
10
10
  * @since 1.0.0
11
11
  * @category services
12
12
  */
13
- export class OpenAiConfig extends ServiceMap.Service<
13
+ export class OpenAiConfig extends Context.Service<
14
14
  OpenAiConfig,
15
15
  OpenAiConfig.Service
16
16
  >()("@effect/ai-openai-compat/OpenAiConfig") {
@@ -18,7 +18,7 @@ export class OpenAiConfig extends ServiceMap.Service<
18
18
  * @since 1.0.0
19
19
  */
20
20
  static readonly getOrUndefined: Effect.Effect<typeof OpenAiConfig.Service | undefined> = Effect.map(
21
- Effect.services<never>(),
21
+ Effect.context<never>(),
22
22
  (context) => context.mapUnsafe.get(OpenAiConfig.key)
23
23
  )
24
24
  }
@@ -0,0 +1,203 @@
1
+ /**
2
+ * OpenAI Embedding Model implementation.
3
+ *
4
+ * Provides an EmbeddingModel implementation for OpenAI-compatible embeddings APIs.
5
+ *
6
+ * @since 1.0.0
7
+ */
8
+ import * as Context from "effect/Context"
9
+ import * as Effect from "effect/Effect"
10
+ import { dual } from "effect/Function"
11
+ import * as Layer from "effect/Layer"
12
+ import type { Simplify } from "effect/Types"
13
+ import * as AiError from "effect/unstable/ai/AiError"
14
+ import * as EmbeddingModel from "effect/unstable/ai/EmbeddingModel"
15
+ import * as AiModel from "effect/unstable/ai/Model"
16
+ import type { CreateEmbedding200, CreateEmbeddingRequestJson } from "./OpenAiClient.ts"
17
+ import { OpenAiClient } from "./OpenAiClient.ts"
18
+
19
+ /**
20
+ * @since 1.0.0
21
+ * @category models
22
+ */
23
+ export type Model = string
24
+
25
+ /**
26
+ * Service definition for OpenAI embedding model configuration.
27
+ *
28
+ * @since 1.0.0
29
+ * @category context
30
+ */
31
+ export class Config extends Context.Service<
32
+ Config,
33
+ Simplify<
34
+ & Partial<
35
+ Omit<
36
+ CreateEmbeddingRequestJson,
37
+ "input"
38
+ >
39
+ >
40
+ & {
41
+ readonly [x: string]: unknown
42
+ }
43
+ >
44
+ >()("@effect/ai-openai-compat/OpenAiEmbeddingModel/Config") {}
45
+
46
+ /**
47
+ * @since 1.0.0
48
+ * @category constructors
49
+ */
50
+ export const model = (
51
+ model: string,
52
+ options: {
53
+ readonly dimensions: number
54
+ readonly config?: Omit<typeof Config.Service, "model" | "dimensions">
55
+ }
56
+ ): AiModel.Model<"openai", EmbeddingModel.EmbeddingModel | EmbeddingModel.Dimensions, OpenAiClient> =>
57
+ AiModel.make(
58
+ "openai",
59
+ model,
60
+ Layer.merge(
61
+ layer({
62
+ model,
63
+ config: {
64
+ ...options.config,
65
+ dimensions: options.dimensions
66
+ }
67
+ }),
68
+ Layer.succeed(EmbeddingModel.Dimensions, options.dimensions)
69
+ )
70
+ )
71
+
72
+ /**
73
+ * Creates an OpenAI embedding model service.
74
+ *
75
+ * @since 1.0.0
76
+ * @category constructors
77
+ */
78
+ export const make = Effect.fnUntraced(function*({ model, config: providerConfig }: {
79
+ readonly model: string
80
+ readonly config?: Omit<typeof Config.Service, "model"> | undefined
81
+ }): Effect.fn.Return<EmbeddingModel.Service, never, OpenAiClient> {
82
+ const client = yield* OpenAiClient
83
+
84
+ const makeConfig = Effect.gen(function*() {
85
+ const services = yield* Effect.context<never>()
86
+ return { model, ...providerConfig, ...services.mapUnsafe.get(Config.key) }
87
+ })
88
+
89
+ return yield* EmbeddingModel.make({
90
+ embedMany: Effect.fnUntraced(function*({ inputs }) {
91
+ const config = yield* makeConfig
92
+ const response = yield* client.createEmbedding({ ...config, input: inputs })
93
+ return yield* mapProviderResponse(inputs.length, response)
94
+ })
95
+ })
96
+ })
97
+
98
+ /**
99
+ * Creates a layer for the OpenAI embedding model.
100
+ *
101
+ * @since 1.0.0
102
+ * @category layers
103
+ */
104
+ export const layer = (options: {
105
+ readonly model: string
106
+ readonly config?: Omit<typeof Config.Service, "model"> | undefined
107
+ }): Layer.Layer<EmbeddingModel.EmbeddingModel, never, OpenAiClient> =>
108
+ Layer.effect(EmbeddingModel.EmbeddingModel, make(options))
109
+
110
+ /**
111
+ * Provides config overrides for OpenAI embedding model operations.
112
+ *
113
+ * @since 1.0.0
114
+ * @category configuration
115
+ */
116
+ export const withConfigOverride: {
117
+ /**
118
+ * Provides config overrides for OpenAI embedding model operations.
119
+ *
120
+ * @since 1.0.0
121
+ * @category configuration
122
+ */
123
+ (overrides: typeof Config.Service): <A, E, R>(self: Effect.Effect<A, E, R>) => Effect.Effect<A, E, Exclude<R, Config>>
124
+ /**
125
+ * Provides config overrides for OpenAI embedding model operations.
126
+ *
127
+ * @since 1.0.0
128
+ * @category configuration
129
+ */
130
+ <A, E, R>(self: Effect.Effect<A, E, R>, overrides: typeof Config.Service): Effect.Effect<A, E, Exclude<R, Config>>
131
+ } = dual<
132
+ /**
133
+ * Provides config overrides for OpenAI embedding model operations.
134
+ *
135
+ * @since 1.0.0
136
+ * @category configuration
137
+ */
138
+ (overrides: typeof Config.Service) => <A, E, R>(self: Effect.Effect<A, E, R>) => Effect.Effect<A, E, Exclude<R, Config>>,
139
+ /**
140
+ * Provides config overrides for OpenAI embedding model operations.
141
+ *
142
+ * @since 1.0.0
143
+ * @category configuration
144
+ */
145
+ <A, E, R>(self: Effect.Effect<A, E, R>, overrides: typeof Config.Service) => Effect.Effect<A, E, Exclude<R, Config>>
146
+ >(2, (self, overrides) =>
147
+ Effect.flatMap(
148
+ Effect.serviceOption(Config),
149
+ (config) =>
150
+ Effect.provideService(self, Config, {
151
+ ...(config._tag === "Some" ? config.value : {}),
152
+ ...overrides
153
+ })
154
+ ))
155
+
156
+ const mapProviderResponse = (
157
+ inputLength: number,
158
+ response: CreateEmbedding200
159
+ ): Effect.Effect<EmbeddingModel.ProviderResponse, AiError.AiError> => {
160
+ if (response.data.length !== inputLength) {
161
+ return Effect.fail(
162
+ invalidOutput(`Provider returned ${response.data.length} embeddings but expected ${inputLength}`)
163
+ )
164
+ }
165
+
166
+ const results = new Array<Array<number>>(inputLength)
167
+ const seen = new Set<number>()
168
+
169
+ for (const entry of response.data) {
170
+ if (!Number.isInteger(entry.index) || entry.index < 0 || entry.index >= inputLength) {
171
+ return Effect.fail(invalidOutput(`Provider returned invalid embedding index: ${entry.index}`))
172
+ }
173
+ if (seen.has(entry.index)) {
174
+ return Effect.fail(invalidOutput(`Provider returned duplicate embedding index: ${entry.index}`))
175
+ }
176
+ if (!Array.isArray(entry.embedding)) {
177
+ return Effect.fail(invalidOutput(`Provider returned non-vector embedding at index ${entry.index}`))
178
+ }
179
+
180
+ seen.add(entry.index)
181
+ results[entry.index] = [...entry.embedding]
182
+ }
183
+
184
+ if (seen.size !== inputLength) {
185
+ return Effect.fail(
186
+ invalidOutput(`Provider returned embeddings for ${seen.size} inputs but expected ${inputLength}`)
187
+ )
188
+ }
189
+
190
+ return Effect.succeed({
191
+ results,
192
+ usage: {
193
+ inputTokens: response.usage?.prompt_tokens
194
+ }
195
+ })
196
+ }
197
+
198
+ const invalidOutput = (description: string): AiError.AiError =>
199
+ AiError.make({
200
+ module: "OpenAiEmbeddingModel",
201
+ method: "embedMany",
202
+ reason: new AiError.InvalidOutputError({ description })
203
+ })
@@ -52,51 +52,43 @@ export type OpenAiRateLimitMetadata = OpenAiErrorMetadata & {
52
52
  }
53
53
 
54
54
  declare module "effect/unstable/ai/AiError" {
55
- export interface RateLimitError {
56
- readonly metadata: {
57
- readonly openai?: OpenAiRateLimitMetadata | null
58
- }
55
+ export interface RateLimitErrorMetadata {
56
+ readonly openai?: OpenAiRateLimitMetadata | null
59
57
  }
60
58
 
61
- export interface QuotaExhaustedError {
62
- readonly metadata: {
63
- readonly openai?: OpenAiErrorMetadata | null
64
- }
59
+ export interface QuotaExhaustedErrorMetadata {
60
+ readonly openai?: OpenAiErrorMetadata | null
65
61
  }
66
62
 
67
- export interface AuthenticationError {
68
- readonly metadata: {
69
- readonly openai?: OpenAiErrorMetadata | null
70
- }
63
+ export interface AuthenticationErrorMetadata {
64
+ readonly openai?: OpenAiErrorMetadata | null
71
65
  }
72
66
 
73
- export interface ContentPolicyError {
74
- readonly metadata: {
75
- readonly openai?: OpenAiErrorMetadata | null
76
- }
67
+ export interface ContentPolicyErrorMetadata {
68
+ readonly openai?: OpenAiErrorMetadata | null
77
69
  }
78
70
 
79
- export interface InvalidRequestError {
80
- readonly metadata: {
81
- readonly openai?: OpenAiErrorMetadata | null
82
- }
71
+ export interface InvalidRequestErrorMetadata {
72
+ readonly openai?: OpenAiErrorMetadata | null
83
73
  }
84
74
 
85
- export interface InternalProviderError {
86
- readonly metadata: {
87
- readonly openai?: OpenAiErrorMetadata | null
88
- }
75
+ export interface InternalProviderErrorMetadata {
76
+ readonly openai?: OpenAiErrorMetadata | null
89
77
  }
90
78
 
91
- export interface InvalidOutputError {
92
- readonly metadata: {
93
- readonly openai?: OpenAiErrorMetadata | null
94
- }
79
+ export interface InvalidOutputErrorMetadata {
80
+ readonly openai?: OpenAiErrorMetadata | null
95
81
  }
96
82
 
97
- export interface UnknownError {
98
- readonly metadata: {
99
- readonly openai?: OpenAiErrorMetadata | null
100
- }
83
+ export interface StructuredOutputErrorMetadata {
84
+ readonly openai?: OpenAiErrorMetadata | null
85
+ }
86
+
87
+ export interface UnsupportedSchemaErrorMetadata {
88
+ readonly openai?: OpenAiErrorMetadata | null
89
+ }
90
+
91
+ export interface UnknownErrorMetadata {
92
+ readonly openai?: OpenAiErrorMetadata | null
101
93
  }
102
94
  }
@@ -6,16 +6,17 @@
6
6
  *
7
7
  * @since 1.0.0
8
8
  */
9
+ import * as Context from "effect/Context"
9
10
  import * as DateTime from "effect/DateTime"
10
11
  import * as Effect from "effect/Effect"
11
- import * as Base64 from "effect/encoding/Base64"
12
+ import * as Encoding from "effect/Encoding"
12
13
  import { dual } from "effect/Function"
13
14
  import * as Layer from "effect/Layer"
15
+ import * as Option from "effect/Option"
14
16
  import * as Predicate from "effect/Predicate"
15
17
  import * as Redactable from "effect/Redactable"
16
18
  import type * as Schema from "effect/Schema"
17
19
  import * as AST from "effect/SchemaAST"
18
- import * as ServiceMap from "effect/ServiceMap"
19
20
  import * as Stream from "effect/Stream"
20
21
  import type { Span } from "effect/Tracer"
21
22
  import type { DeepMutable, Simplify } from "effect/Types"
@@ -63,7 +64,7 @@ type ImageDetail = "auto" | "low" | "high"
63
64
  * @since 1.0.0
64
65
  * @category context
65
66
  */
66
- export class Config extends ServiceMap.Service<
67
+ export class Config extends Context.Service<
67
68
  Config,
68
69
  Simplify<
69
70
  & Partial<
@@ -101,6 +102,7 @@ export class Config extends ServiceMap.Service<
101
102
  * Defaults to `true`.
102
103
  */
103
104
  readonly strictJsonSchema?: boolean | undefined
105
+ readonly [x: string]: unknown
104
106
  }
105
107
  >
106
108
  >()("@effect/ai-openai-compat/OpenAiLanguageModel/Config") {}
@@ -316,7 +318,7 @@ export const model = (
316
318
  model: string,
317
319
  config?: Omit<typeof Config.Service, "model">
318
320
  ): AiModel.Model<"openai", LanguageModel.LanguageModel, OpenAiClient> =>
319
- AiModel.make("openai", layer({ model, config }))
321
+ AiModel.make("openai", model, layer({ model, config }))
320
322
 
321
323
  // TODO
322
324
  // /**
@@ -327,7 +329,7 @@ export const model = (
327
329
  // model: string,
328
330
  // config?: Omit<typeof Config.Service, "model">
329
331
  // ): AiModel.Model<"openai", LanguageModel.LanguageModel | Tokenizer.Tokenizer, OpenAiClient> =>
330
- // AiModel.make("openai", layerWithTokenizer({ model, config }))
332
+ // AiModel.make("openai", model, layerWithTokenizer({ model, config }))
331
333
 
332
334
  /**
333
335
  * Creates an OpenAI language model service.
@@ -342,7 +344,7 @@ export const make = Effect.fnUntraced(function*({ model, config: providerConfig
342
344
  const client = yield* OpenAiClient
343
345
 
344
346
  const makeConfig = Effect.gen(function*() {
345
- const services = yield* Effect.services<never>()
347
+ const services = yield* Effect.context<never>()
346
348
  return { model, ...providerConfig, ...services.mapUnsafe.get(Config.key) }
347
349
  })
348
350
 
@@ -386,6 +388,7 @@ export const make = Effect.fnUntraced(function*({ model, config: providerConfig
386
388
  )
387
389
 
388
390
  return yield* LanguageModel.make({
391
+ codecTransformer: toCodecOpenAI,
389
392
  generateText: Effect.fnUntraced(
390
393
  function*(options) {
391
394
  const config = yield* makeConfig
@@ -423,10 +426,7 @@ export const make = Effect.fnUntraced(function*({ model, config: providerConfig
423
426
  })
424
427
  )
425
428
  )
426
- }).pipe(Effect.provideService(
427
- LanguageModel.CurrentCodecTransformer,
428
- toCodecOpenAI
429
- ))
429
+ })
430
430
  })
431
431
 
432
432
  /**
@@ -561,7 +561,7 @@ const prepareMessages = Effect.fnUntraced(
561
561
  }
562
562
 
563
563
  if (part.data instanceof Uint8Array) {
564
- const base64 = Base64.encode(part.data)
564
+ const base64 = Encoding.encodeBase64(part.data)
565
565
  const imageUrl = `data:${mediaType};base64,${base64}`
566
566
  content.push({ type: "input_image", image_url: imageUrl, detail })
567
567
  }
@@ -575,7 +575,7 @@ const prepareMessages = Effect.fnUntraced(
575
575
  }
576
576
 
577
577
  if (part.data instanceof Uint8Array) {
578
- const base64 = Base64.encode(part.data)
578
+ const base64 = Encoding.encodeBase64(part.data)
579
579
  const fileName = part.fileName ?? `part-${index}.pdf`
580
580
  const fileData = `data:application/pdf;base64,${base64}`
581
581
  content.push({ type: "input_file", filename: fileName, file_data: fileData })
@@ -782,7 +782,7 @@ const buildHttpRequestDetails = (
782
782
  method: request.method,
783
783
  url: request.url,
784
784
  urlParams: Array.from(request.urlParams),
785
- hash: request.hash,
785
+ hash: Option.getOrUndefined(request.hash),
786
786
  headers: Redactable.redact(request.headers) as Record<string, string>
787
787
  })
788
788
 
@@ -1002,11 +1002,13 @@ const makeStreamResponse = Effect.fnUntraced(
1002
1002
  hasToolCalls = hasToolCalls || choice.delta.tool_calls.length > 0
1003
1003
  choice.delta.tool_calls.forEach((deltaTool, indexInChunk) => {
1004
1004
  const toolIndex = deltaTool.index ?? indexInChunk
1005
- const toolId = deltaTool.id ?? `${event.id}_tool_${toolIndex}`
1005
+ const activeToolCall = activeToolCalls[toolIndex]
1006
+ const toolId = activeToolCall?.id ?? deltaTool.id ?? `${event.id}_tool_${toolIndex}`
1006
1007
  const providerToolName = deltaTool.function?.name
1007
- const toolName = toolNameMapper.getCustomName(providerToolName ?? "unknown_tool")
1008
+ const toolName = providerToolName !== undefined
1009
+ ? toolNameMapper.getCustomName(providerToolName)
1010
+ : activeToolCall?.name ?? toolNameMapper.getCustomName("unknown_tool")
1008
1011
  const argumentsDelta = deltaTool.function?.arguments ?? ""
1009
- const activeToolCall = activeToolCalls[toolIndex]
1010
1012
 
1011
1013
  if (Predicate.isUndefined(activeToolCall)) {
1012
1014
  activeToolCalls[toolIndex] = {
@@ -1169,7 +1171,7 @@ const prepareTools = Effect.fnUntraced(function*<Tools extends ReadonlyArray<Too
1169
1171
 
1170
1172
  // Convert the tools in the toolkit to the provider-defined format
1171
1173
  for (const tool of allowedTools) {
1172
- if (Tool.isUserDefined(tool)) {
1174
+ if (Tool.isUserDefined(tool) || Tool.isDynamic(tool)) {
1173
1175
  const strict = Tool.getStrictMode(tool) ?? config.strictJsonSchema ?? true
1174
1176
  const parameters = yield* tryToolJsonSchema(tool, "prepareTools")
1175
1177
  tools.push({
@@ -1220,6 +1222,7 @@ const toChatCompletionsRequest = (payload: CreateResponse): CreateResponseReques
1220
1222
  const toolChoice = toChatToolChoice(payload.tool_choice)
1221
1223
 
1222
1224
  return {
1225
+ ...extractCustomRequestProperties(payload),
1223
1226
  model: payload.model ?? "",
1224
1227
  messages: messages.length > 0 ? messages : [{ role: "user", content: "" }],
1225
1228
  ...(payload.temperature !== undefined ? { temperature: payload.temperature } : undefined),
@@ -1231,12 +1234,54 @@ const toChatCompletionsRequest = (payload: CreateResponse): CreateResponseReques
1231
1234
  ? { parallel_tool_calls: payload.parallel_tool_calls }
1232
1235
  : undefined),
1233
1236
  ...(payload.service_tier !== undefined ? { service_tier: payload.service_tier } : undefined),
1237
+ ...(payload.reasoning !== undefined ? { reasoning: payload.reasoning } : undefined),
1234
1238
  ...(responseFormat !== undefined ? { response_format: responseFormat } : undefined),
1235
1239
  ...(tools.length > 0 ? { tools } : undefined),
1236
1240
  ...(toolChoice !== undefined ? { tool_choice: toolChoice } : undefined)
1237
1241
  }
1238
1242
  }
1239
1243
 
1244
+ const createResponseKnownProperties = new Set<string>([
1245
+ "metadata",
1246
+ "top_logprobs",
1247
+ "temperature",
1248
+ "top_p",
1249
+ "user",
1250
+ "safety_identifier",
1251
+ "prompt_cache_key",
1252
+ "service_tier",
1253
+ "prompt_cache_retention",
1254
+ "previous_response_id",
1255
+ "model",
1256
+ "reasoning",
1257
+ "background",
1258
+ "max_output_tokens",
1259
+ "max_tool_calls",
1260
+ "text",
1261
+ "tools",
1262
+ "tool_choice",
1263
+ "truncation",
1264
+ "input",
1265
+ "include",
1266
+ "parallel_tool_calls",
1267
+ "store",
1268
+ "instructions",
1269
+ "stream",
1270
+ "conversation",
1271
+ "modalities",
1272
+ "seed"
1273
+ ])
1274
+
1275
+ const extractCustomRequestProperties = (payload: CreateResponse): Record<string, unknown> => {
1276
+ const customProperties: Record<string, unknown> = {}
1277
+ for (const [key, value] of Object.entries(payload)) {
1278
+ if (!createResponseKnownProperties.has(key)) {
1279
+ customProperties[key] = value
1280
+ }
1281
+ }
1282
+ return customProperties
1283
+ }
1284
+
1240
1285
  const toChatResponseFormat = (
1241
1286
  format: TextResponseFormatConfiguration | undefined
1242
1287
  ): CreateResponseRequestJson["response_format"] | undefined => {
package/src/index.ts CHANGED
@@ -14,6 +14,15 @@ export * as OpenAiClient from "./OpenAiClient.ts"
14
14
  */
15
15
  export * as OpenAiConfig from "./OpenAiConfig.ts"
16
16
 
17
+ /**
18
+ * OpenAI Embedding Model implementation.
19
+ *
20
+ * Provides an EmbeddingModel implementation for OpenAI-compatible embeddings APIs.
21
+ *
22
+ * @since 1.0.0
23
+ */
24
+ export * as OpenAiEmbeddingModel from "./OpenAiEmbeddingModel.ts"
25
+
17
26
  /**
18
27
  * @since 1.0.0
19
28
  */
@@ -153,12 +153,12 @@ export const parseRateLimitHeaders = (headers: Record<string, string>) => {
153
153
  let retryAfter: Duration.Duration | undefined
154
154
  if (retryAfterRaw !== undefined) {
155
155
  const parsed = Number.parse(retryAfterRaw)
156
- if (parsed !== undefined) {
157
- retryAfter = Duration.seconds(parsed)
156
+ if (Option.isSome(parsed)) {
157
+ retryAfter = Duration.seconds(parsed.value)
158
158
  }
159
159
  }
160
160
  const remainingRaw = headers["x-ratelimit-remaining-requests"]
161
- const remaining = remainingRaw !== undefined ? Number.parse(remainingRaw) ?? null : null
161
+ const remaining = remainingRaw !== undefined ? Option.getOrNull(Number.parse(remainingRaw)) : null
162
162
  return {
163
163
  retryAfter,
164
164
  limit: headers["x-ratelimit-limit-requests"] ?? null,
@@ -175,7 +175,7 @@ export const buildHttpRequestDetails = (
175
175
  method: request.method,
176
176
  url: request.url,
177
177
  urlParams: Array.from(request.urlParams),
178
- hash: request.hash,
178
+ hash: Option.getOrUndefined(request.hash),
179
179
  headers: Redactable.redact(request.headers) as Record<string, string>
180
180
  })
181
181