@opencode/ai 0.0.0-reserved → 2.0.0

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 (288) hide show
  1. package/README.md +879 -2
  2. package/dist/cache-policy.d.ts +2 -0
  3. package/dist/cache-policy.js +129 -0
  4. package/dist/image-client.d.ts +19 -0
  5. package/dist/image-client.js +28 -0
  6. package/dist/image.d.ts +110 -0
  7. package/dist/image.js +103 -0
  8. package/dist/index.d.ts +18 -0
  9. package/dist/index.js +12 -0
  10. package/dist/llm.d.ts +252 -0
  11. package/dist/llm.js +74 -0
  12. package/dist/protocols/alibaba-chat.d.ts +225 -0
  13. package/dist/protocols/alibaba-chat.js +75 -0
  14. package/dist/protocols/alibaba-messages.d.ts +350 -0
  15. package/dist/protocols/alibaba-messages.js +40 -0
  16. package/dist/protocols/alibaba-responses.d.ts +255 -0
  17. package/dist/protocols/alibaba-responses.js +80 -0
  18. package/dist/protocols/anthropic-messages.d.ts +980 -0
  19. package/dist/protocols/anthropic-messages.js +1460 -0
  20. package/dist/protocols/bedrock-converse.d.ts +499 -0
  21. package/dist/protocols/bedrock-converse.js +643 -0
  22. package/dist/protocols/bedrock-event-stream.d.ts +9 -0
  23. package/dist/protocols/bedrock-event-stream.js +97 -0
  24. package/dist/protocols/gemini.d.ts +285 -0
  25. package/dist/protocols/gemini.js +671 -0
  26. package/dist/protocols/google-images.d.ts +30 -0
  27. package/dist/protocols/google-images.js +181 -0
  28. package/dist/protocols/index.d.ts +12 -0
  29. package/dist/protocols/index.js +12 -0
  30. package/dist/protocols/meta-images.d.ts +26 -0
  31. package/dist/protocols/meta-images.js +89 -0
  32. package/dist/protocols/meta-messages.d.ts +356 -0
  33. package/dist/protocols/meta-messages.js +41 -0
  34. package/dist/protocols/meta-responses.d.ts +473 -0
  35. package/dist/protocols/meta-responses.js +173 -0
  36. package/dist/protocols/mistral-chat.d.ts +540 -0
  37. package/dist/protocols/mistral-chat.js +624 -0
  38. package/dist/protocols/open-responses-channel.d.ts +25 -0
  39. package/dist/protocols/open-responses-channel.js +141 -0
  40. package/dist/protocols/open-responses-continuation.d.ts +16 -0
  41. package/dist/protocols/open-responses-continuation.js +170 -0
  42. package/dist/protocols/open-responses.d.ts +1715 -0
  43. package/dist/protocols/open-responses.js +1228 -0
  44. package/dist/protocols/openai-chat.d.ts +860 -0
  45. package/dist/protocols/openai-chat.js +966 -0
  46. package/dist/protocols/openai-compatible-chat.d.ts +109 -0
  47. package/dist/protocols/openai-compatible-chat.js +19 -0
  48. package/dist/protocols/openai-compatible-responses.d.ts +172 -0
  49. package/dist/protocols/openai-compatible-responses.js +18 -0
  50. package/dist/protocols/openai-images.d.ts +32 -0
  51. package/dist/protocols/openai-images.js +183 -0
  52. package/dist/protocols/openai-responses.d.ts +1389 -0
  53. package/dist/protocols/openai-responses.js +260 -0
  54. package/dist/protocols/shared.d.ts +193 -0
  55. package/dist/protocols/shared.js +290 -0
  56. package/dist/protocols/utils/bedrock-auth.d.ts +60 -0
  57. package/dist/protocols/utils/bedrock-auth.js +85 -0
  58. package/dist/protocols/utils/bedrock-cache.d.ts +19 -0
  59. package/dist/protocols/utils/bedrock-cache.js +49 -0
  60. package/dist/protocols/utils/bedrock-media.d.ts +55 -0
  61. package/dist/protocols/utils/bedrock-media.js +73 -0
  62. package/dist/protocols/utils/cache.d.ts +6 -0
  63. package/dist/protocols/utils/cache.js +5 -0
  64. package/dist/protocols/utils/gemini-tool-schema.d.ts +2 -0
  65. package/dist/protocols/utils/gemini-tool-schema.js +103 -0
  66. package/dist/protocols/utils/image-input.d.ts +21 -0
  67. package/dist/protocols/utils/image-input.js +20 -0
  68. package/dist/protocols/utils/lifecycle.d.ts +24 -0
  69. package/dist/protocols/utils/lifecycle.js +72 -0
  70. package/dist/protocols/utils/meta-image.d.ts +2 -0
  71. package/dist/protocols/utils/meta-image.js +13 -0
  72. package/dist/protocols/utils/open-responses-options.d.ts +56 -0
  73. package/dist/protocols/utils/open-responses-options.js +61 -0
  74. package/dist/protocols/utils/openai-image.d.ts +5 -0
  75. package/dist/protocols/utils/openai-image.js +18 -0
  76. package/dist/protocols/utils/openai-options.d.ts +16 -0
  77. package/dist/protocols/utils/openai-options.js +14 -0
  78. package/dist/protocols/utils/partial-json-options.d.ts +62 -0
  79. package/dist/protocols/utils/partial-json-options.js +52 -0
  80. package/dist/protocols/utils/partial-json.d.ts +8 -0
  81. package/dist/protocols/utils/partial-json.js +279 -0
  82. package/dist/protocols/utils/responses-checkpoint.d.ts +4 -0
  83. package/dist/protocols/utils/responses-checkpoint.js +107 -0
  84. package/dist/protocols/utils/responses-compaction.d.ts +4 -0
  85. package/dist/protocols/utils/responses-compaction.js +129 -0
  86. package/dist/protocols/utils/responses-hosted-tools.d.ts +26 -0
  87. package/dist/protocols/utils/responses-hosted-tools.js +32 -0
  88. package/dist/protocols/utils/tool-schema.d.ts +8 -0
  89. package/dist/protocols/utils/tool-schema.js +54 -0
  90. package/dist/protocols/utils/tool-stream.d.ts +710 -0
  91. package/dist/protocols/utils/tool-stream.js +155 -0
  92. package/dist/protocols/xai-images.d.ts +26 -0
  93. package/dist/protocols/xai-images.js +106 -0
  94. package/dist/protocols/xai-responses.d.ts +250 -0
  95. package/dist/protocols/xai-responses.js +81 -0
  96. package/dist/protocols/zai-chat.d.ts +202 -0
  97. package/dist/protocols/zai-chat.js +49 -0
  98. package/dist/protocols/zai-images.d.ts +22 -0
  99. package/dist/protocols/zai-images.js +79 -0
  100. package/dist/protocols/zai-messages.d.ts +344 -0
  101. package/dist/protocols/zai-messages.js +27 -0
  102. package/dist/protocols.d.ts +1 -0
  103. package/dist/protocols.js +1 -0
  104. package/dist/provider-error.d.ts +15 -0
  105. package/dist/provider-error.js +125 -0
  106. package/dist/provider-package.d.ts +15 -0
  107. package/dist/provider-package.js +1 -0
  108. package/dist/provider.d.ts +23 -0
  109. package/dist/provider.js +2 -0
  110. package/dist/providers/alibaba/chat.d.ts +1 -0
  111. package/dist/providers/alibaba/chat.js +1 -0
  112. package/dist/providers/alibaba/messages.d.ts +3 -0
  113. package/dist/providers/alibaba/messages.js +1 -0
  114. package/dist/providers/alibaba/responses.d.ts +3 -0
  115. package/dist/providers/alibaba/responses.js +1 -0
  116. package/dist/providers/alibaba.d.ts +711 -0
  117. package/dist/providers/alibaba.js +94 -0
  118. package/dist/providers/amazon-bedrock/mantle/chat.d.ts +2 -0
  119. package/dist/providers/amazon-bedrock/mantle/chat.js +1 -0
  120. package/dist/providers/amazon-bedrock/mantle/responses.d.ts +2 -0
  121. package/dist/providers/amazon-bedrock/mantle/responses.js +1 -0
  122. package/dist/providers/amazon-bedrock/mantle.d.ts +2 -0
  123. package/dist/providers/amazon-bedrock/mantle.js +1 -0
  124. package/dist/providers/amazon-bedrock-mantle.d.ts +313 -0
  125. package/dist/providers/amazon-bedrock-mantle.js +79 -0
  126. package/dist/providers/amazon-bedrock.d.ts +173 -0
  127. package/dist/providers/amazon-bedrock.js +41 -0
  128. package/dist/providers/anthropic-compatible.d.ts +309 -0
  129. package/dist/providers/anthropic-compatible.js +50 -0
  130. package/dist/providers/anthropic.d.ts +307 -0
  131. package/dist/providers/anthropic.js +42 -0
  132. package/dist/providers/azure/chat.d.ts +2 -0
  133. package/dist/providers/azure/chat.js +1 -0
  134. package/dist/providers/azure/responses.d.ts +2 -0
  135. package/dist/providers/azure/responses.js +1 -0
  136. package/dist/providers/azure.d.ts +598 -0
  137. package/dist/providers/azure.js +115 -0
  138. package/dist/providers/baseten.d.ts +228 -0
  139. package/dist/providers/baseten.js +38 -0
  140. package/dist/providers/cerebras.d.ts +227 -0
  141. package/dist/providers/cerebras.js +40 -0
  142. package/dist/providers/cloudflare-ai-gateway.d.ts +237 -0
  143. package/dist/providers/cloudflare-ai-gateway.js +65 -0
  144. package/dist/providers/cloudflare-workers-ai.d.ts +231 -0
  145. package/dist/providers/cloudflare-workers-ai.js +51 -0
  146. package/dist/providers/deepinfra.d.ts +227 -0
  147. package/dist/providers/deepinfra.js +43 -0
  148. package/dist/providers/deepseek.d.ts +228 -0
  149. package/dist/providers/deepseek.js +41 -0
  150. package/dist/providers/fireworks.d.ts +228 -0
  151. package/dist/providers/fireworks.js +38 -0
  152. package/dist/providers/google-vertex/chat.d.ts +2 -0
  153. package/dist/providers/google-vertex/chat.js +1 -0
  154. package/dist/providers/google-vertex/gemini.d.ts +2 -0
  155. package/dist/providers/google-vertex/gemini.js +1 -0
  156. package/dist/providers/google-vertex/messages.d.ts +2 -0
  157. package/dist/providers/google-vertex/messages.js +1 -0
  158. package/dist/providers/google-vertex/responses.d.ts +2 -0
  159. package/dist/providers/google-vertex/responses.js +1 -0
  160. package/dist/providers/google-vertex-chat.d.ts +132 -0
  161. package/dist/providers/google-vertex-chat.js +55 -0
  162. package/dist/providers/google-vertex-messages.d.ts +306 -0
  163. package/dist/providers/google-vertex-messages.js +75 -0
  164. package/dist/providers/google-vertex-responses.d.ts +197 -0
  165. package/dist/providers/google-vertex-responses.js +56 -0
  166. package/dist/providers/google-vertex-shared.d.ts +22 -0
  167. package/dist/providers/google-vertex-shared.js +72 -0
  168. package/dist/providers/google-vertex.d.ts +119 -0
  169. package/dist/providers/google-vertex.js +98 -0
  170. package/dist/providers/google.d.ts +114 -0
  171. package/dist/providers/google.js +42 -0
  172. package/dist/providers/groq.d.ts +314 -0
  173. package/dist/providers/groq.js +81 -0
  174. package/dist/providers/index.d.ts +31 -0
  175. package/dist/providers/index.js +31 -0
  176. package/dist/providers/meta/chat.d.ts +2 -0
  177. package/dist/providers/meta/chat.js +1 -0
  178. package/dist/providers/meta/messages.d.ts +2 -0
  179. package/dist/providers/meta/messages.js +1 -0
  180. package/dist/providers/meta/responses.d.ts +2 -0
  181. package/dist/providers/meta/responses.js +1 -0
  182. package/dist/providers/meta.d.ts +636 -0
  183. package/dist/providers/meta.js +113 -0
  184. package/dist/providers/minimax/chat.d.ts +4 -0
  185. package/dist/providers/minimax/chat.js +8 -0
  186. package/dist/providers/minimax/messages.d.ts +1 -0
  187. package/dist/providers/minimax/messages.js +1 -0
  188. package/dist/providers/minimax/responses.d.ts +4 -0
  189. package/dist/providers/minimax/responses.js +8 -0
  190. package/dist/providers/minimax.d.ts +598 -0
  191. package/dist/providers/minimax.js +89 -0
  192. package/dist/providers/mistral.d.ts +207 -0
  193. package/dist/providers/mistral.js +28 -0
  194. package/dist/providers/moonshot/chat.d.ts +1 -0
  195. package/dist/providers/moonshot/chat.js +1 -0
  196. package/dist/providers/moonshot/messages.d.ts +4 -0
  197. package/dist/providers/moonshot/messages.js +8 -0
  198. package/dist/providers/moonshot/responses.d.ts +4 -0
  199. package/dist/providers/moonshot/responses.js +8 -0
  200. package/dist/providers/moonshot.d.ts +596 -0
  201. package/dist/providers/moonshot.js +90 -0
  202. package/dist/providers/open-responses-options.d.ts +6 -0
  203. package/dist/providers/open-responses-options.js +1 -0
  204. package/dist/providers/openai/chat.d.ts +2 -0
  205. package/dist/providers/openai/chat.js +1 -0
  206. package/dist/providers/openai/responses.d.ts +2 -0
  207. package/dist/providers/openai/responses.js +1 -0
  208. package/dist/providers/openai-compatible/responses.d.ts +1 -0
  209. package/dist/providers/openai-compatible/responses.js +1 -0
  210. package/dist/providers/openai-compatible-responses.d.ts +195 -0
  211. package/dist/providers/openai-compatible-responses.js +32 -0
  212. package/dist/providers/openai-compatible.d.ts +130 -0
  213. package/dist/providers/openai-compatible.js +33 -0
  214. package/dist/providers/openai-options.d.ts +26 -0
  215. package/dist/providers/openai-options.js +41 -0
  216. package/dist/providers/openai.d.ts +404 -0
  217. package/dist/providers/openai.js +82 -0
  218. package/dist/providers/openrouter.d.ts +563 -0
  219. package/dist/providers/openrouter.js +114 -0
  220. package/dist/providers/togetherai.d.ts +227 -0
  221. package/dist/providers/togetherai.js +40 -0
  222. package/dist/providers/xai.d.ts +163 -0
  223. package/dist/providers/xai.js +88 -0
  224. package/dist/providers/zai/chat.d.ts +1 -0
  225. package/dist/providers/zai/chat.js +1 -0
  226. package/dist/providers/zai-coding-plan/chat.d.ts +1 -0
  227. package/dist/providers/zai-coding-plan/chat.js +1 -0
  228. package/dist/providers/zai-coding-plan/messages.d.ts +4 -0
  229. package/dist/providers/zai-coding-plan/messages.js +8 -0
  230. package/dist/providers/zai-coding-plan/responses.d.ts +4 -0
  231. package/dist/providers/zai-coding-plan/responses.js +8 -0
  232. package/dist/providers/zai-coding-plan.d.ts +577 -0
  233. package/dist/providers/zai-coding-plan.js +63 -0
  234. package/dist/providers/zai.d.ts +149 -0
  235. package/dist/providers/zai.js +53 -0
  236. package/dist/providers.d.ts +1 -0
  237. package/dist/providers.js +1 -0
  238. package/dist/route/auth-options.d.ts +34 -0
  239. package/dist/route/auth-options.js +14 -0
  240. package/dist/route/auth.d.ts +51 -0
  241. package/dist/route/auth.js +87 -0
  242. package/dist/route/client.d.ts +430 -0
  243. package/dist/route/client.js +307 -0
  244. package/dist/route/endpoint.d.ts +28 -0
  245. package/dist/route/endpoint.js +21 -0
  246. package/dist/route/executor.d.ts +27 -0
  247. package/dist/route/executor.js +183 -0
  248. package/dist/route/framing.d.ts +29 -0
  249. package/dist/route/framing.js +14 -0
  250. package/dist/route/index.d.ts +16 -0
  251. package/dist/route/index.js +9 -0
  252. package/dist/route/protocol.d.ts +76 -0
  253. package/dist/route/protocol.js +16 -0
  254. package/dist/route/transport/http.d.ts +34 -0
  255. package/dist/route/transport/http.js +68 -0
  256. package/dist/route/transport/index.d.ts +41 -0
  257. package/dist/route/transport/index.js +4 -0
  258. package/dist/route/transport/websocket-channel.d.ts +57 -0
  259. package/dist/route/transport/websocket-channel.js +1 -0
  260. package/dist/route/transport/websocket.d.ts +56 -0
  261. package/dist/route/transport/websocket.js +370 -0
  262. package/dist/route.d.ts +1 -0
  263. package/dist/route.js +1 -0
  264. package/dist/schema/errors.d.ts +170 -0
  265. package/dist/schema/errors.js +131 -0
  266. package/dist/schema/events.d.ts +8123 -0
  267. package/dist/schema/events.js +550 -0
  268. package/dist/schema/ids.d.ts +17 -0
  269. package/dist/schema/ids.js +10 -0
  270. package/dist/schema/index.d.ts +5 -0
  271. package/dist/schema/index.js +5 -0
  272. package/dist/schema/messages.d.ts +646 -0
  273. package/dist/schema/messages.js +301 -0
  274. package/dist/schema/options.d.ts +159 -0
  275. package/dist/schema/options.js +189 -0
  276. package/dist/testing.d.ts +1693 -0
  277. package/dist/testing.js +129 -0
  278. package/dist/tool-history.d.ts +2 -0
  279. package/dist/tool-history.js +84 -0
  280. package/dist/tool-runtime.d.ts +15 -0
  281. package/dist/tool-runtime.js +62 -0
  282. package/dist/tool.d.ts +136 -0
  283. package/dist/tool.js +66 -0
  284. package/dist/utils/record.d.ts +2 -0
  285. package/dist/utils/record.js +2 -0
  286. package/dist/utils/sanitize.d.ts +1 -0
  287. package/dist/utils/sanitize.js +12 -0
  288. package/package.json +43 -10
@@ -0,0 +1,966 @@
1
+ import { Effect, Schema } from "effect";
2
+ import { Tool } from "@opencode/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 { Framing } from "../route/framing.js";
7
+ import { HttpTransport } from "../route/transport/index.js";
8
+ import { Protocol } from "../route/protocol.js";
9
+ import { AIError, AIErrorReason, InvalidProviderOutputError, LLMEvent, ProviderInternalError, UnknownProviderError, Usage, } from "../schema/index.js";
10
+ import { classifyProviderFailure } from "../provider-error.js";
11
+ import { isRecord, JsonObject, optionalArray, optionalNull, ProviderShared } from "./shared.js";
12
+ import { OpenAIOptions } from "./utils/openai-options.js";
13
+ import { Lifecycle } from "./utils/lifecycle.js";
14
+ import { ToolSchemaProjection } from "./utils/tool-schema.js";
15
+ import { ToolStream } from "./utils/tool-stream.js";
16
+ const ADAPTER = "openai-chat";
17
+ const RESERVED_REASONING_FIELDS = new Set(["role", "content", "refusal", "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: Schema.Struct({
38
+ name: Schema.String,
39
+ description: Schema.String,
40
+ parameters: JsonObject,
41
+ strict: Schema.optional(Schema.Boolean),
42
+ }),
43
+ cache_control: Schema.optional(OpenAIChatCacheControl),
44
+ });
45
+ const OpenAIChatAssistantToolCall = Schema.Struct({
46
+ id: Schema.String,
47
+ type: Schema.tag("function"),
48
+ function: Schema.Struct({
49
+ name: Schema.String,
50
+ arguments: Schema.String,
51
+ }),
52
+ });
53
+ // Intentionally omit Gemini's provider-specific `extra_content.google.thought_signature`
54
+ // extension until direct Google OpenAI-compatible routing is supported here:
55
+ // https://github.com/vercel/ai/issues/11590
56
+ // https://github.com/vercel/ai/pull/11745
57
+ // https://ai.google.dev/gemini-api/docs/thought-signatures#openai
58
+ const OpenAIChatUserContent = Schema.Union([
59
+ Schema.Struct({
60
+ type: Schema.Literal("text"),
61
+ text: Schema.String,
62
+ cache_control: Schema.optional(OpenAIChatCacheControl),
63
+ }),
64
+ Schema.Struct({
65
+ type: Schema.Literal("image_url"),
66
+ image_url: Schema.Struct({ url: Schema.String }),
67
+ }),
68
+ ]);
69
+ const OpenAIChatMessage = Schema.Union([
70
+ Schema.Struct({
71
+ role: Schema.Literal("system"),
72
+ content: Schema.Union([Schema.String, Schema.Array(OpenAIChatUserContent)]),
73
+ }),
74
+ Schema.Struct({
75
+ role: Schema.Literal("user"),
76
+ content: Schema.Union([Schema.String, Schema.Array(OpenAIChatUserContent)]),
77
+ }),
78
+ Schema.StructWithRest(Schema.Struct({
79
+ role: Schema.Literal("assistant"),
80
+ content: Schema.NullOr(Schema.String),
81
+ tool_calls: optionalArray(OpenAIChatAssistantToolCall),
82
+ reasoning_content: Schema.optional(Schema.String),
83
+ reasoning: Schema.optional(Schema.String),
84
+ reasoning_text: Schema.optional(Schema.String),
85
+ reasoning_details: Schema.optional(Schema.Unknown),
86
+ cache_control: Schema.optional(OpenAIChatCacheControl),
87
+ }), [Schema.Record(Schema.String, Schema.Unknown)]),
88
+ Schema.Struct({
89
+ role: Schema.Literal("tool"),
90
+ tool_call_id: Schema.String,
91
+ content: Schema.String,
92
+ cache_control: Schema.optional(OpenAIChatCacheControl),
93
+ }),
94
+ ]).pipe(Schema.toTaggedUnion("role"));
95
+ const OpenAIChatToolChoice = Schema.Union([
96
+ Schema.Literals(["auto", "none", "required"]),
97
+ Schema.Struct({
98
+ type: Schema.tag("function"),
99
+ function: Schema.Struct({ name: Schema.String }),
100
+ }),
101
+ ]);
102
+ export const bodyFields = {
103
+ model: Schema.String,
104
+ messages: Schema.Array(OpenAIChatMessage),
105
+ tools: optionalArray(OpenAIChatTool),
106
+ tool_choice: Schema.optional(OpenAIChatToolChoice),
107
+ stream: Schema.Literal(true),
108
+ stream_options: Schema.optional(Schema.Struct({ include_usage: Schema.Boolean })),
109
+ store: Schema.optional(Schema.Boolean),
110
+ prompt_cache_key: Schema.optional(Schema.String),
111
+ reasoning_effort: Schema.optional(OpenAIOptions.OpenAIReasoningEffort),
112
+ tool_stream: Schema.optional(Schema.Boolean),
113
+ max_completion_tokens: Schema.optional(Schema.Number),
114
+ max_tokens: Schema.optional(Schema.Number),
115
+ temperature: Schema.optional(Schema.Number),
116
+ top_p: Schema.optional(Schema.Number),
117
+ frequency_penalty: Schema.optional(Schema.Number),
118
+ presence_penalty: Schema.optional(Schema.Number),
119
+ seed: Schema.optional(Schema.Number),
120
+ stop: optionalArray(Schema.String),
121
+ };
122
+ const OpenAIChatBody = Schema.Struct(bodyFields);
123
+ // =============================================================================
124
+ // Streaming Event Schema
125
+ // =============================================================================
126
+ // The event schema is one decoded SSE `data:` payload. `Framing.sse` splits the
127
+ // byte stream into strings, then `Protocol.jsonEvent` decodes each string into
128
+ // this provider-native event shape.
129
+ const OpenAIChatUsage = Schema.StructWithRest(Schema.Struct({
130
+ prompt_tokens: optionalNull(Schema.Number),
131
+ completion_tokens: optionalNull(Schema.Number),
132
+ total_tokens: optionalNull(Schema.Number),
133
+ // Zai reports cache hits as top-level `cached_tokens`; DeepSeek uses `prompt_cache_hit_tokens`.
134
+ cached_tokens: optionalNull(Schema.Number),
135
+ prompt_cache_hit_tokens: optionalNull(Schema.Number),
136
+ prompt_tokens_details: optionalNull(Schema.StructWithRest(Schema.Struct({
137
+ cached_tokens: optionalNull(Schema.Number),
138
+ cache_write_tokens: optionalNull(Schema.Number),
139
+ }), [Schema.Record(Schema.String, Schema.Unknown)])),
140
+ completion_tokens_details: optionalNull(Schema.StructWithRest(Schema.Struct({
141
+ reasoning_tokens: optionalNull(Schema.Number),
142
+ accepted_prediction_tokens: optionalNull(Schema.Number),
143
+ rejected_prediction_tokens: optionalNull(Schema.Number),
144
+ }), [Schema.Record(Schema.String, Schema.Unknown)])),
145
+ }), [Schema.Record(Schema.String, Schema.Unknown)]);
146
+ const OpenAIChatToolCallDeltaFunction = Schema.Struct({
147
+ name: optionalNull(Schema.String),
148
+ arguments: optionalNull(Schema.String),
149
+ });
150
+ const OpenAIChatToolCallDelta = Schema.Struct({
151
+ index: optionalNull(Schema.Number),
152
+ id: optionalNull(Schema.String),
153
+ function: optionalNull(OpenAIChatToolCallDeltaFunction),
154
+ });
155
+ const OpenAIChatDelta = Schema.StructWithRest(Schema.Struct({
156
+ content: optionalNull(Schema.String),
157
+ refusal: optionalNull(Schema.String),
158
+ reasoning_content: optionalNull(Schema.String),
159
+ reasoning: optionalNull(Schema.String),
160
+ reasoning_text: optionalNull(Schema.String),
161
+ reasoning_details: optionalNull(Schema.Unknown),
162
+ tool_calls: optionalNull(Schema.Array(OpenAIChatToolCallDelta)),
163
+ }), [Schema.Record(Schema.String, Schema.Unknown)]);
164
+ const OpenAIChatChoice = Schema.StructWithRest(Schema.Struct({
165
+ delta: optionalNull(OpenAIChatDelta),
166
+ finish_reason: optionalNull(Schema.String),
167
+ native_finish_reason: optionalNull(Schema.String),
168
+ // Moonshot streams usage on `choice.usage` instead of top-level `usage`.
169
+ usage: optionalNull(OpenAIChatUsage),
170
+ }), [Schema.Record(Schema.String, Schema.Unknown)]);
171
+ const OpenAIChatError = Schema.StructWithRest(Schema.Struct({
172
+ code: optionalNull(Schema.Union([Schema.String, Schema.Number])),
173
+ message: Schema.String,
174
+ }), [Schema.Record(Schema.String, Schema.Unknown)]);
175
+ export const OpenAIChatEvent = Schema.StructWithRest(Schema.Struct({
176
+ choices: optionalNull(Schema.Array(OpenAIChatChoice)),
177
+ usage: optionalNull(OpenAIChatUsage),
178
+ error: optionalNull(OpenAIChatError),
179
+ }), [Schema.Record(Schema.String, Schema.Unknown)]);
180
+ const DONE = "[DONE]";
181
+ const OpenAIChatStreamEvent = Schema.Union([Schema.Literal(DONE), Protocol.jsonEvent(OpenAIChatEvent)]);
182
+ const lowerTool = (tool, inputSchema, options, supportsStrictMode) => ({
183
+ type: "function",
184
+ function: {
185
+ name: tool.name,
186
+ description: tool.description,
187
+ parameters: inputSchema,
188
+ ...(supportsStrictMode ? { strict: false } : {}),
189
+ },
190
+ cache_control: options.cacheControl?.(tool.cache),
191
+ });
192
+ const lowerToolChoice = (toolChoice) => ProviderShared.matchToolChoice("OpenAI Chat", toolChoice, {
193
+ auto: () => "auto",
194
+ none: () => "none",
195
+ required: () => "required",
196
+ tool: (name) => ({ type: "function", function: { name } }),
197
+ });
198
+ const lowerToolCall = (part, options) => ({
199
+ id: options.toolCallID?.(part.id) ?? part.id,
200
+ type: "function",
201
+ function: {
202
+ name: part.name,
203
+ arguments: ProviderShared.encodeJson(part.input),
204
+ },
205
+ });
206
+ const lowerMedia = Effect.fn("OpenAIChat.lowerMedia")(function* (part) {
207
+ const media = ProviderShared.normalizeMedia(part);
208
+ if (!media.mime.startsWith("image/"))
209
+ return yield* ProviderShared.invalidRequest(`OpenAI Chat does not support media type ${part.mediaType}`);
210
+ return { type: "image_url", image_url: { url: media.dataUrl } };
211
+ });
212
+ const openAICompatibleReasoningContent = (native) => isRecord(native) && typeof native.reasoning_content === "string" ? native.reasoning_content : undefined;
213
+ const reasoningField = (part, providerMetadataKey) => {
214
+ const field = part.providerMetadata?.[providerMetadataKey]?.reasoningField;
215
+ return typeof field === "string" ? field : undefined;
216
+ };
217
+ const reasoningDetails = (parts, native, providerMetadataKey) => {
218
+ const observed = parts.flatMap((part) => {
219
+ const details = part.providerMetadata?.[providerMetadataKey]?.reasoningDetails;
220
+ return Array.isArray(details) ? details : [];
221
+ });
222
+ if (parts.some((part) => Array.isArray(part.providerMetadata?.[providerMetadataKey]?.reasoningDetails)))
223
+ return observed;
224
+ if (isRecord(native) && Array.isArray(native.reasoning_details))
225
+ return native.reasoning_details;
226
+ };
227
+ const lowerUserMessage = Effect.fn("OpenAIChat.lowerUserMessage")(function* (message, options) {
228
+ const content = [];
229
+ for (const part of message.content) {
230
+ if (part.type === "text") {
231
+ content.push({ type: "text", text: part.text, cache_control: options.cacheControl?.(part.cache) });
232
+ continue;
233
+ }
234
+ if (part.type === "media") {
235
+ content.push(yield* lowerMedia(part));
236
+ continue;
237
+ }
238
+ return yield* ProviderShared.unsupportedContent("OpenAI Chat", "user", ["text", "media"]);
239
+ }
240
+ if (content.every((part) => part.type === "text" && part.cache_control === undefined))
241
+ return {
242
+ role: "user",
243
+ content: content.map((part) => (part.type === "text" ? part.text : "")).join(""),
244
+ };
245
+ return { role: "user", content };
246
+ });
247
+ const lowerAssistantMessage = Effect.fn("OpenAIChat.lowerAssistantMessage")(function* (message, configuredField, requireReasoning, options) {
248
+ const content = [];
249
+ const reasoning = [];
250
+ const toolCalls = [];
251
+ for (const part of message.content) {
252
+ if (!ProviderShared.supportsContent(part, ["text", "reasoning", "tool-call"]))
253
+ return yield* ProviderShared.unsupportedContent("OpenAI Chat", "assistant", ["text", "reasoning", "tool-call"]);
254
+ if (part.type === "text") {
255
+ content.push(part);
256
+ continue;
257
+ }
258
+ if (part.type === "reasoning") {
259
+ reasoning.push(part);
260
+ continue;
261
+ }
262
+ if (part.type === "tool-call") {
263
+ toolCalls.push(lowerToolCall(part, options));
264
+ continue;
265
+ }
266
+ }
267
+ const text = reasoning.map((part) => part.text).join("");
268
+ const details = reasoningDetails(reasoning, message.native?.openaiCompatible, options.providerMetadataKey);
269
+ const observedField = reasoning
270
+ .map((part) => reasoningField(part, options.providerMetadataKey))
271
+ .find((value) => value !== undefined);
272
+ const nativeReasoning = openAICompatibleReasoningContent(message.native?.openaiCompatible);
273
+ const fullyStructured = reasoning.every((part) => Array.isArray(part.providerMetadata?.[options.providerMetadataKey]?.reasoningDetails));
274
+ const field = (() => {
275
+ if (configuredField !== undefined && (requireReasoning || reasoning.length > 0 || nativeReasoning !== undefined))
276
+ return configuredField;
277
+ if (reasoning.length === 0)
278
+ return requireReasoning ? "reasoning_content" : undefined;
279
+ if (observedField !== undefined)
280
+ return observedField;
281
+ if (nativeReasoning !== undefined)
282
+ return "reasoning_content";
283
+ if (!fullyStructured || requireReasoning)
284
+ return "reasoning_content";
285
+ })();
286
+ const reasoningText = (() => {
287
+ if (configuredField !== undefined)
288
+ return reasoning.length === 0 ? (nativeReasoning ?? (requireReasoning ? "" : undefined)) : text;
289
+ if (reasoning.length === 0)
290
+ return nativeReasoning ?? (requireReasoning ? "" : undefined);
291
+ return text;
292
+ })();
293
+ const cached = message.content.findLast((part) => "cache" in part && part.cache !== undefined);
294
+ const cacheControl = options.cacheControl?.(cached && "cache" in cached ? cached.cache : undefined);
295
+ const result = {
296
+ role: "assistant",
297
+ content: content.length > 0 ? content.map((part) => part.text).join("") : toolCalls.length > 0 ? null : "",
298
+ ...(toolCalls.length > 0 ? { tool_calls: toolCalls } : {}),
299
+ ...(details !== undefined ? { reasoning_details: details } : {}),
300
+ ...(cacheControl !== undefined ? { cache_control: cacheControl } : {}),
301
+ };
302
+ if (field === undefined || reasoningText === undefined)
303
+ return result;
304
+ return { ...result, [field]: reasoningText };
305
+ });
306
+ const lowerToolMessages = Effect.fn("OpenAIChat.lowerToolMessages")(function* (message, options) {
307
+ const messages = [];
308
+ const images = [];
309
+ for (const part of message.content) {
310
+ if (!ProviderShared.supportsContent(part, ["tool-result"]))
311
+ return yield* ProviderShared.unsupportedContent("OpenAI Chat", "tool", ["tool-result"]);
312
+ if (part.result.type !== "content") {
313
+ messages.push({
314
+ role: "tool",
315
+ tool_call_id: options.toolCallID?.(part.id) ?? part.id,
316
+ content: ProviderShared.toolResultText(part),
317
+ cache_control: options.cacheControl?.(part.cache),
318
+ });
319
+ continue;
320
+ }
321
+ const content = part.result.value;
322
+ const text = content.filter((item) => item.type === "text").map((item) => item.text);
323
+ messages.push({
324
+ role: "tool",
325
+ tool_call_id: options.toolCallID?.(part.id) ?? part.id,
326
+ content: text.join("\n"),
327
+ cache_control: options.cacheControl?.(part.cache),
328
+ });
329
+ const files = content.filter((item) => item.type === "file");
330
+ images.push(...(yield* Effect.forEach(files, (item) => lowerMedia({ type: "media", mediaType: item.mime, data: item.uri, filename: item.name }))));
331
+ }
332
+ return { messages, images };
333
+ });
334
+ const lowerMessage = Effect.fn("OpenAIChat.lowerMessage")(function* (message, reasoningField, requireReasoning, options) {
335
+ if (message.role === "user")
336
+ return [yield* lowerUserMessage(message, options)];
337
+ if (message.role === "assistant")
338
+ return [yield* lowerAssistantMessage(message, reasoningField, requireReasoning, options)];
339
+ return (yield* lowerToolMessages(message, options)).messages;
340
+ });
341
+ const lowerMessages = Effect.fn("OpenAIChat.lowerMessages")(function* (request, options) {
342
+ const system = request.system.length === 0
343
+ ? []
344
+ : request.system.some((part) => part.cache !== undefined) && options.cacheControl !== undefined
345
+ ? [
346
+ {
347
+ role: "system",
348
+ content: request.system.map((part) => ({
349
+ type: "text",
350
+ text: part.text,
351
+ cache_control: options.cacheControl?.(part.cache),
352
+ })),
353
+ },
354
+ ]
355
+ : [{ role: "system", content: ProviderShared.joinText(request.system) }];
356
+ const messages = [...system];
357
+ const modelID = request.model.id.toLowerCase();
358
+ const requireReasoning = request.model.compatibility?.requireReasoning ??
359
+ (request.model.compatibility?.reasoningField !== undefined ||
360
+ request.model.provider === "deepseek" ||
361
+ request.model.route.endpoint.baseURL?.toLowerCase().includes("deepseek.com") ||
362
+ modelID.includes("deepseek"));
363
+ const reasoningField = request.model.compatibility?.reasoningField;
364
+ const mistral = ["mistral", "devstral", "codestral", "pixtral", "mixtral"].some((family) => modelID.includes(family));
365
+ const lowering = {
366
+ ...options,
367
+ providerMetadataKey: request.model.route.providerMetadataKey ?? String(request.model.provider),
368
+ toolCallID: (id) => {
369
+ if (mistral)
370
+ return id
371
+ .replace(/[^a-zA-Z0-9]/g, "")
372
+ .slice(0, 9)
373
+ .padEnd(9, "0");
374
+ if (modelID.includes("claude"))
375
+ return id.replace(/[^a-zA-Z0-9_-]/g, "_");
376
+ if (request.model.provider === "openai" || request.model.provider === "azure" || modelID.startsWith("openai/"))
377
+ return id.slice(0, 40);
378
+ return id;
379
+ },
380
+ };
381
+ const requireAssistantAfterTool = request.model.compatibility?.requireAssistantAfterTool ?? mistral;
382
+ const bridgeTools = () => {
383
+ if (requireAssistantAfterTool && messages.at(-1)?.role === "tool")
384
+ messages.push({ role: "assistant", content: "Done." });
385
+ };
386
+ const pendingImages = [];
387
+ const flushImages = () => {
388
+ if (pendingImages.length === 0)
389
+ return;
390
+ bridgeTools();
391
+ messages.push({ role: "user", content: pendingImages.splice(0) });
392
+ };
393
+ for (const message of request.messages) {
394
+ if (message.role === "user")
395
+ bridgeTools();
396
+ if (message.role === "system") {
397
+ const part = yield* ProviderShared.wrappedSystemUpdate("OpenAI Chat", message);
398
+ if (pendingImages.length > 0) {
399
+ messages.push({
400
+ role: "user",
401
+ content: [
402
+ ...pendingImages.splice(0),
403
+ { type: "text", text: part.text, cache_control: options.cacheControl?.(part.cache) },
404
+ ],
405
+ });
406
+ continue;
407
+ }
408
+ const previous = messages.at(-1);
409
+ if (previous?.role === "user" && typeof previous.content === "string")
410
+ messages[messages.length - 1] = options.cacheControl?.(part.cache)
411
+ ? {
412
+ role: "user",
413
+ content: [
414
+ { type: "text", text: previous.content },
415
+ { type: "text", text: part.text, cache_control: options.cacheControl(part.cache) },
416
+ ],
417
+ }
418
+ : { role: "user", content: `${previous.content}\n${part.text}` };
419
+ else if (previous?.role === "user" && Array.isArray(previous.content))
420
+ messages[messages.length - 1] = {
421
+ role: "user",
422
+ content: [
423
+ ...previous.content,
424
+ { type: "text", text: part.text, cache_control: options.cacheControl?.(part.cache) },
425
+ ],
426
+ };
427
+ else
428
+ messages.push(options.cacheControl?.(part.cache)
429
+ ? {
430
+ role: "user",
431
+ content: [{ type: "text", text: part.text, cache_control: options.cacheControl(part.cache) }],
432
+ }
433
+ : { role: "user", content: part.text });
434
+ continue;
435
+ }
436
+ if (message.role === "assistant" &&
437
+ message.content.every((part) => part.type === "text" && part.text.trim() === ""))
438
+ continue;
439
+ if (message.role === "tool") {
440
+ const lowered = yield* lowerToolMessages(message, lowering);
441
+ messages.push(...lowered.messages);
442
+ pendingImages.push(...lowered.images);
443
+ continue;
444
+ }
445
+ flushImages();
446
+ messages.push(...(yield* lowerMessage(message, reasoningField, requireReasoning, lowering)));
447
+ }
448
+ flushImages();
449
+ return messages;
450
+ });
451
+ // Anthropic via LiteLLM and Amazon Bedrock require `tools` to be present
452
+ // whenever the conversation history contains tool calls/results. Send an
453
+ // explicit empty array when we have history but no active tools.
454
+ const hasToolHistory = (messages) => {
455
+ for (const message of messages) {
456
+ if (message.role === "tool")
457
+ return true;
458
+ if (message.role === "assistant" && message.content.some((part) => part.type === "tool-call"))
459
+ return true;
460
+ }
461
+ return false;
462
+ };
463
+ // Derive `max_tokens` vs `max_completion_tokens` from provider/baseURL when
464
+ // explicit `compatibility.maxTokensField` is not set. Aligned with
465
+ // models.dev provider naming: DeepSeek, Moonshot AI, Together AI, ZAI
466
+ // (Zhipu + Coding Plan variants), Nvidia, Cerebras, Chutes, etc. still
467
+ // require `max_tokens`.
468
+ const detectMaxTokensField = (provider, baseURL) => {
469
+ const p = provider.toLowerCase();
470
+ const url = (baseURL ?? "").toLowerCase();
471
+ if (p === "deepseek" ||
472
+ url.includes("deepseek.com") ||
473
+ p === "moonshotai" ||
474
+ url.includes("api.moonshot.ai") ||
475
+ p === "togetherai" ||
476
+ url.includes("api.together.") ||
477
+ p === "zai" ||
478
+ p === "zai-coding-plan" ||
479
+ p === "zhipuai" ||
480
+ p === "zhipuai-coding-plan" ||
481
+ url.includes("api.z.ai") ||
482
+ url.includes("open.bigmodel.cn") ||
483
+ p === "nvidia" ||
484
+ url.includes("integrate.api.nvidia.com") ||
485
+ p === "cerebras" ||
486
+ url.includes("cerebras.ai") ||
487
+ url.includes("llm.chutes.ai") ||
488
+ p === "chutes" ||
489
+ p === "cloudflare-ai-gateway" ||
490
+ url.includes("gateway.ai.cloudflare.com") ||
491
+ p === "cloudflare-workers-ai" ||
492
+ url.includes("api.cloudflare.com"))
493
+ return "max_tokens";
494
+ return "max_completion_tokens";
495
+ };
496
+ const detectSupportsStore = (provider, baseURL) => {
497
+ const p = provider.toLowerCase();
498
+ const url = (baseURL ?? "").toLowerCase();
499
+ const isNvidia = p === "nvidia" || url.includes("integrate.api.nvidia.com");
500
+ const isMoonshot = p === "moonshotai" || p === "moonshotai-cn" || url.includes("api.moonshot.");
501
+ const isTogether = p === "togetherai" || p === "together" || url.includes("api.together.");
502
+ const isZai = p === "zai" ||
503
+ p === "zai-coding-plan" ||
504
+ p === "zhipuai" ||
505
+ p === "zhipuai-coding-plan" ||
506
+ url.includes("api.z.ai") ||
507
+ url.includes("open.bigmodel.cn");
508
+ const isDeepSeek = p === "deepseek" || url.includes("deepseek.com");
509
+ const isCerebras = p === "cerebras" || url.includes("cerebras.ai");
510
+ const isXai = p === "xai" || url.includes("api.x.ai");
511
+ const isChutes = p === "chutes" || url.includes("chutes.ai");
512
+ const isCloudflareWorkersAI = p === "cloudflare-workers-ai" || url.includes("api.cloudflare.com");
513
+ const isCloudflareAiGateway = p === "cloudflare-ai-gateway" || url.includes("gateway.ai.cloudflare.com");
514
+ const isVercelAiGateway = p === "vercel-ai-gateway" || url.includes("ai-gateway.vercel.sh") || url.includes("vercel.sh");
515
+ const isAntLing = p === "ant-ling" || url.includes("api.ant-ling.com");
516
+ const isOpencode = p === "opencode" || url.includes("opencode.ai");
517
+ const isNonStandard = isNvidia ||
518
+ isCerebras ||
519
+ isXai ||
520
+ isTogether ||
521
+ isChutes ||
522
+ isDeepSeek ||
523
+ isZai ||
524
+ isMoonshot ||
525
+ isOpencode ||
526
+ isCloudflareWorkersAI ||
527
+ isCloudflareAiGateway ||
528
+ isVercelAiGateway ||
529
+ isAntLing;
530
+ return !isNonStandard;
531
+ };
532
+ const detectSupportsUsageInStreaming = () => true;
533
+ const detectSupportsStrictMode = (provider, baseURL) => {
534
+ const p = provider.toLowerCase();
535
+ const url = (baseURL ?? "").toLowerCase();
536
+ const isMoonshot = p === "moonshotai" || p === "moonshotai-cn" || url.includes("api.moonshot.");
537
+ const isTogether = p === "togetherai" || p === "together" || url.includes("api.together.");
538
+ const isCloudflareAiGateway = p === "cloudflare-ai-gateway" || url.includes("gateway.ai.cloudflare.com");
539
+ const isNvidia = p === "nvidia" || url.includes("integrate.api.nvidia.com");
540
+ return !isMoonshot && !isTogether && !isCloudflareAiGateway && !isNvidia;
541
+ };
542
+ const detectZaiToolStream = (provider, baseURL, modelID) => {
543
+ const p = provider.toLowerCase();
544
+ const url = (baseURL ?? "").toLowerCase();
545
+ const isZai = p === "zai" ||
546
+ p === "zai-coding-plan" ||
547
+ p === "zhipuai" ||
548
+ p === "zhipuai-coding-plan" ||
549
+ url.includes("api.z.ai") ||
550
+ url.includes("open.bigmodel.cn");
551
+ if (!isZai)
552
+ return false;
553
+ const id = modelID.toLowerCase();
554
+ if (id === "glm-4.5" || id === "glm-4.5-air" || id === "glm-4.5-flash" || id === "glm-4.5v")
555
+ return false;
556
+ return true;
557
+ };
558
+ const lowerOptions = (request, supportsStore) => {
559
+ const options = OpenAIOptions.resolve(request);
560
+ const cacheKey = ProviderShared.promptCacheKey(request);
561
+ return {
562
+ ...(supportsStore && options.store !== undefined ? { store: options.store } : {}),
563
+ // For providers that support `store`, ensure stateless `store:false` is sent
564
+ // even when no explicit `providerOptions.store` was supplied, mirroring the
565
+ // native OpenAI Chat default. Non-standard providers omit `store` entirely.
566
+ ...(supportsStore && options.store === undefined ? { store: false } : {}),
567
+ ...(cacheKey ? { prompt_cache_key: cacheKey } : {}),
568
+ ...(options.reasoningEffort ? { reasoning_effort: options.reasoningEffort } : {}),
569
+ };
570
+ };
571
+ export const fromRequest = Effect.fn("OpenAIChat.fromRequest")(function* (request, options = {}) {
572
+ // `fromRequest` returns the provider body only. Endpoint, auth, framing,
573
+ // validation, and HTTP execution are composed by `Route.make`.
574
+ const reasoningField = request.model.compatibility?.reasoningField;
575
+ if (reasoningField && RESERVED_REASONING_FIELDS.has(reasoningField))
576
+ return yield* ProviderShared.invalidRequest(`OpenAI Chat reasoning field conflicts with reserved field ${reasoningField}`);
577
+ const generation = request.generation;
578
+ const toolSchemaCompatibility = request.model.compatibility?.toolSchema;
579
+ const flattened = ProviderShared.flattenToolRequest(request);
580
+ const provider = String(request.model.provider);
581
+ const baseURL = request.model.route.endpoint.baseURL;
582
+ const detectedMaxTokensField = detectMaxTokensField(provider, baseURL);
583
+ const maxTokensField = request.model.compatibility?.maxTokensField ?? detectedMaxTokensField;
584
+ const supportsStore = request.model.compatibility?.supportsStore ?? detectSupportsStore(provider, baseURL);
585
+ const supportsUsageInStreaming = request.model.compatibility?.supportsUsageInStreaming ?? detectSupportsUsageInStreaming();
586
+ const supportsStrictMode = request.model.compatibility?.supportsStrictMode ?? detectSupportsStrictMode(provider, baseURL);
587
+ const zaiToolStream = request.model.compatibility?.zaiToolStream ?? detectZaiToolStream(provider, baseURL, request.model.id);
588
+ const hasHistory = hasToolHistory(request.messages);
589
+ const hasActiveTools = flattened.tools.length > 0;
590
+ return {
591
+ model: request.model.id,
592
+ messages: yield* lowerMessages(flattened.request, options),
593
+ tools: flattened.tools.length === 0
594
+ ? hasHistory
595
+ ? []
596
+ : undefined
597
+ : flattened.tools.map((tool) => lowerTool(tool, ToolSchemaProjection.modelCompatibility(tool.inputSchema, toolSchemaCompatibility), options, supportsStrictMode)),
598
+ tool_choice: request.toolChoice ? yield* lowerToolChoice(request.toolChoice) : undefined,
599
+ stream: true,
600
+ ...(supportsUsageInStreaming ? { stream_options: { include_usage: true } } : {}),
601
+ ...(zaiToolStream && hasActiveTools ? { tool_stream: true } : {}),
602
+ ...(maxTokensField === "max_completion_tokens"
603
+ ? { max_completion_tokens: generation?.maxTokens }
604
+ : { max_tokens: generation?.maxTokens }),
605
+ temperature: generation?.temperature,
606
+ top_p: generation?.topP,
607
+ frequency_penalty: generation?.frequencyPenalty,
608
+ presence_penalty: generation?.presencePenalty,
609
+ seed: generation?.seed,
610
+ stop: generation?.stop,
611
+ ...lowerOptions(request, supportsStore),
612
+ };
613
+ });
614
+ // =============================================================================
615
+ // Stream Parsing
616
+ // =============================================================================
617
+ // Streaming parsers are small state machines: every event returns a new state
618
+ // plus the common `LLMEvent`s produced by that event. Tool calls are accumulated
619
+ // because OpenAI streams JSON arguments across multiple deltas.
620
+ const mapFinishReason = Effect.fn("OpenAIChat.mapFinishReason")(function* (event, reason) {
621
+ switch (reason) {
622
+ case "error":
623
+ return yield* new AIError({
624
+ reason: new UnknownProviderError({
625
+ message: "Provider reported an error (finish_reason: error)",
626
+ body: ProviderShared.encodeJson(event),
627
+ }),
628
+ });
629
+ case "network_error":
630
+ return yield* new AIError({
631
+ reason: new ProviderInternalError({
632
+ message: "Provider reported a network error (finish_reason: network_error)",
633
+ body: ProviderShared.encodeJson(event),
634
+ }),
635
+ });
636
+ case "stop":
637
+ case "end":
638
+ return "stop";
639
+ case "length":
640
+ return "length";
641
+ case "content_filter":
642
+ return "content-filter";
643
+ case "function_call":
644
+ case "tool_calls":
645
+ return "tool-calls";
646
+ default:
647
+ return yield* new AIError({
648
+ reason: new UnknownProviderError({
649
+ message: `Provider finish_reason: ${reason}`,
650
+ body: ProviderShared.encodeJson(event),
651
+ }),
652
+ });
653
+ }
654
+ });
655
+ // OpenAI Chat reports `prompt_tokens` (inclusive total) with a
656
+ // cached-read and cache-write subsets, and `completion_tokens` (inclusive
657
+ // total) with a `reasoning_tokens` subset. We pass the inclusive totals
658
+ // through and derive the non-cached breakdown so the `AI.Usage` contract is
659
+ // satisfied on both sides.
660
+ // Providers differ on cache-hit location: OpenAI uses
661
+ // `prompt_tokens_details.cached_tokens`, DeepSeek uses
662
+ // `prompt_cache_hit_tokens`, and Zai uses top-level `cached_tokens`.
663
+ const mapUsage = (usage, providerMetadataKey) => {
664
+ if (!usage)
665
+ return undefined;
666
+ const input = usage.prompt_tokens ?? undefined;
667
+ const output = usage.completion_tokens ?? undefined;
668
+ const cached = (usage.prompt_tokens_details?.cached_tokens ??
669
+ usage.prompt_cache_hit_tokens ??
670
+ usage.cached_tokens ??
671
+ undefined);
672
+ const cacheWrite = usage.prompt_tokens_details?.cache_write_tokens ?? undefined;
673
+ const reasoning = usage.completion_tokens_details?.reasoning_tokens ?? undefined;
674
+ const nonCached = ProviderShared.subtractTokens(input, ProviderShared.sumTokens(cached, cacheWrite));
675
+ return new Usage({
676
+ inputTokens: input,
677
+ outputTokens: output,
678
+ nonCachedInputTokens: nonCached,
679
+ cacheReadInputTokens: cached,
680
+ cacheWriteInputTokens: cacheWrite,
681
+ reasoningTokens: reasoning,
682
+ totalTokens: ProviderShared.totalTokens(input, output, usage.total_tokens ?? undefined),
683
+ providerMetadata: { [providerMetadataKey]: usage },
684
+ });
685
+ };
686
+ const toolIndexByID = (tools, pendingTools, id) => {
687
+ if (!id)
688
+ return undefined;
689
+ const entry = Object.entries({ ...pendingTools, ...tools }).find(([, tool]) => tool?.id === id);
690
+ return entry ? Number(entry[0]) : undefined;
691
+ };
692
+ const reasoningDelta = (delta, configuredField) => {
693
+ if (!delta)
694
+ return undefined;
695
+ const fields = new Set([configuredField, "reasoning_content", "reasoning", "reasoning_text"]);
696
+ for (const field of fields) {
697
+ if (field === undefined)
698
+ continue;
699
+ const text = delta[field];
700
+ if (typeof text === "string" && text.length > 0)
701
+ return { field, text };
702
+ }
703
+ return undefined;
704
+ };
705
+ const detailText = (details) => {
706
+ const text = details.flatMap((detail) => {
707
+ if (!isRecord(detail))
708
+ return [];
709
+ if (detail.type === "reasoning.text" && typeof detail.text === "string" && detail.text)
710
+ return [detail.text];
711
+ if (detail.type === "reasoning.summary" && typeof detail.summary === "string" && detail.summary)
712
+ return [detail.summary];
713
+ return [];
714
+ });
715
+ if (text.length > 0)
716
+ return text.join("");
717
+ };
718
+ const appendReasoningDetails = (result, details) => {
719
+ for (const detail of details) {
720
+ const previous = result.at(-1);
721
+ if (!isRecord(previous) ||
722
+ previous.type !== "reasoning.text" ||
723
+ !isRecord(detail) ||
724
+ detail.type !== "reasoning.text" ||
725
+ conflictingReasoningTextDetails(previous, detail)) {
726
+ result.push(detail);
727
+ continue;
728
+ }
729
+ result[result.length - 1] = {
730
+ ...previous,
731
+ ...Object.fromEntries(Object.entries(detail).filter((entry) => entry[1] !== undefined)),
732
+ text: `${typeof previous.text === "string" ? previous.text : ""}${typeof detail.text === "string" ? detail.text : ""}`,
733
+ signature: mergeDetailValue(previous.signature, detail.signature),
734
+ format: mergeDetailValue(previous.format, detail.format),
735
+ };
736
+ }
737
+ };
738
+ const mergeDetailValue = (previous, current) => previous || current || (previous !== undefined ? previous : current);
739
+ const conflictingReasoningTextDetails = (previous, current) => conflictingDetailValue(previous.id, current.id) ||
740
+ conflictingDetailValue(previous.index, current.index) ||
741
+ conflictingDetailValue(previous.format, current.format) ||
742
+ (Boolean(previous.signature) && Boolean(current.signature) && previous.signature !== current.signature);
743
+ const conflictingDetailValue = (previous, current) => previous !== undefined && previous !== null && current !== undefined && current !== null && previous !== current;
744
+ const reasoningMetadata = (providerMetadataKey, field, details) => ({
745
+ [providerMetadataKey]: {
746
+ ...(field ? { reasoningField: field } : {}),
747
+ ...(details ? { reasoningDetails: details } : {}),
748
+ },
749
+ });
750
+ const step = (state, event) => Effect.gen(function* () {
751
+ if (event.error) {
752
+ const body = ProviderShared.encodeJson(event);
753
+ return yield* new AIError({
754
+ reason: classifyProviderFailure({
755
+ message: event.error.message,
756
+ status: typeof event.error.code === "number" ? event.error.code : undefined,
757
+ rawBody: body,
758
+ }),
759
+ });
760
+ }
761
+ const events = [];
762
+ const choice = event.choices?.[0];
763
+ // Moonshot (and a few other OpenAI-compatible providers) attach usage to
764
+ // `choice.usage` instead of the top-level `usage` field.
765
+ const choiceUsage = choice?.usage;
766
+ const usage = mapUsage(event.usage, state.providerMetadataKey) ??
767
+ (choiceUsage ? mapUsage(choiceUsage, state.providerMetadataKey) : undefined) ??
768
+ state.usage;
769
+ const rawFinishReason = choice?.finish_reason;
770
+ const finishReason = rawFinishReason
771
+ ? {
772
+ normalized: yield* mapFinishReason(event, rawFinishReason),
773
+ raw: choice?.native_finish_reason ?? rawFinishReason,
774
+ }
775
+ : state.finishReason;
776
+ const delta = choice?.delta;
777
+ const toolDeltas = delta?.tool_calls ?? [];
778
+ let tools = state.tools;
779
+ let pendingTools = state.pendingTools;
780
+ let latestToolIndex = state.latestToolIndex;
781
+ let nextToolIndex = state.nextToolIndex;
782
+ let lifecycle = state.lifecycle;
783
+ const reasoning = reasoningDelta(delta, state.reasoningField);
784
+ const hasLateContent = Boolean(delta?.content) ||
785
+ Boolean(delta?.refusal) ||
786
+ reasoning !== undefined ||
787
+ (Array.isArray(delta?.reasoning_details) && delta.reasoning_details.length > 0) ||
788
+ toolDeltas.some((tool) => Boolean(tool.id) || Boolean(tool.function?.name) || Boolean(tool.function?.arguments));
789
+ if (state.finishReason !== undefined) {
790
+ if (hasLateContent)
791
+ return yield* ProviderShared.eventError(ADAPTER, "OpenAI Chat received content after the finish reason", ProviderShared.encodeJson(event));
792
+ return [{ ...state, usage }, events];
793
+ }
794
+ const reasoningField = state.reasoningField ?? reasoning?.field;
795
+ const detailDelta = Array.isArray(delta?.reasoning_details) ? delta.reasoning_details : undefined;
796
+ if (detailDelta !== undefined)
797
+ appendReasoningDetails(state.reasoningDetails, detailDelta);
798
+ const reasoningDetailsObserved = state.reasoningDetailsObserved || detailDelta !== undefined;
799
+ const deltaMetadata = reasoningMetadata(state.providerMetadataKey, reasoningField);
800
+ const text = detailDelta?.length ? (detailText(detailDelta) ?? reasoning?.text) : reasoning?.text;
801
+ if (text !== undefined)
802
+ lifecycle = Lifecycle.reasoningDelta(lifecycle, events, "reasoning-0", text, deltaMetadata);
803
+ else if (reasoningDetailsObserved &&
804
+ !lifecycle.reasoning.has("reasoning-0") &&
805
+ (Boolean(delta?.content) || Boolean(delta?.refusal) || toolDeltas.length > 0))
806
+ lifecycle = Lifecycle.reasoningStart(lifecycle, events, "reasoning-0", deltaMetadata);
807
+ const reasoningEmitted = state.reasoningEmitted || lifecycle.reasoning.has("reasoning-0");
808
+ // Reasoning is one response-wide channel: it stays open alongside text and
809
+ // refusal output so late reasoning deltas and details join the same block,
810
+ // and `finishEvents` closes it once with the complete metadata.
811
+ if (delta?.content)
812
+ lifecycle = Lifecycle.textDelta(lifecycle, events, "text-0", delta.content);
813
+ if (delta?.refusal)
814
+ lifecycle = Lifecycle.textDelta(lifecycle, events, "text-0", delta.refusal);
815
+ // Compatible providers may omit indexes. Prefer durable identity, then use
816
+ // batch position for parallel deltas or the latest call for sparse chunks.
817
+ for (const [position, tool] of toolDeltas.entries()) {
818
+ const matched = toolIndexByID(tools, pendingTools, tool.id || undefined);
819
+ const fallback = toolDeltas.length > 1 ? position : (latestToolIndex ?? position);
820
+ const fallbackTool = tools[fallback] ?? pendingTools[fallback];
821
+ const index = tool.index ?? matched ?? (tool.id && fallbackTool?.id && fallbackTool.id !== tool.id ? nextToolIndex : fallback);
822
+ const current = tools[index];
823
+ const pending = pendingTools[index];
824
+ const id = current?.id ?? pending?.id ?? (tool.id || undefined);
825
+ const name = current?.name ?? pending?.name ?? (tool.function?.name || undefined);
826
+ const text = `${pending?.input ?? ""}${tool.function?.arguments ?? ""}`;
827
+ latestToolIndex = index;
828
+ nextToolIndex = Math.max(nextToolIndex, index + 1);
829
+ if (!current && (!id || !name)) {
830
+ pendingTools = {
831
+ ...pendingTools,
832
+ [index]: { id: id || undefined, name: name || undefined, input: text },
833
+ };
834
+ continue;
835
+ }
836
+ if (pending) {
837
+ pendingTools = { ...pendingTools };
838
+ delete pendingTools[index];
839
+ }
840
+ const result = ToolStream.appendOrStart(ADAPTER, tools, index, { id: id || undefined, name: name || undefined, text }, "OpenAI Chat tool call delta is missing id or name");
841
+ if (ToolStream.isError(result))
842
+ return yield* new AIError({
843
+ reason: AIErrorReason.make({
844
+ ...result.reason,
845
+ message: result.message,
846
+ cause: result.reason.cause,
847
+ body: ProviderShared.encodeJson(event),
848
+ }),
849
+ });
850
+ tools = result.tools;
851
+ if (result.events.length)
852
+ lifecycle = Lifecycle.stepStart(lifecycle, events);
853
+ events.push(...result.events);
854
+ }
855
+ const incompleteTools = finishReason?.normalized === "content-filter" || finishReason?.normalized === "length";
856
+ if (finishReason !== undefined &&
857
+ !incompleteTools &&
858
+ state.finishReason === undefined &&
859
+ Object.keys(pendingTools).length)
860
+ return yield* ProviderShared.eventError(ADAPTER, "OpenAI Chat tool call delta is missing id or name", ProviderShared.encodeJson(event));
861
+ // Filtering or truncation terminates the response without confirming pending tool calls.
862
+ const finished = finishReason !== undefined &&
863
+ !incompleteTools &&
864
+ state.finishReason === undefined &&
865
+ Object.keys(tools).length > 0
866
+ ? yield* ToolStream.finishAll(ADAPTER, tools)
867
+ : undefined;
868
+ return [
869
+ {
870
+ providerMetadataKey: state.providerMetadataKey,
871
+ tools: finished?.tools ?? tools,
872
+ pendingTools,
873
+ toolCallEvents: finished?.events ?? state.toolCallEvents,
874
+ usage,
875
+ finishReason,
876
+ lifecycle,
877
+ reasoningField,
878
+ reasoningDetails: state.reasoningDetails,
879
+ reasoningDetailsObserved,
880
+ reasoningEmitted,
881
+ latestToolIndex,
882
+ nextToolIndex,
883
+ requireFinishReason: state.requireFinishReason,
884
+ },
885
+ events,
886
+ ];
887
+ });
888
+ const finishEvents = Effect.fn("OpenAIChat.finishEvents")(function* (state) {
889
+ if (state.finishReason === undefined && state.requireFinishReason)
890
+ return yield* new AIError({
891
+ reason: new InvalidProviderOutputError({
892
+ message: "OpenAI Chat stream ended without finish_reason",
893
+ classification: "incomplete-stream",
894
+ route: ADAPTER,
895
+ }),
896
+ });
897
+ const events = [];
898
+ const toolCallEvents = state.finishReason === undefined && Object.keys(state.tools).length > 0
899
+ ? (yield* ToolStream.finishAll(ADAPTER, state.tools)).events
900
+ : state.toolCallEvents;
901
+ const hasToolCalls = toolCallEvents.length > 0;
902
+ const reason = state.finishReason
903
+ ? {
904
+ ...state.finishReason,
905
+ normalized: state.finishReason.normalized === "stop" && hasToolCalls ? "tool-calls" : state.finishReason.normalized,
906
+ }
907
+ : { normalized: hasToolCalls ? "tool-calls" : "stop" };
908
+ // Snapshot details at publish time so the emitted event never observes later
909
+ // mutation of the accumulated `reasoningDetails` array.
910
+ const metadata = reasoningMetadata(state.providerMetadataKey, state.reasoningField, state.reasoningDetailsObserved ? [...state.reasoningDetails] : undefined);
911
+ const started = state.reasoningDetailsObserved && !state.reasoningEmitted
912
+ ? Lifecycle.reasoningStart(state.lifecycle, events, "reasoning-0", reasoningMetadata(state.providerMetadataKey, state.reasoningField))
913
+ : state.lifecycle;
914
+ const ended = Lifecycle.reasoningEnd(started, events, "reasoning-0", metadata);
915
+ const lifecycle = toolCallEvents.length ? Lifecycle.stepStart(ended, events) : ended;
916
+ events.push(...toolCallEvents);
917
+ Lifecycle.finish(lifecycle, events, { reason, usage: state.usage });
918
+ return events;
919
+ });
920
+ // =============================================================================
921
+ // Protocol And OpenAI Route
922
+ // =============================================================================
923
+ /**
924
+ * The OpenAI Chat protocol — request body construction, body schema, and the
925
+ * streaming-event state machine. Reused by every route that speaks OpenAI Chat
926
+ * over HTTP+SSE: native OpenAI, DeepSeek, TogetherAI, Cerebras, Baseten,
927
+ * Fireworks, DeepInfra, and (once added) Azure OpenAI Chat.
928
+ */
929
+ export const protocol = Protocol.make({
930
+ id: ADAPTER,
931
+ body: {
932
+ schema: OpenAIChatBody,
933
+ from: fromRequest,
934
+ },
935
+ stream: {
936
+ event: OpenAIChatStreamEvent,
937
+ initial: (request) => ({
938
+ providerMetadataKey: request.model.route.providerMetadataKey ?? String(request.model.provider),
939
+ tools: ToolStream.empty(),
940
+ pendingTools: {},
941
+ toolCallEvents: [],
942
+ lifecycle: Lifecycle.initial(),
943
+ reasoningField: request.model.compatibility?.reasoningField,
944
+ reasoningDetails: [],
945
+ reasoningDetailsObserved: false,
946
+ reasoningEmitted: false,
947
+ nextToolIndex: 0,
948
+ requireFinishReason: request.model.compatibility?.requireFinishReason ?? true,
949
+ }),
950
+ step: (state, event) => (event === DONE ? Effect.succeed([state, []]) : step(state, event)),
951
+ terminal: (event) => event === DONE,
952
+ onHalt: finishEvents,
953
+ },
954
+ });
955
+ export const framing = Framing.sseWithDone;
956
+ export const httpTransport = HttpTransport.sseJson.with().with({ framing });
957
+ export const route = Route.make({
958
+ id: ADAPTER,
959
+ provider: "openai",
960
+ providerMetadataKey: "openai",
961
+ protocol,
962
+ endpoint: Endpoint.path(PATH, { baseURL: DEFAULT_BASE_URL }),
963
+ auth: Auth.none,
964
+ transport: httpTransport,
965
+ });
966
+ export * as OpenAIChat from "./openai-chat.js";