1/**
2 * The `OpenAiClient` module provides an Effect service for OpenAI-compatible
3 * chat completions and embeddings APIs. It builds on the Effect HTTP client,
4 * adds authentication and OpenAI organization or project headers, and exposes
5 * typed helpers for non-streaming chat completions, streaming chat completions,
6 * and embedding requests.
7 *
8 * @stability unstable
9 * @since 4.0.0
10 */
11import type * as AiError from "effect/ai/AiError"
12import * as Array from "effect/Array"
13import type * as Config from "effect/Config"
14import * as Context from "effect/Context"
15import * as Effect from "effect/Effect"
16import * as Sse from "effect/encoding/Sse"
17import { identity } from "effect/Function"
18import * as Headers from "effect/http/Headers"
19import * as HttpClient from "effect/http/HttpClient"
20import * as HttpClientRequest from "effect/http/HttpClientRequest"
21import * as HttpClientResponse from "effect/http/HttpClientResponse"
22import * as Layer from "effect/Layer"
23import * as Redacted from "effect/Redacted"
24import * as Schema from "effect/Schema"
25import * as Stream from "effect/Stream"
26import * as Errors from "./internal/errors.ts"
27import { OpenAiConfig } from "./OpenAiConfig.ts"
28
29/**
30 * Effect service interface for OpenAI-compatible chat completions and embeddings.
31 *
32 * **Details**
33 *
34 * Exposes the configured HTTP client plus helpers for non-streaming chat
35 * completions, streaming chat completions, and embeddings. Transport and
36 * schema decoding failures are mapped to `AiError`.
37 *
38 * @stability unstable
39 * @category services
40 * @since 4.0.0
41 */
42export interface Service {
43 readonly client: HttpClient.HttpClient
44 readonly createResponse: (
45 options: CreateResponseRequestJson
46 ) => Effect.Effect<
47 [body: CreateResponse200, response: HttpClientResponse.HttpClientResponse],
48 AiError.AiError
49 >
50 readonly createResponseStream: (
51 options: Omit<CreateResponseRequestJson, "stream" | "stream_options">
52 ) => Effect.Effect<
53 [
54 response: HttpClientResponse.HttpClientResponse,
55 stream: Stream.Stream<CreateResponse200Sse, AiError.AiError>
56 ],
57 AiError.AiError
58 >
59 readonly createEmbedding: (
60 options: CreateEmbeddingRequestJson
61 ) => Effect.Effect<CreateEmbedding200, AiError.AiError>
62}
63
64/**
65 * Service tag for the OpenAI-compatible chat completions and embeddings client.
66 *
67 * **When to use**
68 *
69 * Use when building effects that depend on the low-level OpenAI-compatible
70 * client through context rather than receiving the client as a value.
71 *
72 * **Details**
73 *
74 * The tagged service is the `Service` interface produced by `make` and provided
75 * by `layer` or `layerConfig`.
76 *
77 * @see {@link Service} for the operations provided by the service
78 * @see {@link make} for constructing the service from explicit options
79 * @see {@link layer} for providing the service from explicit options
80 * @see {@link layerConfig} for loading client settings from `Config`
81 *
82 * @stability unstable
83 * @category services
84 * @since 4.0.0
85 */
86export class OpenAiClient extends Context.Service<OpenAiClient, Service>()(
87 "@effect/ai-openai-compat/OpenAiClient"
88) {}
89
90/**
91 * Configuration options used to construct an OpenAI-compatible client.
92 *
93 * @stability unstable
94 * @category options
95 * @since 4.0.0
96 */
97export type Options = {
98 readonly apiKey?: Redacted.Redacted<string> | undefined
99 readonly apiUrl?: string | undefined
100 readonly organizationId?: Redacted.Redacted<string> | undefined
101 readonly projectId?: Redacted.Redacted<string> | undefined
102 readonly transformClient?: ((client: HttpClient.HttpClient) => HttpClient.HttpClient) | undefined
103}
104
105const RedactedOpenAiHeaders = {
106 OpenAiOrganization: "openai-organization",
107 OpenAiProject: "openai-project"
108}
109
110const withRedactedHeaders = Effect.updateService(
111 Headers.CurrentRedactedNames,
112 Array.appendAll(Object.values(RedactedOpenAiHeaders))
113)
114
115/**
116 * Constructs an OpenAI-compatible client service from explicit options.
117 *
118 * **When to use**
119 *
120 * Use when you need the OpenAI-compatible client service value inside an effect.
121 *
122 * **Details**
123 *
124 * The returned service uses the current `HttpClient`, prepends `apiUrl` or
125 * `https://api.openai.com/v1`, adds authentication and OpenAI
126 * organization/project headers, accepts JSON responses, and applies
127 * `transformClient` when provided.
128 *
129 * **Gotchas**
130 *
131 * A scoped `OpenAiConfig.withClientTransform` is applied when request helpers
132 * run, after the `transformClient` option supplied to `make`.
133 *
134 * @see {@link layer} for providing this client from explicit options
135 * @see {@link layerConfig} for loading client settings from `Config`
136 *
137 * @stability unstable
138 * @category constructors
139 * @since 4.0.0
140 */
141export const make = Effect.fnUntraced(
142 function*(options: Options): Effect.fn.Return<Service, never, HttpClient.HttpClient> {
143 const baseClient = yield* HttpClient.HttpClient
144
145 const httpClient = baseClient.pipe(
146 HttpClient.mapRequest((request) =>
147 request.pipe(
148 HttpClientRequest.prependUrl(options.apiUrl ?? "https://api.openai.com/v1"),
149 options.apiKey !== undefined
150 ? HttpClientRequest.bearerToken(Redacted.value(options.apiKey))
151 : identity,
152 options.organizationId !== undefined
153 ? HttpClientRequest.setHeader(
154 RedactedOpenAiHeaders.OpenAiOrganization,
155 Redacted.value(options.organizationId)
156 )
157 : identity,
158 options.projectId !== undefined
159 ? HttpClientRequest.setHeader(
160 RedactedOpenAiHeaders.OpenAiProject,
161 Redacted.value(options.projectId)
162 )
163 : identity,
164 HttpClientRequest.acceptJson
165 )
166 ),
167 options.transformClient !== undefined
168 ? options.transformClient
169 : identity
170 )
171
172 const resolveHttpClient = Effect.map(
173 OpenAiConfig.getOrUndefined,
174 (config) =>
175 config?.transformClient !== undefined
176 ? config.transformClient(httpClient)
177 : httpClient
178 )
179
180 const decodeResponse = HttpClientResponse.schemaBodyJson(ChatCompletionResponse)
181
182 const createResponse = (
183 payload: CreateResponseRequestJson
184 ): Effect.Effect<
185 [body: CreateResponse200, response: HttpClientResponse.HttpClientResponse],
186 AiError.AiError
187 > =>
188 resolveHttpClient.pipe(
189 Effect.flatMap((client) =>
190 HttpClientRequest.post("/chat/completions").pipe(
191 HttpClientRequest.bodyJsonUnsafe(payload),
192 HttpClient.filterStatusOk(client).execute,
193 Effect.flatMap((response) =>
194 Effect.map(decodeResponse(response), (
195 body
196 ): [CreateResponse200, HttpClientResponse.HttpClientResponse] => [
197 body,
198 response
199 ])
200 ),
201 Effect.catchTags({
202 HttpClientError: (error) => Errors.mapHttpClientError(error, "createResponse"),
203 SchemaError: (error) => Effect.fail(Errors.mapSchemaError(error, "createResponse"))
204 })
205 )
206 ),
207 withRedactedHeaders
208 )
209
210 const buildResponseStream = (
211 response: HttpClientResponse.HttpClientResponse
212 ): [
213 HttpClientResponse.HttpClientResponse,
214 Stream.Stream<CreateResponse200Sse, AiError.AiError>
215 ] => {
216 const stream = response.stream.pipe(
217 Stream.decodeText(),
218 Stream.pipeThroughChannel(Sse.decode()),
219 Stream.flatMap((event) => {
220 const data = decodeChatCompletionSseData(event.data)
221 return Stream.fromIterable(data !== undefined ? [data] : [])
222 }),
223 Stream.takeUntil((event) => event === "[DONE]"),
224 Stream.catchTags({
225 Retry: (error) => Stream.die(error),
226 SseError: (error) => Stream.fail(Errors.mapSseError(error, "createResponseStream")),
227 HttpClientError: (error) => Stream.fromEffect(Errors.mapHttpClientError(error, "createResponseStream"))
228 })
229 ) as any
230 return [response, stream]
231 }
232
233 const createResponseStream: Service["createResponseStream"] = (payload) =>
234 resolveHttpClient.pipe(
235 Effect.flatMap((client) =>
236 HttpClientRequest.post("/chat/completions").pipe(
237 HttpClientRequest.bodyJsonUnsafe({
238 ...payload,
239 stream: true,
240 stream_options: {
241 include_usage: true
242 }
243 }),
244 HttpClient.filterStatusOk(client).execute,
245 Effect.map(buildResponseStream),
246 Effect.catchTag(
247 "HttpClientError",
248 (error) => Errors.mapHttpClientError(error, "createResponseStream")
249 )
250 )
251 ),
252 withRedactedHeaders
253 )
254
255 const decodeEmbedding = HttpClientResponse.schemaBodyJson(CreateEmbeddingResponseSchema)
256
257 const createEmbedding = (
258 payload: CreateEmbeddingRequestJson
259 ): Effect.Effect<CreateEmbedding200, AiError.AiError> =>
260 resolveHttpClient.pipe(
261 Effect.flatMap((client) =>
262 HttpClientRequest.post("/embeddings").pipe(
263 HttpClientRequest.bodyJsonUnsafe(payload),
264 HttpClient.filterStatusOk(client).execute,
265 Effect.flatMap(decodeEmbedding),
266 Effect.catchTags({
267 HttpClientError: (error) => Errors.mapHttpClientError(error, "createEmbedding"),
268 SchemaError: (error) => Effect.fail(Errors.mapSchemaError(error, "createEmbedding"))
269 })
270 )
271 ),
272 withRedactedHeaders
273 )
274
275 return OpenAiClient.of({
276 client: httpClient,
277 createResponse,
278 createResponseStream,
279 createEmbedding
280 })
281 },
282 withRedactedHeaders
283)
284
285/**
286 * Creates a layer that provides an OpenAI-compatible client from explicit options.
287 *
288 * **When to use**
289 *
290 * Use to install `OpenAiClient` in an application layer when the client options
291 * are already available as values rather than loaded from `Config`.
292 *
293 * @see {@link make} for constructing the client service effectfully
294 * @see {@link layerConfig} for loading client settings from `Config`
295 *
296 * @stability unstable
297 * @category layers
298 * @since 4.0.0
299 */
300export const layer = (options: Options): Layer.Layer<OpenAiClient, never, HttpClient.HttpClient> =>
301 Layer.effect(OpenAiClient, make(options))
302
303/**
304 * Creates a layer that loads OpenAI-compatible client settings from `Config`
305 * values before constructing the service.
306 *
307 * **When to use**
308 *
309 * Use when you need client settings for OpenAI-compatible APIs to be read from
310 * Effect `Config` values while providing `OpenAiClient` as a layer.
311 *
312 * **Details**
313 *
314 * Only config values supplied in `options` are loaded. Omitted fields are
315 * passed to `make` as `undefined`, and `transformClient` is forwarded as a
316 * plain option.
317 *
318 * @see {@link make} for constructing the client service effectfully
319 * @see {@link layer} for providing the client from already-resolved options
320 *
321 * @stability unstable
322 * @category layers
323 * @since 4.0.0
324 */
325export const layerConfig = (options?: {
326 readonly apiKey?: Config.Config<Redacted.Redacted<string> | undefined> | undefined
327 readonly apiUrl?: Config.Config<string> | undefined
328 readonly organizationId?: Config.Config<Redacted.Redacted<string> | undefined> | undefined
329 readonly projectId?: Config.Config<Redacted.Redacted<string> | undefined> | undefined
330 readonly transformClient?: ((client: HttpClient.HttpClient) => HttpClient.HttpClient) | undefined
331}): Layer.Layer<OpenAiClient, Config.ConfigError, HttpClient.HttpClient> =>
332 Layer.effect(
333 OpenAiClient,
334 Effect.gen(function*() {
335 const apiKey = options?.apiKey !== undefined
336 ? yield* options.apiKey :
337 undefined
338 const apiUrl = options?.apiUrl !== undefined
339 ? yield* options.apiUrl :
340 undefined
341 const organizationId = options?.organizationId !== undefined
342 ? yield* options.organizationId
343 : undefined
344 const projectId = options?.projectId !== undefined
345 ? yield* options.projectId :
346 undefined
347 return yield* make({
348 apiKey,
349 apiUrl,
350 organizationId,
351 projectId,
352 transformClient: options?.transformClient
353 })
354 })
355 )
356
357type JsonObject = { readonly [x: string]: Schema.Json }
358
359/**
360 * Optional response fields that can be requested with the `include` parameter.
361 *
362 * @stability unstable
363 * @category models
364 * @since 4.0.0
365 */
366export type IncludeEnum =
367 | "message.input_image.image_url"
368 | "reasoning.encrypted_content"
369 | "message.output_text.logprobs"
370
371/**
372 * Lifecycle status shared by message, reasoning, and tool-call items.
373 *
374 * @stability unstable
375 * @category models
376 * @since 4.0.0
377 */
378export type MessageStatus = "in_progress" | "completed" | "incomplete"
379
380type InputTextContent = {
381 readonly type: "input_text"
382 readonly text: string
383}
384
385type InputImageContent = {
386 readonly type: "input_image"
387 readonly image_url?: string | null | undefined
388 readonly file_id?: string | null | undefined
389 readonly detail?: "low" | "high" | "auto" | null | undefined
390}
391
392type InputFileContent = {
393 readonly type: "input_file"
394 readonly file_id?: string | null | undefined
395 readonly filename?: string | undefined
396 readonly file_url?: string | undefined
397 readonly file_data?: string | undefined
398}
399
400/**
401 * Content blocks accepted in input messages.
402 *
403 * @stability unstable
404 * @category models
405 * @since 4.0.0
406 */
407export type InputContent = InputTextContent | InputImageContent | InputFileContent
408
409/**
410 * Text content block used for model-provided reasoning summaries.
411 *
412 * @stability unstable
413 * @category models
414 * @since 4.0.0
415 */
416export type SummaryTextContent = {
417 readonly type: "summary_text"
418 readonly text: string
419}
420
421type ReasoningTextContent = {
422 readonly type: "reasoning_text"
423 readonly text: string
424}
425
426type RefusalContent = {
427 readonly type: "refusal"
428 readonly refusal: string
429}
430
431type TextContent = {
432 readonly type: "text"
433 readonly text: string
434}
435
436type ComputerScreenshotContent = {
437 readonly type: "computer_screenshot"
438 readonly image_url: string | null
439 readonly file_id: string | null
440}
441
442type FileCitationAnnotation = {
443 readonly type: "file_citation"
444 readonly file_id: string
445 readonly index: number
446 readonly filename: string
447}
448
449type UrlCitationAnnotation = {
450 readonly type: "url_citation"
451 readonly url: string
452 readonly start_index: number
453 readonly end_index: number
454 readonly title: string
455}
456
457type ContainerFileCitationAnnotation = {
458 readonly type: "container_file_citation"
459 readonly container_id: string
460 readonly file_id: string
461 readonly start_index: number
462 readonly end_index: number
463 readonly filename: string
464}
465
466type FilePathAnnotation = {
467 readonly type: "file_path"
468 readonly file_id: string
469 readonly index: number
470}
471
472/**
473 * Citation and file-path annotations attached to output text content.
474 *
475 * @stability unstable
476 * @category models
477 * @since 4.0.0
478 */
479export type Annotation =
480 | FileCitationAnnotation
481 | UrlCitationAnnotation
482 | ContainerFileCitationAnnotation
483 | FilePathAnnotation
484
485type OutputTextContent = {
486 readonly type: "output_text"
487 readonly text: string
488 readonly annotations?: ReadonlyArray<Annotation> | undefined
489 readonly logprobs?: ReadonlyArray<unknown> | undefined
490}
491
492type OutputMessageContent =
493 | InputTextContent
494 | OutputTextContent
495 | TextContent
496 | SummaryTextContent
497 | ReasoningTextContent
498 | RefusalContent
499 | InputImageContent
500 | ComputerScreenshotContent
501 | InputFileContent
502
503type OutputMessage = {
504 readonly id: string
505 readonly type: "message"
506 readonly role: "assistant"
507 readonly content: ReadonlyArray<OutputMessageContent>
508 readonly status: MessageStatus
509}
510
511/**
512 * Reasoning output item containing encrypted reasoning content, summaries, and
513 * optional reasoning text.
514 *
515 * @stability unstable
516 * @category models
517 * @since 4.0.0
518 */
519export type ReasoningItem = {
520 readonly type: "reasoning"
521 readonly id: string
522 readonly encrypted_content?: string | null | undefined
523 readonly summary: ReadonlyArray<SummaryTextContent>
524 readonly content?: ReadonlyArray<ReasoningTextContent> | undefined
525 readonly status?: MessageStatus | undefined
526}
527
528type FunctionCall = {
529 readonly id?: string | undefined
530 readonly type: "function_call"
531 readonly call_id: string
532 readonly name: string
533 readonly arguments: string
534 readonly status?: MessageStatus | undefined
535}
536
537type FunctionCallOutput = {
538 readonly id?: string | null | undefined
539 readonly call_id: string
540 readonly type: "function_call_output"
541 readonly output: string | ReadonlyArray<InputTextContent | InputImageContent | InputFileContent>
542 readonly status?: MessageStatus | null | undefined
543}
544
545type CustomToolCall = {
546 readonly type: "custom_tool_call"
547 readonly id?: string | undefined
548 readonly call_id: string
549 readonly name: string
550 readonly input: string
551}
552
553type CustomToolCallOutput = {
554 readonly type: "custom_tool_call_output"
555 readonly id?: string | undefined
556 readonly call_id: string
557 readonly output: string | ReadonlyArray<InputTextContent | InputImageContent | InputFileContent>
558}
559
560type ItemReference = {
561 readonly type?: "item_reference" | null | undefined
562 readonly id: string
563}
564
565/**
566 * Item shapes accepted by a Responses-style `input` field.
567 *
568 * **Details**
569 *
570 * Supports input messages, output messages, tool calls, tool outputs, reasoning
571 * items, custom tool interactions, and item references.
572 *
573 * @stability unstable
574 * @category models
575 * @since 4.0.0
576 */
577export type InputItem =
578 | {
579 readonly role: "user" | "assistant" | "system" | "developer"
580 readonly content: string | ReadonlyArray<InputContent>
581 readonly type?: "message" | undefined
582 }
583 | {
584 readonly type?: "message" | undefined
585 readonly role: "user" | "system" | "developer"
586 readonly status?: MessageStatus | undefined
587 readonly content: ReadonlyArray<InputContent>
588 }
589 | OutputMessage
590 | FunctionCall
591 | FunctionCallOutput
592 | ReasoningItem
593 | CustomToolCallOutput
594 | CustomToolCall
595 | ItemReference
596
597type FunctionTool = {
598 readonly type: "function"
599 readonly name: string
600 readonly description?: string | null | undefined
601 readonly parameters?: JsonObject | null | undefined
602 readonly strict?: boolean | null | undefined
603}
604
605type CustomToolParam = {
606 readonly type: "custom"
607 readonly name: string
608 readonly description?: string | undefined
609 readonly format?: unknown
610}
611
612/**
613 * Tool definitions that can be supplied to a Responses-style request.
614 *
615 * @stability unstable
616 * @category models
617 * @since 4.0.0
618 */
619export type Tool =
620 | FunctionTool
621 | CustomToolParam
622
623type ToolChoice =
624 | "none"
625 | "auto"
626 | "required"
627 | {
628 readonly type: "allowed_tools"
629 readonly mode: "auto" | "required"
630 readonly tools: ReadonlyArray<JsonObject>
631 }
632 | {
633 readonly type: "function"
634 readonly name: string
635 }
636 | {
637 readonly type: "custom"
638 readonly name: string
639 }
640
641/**
642 * Text output format configuration for plain text, JSON object, or JSON Schema
643 * responses.
644 *
645 * @stability unstable
646 * @category configuration
647 * @since 4.0.0
648 */
649export type TextResponseFormatConfiguration =
650 | {
651 readonly type: "text"
652 }
653 | {
654 readonly type: "json_schema"
655 readonly description?: string | undefined
656 readonly name: string
657 readonly schema: JsonObject
658 readonly strict?: boolean | null | undefined
659 }
660 | {
661 readonly type: "json_object"
662 }
663
664/**
665 * Request options for creating a Responses-style response with an
666 * OpenAI-compatible provider.
667 *
668 * @stability unstable
669 * @category models
670 * @since 4.0.0
671 */
672export type CreateResponse = {
673 readonly metadata?: Readonly<Record<string, string>> | null | undefined
674 readonly top_logprobs?: number | undefined
675 readonly temperature?: number | null | undefined
676 readonly top_p?: number | null | undefined
677 readonly user?: string | null | undefined
678 readonly safety_identifier?: string | null | undefined
679 readonly prompt_cache_key?: string | null | undefined
680 readonly service_tier?: string | undefined
681 readonly prompt_cache_retention?: "in-memory" | "24h" | null | undefined
682 readonly previous_response_id?: string | null | undefined
683 readonly model?: string | undefined
684 readonly reasoning?: unknown
685 readonly background?: boolean | null | undefined
686 readonly max_output_tokens?: number | null | undefined
687 readonly max_tool_calls?: number | null | undefined
688 readonly text?: {
689 readonly format?: TextResponseFormatConfiguration | undefined
690 readonly verbosity?: "low" | "medium" | "high" | null | undefined
691 } | undefined
692 readonly tools?: ReadonlyArray<Tool> | undefined
693 readonly tool_choice?: ToolChoice | undefined
694 readonly truncation?: "auto" | "disabled" | null | undefined
695 readonly input?: string | ReadonlyArray<InputItem> | undefined
696 readonly include?: ReadonlyArray<IncludeEnum> | null | undefined
697 readonly parallel_tool_calls?: boolean | null | undefined
698 readonly store?: boolean | null | undefined
699 readonly instructions?: string | null | undefined
700 readonly stream?: boolean | null | undefined
701 readonly conversation?: string | null | undefined
702 readonly modalities?: ReadonlyArray<"text" | "audio"> | undefined
703 readonly seed?: number | undefined
704}
705
706/**
707 * Token accounting reported on Responses-style response objects.
708 *
709 * @stability unstable
710 * @category models
711 * @since 4.0.0
712 */
713export type ResponseUsage = {
714 readonly input_tokens: number
715 readonly output_tokens: number
716 readonly total_tokens: number
717 readonly input_tokens_details?: unknown
718 readonly output_tokens_details?: unknown
719}
720
721type OutputItem =
722 | OutputMessage
723 | FunctionCall
724 | ReasoningItem
725 | CustomToolCall
726
727/**
728 * Responses-style response object returned by compatible providers or embedded
729 * in response stream lifecycle events.
730 *
731 * @stability unstable
732 * @category models
733 * @since 4.0.0
734 */
735export type Response = {
736 readonly id: string
737 readonly object?: "response" | undefined
738 readonly model: string
739 readonly status?: "completed" | "failed" | "in_progress" | "cancelled" | "queued" | "incomplete" | undefined
740 readonly created_at: number
741 readonly output: ReadonlyArray<OutputItem>
742 readonly usage?: ResponseUsage | null | undefined
743 readonly incomplete_details?:
744 | {
745 readonly reason?: "max_output_tokens" | "content_filter" | undefined
746 }
747 | null
748 | undefined
749 readonly service_tier?: string | undefined
750}
751
752type ResponseCreatedEvent = {
753 readonly type: "response.created"
754 readonly response: Response
755 readonly sequence_number: number
756}
757
758type ResponseCompletedEvent = {
759 readonly type: "response.completed"
760 readonly response: Response
761 readonly sequence_number: number
762}
763
764type ResponseIncompleteEvent = {
765 readonly type: "response.incomplete"
766 readonly response: Response
767 readonly sequence_number: number
768}
769
770type ResponseFailedEvent = {
771 readonly type: "response.failed"
772 readonly response: Response
773 readonly sequence_number: number
774}
775
776type ResponseOutputItemAddedEvent = {
777 readonly type: "response.output_item.added"
778 readonly output_index: number
779 readonly sequence_number: number
780 readonly item: OutputItem
781}
782
783type ResponseOutputItemDoneEvent = {
784 readonly type: "response.output_item.done"
785 readonly output_index: number
786 readonly sequence_number: number
787 readonly item: OutputItem
788}
789
790type ResponseTextDeltaEvent = {
791 readonly type: "response.output_text.delta"
792 readonly item_id: string
793 readonly output_index: number
794 readonly content_index: number
795 readonly delta: string
796 readonly sequence_number: number
797 readonly logprobs?: ReadonlyArray<unknown> | undefined
798}
799
800type ResponseOutputTextAnnotationAddedEvent = {
801 readonly type: "response.output_text.annotation.added"
802 readonly item_id: string
803 readonly output_index: number
804 readonly content_index: number
805 readonly annotation_index: number
806 readonly sequence_number: number
807 readonly annotation: Annotation
808}
809
810type ResponseFunctionCallArgumentsDeltaEvent = {
811 readonly type: "response.function_call_arguments.delta"
812 readonly item_id: string
813 readonly output_index: number
814 readonly sequence_number: number
815 readonly delta: string
816}
817
818type ResponseReasoningSummaryPartAddedEvent = {
819 readonly type: "response.reasoning_summary_part.added"
820 readonly item_id: string
821 readonly output_index: number
822 readonly summary_index: number
823 readonly sequence_number: number
824 readonly part: SummaryTextContent
825}
826
827type ResponseReasoningSummaryPartDoneEvent = {
828 readonly type: "response.reasoning_summary_part.done"
829 readonly item_id: string
830 readonly output_index: number
831 readonly summary_index: number
832 readonly sequence_number: number
833 readonly part: SummaryTextContent
834}
835
836type ResponseReasoningSummaryTextDeltaEvent = {
837 readonly type: "response.reasoning_summary_text.delta"
838 readonly item_id: string
839 readonly output_index: number
840 readonly summary_index: number
841 readonly delta: string
842 readonly sequence_number: number
843}
844
845type ResponseErrorEvent = {
846 readonly type: "error"
847 readonly code: string | null
848 readonly message: string
849 readonly param: string | null
850 readonly sequence_number: number
851}
852
853type UnknownResponseStreamEvent = {
854 readonly type: string
855 readonly [key: string]: unknown
856}
857
858/**
859 * Server-sent event shapes emitted by Responses-style response streams.
860 *
861 * @stability unstable
862 * @category streaming
863 * @since 4.0.0
864 */
865export type ResponseStreamEvent =
866 | ResponseCreatedEvent
867 | ResponseCompletedEvent
868 | ResponseIncompleteEvent
869 | ResponseFailedEvent
870 | ResponseOutputItemAddedEvent
871 | ResponseOutputItemDoneEvent
872 | ResponseTextDeltaEvent
873 | ResponseOutputTextAnnotationAddedEvent
874 | ResponseFunctionCallArgumentsDeltaEvent
875 | ResponseReasoningSummaryPartAddedEvent
876 | ResponseReasoningSummaryPartDoneEvent
877 | ResponseReasoningSummaryTextDeltaEvent
878 | ResponseErrorEvent
879 | UnknownResponseStreamEvent
880
881/**
882 * Represents one embedding item returned by an OpenAI-compatible embeddings API.
883 *
884 * **Details**
885 *
886 * The embedding can be returned either as a numeric vector or as a base64-encoded
887 * string. The `index` field identifies the input item that produced this
888 * embedding.
889 *
890 * @stability unstable
891 * @category models
892 * @since 4.0.0
893 */
894export type Embedding = {
895 readonly embedding: ReadonlyArray<number> | string
896 readonly index: number
897 readonly object?: string | undefined
898}
899
900/**
901 * Request payload for the embeddings endpoint.
902 *
903 * @stability unstable
904 * @category models
905 * @since 4.0.0
906 */
907export type CreateEmbeddingRequest = {
908 readonly input: string | ReadonlyArray<string> | ReadonlyArray<number> | ReadonlyArray<ReadonlyArray<number>>
909 readonly model: string
910 readonly encoding_format?: "float" | "base64" | undefined
911 readonly dimensions?: number | undefined
912 readonly user?: string | undefined
913}
914
915/**
916 * Successful response payload returned by the embeddings endpoint.
917 *
918 * @stability unstable
919 * @category models
920 * @since 4.0.0
921 */
922export type CreateEmbeddingResponse = {
923 readonly data: ReadonlyArray<Embedding>
924 readonly model: string
925 readonly object?: "list" | undefined
926 readonly usage?: {
927 readonly prompt_tokens: number
928 readonly total_tokens: number
929 } | undefined
930}
931
932/**
933 * JSON request body accepted by the embeddings endpoint.
934 *
935 * @stability unstable
936 * @category models
937 * @since 4.0.0
938 */
939export type CreateEmbeddingRequestJson = CreateEmbeddingRequest
940/**
941 * Decoded successful embeddings response body.
942 *
943 * @stability unstable
944 * @category models
945 * @since 4.0.0
946 */
947export type CreateEmbedding200 = CreateEmbeddingResponse
948/**
949 * Structured content parts accepted in chat completion messages.
950 *
951 * @stability unstable
952 * @category models
953 * @since 4.0.0
954 */
955export type ChatCompletionContentPart =
956 | {
957 readonly type: "text"
958 readonly text: string
959 }
960 | {
961 readonly type: "image_url"
962 readonly image_url: {
963 readonly url: string
964 readonly detail?: "low" | "high" | "auto" | undefined
965 }
966 }
967/**
968 * Tool call data attached to an assistant chat completion message.
969 *
970 * @stability unstable
971 * @category models
972 * @since 4.0.0
973 */
974export type ChatCompletionRequestToolCall = {
975 readonly id: string
976 readonly type: "function"
977 readonly function: {
978 readonly name: string
979 readonly arguments: string
980 }
981}
982/**
983 * Message shapes accepted by the chat completions endpoint.
984 *
985 * @stability unstable
986 * @category models
987 * @since 4.0.0
988 */
989export type ChatCompletionRequestMessage =
990 | {
991 readonly role: "system" | "developer" | "user" | "assistant"
992 readonly content: string | ReadonlyArray<ChatCompletionContentPart> | null
993 readonly tool_calls?: ReadonlyArray<ChatCompletionRequestToolCall> | undefined
994 }
995 | {
996 readonly role: "tool"
997 readonly tool_call_id: string
998 readonly content: string
999 }
1000/**
1001 * Function tool definition accepted by the chat completions endpoint.
1002 *
1003 * @stability unstable
1004 * @category models
1005 * @since 4.0.0
1006 */
1007export type ChatCompletionTool = {
1008 readonly type: "function"
1009 readonly function: {
1010 readonly name: string
1011 readonly description?: string | null | undefined
1012 readonly parameters?: JsonObject | undefined
1013 readonly strict?: boolean | undefined
1014 }
1015}
1016/**
1017 * Controls whether the model may call tools and can force a specific function.
1018 *
1019 * @stability unstable
1020 * @category configuration
1021 * @since 4.0.0
1022 */
1023export type ChatCompletionToolChoice =
1024 | "none"
1025 | "auto"
1026 | "required"
1027 | {
1028 readonly type: "function"
1029 readonly function: {
1030 readonly name: string
1031 }
1032 }
1033/**
1034 * JSON response format configuration for chat completion requests.
1035 *
1036 * @stability unstable
1037 * @category configuration
1038 * @since 4.0.0
1039 */
1040export type ChatCompletionResponseFormat =
1041 | {
1042 readonly type: "json_object"
1043 }
1044 | {
1045 readonly type: "json_schema"
1046 readonly json_schema: {
1047 readonly name: string
1048 readonly schema: JsonObject
1049 readonly description?: string | undefined
1050 readonly strict?: boolean | undefined
1051 }
1052 }
1053/**
1054 * Request payload for the OpenAI-compatible chat completions endpoint.
1055 *
1056 * @stability unstable
1057 * @category models
1058 * @since 4.0.0
1059 */
1060export type ChatCompletionRequest = {
1061 readonly model: string
1062 readonly messages: ReadonlyArray<ChatCompletionRequestMessage>
1063 readonly temperature?: number | null | undefined
1064 readonly top_p?: number | null | undefined
1065 readonly max_tokens?: number | null | undefined
1066 readonly user?: string | null | undefined
1067 readonly seed?: number | undefined
1068 readonly parallel_tool_calls?: boolean | null | undefined
1069 readonly response_format?: ChatCompletionResponseFormat | undefined
1070 readonly tools?: ReadonlyArray<ChatCompletionTool> | undefined
1071 readonly tool_choice?: ChatCompletionToolChoice | undefined
1072 readonly service_tier?: string | undefined
1073 readonly reasoning?: unknown
1074 readonly stream?: boolean | undefined
1075 readonly stream_options?: {
1076 readonly include_usage?: boolean | undefined
1077 } | undefined
1078 readonly [x: string]: unknown
1079}
1080/**
1081 * JSON request body used by this client when creating a chat completion response.
1082 *
1083 * @stability unstable
1084 * @category models
1085 * @since 4.0.0
1086 */
1087export type CreateResponseRequestJson = ChatCompletionRequest
1088/**
1089 * Decoded successful chat completion response body returned by `createResponse`.
1090 *
1091 * @stability unstable
1092 * @category models
1093 * @since 4.0.0
1094 */
1095export type CreateResponse200 = ChatCompletionResponse
1096/**
1097 * Decoded server-sent event payload emitted by `createResponseStream`.
1098 *
1099 * @stability unstable
1100 * @category streaming
1101 * @since 4.0.0
1102 */
1103export type CreateResponse200Sse = ChatCompletionStreamEvent
1104
1105const EmbeddingSchema = Schema.Struct({
1106 embedding: Schema.Union([Schema.Array(Schema.Finite), Schema.String]),
1107 index: Schema.Int,
1108 object: Schema.optionalKey(Schema.String)
1109})
1110
1111const CreateEmbeddingResponseSchema = Schema.Struct({
1112 data: Schema.Array(EmbeddingSchema),
1113 model: Schema.String,
1114 object: Schema.optionalKey(Schema.Literal("list")),
1115 usage: Schema.optionalKey(Schema.Struct({
1116 prompt_tokens: Schema.Int,
1117 total_tokens: Schema.Int
1118 }))
1119})
1120
1121const ChatCompletionToolFunction = Schema.Struct({
1122 name: Schema.String,
1123 arguments: Schema.optionalKey(Schema.String)
1124})
1125
1126const ChatCompletionToolFunctionDelta = Schema.Struct({
1127 // Some OpenAI-compatible providers (e.g. Fireworks) send `name: null` on
1128 // streamed tool-call continuation fragments. `name` must be nullable, else
1129 // the whole chunk fails validation and its argument delta is dropped.
1130 name: Schema.optionalKey(Schema.NullOr(Schema.String)),
1131 arguments: Schema.optionalKey(Schema.String)
1132})
1133
1134const ChatCompletionToolCall = Schema.Struct({
1135 id: Schema.optionalKey(Schema.String),
1136 index: Schema.optionalKey(Schema.Int),
1137 type: Schema.optionalKey(Schema.String),
1138 function: Schema.optionalKey(ChatCompletionToolFunction)
1139})
1140
1141const ChatCompletionToolCallDelta = Schema.Struct({
1142 // Some providers send `id: null` on tool-call continuation fragments.
1143 id: Schema.optionalKey(Schema.NullOr(Schema.String)),
1144 index: Schema.optionalKey(Schema.Int),
1145 type: Schema.optionalKey(Schema.String),
1146 function: Schema.optionalKey(ChatCompletionToolFunctionDelta)
1147})
1148
1149const ChatCompletionMessage = Schema.Struct({
1150 role: Schema.optionalKey(Schema.String),
1151 content: Schema.optionalKey(Schema.NullOr(Schema.String)),
1152 reasoning: Schema.optionalKey(Schema.NullOr(Schema.String)),
1153 reasoning_content: Schema.optionalKey(Schema.NullOr(Schema.String)),
1154 tool_calls: Schema.optionalKey(Schema.Array(ChatCompletionToolCall))
1155})
1156
1157const ChatCompletionDelta = Schema.Struct({
1158 // Some providers send `role: null` on streamed text deltas.
1159 role: Schema.optionalKey(Schema.NullOr(Schema.String)),
1160 content: Schema.optionalKey(Schema.NullOr(Schema.String)),
1161 reasoning: Schema.optionalKey(Schema.NullOr(Schema.String)),
1162 reasoning_content: Schema.optionalKey(Schema.NullOr(Schema.String)),
1163 // Some OpenAI-compatible providers send `tool_calls: null` when a streamed
1164 // chunk contains only text. Accepting null keeps the text-bearing chunk from
1165 // being classified as an unknown event.
1166 tool_calls: Schema.optionalKey(Schema.NullOr(Schema.Array(ChatCompletionToolCallDelta)))
1167})
1168
1169const ChatCompletionChoice = Schema.Struct({
1170 index: Schema.Int,
1171 finish_reason: Schema.optionalKey(Schema.NullOr(Schema.String)),
1172 message: Schema.optionalKey(ChatCompletionMessage),
1173 delta: Schema.optionalKey(ChatCompletionDelta)
1174})
1175
1176const ChatCompletionUsage = Schema.Struct({
1177 prompt_tokens: Schema.Int,
1178 completion_tokens: Schema.Int,
1179 total_tokens: Schema.Int,
1180 prompt_tokens_details: Schema.optionalKey(Schema.Any),
1181 completion_tokens_details: Schema.optionalKey(Schema.Any)
1182})
1183
1184const ChatCompletionResponse = Schema.Struct({
1185 id: Schema.String,
1186 model: Schema.String,
1187 created: Schema.Int,
1188 choices: Schema.Array(ChatCompletionChoice),
1189 usage: Schema.optionalKey(Schema.NullOr(ChatCompletionUsage)),
1190 service_tier: Schema.optionalKey(Schema.String)
1191})
1192
1193const ChatCompletionChunk = Schema.Struct({
1194 id: Schema.String,
1195 model: Schema.String,
1196 created: Schema.Int,
1197 choices: Schema.Array(ChatCompletionChoice),
1198 usage: Schema.optionalKey(Schema.NullOr(ChatCompletionUsage)),
1199 service_tier: Schema.optionalKey(Schema.String)
1200})
1201
1202/**
1203 * Decoded tool-call object from a chat completion response or streaming chunk.
1204 *
1205 * @stability unstable
1206 * @category models
1207 * @since 4.0.0
1208 */
1209export type ChatCompletionToolCall = typeof ChatCompletionToolCall.Type
1210/**
1211 * Decoded message object from a non-streaming chat completion choice.
1212 *
1213 * @stability unstable
1214 * @category models
1215 * @since 4.0.0
1216 */
1217export type ChatCompletionMessage = typeof ChatCompletionMessage.Type
1218/**
1219 * Decoded choice object returned by chat completion responses and chunks.
1220 *
1221 * @stability unstable
1222 * @category models
1223 * @since 4.0.0
1224 */
1225export type ChatCompletionChoice = typeof ChatCompletionChoice.Type
1226/**
1227 * Decoded token usage summary returned by chat completions.
1228 *
1229 * @stability unstable
1230 * @category models
1231 * @since 4.0.0
1232 */
1233export type ChatCompletionUsage = typeof ChatCompletionUsage.Type
1234/**
1235 * Decoded successful response from the chat completions endpoint.
1236 *
1237 * @stability unstable
1238 * @category models
1239 * @since 4.0.0
1240 */
1241export type ChatCompletionResponse = typeof ChatCompletionResponse.Type
1242/**
1243 * Decoded streaming chunk emitted by the chat completions endpoint.
1244 *
1245 * @stability unstable
1246 * @category streaming
1247 * @since 4.0.0
1248 */
1249export type ChatCompletionChunk = typeof ChatCompletionChunk.Type
1250/**
1251 * A parsed chat completion event that does not match the expected chunk schema.
1252 *
1253 * @stability unstable
1254 * @category streaming
1255 * @since 4.0.0
1256 */
1257export interface UnknownChatCompletionEvent {
1258 readonly _tag: "UnknownChatCompletionEvent"
1259 readonly data: unknown
1260}
1261/**
1262 * Streaming chat completion event, including decoded chunks, unknown parsed
1263 * events, and the `[DONE]` sentinel.
1264 *
1265 * @stability unstable
1266 * @category streaming
1267 * @since 4.0.0
1268 */
1269export type ChatCompletionStreamEvent = ChatCompletionChunk | UnknownChatCompletionEvent | "[DONE]"
1270
1271const parseJson = (value: string): unknown => {
1272 try {
1273 return JSON.parse(value)
1274 } catch {
1275 return undefined
1276 }
1277}
1278
1279const isChatCompletionChunk = Schema.is(ChatCompletionChunk)
1280
1281const decodeChatCompletionSseData = (
1282 data: string
1283): ChatCompletionStreamEvent | undefined => {
1284 if (data === "[DONE]") {
1285 return data
1286 }
1287 const parsed = parseJson(data)
1288 if (parsed === undefined) {
1289 return undefined
1290 }
1291 return isChatCompletionChunk(parsed) ? parsed : {
1292 _tag: "UnknownChatCompletionEvent",
1293 data: parsed
1294 }
1295}