@opencode-ai/ai 0.0.0-beta-17492

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.
Files changed (194) hide show
  1. package/README.md +392 -0
  2. package/dist/cache-policy.d.ts +2 -0
  3. package/dist/cache-policy.js +121 -0
  4. package/dist/image-client.d.ts +19 -0
  5. package/dist/image-client.js +19 -0
  6. package/dist/image.d.ts +110 -0
  7. package/dist/image.js +102 -0
  8. package/dist/index.d.ts +18 -0
  9. package/dist/index.js +12 -0
  10. package/dist/llm.d.ts +230 -0
  11. package/dist/llm.js +77 -0
  12. package/dist/protocols/anthropic-messages.d.ts +633 -0
  13. package/dist/protocols/anthropic-messages.js +860 -0
  14. package/dist/protocols/bedrock-converse.d.ts +533 -0
  15. package/dist/protocols/bedrock-converse.js +584 -0
  16. package/dist/protocols/bedrock-event-stream.d.ts +9 -0
  17. package/dist/protocols/bedrock-event-stream.js +73 -0
  18. package/dist/protocols/gemini.d.ts +300 -0
  19. package/dist/protocols/gemini.js +512 -0
  20. package/dist/protocols/google-images.d.ts +30 -0
  21. package/dist/protocols/google-images.js +191 -0
  22. package/dist/protocols/index.d.ts +10 -0
  23. package/dist/protocols/index.js +10 -0
  24. package/dist/protocols/open-responses-channel.d.ts +26 -0
  25. package/dist/protocols/open-responses-channel.js +120 -0
  26. package/dist/protocols/open-responses.d.ts +951 -0
  27. package/dist/protocols/open-responses.js +873 -0
  28. package/dist/protocols/openai-chat.d.ts +795 -0
  29. package/dist/protocols/openai-chat.js +718 -0
  30. package/dist/protocols/openai-compatible-chat.d.ts +107 -0
  31. package/dist/protocols/openai-compatible-chat.js +20 -0
  32. package/dist/protocols/openai-compatible-responses.d.ts +96 -0
  33. package/dist/protocols/openai-compatible-responses.js +17 -0
  34. package/dist/protocols/openai-images.d.ts +32 -0
  35. package/dist/protocols/openai-images.js +188 -0
  36. package/dist/protocols/openai-responses-channel.d.ts +12 -0
  37. package/dist/protocols/openai-responses-channel.js +142 -0
  38. package/dist/protocols/openai-responses.d.ts +596 -0
  39. package/dist/protocols/openai-responses.js +208 -0
  40. package/dist/protocols/shared.d.ts +194 -0
  41. package/dist/protocols/shared.js +238 -0
  42. package/dist/protocols/utils/bedrock-auth.d.ts +21 -0
  43. package/dist/protocols/utils/bedrock-auth.js +43 -0
  44. package/dist/protocols/utils/bedrock-cache.d.ts +15 -0
  45. package/dist/protocols/utils/bedrock-cache.js +29 -0
  46. package/dist/protocols/utils/bedrock-media.d.ts +49 -0
  47. package/dist/protocols/utils/bedrock-media.js +70 -0
  48. package/dist/protocols/utils/cache.d.ts +6 -0
  49. package/dist/protocols/utils/cache.js +8 -0
  50. package/dist/protocols/utils/gemini-tool-schema.d.ts +2 -0
  51. package/dist/protocols/utils/gemini-tool-schema.js +103 -0
  52. package/dist/protocols/utils/image-input.d.ts +21 -0
  53. package/dist/protocols/utils/image-input.js +22 -0
  54. package/dist/protocols/utils/lifecycle.d.ts +20 -0
  55. package/dist/protocols/utils/lifecycle.js +68 -0
  56. package/dist/protocols/utils/open-responses-options.d.ts +21 -0
  57. package/dist/protocols/utils/open-responses-options.js +41 -0
  58. package/dist/protocols/utils/openai-image.d.ts +5 -0
  59. package/dist/protocols/utils/openai-image.js +18 -0
  60. package/dist/protocols/utils/openai-options.d.ts +14 -0
  61. package/dist/protocols/utils/openai-options.js +14 -0
  62. package/dist/protocols/utils/tool-schema.d.ts +8 -0
  63. package/dist/protocols/utils/tool-schema.js +81 -0
  64. package/dist/protocols/utils/tool-stream.d.ts +639 -0
  65. package/dist/protocols/utils/tool-stream.js +141 -0
  66. package/dist/protocols/xai-images.d.ts +26 -0
  67. package/dist/protocols/xai-images.js +111 -0
  68. package/dist/protocols/zai-images.d.ts +22 -0
  69. package/dist/protocols/zai-images.js +84 -0
  70. package/dist/protocols.d.ts +1 -0
  71. package/dist/protocols.js +1 -0
  72. package/dist/provider-error.d.ts +14 -0
  73. package/dist/provider-error.js +130 -0
  74. package/dist/provider-package.d.ts +15 -0
  75. package/dist/provider-package.js +1 -0
  76. package/dist/provider.d.ts +23 -0
  77. package/dist/provider.js +2 -0
  78. package/dist/providers/amazon-bedrock/mantle/chat.d.ts +2 -0
  79. package/dist/providers/amazon-bedrock/mantle/chat.js +1 -0
  80. package/dist/providers/amazon-bedrock/mantle/responses.d.ts +2 -0
  81. package/dist/providers/amazon-bedrock/mantle/responses.js +1 -0
  82. package/dist/providers/amazon-bedrock/mantle.d.ts +2 -0
  83. package/dist/providers/amazon-bedrock/mantle.js +1 -0
  84. package/dist/providers/amazon-bedrock-mantle.d.ts +247 -0
  85. package/dist/providers/amazon-bedrock-mantle.js +68 -0
  86. package/dist/providers/amazon-bedrock.d.ts +172 -0
  87. package/dist/providers/amazon-bedrock.js +41 -0
  88. package/dist/providers/anthropic-compatible.d.ts +218 -0
  89. package/dist/providers/anthropic-compatible.js +45 -0
  90. package/dist/providers/anthropic.d.ts +216 -0
  91. package/dist/providers/anthropic.js +40 -0
  92. package/dist/providers/azure/chat.d.ts +2 -0
  93. package/dist/providers/azure/chat.js +1 -0
  94. package/dist/providers/azure/responses.d.ts +2 -0
  95. package/dist/providers/azure/responses.js +1 -0
  96. package/dist/providers/azure.d.ts +248 -0
  97. package/dist/providers/azure.js +90 -0
  98. package/dist/providers/cloudflare.d.ts +338 -0
  99. package/dist/providers/cloudflare.js +87 -0
  100. package/dist/providers/google-vertex/chat.d.ts +2 -0
  101. package/dist/providers/google-vertex/chat.js +1 -0
  102. package/dist/providers/google-vertex/gemini.d.ts +2 -0
  103. package/dist/providers/google-vertex/gemini.js +1 -0
  104. package/dist/providers/google-vertex/messages.d.ts +2 -0
  105. package/dist/providers/google-vertex/messages.js +1 -0
  106. package/dist/providers/google-vertex/responses.d.ts +2 -0
  107. package/dist/providers/google-vertex/responses.js +1 -0
  108. package/dist/providers/google-vertex-chat.d.ts +131 -0
  109. package/dist/providers/google-vertex-chat.js +50 -0
  110. package/dist/providers/google-vertex-messages.d.ts +215 -0
  111. package/dist/providers/google-vertex-messages.js +75 -0
  112. package/dist/providers/google-vertex-responses.d.ts +122 -0
  113. package/dist/providers/google-vertex-responses.js +51 -0
  114. package/dist/providers/google-vertex-shared.d.ts +22 -0
  115. package/dist/providers/google-vertex-shared.js +61 -0
  116. package/dist/providers/google-vertex.d.ts +115 -0
  117. package/dist/providers/google-vertex.js +64 -0
  118. package/dist/providers/google.d.ts +112 -0
  119. package/dist/providers/google.js +43 -0
  120. package/dist/providers/index.d.ts +18 -0
  121. package/dist/providers/index.js +18 -0
  122. package/dist/providers/open-responses-options.d.ts +16 -0
  123. package/dist/providers/open-responses-options.js +1 -0
  124. package/dist/providers/openai/chat.d.ts +2 -0
  125. package/dist/providers/openai/chat.js +1 -0
  126. package/dist/providers/openai/responses.d.ts +2 -0
  127. package/dist/providers/openai/responses.js +1 -0
  128. package/dist/providers/openai-compatible/responses.d.ts +1 -0
  129. package/dist/providers/openai-compatible/responses.js +1 -0
  130. package/dist/providers/openai-compatible-profile.d.ts +43 -0
  131. package/dist/providers/openai-compatible-profile.js +12 -0
  132. package/dist/providers/openai-compatible-responses.d.ts +120 -0
  133. package/dist/providers/openai-compatible-responses.js +33 -0
  134. package/dist/providers/openai-compatible.d.ts +167 -0
  135. package/dist/providers/openai-compatible.js +55 -0
  136. package/dist/providers/openai-options.d.ts +21 -0
  137. package/dist/providers/openai-options.js +41 -0
  138. package/dist/providers/openai.d.ts +264 -0
  139. package/dist/providers/openai.js +83 -0
  140. package/dist/providers/openrouter.d.ts +537 -0
  141. package/dist/providers/openrouter.js +117 -0
  142. package/dist/providers/xai.d.ts +249 -0
  143. package/dist/providers/xai.js +79 -0
  144. package/dist/providers/zai.d.ts +24 -0
  145. package/dist/providers/zai.js +21 -0
  146. package/dist/providers.d.ts +1 -0
  147. package/dist/providers.js +1 -0
  148. package/dist/route/auth-options.d.ts +34 -0
  149. package/dist/route/auth-options.js +14 -0
  150. package/dist/route/auth.d.ts +51 -0
  151. package/dist/route/auth.js +89 -0
  152. package/dist/route/client.d.ts +348 -0
  153. package/dist/route/client.js +214 -0
  154. package/dist/route/endpoint.d.ts +28 -0
  155. package/dist/route/endpoint.js +21 -0
  156. package/dist/route/executor.d.ts +23 -0
  157. package/dist/route/executor.js +227 -0
  158. package/dist/route/framing.d.ts +23 -0
  159. package/dist/route/framing.js +4 -0
  160. package/dist/route/index.d.ts +16 -0
  161. package/dist/route/index.js +9 -0
  162. package/dist/route/protocol.d.ts +77 -0
  163. package/dist/route/protocol.js +17 -0
  164. package/dist/route/transport/http.d.ts +34 -0
  165. package/dist/route/transport/http.js +63 -0
  166. package/dist/route/transport/index.d.ts +39 -0
  167. package/dist/route/transport/index.js +4 -0
  168. package/dist/route/transport/websocket-channel.d.ts +56 -0
  169. package/dist/route/transport/websocket-channel.js +1 -0
  170. package/dist/route/transport/websocket.d.ts +55 -0
  171. package/dist/route/transport/websocket.js +326 -0
  172. package/dist/route.d.ts +1 -0
  173. package/dist/route.js +1 -0
  174. package/dist/schema/errors.d.ts +159 -0
  175. package/dist/schema/errors.js +155 -0
  176. package/dist/schema/events.d.ts +4587 -0
  177. package/dist/schema/events.js +492 -0
  178. package/dist/schema/ids.d.ts +30 -0
  179. package/dist/schema/ids.js +19 -0
  180. package/dist/schema/index.d.ts +5 -0
  181. package/dist/schema/index.js +5 -0
  182. package/dist/schema/messages.d.ts +402 -0
  183. package/dist/schema/messages.js +244 -0
  184. package/dist/schema/options.d.ts +159 -0
  185. package/dist/schema/options.js +202 -0
  186. package/dist/testing.d.ts +1493 -0
  187. package/dist/testing.js +88 -0
  188. package/dist/tool-runtime.d.ts +15 -0
  189. package/dist/tool-runtime.js +54 -0
  190. package/dist/tool.d.ts +135 -0
  191. package/dist/tool.js +66 -0
  192. package/dist/utils/record.d.ts +2 -0
  193. package/dist/utils/record.js +2 -0
  194. package/package.json +47 -0
@@ -0,0 +1,718 @@
1
+ import { Effect, Schema } from "effect";
2
+ import { Tool } from "@opencode-ai/schema/tool";
3
+ import { Route } from "../route/client.js";
4
+ import { Auth } from "../route/auth.js";
5
+ import { Endpoint } from "../route/endpoint.js";
6
+ import { HttpTransport } from "../route/transport/index.js";
7
+ import { Protocol } from "../route/protocol.js";
8
+ import { AIError, LLMEvent, Usage, } from "../schema/index.js";
9
+ import { classifyProviderFailure } from "../provider-error.js";
10
+ import { isRecord, JsonObject, optionalArray, optionalNull, ProviderShared } from "./shared.js";
11
+ import { OpenAIOptions } from "./utils/openai-options.js";
12
+ import { Lifecycle } from "./utils/lifecycle.js";
13
+ import { ToolSchemaProjection } from "./utils/tool-schema.js";
14
+ import { ToolStream } from "./utils/tool-stream.js";
15
+ const ADAPTER = "openai-chat";
16
+ const IMAGE_MIMES = new Set(ProviderShared.IMAGE_MIMES);
17
+ const RESERVED_REASONING_FIELDS = new Set(["role", "content", "tool_calls"]);
18
+ export const DEFAULT_BASE_URL = "https://api.openai.com/v1";
19
+ export const PATH = "/chat/completions";
20
+ // =============================================================================
21
+ // Request Body Schema
22
+ // =============================================================================
23
+ // The body schema is the provider-native JSON body. `fromRequest` below builds
24
+ // this shape from the common `LLMRequest`, then `Route.make` validates and
25
+ // JSON-encodes it before transport.
26
+ const OpenAIChatCacheControl = Schema.Struct({
27
+ type: Schema.Literal("ephemeral"),
28
+ ttl: Schema.optional(Schema.String),
29
+ });
30
+ const OpenAIChatFunction = Schema.Struct({
31
+ name: Schema.String,
32
+ description: Schema.String,
33
+ parameters: JsonObject,
34
+ });
35
+ const OpenAIChatTool = Schema.Struct({
36
+ type: Schema.tag("function"),
37
+ function: OpenAIChatFunction,
38
+ cache_control: Schema.optional(OpenAIChatCacheControl),
39
+ });
40
+ const OpenAIChatAssistantToolCall = Schema.Struct({
41
+ id: Schema.String,
42
+ type: Schema.tag("function"),
43
+ function: Schema.Struct({
44
+ name: Schema.String,
45
+ arguments: Schema.String,
46
+ }),
47
+ });
48
+ // Intentionally omit Gemini's provider-specific `extra_content.google.thought_signature`
49
+ // extension until direct Google OpenAI-compatible routing is supported here:
50
+ // https://github.com/vercel/ai/issues/11590
51
+ // https://github.com/vercel/ai/pull/11745
52
+ // https://ai.google.dev/gemini-api/docs/thought-signatures#openai
53
+ const OpenAIChatUserContent = Schema.Union([
54
+ Schema.Struct({
55
+ type: Schema.Literal("text"),
56
+ text: Schema.String,
57
+ cache_control: Schema.optional(OpenAIChatCacheControl),
58
+ }),
59
+ Schema.Struct({
60
+ type: Schema.Literal("image_url"),
61
+ image_url: Schema.Struct({ url: Schema.String }),
62
+ }),
63
+ ]);
64
+ const OpenAIChatMessage = Schema.Union([
65
+ Schema.Struct({
66
+ role: Schema.Literal("system"),
67
+ content: Schema.Union([Schema.String, Schema.Array(OpenAIChatUserContent)]),
68
+ }),
69
+ Schema.Struct({
70
+ role: Schema.Literal("user"),
71
+ content: Schema.Union([Schema.String, Schema.Array(OpenAIChatUserContent)]),
72
+ }),
73
+ Schema.StructWithRest(Schema.Struct({
74
+ role: Schema.Literal("assistant"),
75
+ content: Schema.NullOr(Schema.String),
76
+ tool_calls: optionalArray(OpenAIChatAssistantToolCall),
77
+ reasoning_content: Schema.optional(Schema.String),
78
+ reasoning: Schema.optional(Schema.String),
79
+ reasoning_text: Schema.optional(Schema.String),
80
+ reasoning_details: Schema.optional(Schema.Unknown),
81
+ cache_control: Schema.optional(OpenAIChatCacheControl),
82
+ }), [Schema.Record(Schema.String, Schema.Unknown)]),
83
+ Schema.Struct({
84
+ role: Schema.Literal("tool"),
85
+ tool_call_id: Schema.String,
86
+ content: Schema.String,
87
+ cache_control: Schema.optional(OpenAIChatCacheControl),
88
+ }),
89
+ ]).pipe(Schema.toTaggedUnion("role"));
90
+ const OpenAIChatToolChoice = Schema.Union([
91
+ Schema.Literals(["auto", "none", "required"]),
92
+ Schema.Struct({
93
+ type: Schema.tag("function"),
94
+ function: Schema.Struct({ name: Schema.String }),
95
+ }),
96
+ ]);
97
+ export const bodyFields = {
98
+ model: Schema.String,
99
+ messages: Schema.Array(OpenAIChatMessage),
100
+ tools: optionalArray(OpenAIChatTool),
101
+ tool_choice: Schema.optional(OpenAIChatToolChoice),
102
+ stream: Schema.Literal(true),
103
+ stream_options: Schema.optional(Schema.Struct({ include_usage: Schema.Boolean })),
104
+ store: Schema.optional(Schema.Boolean),
105
+ prompt_cache_key: Schema.optional(Schema.String),
106
+ reasoning_effort: Schema.optional(OpenAIOptions.OpenAIReasoningEffort),
107
+ max_completion_tokens: Schema.optional(Schema.Number),
108
+ max_tokens: Schema.optional(Schema.Number),
109
+ temperature: Schema.optional(Schema.Number),
110
+ top_p: Schema.optional(Schema.Number),
111
+ frequency_penalty: Schema.optional(Schema.Number),
112
+ presence_penalty: Schema.optional(Schema.Number),
113
+ seed: Schema.optional(Schema.Number),
114
+ stop: optionalArray(Schema.String),
115
+ };
116
+ const OpenAIChatBody = Schema.Struct(bodyFields);
117
+ // =============================================================================
118
+ // Streaming Event Schema
119
+ // =============================================================================
120
+ // The event schema is one decoded SSE `data:` payload. `Framing.sse` splits the
121
+ // byte stream into strings, then `Protocol.jsonEvent` decodes each string into
122
+ // this provider-native event shape.
123
+ const OpenAIChatUsage = Schema.StructWithRest(Schema.Struct({
124
+ prompt_tokens: optionalNull(Schema.Number),
125
+ completion_tokens: optionalNull(Schema.Number),
126
+ total_tokens: optionalNull(Schema.Number),
127
+ prompt_tokens_details: optionalNull(Schema.StructWithRest(Schema.Struct({
128
+ cached_tokens: optionalNull(Schema.Number),
129
+ cache_write_tokens: optionalNull(Schema.Number),
130
+ }), [Schema.Record(Schema.String, Schema.Unknown)])),
131
+ completion_tokens_details: optionalNull(Schema.StructWithRest(Schema.Struct({
132
+ reasoning_tokens: optionalNull(Schema.Number),
133
+ accepted_prediction_tokens: optionalNull(Schema.Number),
134
+ rejected_prediction_tokens: optionalNull(Schema.Number),
135
+ }), [Schema.Record(Schema.String, Schema.Unknown)])),
136
+ }), [Schema.Record(Schema.String, Schema.Unknown)]);
137
+ const OpenAIChatToolCallDeltaFunction = Schema.Struct({
138
+ name: optionalNull(Schema.String),
139
+ arguments: optionalNull(Schema.String),
140
+ });
141
+ const OpenAIChatToolCallDelta = Schema.Struct({
142
+ index: optionalNull(Schema.Number),
143
+ id: optionalNull(Schema.String),
144
+ function: optionalNull(OpenAIChatToolCallDeltaFunction),
145
+ });
146
+ const OpenAIChatDelta = Schema.StructWithRest(Schema.Struct({
147
+ content: optionalNull(Schema.String),
148
+ reasoning_content: optionalNull(Schema.String),
149
+ reasoning: optionalNull(Schema.String),
150
+ reasoning_text: optionalNull(Schema.String),
151
+ reasoning_details: optionalNull(Schema.Unknown),
152
+ tool_calls: optionalNull(Schema.Array(OpenAIChatToolCallDelta)),
153
+ }), [Schema.Record(Schema.String, Schema.Unknown)]);
154
+ const OpenAIChatChoice = Schema.Struct({
155
+ delta: optionalNull(OpenAIChatDelta),
156
+ finish_reason: optionalNull(Schema.String),
157
+ native_finish_reason: optionalNull(Schema.String),
158
+ });
159
+ const OpenAIChatError = Schema.Struct({
160
+ code: optionalNull(Schema.Union([Schema.String, Schema.Number])),
161
+ message: Schema.String,
162
+ });
163
+ export const OpenAIChatEvent = Schema.Struct({
164
+ choices: optionalNull(Schema.Array(OpenAIChatChoice)),
165
+ usage: optionalNull(OpenAIChatUsage),
166
+ error: optionalNull(OpenAIChatError),
167
+ });
168
+ const lowerTool = (tool, inputSchema, options) => ({
169
+ type: "function",
170
+ function: {
171
+ name: tool.name,
172
+ description: tool.description,
173
+ parameters: ToolSchemaProjection.openAI(inputSchema),
174
+ },
175
+ cache_control: options.cacheControl?.(tool.cache),
176
+ });
177
+ const lowerToolChoice = (toolChoice) => ProviderShared.matchToolChoice("OpenAI Chat", toolChoice, {
178
+ auto: () => "auto",
179
+ none: () => "none",
180
+ required: () => "required",
181
+ tool: (name) => ({ type: "function", function: { name } }),
182
+ });
183
+ const lowerToolCall = (part) => ({
184
+ id: part.id,
185
+ type: "function",
186
+ function: {
187
+ name: part.name,
188
+ arguments: ProviderShared.encodeJson(part.input),
189
+ },
190
+ });
191
+ const lowerMedia = Effect.fn("OpenAIChat.lowerMedia")(function* (part) {
192
+ const media = yield* ProviderShared.validateMedia("OpenAI Chat", part, IMAGE_MIMES);
193
+ return { type: "image_url", image_url: { url: media.dataUrl } };
194
+ });
195
+ const openAICompatibleReasoningContent = (native) => isRecord(native) && typeof native.reasoning_content === "string" ? native.reasoning_content : undefined;
196
+ const reasoningField = (part) => {
197
+ const field = part.providerMetadata?.openai?.reasoningField;
198
+ return typeof field === "string" ? field : undefined;
199
+ };
200
+ const reasoningDetails = (parts, native) => {
201
+ const observed = parts.flatMap((part) => {
202
+ const details = part.providerMetadata?.openai?.reasoningDetails;
203
+ return Array.isArray(details) ? details : [];
204
+ });
205
+ if (parts.some((part) => Array.isArray(part.providerMetadata?.openai?.reasoningDetails)))
206
+ return observed;
207
+ if (isRecord(native) && Array.isArray(native.reasoning_details))
208
+ return native.reasoning_details;
209
+ };
210
+ const lowerUserMessage = Effect.fn("OpenAIChat.lowerUserMessage")(function* (message, options) {
211
+ const content = [];
212
+ for (const part of message.content) {
213
+ if (part.type === "text") {
214
+ content.push({ type: "text", text: part.text, cache_control: options.cacheControl?.(part.cache) });
215
+ continue;
216
+ }
217
+ if (part.type === "media") {
218
+ content.push(yield* lowerMedia(part));
219
+ continue;
220
+ }
221
+ return yield* ProviderShared.unsupportedContent("OpenAI Chat", "user", ["text", "media"]);
222
+ }
223
+ if (content.every((part) => part.type === "text" && part.cache_control === undefined))
224
+ return {
225
+ role: "user",
226
+ content: content.map((part) => (part.type === "text" ? part.text : "")).join(""),
227
+ };
228
+ return { role: "user", content };
229
+ });
230
+ const lowerAssistantMessage = Effect.fn("OpenAIChat.lowerAssistantMessage")(function* (message, configuredField, options = {}) {
231
+ const content = [];
232
+ const reasoning = [];
233
+ const toolCalls = [];
234
+ for (const part of message.content) {
235
+ if (!ProviderShared.supportsContent(part, ["text", "reasoning", "tool-call"]))
236
+ return yield* ProviderShared.unsupportedContent("OpenAI Chat", "assistant", ["text", "reasoning", "tool-call"]);
237
+ if (part.type === "text") {
238
+ content.push(part);
239
+ continue;
240
+ }
241
+ if (part.type === "reasoning") {
242
+ reasoning.push(part);
243
+ continue;
244
+ }
245
+ if (part.type === "tool-call") {
246
+ toolCalls.push(lowerToolCall(part));
247
+ continue;
248
+ }
249
+ }
250
+ const text = reasoning.map((part) => part.text).join("");
251
+ const details = reasoningDetails(reasoning, message.native?.openaiCompatible);
252
+ const observedField = reasoning.map(reasoningField).find((value) => value !== undefined);
253
+ const nativeReasoning = openAICompatibleReasoningContent(message.native?.openaiCompatible);
254
+ const fullyStructured = reasoning.every((part) => Array.isArray(part.providerMetadata?.openai?.reasoningDetails));
255
+ const field = (() => {
256
+ if (configuredField !== undefined)
257
+ return configuredField;
258
+ if (reasoning.length === 0)
259
+ return undefined;
260
+ if (observedField !== undefined)
261
+ return observedField;
262
+ if (nativeReasoning !== undefined)
263
+ return "reasoning_content";
264
+ if (!fullyStructured)
265
+ return "reasoning_content";
266
+ })();
267
+ const reasoningText = (() => {
268
+ if (configuredField !== undefined)
269
+ return reasoning.length === 0 ? (nativeReasoning ?? "") : text;
270
+ if (reasoning.length === 0)
271
+ return nativeReasoning;
272
+ return text;
273
+ })();
274
+ const cached = message.content.findLast((part) => "cache" in part && part.cache !== undefined);
275
+ const cacheControl = options.cacheControl?.(cached && "cache" in cached ? cached.cache : undefined);
276
+ const result = {
277
+ role: "assistant",
278
+ content: content.length > 0 ? content.map((part) => part.text).join("") : toolCalls.length > 0 ? null : "",
279
+ ...(toolCalls.length > 0 ? { tool_calls: toolCalls } : {}),
280
+ ...(details !== undefined ? { reasoning_details: details } : {}),
281
+ ...(cacheControl !== undefined ? { cache_control: cacheControl } : {}),
282
+ };
283
+ if (field === undefined || reasoningText === undefined)
284
+ return result;
285
+ return { ...result, [field]: reasoningText };
286
+ });
287
+ const lowerToolMessages = Effect.fn("OpenAIChat.lowerToolMessages")(function* (message, options) {
288
+ const messages = [];
289
+ const images = [];
290
+ for (const part of message.content) {
291
+ if (!ProviderShared.supportsContent(part, ["tool-result"]))
292
+ return yield* ProviderShared.unsupportedContent("OpenAI Chat", "tool", ["tool-result"]);
293
+ if (part.result.type !== "content") {
294
+ messages.push({
295
+ role: "tool",
296
+ tool_call_id: part.id,
297
+ content: ProviderShared.toolResultText(part),
298
+ cache_control: options.cacheControl?.(part.cache),
299
+ });
300
+ continue;
301
+ }
302
+ const content = part.result.value;
303
+ const text = content.filter((item) => item.type === "text").map((item) => item.text);
304
+ messages.push({
305
+ role: "tool",
306
+ tool_call_id: part.id,
307
+ content: text.join("\n"),
308
+ cache_control: options.cacheControl?.(part.cache),
309
+ });
310
+ const files = content.filter((item) => item.type === "file");
311
+ images.push(...(yield* Effect.forEach(files, (item) => lowerMedia({ type: "media", mediaType: item.mime, data: item.uri, filename: item.name }))));
312
+ }
313
+ return { messages, images };
314
+ });
315
+ const lowerMessage = Effect.fn("OpenAIChat.lowerMessage")(function* (message, reasoningField, options = {}) {
316
+ if (message.role === "user")
317
+ return [yield* lowerUserMessage(message, options)];
318
+ if (message.role === "assistant")
319
+ return [yield* lowerAssistantMessage(message, reasoningField, options)];
320
+ return (yield* lowerToolMessages(message, options)).messages;
321
+ });
322
+ const lowerMessages = Effect.fn("OpenAIChat.lowerMessages")(function* (request, options) {
323
+ const system = request.system.length === 0
324
+ ? []
325
+ : request.system.some((part) => part.cache !== undefined) && options.cacheControl !== undefined
326
+ ? [
327
+ {
328
+ role: "system",
329
+ content: request.system.map((part) => ({
330
+ type: "text",
331
+ text: part.text,
332
+ cache_control: options.cacheControl?.(part.cache),
333
+ })),
334
+ },
335
+ ]
336
+ : [{ role: "system", content: ProviderShared.joinText(request.system) }];
337
+ const messages = [...system];
338
+ const pendingImages = [];
339
+ const flushImages = () => {
340
+ if (pendingImages.length === 0)
341
+ return;
342
+ messages.push({ role: "user", content: pendingImages.splice(0) });
343
+ };
344
+ for (const message of request.messages) {
345
+ if (message.role === "system") {
346
+ const part = yield* ProviderShared.wrappedSystemUpdate("OpenAI Chat", message);
347
+ if (pendingImages.length > 0) {
348
+ messages.push({
349
+ role: "user",
350
+ content: [
351
+ ...pendingImages.splice(0),
352
+ { type: "text", text: part.text, cache_control: options.cacheControl?.(part.cache) },
353
+ ],
354
+ });
355
+ continue;
356
+ }
357
+ const previous = messages.at(-1);
358
+ if (previous?.role === "user" && typeof previous.content === "string")
359
+ messages[messages.length - 1] = options.cacheControl?.(part.cache)
360
+ ? {
361
+ role: "user",
362
+ content: [
363
+ { type: "text", text: previous.content },
364
+ { type: "text", text: part.text, cache_control: options.cacheControl(part.cache) },
365
+ ],
366
+ }
367
+ : { role: "user", content: `${previous.content}\n${part.text}` };
368
+ else if (previous?.role === "user" && Array.isArray(previous.content))
369
+ messages[messages.length - 1] = {
370
+ role: "user",
371
+ content: [
372
+ ...previous.content,
373
+ { type: "text", text: part.text, cache_control: options.cacheControl?.(part.cache) },
374
+ ],
375
+ };
376
+ else
377
+ messages.push(options.cacheControl?.(part.cache)
378
+ ? {
379
+ role: "user",
380
+ content: [{ type: "text", text: part.text, cache_control: options.cacheControl(part.cache) }],
381
+ }
382
+ : { role: "user", content: part.text });
383
+ continue;
384
+ }
385
+ if (message.role === "tool") {
386
+ const lowered = yield* lowerToolMessages(message, options);
387
+ messages.push(...lowered.messages);
388
+ pendingImages.push(...lowered.images);
389
+ continue;
390
+ }
391
+ flushImages();
392
+ messages.push(...(yield* lowerMessage(message, request.model.compatibility?.reasoningField, options)));
393
+ }
394
+ flushImages();
395
+ return messages;
396
+ });
397
+ const lowerOptions = (request) => {
398
+ const options = OpenAIOptions.resolve(request);
399
+ return {
400
+ ...(options.store !== undefined ? { store: options.store } : {}),
401
+ ...(request.promptCacheKey ? { prompt_cache_key: request.promptCacheKey } : {}),
402
+ ...(options.reasoningEffort ? { reasoning_effort: options.reasoningEffort } : {}),
403
+ };
404
+ };
405
+ export const fromRequest = Effect.fn("OpenAIChat.fromRequest")(function* (request, options = {}) {
406
+ // `fromRequest` returns the provider body only. Endpoint, auth, framing,
407
+ // validation, and HTTP execution are composed by `Route.make`.
408
+ const reasoningField = request.model.compatibility?.reasoningField;
409
+ if (reasoningField && RESERVED_REASONING_FIELDS.has(reasoningField))
410
+ return yield* ProviderShared.invalidRequest(`OpenAI Chat reasoning field conflicts with reserved field ${reasoningField}`);
411
+ const generation = request.generation;
412
+ const toolSchemaCompatibility = request.model.compatibility?.toolSchema;
413
+ const maxTokensField = request.model.compatibility?.maxTokensField ?? "max_tokens";
414
+ return {
415
+ model: request.model.id,
416
+ messages: yield* lowerMessages(request, options),
417
+ tools: request.tools.length === 0
418
+ ? undefined
419
+ : request.tools.map((tool) => lowerTool(tool, ToolSchemaProjection.modelCompatibility(tool.inputSchema, toolSchemaCompatibility), options)),
420
+ tool_choice: request.toolChoice ? yield* lowerToolChoice(request.toolChoice) : undefined,
421
+ stream: true,
422
+ stream_options: { include_usage: true },
423
+ ...(maxTokensField === "max_completion_tokens"
424
+ ? { max_completion_tokens: generation?.maxTokens }
425
+ : { max_tokens: generation?.maxTokens }),
426
+ temperature: generation?.temperature,
427
+ top_p: generation?.topP,
428
+ frequency_penalty: generation?.frequencyPenalty,
429
+ presence_penalty: generation?.presencePenalty,
430
+ seed: generation?.seed,
431
+ stop: generation?.stop,
432
+ ...lowerOptions(request),
433
+ };
434
+ });
435
+ // =============================================================================
436
+ // Stream Parsing
437
+ // =============================================================================
438
+ // Streaming parsers are small state machines: every event returns a new state
439
+ // plus the common `LLMEvent`s produced by that event. Tool calls are accumulated
440
+ // because OpenAI streams JSON arguments across multiple deltas.
441
+ const mapFinishReason = (reason) => {
442
+ if (reason === "stop")
443
+ return "stop";
444
+ if (reason === "length")
445
+ return "length";
446
+ if (reason === "content_filter")
447
+ return "content-filter";
448
+ if (reason === "function_call" || reason === "tool_calls")
449
+ return "tool-calls";
450
+ if (reason === "error")
451
+ return "error";
452
+ return "unknown";
453
+ };
454
+ // OpenAI Chat reports `prompt_tokens` (inclusive total) with a
455
+ // cached-read and cache-write subsets, and `completion_tokens` (inclusive
456
+ // total) with a `reasoning_tokens` subset. We pass the inclusive totals
457
+ // through and derive the non-cached breakdown so the `AI.Usage` contract is
458
+ // satisfied on both sides.
459
+ const mapUsage = (usage) => {
460
+ if (!usage)
461
+ return undefined;
462
+ const input = usage.prompt_tokens ?? undefined;
463
+ const output = usage.completion_tokens ?? undefined;
464
+ const cached = usage.prompt_tokens_details?.cached_tokens ?? undefined;
465
+ const cacheWrite = usage.prompt_tokens_details?.cache_write_tokens ?? undefined;
466
+ const reasoning = usage.completion_tokens_details?.reasoning_tokens ?? undefined;
467
+ const nonCached = ProviderShared.subtractTokens(input, ProviderShared.sumTokens(cached, cacheWrite));
468
+ return new Usage({
469
+ inputTokens: input,
470
+ outputTokens: output,
471
+ nonCachedInputTokens: nonCached,
472
+ cacheReadInputTokens: cached,
473
+ cacheWriteInputTokens: cacheWrite,
474
+ reasoningTokens: reasoning,
475
+ totalTokens: ProviderShared.totalTokens(input, output, usage.total_tokens ?? undefined),
476
+ providerMetadata: { openai: usage },
477
+ });
478
+ };
479
+ const toolIndexByID = (tools, pendingTools, id) => {
480
+ if (!id)
481
+ return undefined;
482
+ const entry = Object.entries({ ...pendingTools, ...tools }).find(([, tool]) => tool?.id === id);
483
+ return entry ? Number(entry[0]) : undefined;
484
+ };
485
+ const reasoningDelta = (delta, configuredField) => {
486
+ if (!delta)
487
+ return undefined;
488
+ const fields = new Set([configuredField, "reasoning_content", "reasoning", "reasoning_text"]);
489
+ for (const field of fields) {
490
+ if (field === undefined)
491
+ continue;
492
+ const text = delta[field];
493
+ if (typeof text === "string" && text.length > 0)
494
+ return { field, text };
495
+ }
496
+ return undefined;
497
+ };
498
+ const detailText = (details) => {
499
+ const text = details.flatMap((detail) => {
500
+ if (!isRecord(detail))
501
+ return [];
502
+ if (detail.type === "reasoning.text" && typeof detail.text === "string" && detail.text)
503
+ return [detail.text];
504
+ if (detail.type === "reasoning.summary" && typeof detail.summary === "string" && detail.summary)
505
+ return [detail.summary];
506
+ return [];
507
+ });
508
+ if (text.length > 0)
509
+ return text.join("");
510
+ };
511
+ const appendReasoningDetails = (result, details) => {
512
+ for (const detail of details) {
513
+ const previous = result.at(-1);
514
+ if (!isRecord(previous) ||
515
+ previous.type !== "reasoning.text" ||
516
+ !isRecord(detail) ||
517
+ detail.type !== "reasoning.text" ||
518
+ conflictingReasoningTextDetails(previous, detail)) {
519
+ result.push(detail);
520
+ continue;
521
+ }
522
+ result[result.length - 1] = {
523
+ ...previous,
524
+ ...Object.fromEntries(Object.entries(detail).filter((entry) => entry[1] !== undefined)),
525
+ text: `${typeof previous.text === "string" ? previous.text : ""}${typeof detail.text === "string" ? detail.text : ""}`,
526
+ signature: mergeDetailValue(previous.signature, detail.signature),
527
+ format: mergeDetailValue(previous.format, detail.format),
528
+ };
529
+ }
530
+ };
531
+ const mergeDetailValue = (previous, current) => previous || current || (previous !== undefined ? previous : current);
532
+ const conflictingReasoningTextDetails = (previous, current) => conflictingDetailValue(previous.id, current.id) ||
533
+ conflictingDetailValue(previous.index, current.index) ||
534
+ conflictingDetailValue(previous.format, current.format) ||
535
+ (Boolean(previous.signature) && Boolean(current.signature) && previous.signature !== current.signature);
536
+ const conflictingDetailValue = (previous, current) => previous !== undefined && previous !== null && current !== undefined && current !== null && previous !== current;
537
+ const reasoningMetadata = (field, details) => ({
538
+ openai: {
539
+ ...(field ? { reasoningField: field } : {}),
540
+ ...(details ? { reasoningDetails: details } : {}),
541
+ },
542
+ });
543
+ const step = (state, event) => Effect.gen(function* () {
544
+ if (event.error)
545
+ return yield* new AIError({
546
+ module: ADAPTER,
547
+ method: "stream",
548
+ reason: classifyProviderFailure({
549
+ message: event.error.message,
550
+ code: event.error.code === undefined || event.error.code === null ? undefined : String(event.error.code),
551
+ status: typeof event.error.code === "number" ? event.error.code : undefined,
552
+ }),
553
+ });
554
+ const events = [];
555
+ const usage = mapUsage(event.usage) ?? state.usage;
556
+ const choice = event.choices?.[0];
557
+ const rawFinishReason = choice?.finish_reason;
558
+ const finishReason = rawFinishReason !== undefined && rawFinishReason !== null
559
+ ? { normalized: mapFinishReason(rawFinishReason), raw: choice?.native_finish_reason ?? rawFinishReason }
560
+ : state.finishReason;
561
+ const delta = choice?.delta;
562
+ const toolDeltas = delta?.tool_calls ?? [];
563
+ let tools = state.tools;
564
+ let pendingTools = state.pendingTools;
565
+ let latestToolIndex = state.latestToolIndex;
566
+ let nextToolIndex = state.nextToolIndex;
567
+ let lifecycle = state.lifecycle;
568
+ const reasoning = reasoningDelta(delta, state.reasoningField);
569
+ const hasLateContent = Boolean(delta?.content) ||
570
+ reasoning !== undefined ||
571
+ (Array.isArray(delta?.reasoning_details) && delta.reasoning_details.length > 0) ||
572
+ toolDeltas.some((tool) => Boolean(tool.id) || Boolean(tool.function?.name) || Boolean(tool.function?.arguments));
573
+ if (state.finishReason !== undefined) {
574
+ if (hasLateContent)
575
+ return yield* ProviderShared.eventError(ADAPTER, "OpenAI Chat received content after the finish reason");
576
+ return [{ ...state, usage }, events];
577
+ }
578
+ const reasoningField = state.reasoningField ?? reasoning?.field;
579
+ const detailDelta = Array.isArray(delta?.reasoning_details) ? delta.reasoning_details : undefined;
580
+ if (detailDelta !== undefined)
581
+ appendReasoningDetails(state.reasoningDetails, detailDelta);
582
+ const reasoningDetailsObserved = state.reasoningDetailsObserved || detailDelta !== undefined;
583
+ const deltaMetadata = reasoningMetadata(reasoningField);
584
+ const text = detailDelta?.length ? (detailText(detailDelta) ?? reasoning?.text) : reasoning?.text;
585
+ if (text !== undefined)
586
+ lifecycle = Lifecycle.reasoningDelta(lifecycle, events, "reasoning-0", text, deltaMetadata);
587
+ else if (reasoningDetailsObserved &&
588
+ !lifecycle.reasoning.has("reasoning-0") &&
589
+ (Boolean(delta?.content) || toolDeltas.length > 0))
590
+ lifecycle = Lifecycle.reasoningStart(lifecycle, events, "reasoning-0", deltaMetadata);
591
+ const reasoningEmitted = state.reasoningEmitted || lifecycle.reasoning.has("reasoning-0");
592
+ if (delta?.content) {
593
+ lifecycle = Lifecycle.reasoningEnd(lifecycle, events, "reasoning-0", reasoningMetadata(reasoningField, reasoningDetailsObserved ? state.reasoningDetails : undefined));
594
+ lifecycle = Lifecycle.textDelta(lifecycle, events, "text-0", delta.content);
595
+ }
596
+ // Compatible providers may omit indexes. Prefer durable identity, then use
597
+ // batch position for parallel deltas or the latest call for sparse chunks.
598
+ for (const [position, tool] of toolDeltas.entries()) {
599
+ const matched = toolIndexByID(tools, pendingTools, tool.id || undefined);
600
+ const fallback = toolDeltas.length > 1 ? position : (latestToolIndex ?? position);
601
+ const fallbackTool = tools[fallback] ?? pendingTools[fallback];
602
+ const index = tool.index ?? matched ?? (tool.id && fallbackTool?.id && fallbackTool.id !== tool.id ? nextToolIndex : fallback);
603
+ const current = tools[index];
604
+ const pending = pendingTools[index];
605
+ const id = current?.id ?? pending?.id ?? (tool.id || undefined);
606
+ const name = current?.name ?? pending?.name ?? (tool.function?.name || undefined);
607
+ const text = `${pending?.input ?? ""}${tool.function?.arguments ?? ""}`;
608
+ latestToolIndex = index;
609
+ nextToolIndex = Math.max(nextToolIndex, index + 1);
610
+ if (!current && (!id || !name)) {
611
+ pendingTools = {
612
+ ...pendingTools,
613
+ [index]: { id: id || undefined, name: name || undefined, input: text },
614
+ };
615
+ continue;
616
+ }
617
+ if (pending) {
618
+ pendingTools = { ...pendingTools };
619
+ delete pendingTools[index];
620
+ }
621
+ const result = ToolStream.appendOrStart(ADAPTER, tools, index, { id: id || undefined, name: name || undefined, text }, "OpenAI Chat tool call delta is missing id or name");
622
+ if (ToolStream.isError(result))
623
+ return yield* result;
624
+ tools = result.tools;
625
+ if (result.events.length)
626
+ lifecycle = Lifecycle.stepStart(lifecycle, events);
627
+ events.push(...result.events);
628
+ }
629
+ if (finishReason !== undefined && state.finishReason === undefined && Object.keys(pendingTools).length > 0)
630
+ return yield* ProviderShared.eventError(ADAPTER, "OpenAI Chat tool call delta is missing id or name");
631
+ // Finalize accumulated tool inputs eagerly when finish_reason arrives so
632
+ // valid calls and malformed local calls settle independently.
633
+ const finished = finishReason !== undefined && state.finishReason === undefined && Object.keys(tools).length > 0
634
+ ? yield* ToolStream.finishAll(ADAPTER, tools)
635
+ : undefined;
636
+ return [
637
+ {
638
+ tools: finished?.tools ?? tools,
639
+ pendingTools,
640
+ toolCallEvents: finished?.events ?? state.toolCallEvents,
641
+ usage,
642
+ finishReason,
643
+ lifecycle,
644
+ reasoningField,
645
+ reasoningDetails: state.reasoningDetails,
646
+ reasoningDetailsObserved,
647
+ reasoningEmitted,
648
+ latestToolIndex,
649
+ nextToolIndex,
650
+ },
651
+ events,
652
+ ];
653
+ });
654
+ const finishEvents = (state) => {
655
+ const events = [];
656
+ const toolCallEvents = state.finishReason === undefined && Object.keys(state.tools).length > 0
657
+ ? Effect.runSync(ToolStream.finishAll(ADAPTER, state.tools)).events
658
+ : state.toolCallEvents;
659
+ const hasToolCalls = toolCallEvents.length > 0;
660
+ const reason = state.finishReason
661
+ ? {
662
+ ...state.finishReason,
663
+ normalized: state.finishReason.normalized === "stop" && hasToolCalls ? "tool-calls" : state.finishReason.normalized,
664
+ }
665
+ : { normalized: hasToolCalls ? "tool-calls" : "unknown" };
666
+ const metadata = reasoningMetadata(state.reasoningField, state.reasoningDetailsObserved ? state.reasoningDetails : undefined);
667
+ const started = state.reasoningDetailsObserved && !state.reasoningEmitted
668
+ ? Lifecycle.reasoningStart(state.lifecycle, events, "reasoning-0", reasoningMetadata(state.reasoningField))
669
+ : state.lifecycle;
670
+ const ended = Lifecycle.reasoningEnd(started, events, "reasoning-0", metadata);
671
+ const lifecycle = toolCallEvents.length ? Lifecycle.stepStart(ended, events) : ended;
672
+ events.push(...toolCallEvents);
673
+ Lifecycle.finish(lifecycle, events, { reason, usage: state.usage });
674
+ return events;
675
+ };
676
+ // =============================================================================
677
+ // Protocol And OpenAI Route
678
+ // =============================================================================
679
+ /**
680
+ * The OpenAI Chat protocol — request body construction, body schema, and the
681
+ * streaming-event state machine. Reused by every route that speaks OpenAI Chat
682
+ * over HTTP+SSE: native OpenAI, DeepSeek, TogetherAI, Cerebras, Baseten,
683
+ * Fireworks, DeepInfra, and (once added) Azure OpenAI Chat.
684
+ */
685
+ export const protocol = Protocol.make({
686
+ id: ADAPTER,
687
+ body: {
688
+ schema: OpenAIChatBody,
689
+ from: fromRequest,
690
+ },
691
+ stream: {
692
+ event: Protocol.jsonEvent(OpenAIChatEvent),
693
+ initial: (request) => ({
694
+ tools: ToolStream.empty(),
695
+ pendingTools: {},
696
+ toolCallEvents: [],
697
+ lifecycle: Lifecycle.initial(),
698
+ reasoningField: request.model.compatibility?.reasoningField,
699
+ reasoningDetails: [],
700
+ reasoningDetailsObserved: false,
701
+ reasoningEmitted: false,
702
+ nextToolIndex: 0,
703
+ }),
704
+ step,
705
+ onHalt: finishEvents,
706
+ },
707
+ });
708
+ export const httpTransport = HttpTransport.sseJson.with();
709
+ export const route = Route.make({
710
+ id: ADAPTER,
711
+ provider: "openai",
712
+ providerMetadataKey: "openai",
713
+ protocol,
714
+ endpoint: Endpoint.path(PATH, { baseURL: DEFAULT_BASE_URL }),
715
+ auth: Auth.none,
716
+ transport: httpTransport,
717
+ });
718
+ export * as OpenAIChat from "./openai-chat.js";