@opencode-ai/ai 0.0.0-bootstrap.0 → 0.0.0-next-15618

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 (151) hide show
  1. package/README.md +176 -1
  2. package/dist/cache-policy.d.ts +2 -0
  3. package/dist/cache-policy.js +108 -0
  4. package/dist/index.d.ts +14 -0
  5. package/dist/index.js +9 -0
  6. package/dist/llm.d.ts +223 -0
  7. package/dist/llm.js +81 -0
  8. package/dist/protocols/anthropic-messages.d.ts +503 -0
  9. package/dist/protocols/anthropic-messages.js +745 -0
  10. package/dist/protocols/bedrock-converse.d.ts +476 -0
  11. package/dist/protocols/bedrock-converse.js +526 -0
  12. package/dist/protocols/bedrock-event-stream.d.ts +9 -0
  13. package/dist/protocols/bedrock-event-stream.js +64 -0
  14. package/dist/protocols/gemini.d.ts +220 -0
  15. package/dist/protocols/gemini.js +420 -0
  16. package/dist/protocols/google-vertex-anthropic.d.ts +494 -0
  17. package/dist/protocols/google-vertex-anthropic.js +33 -0
  18. package/dist/protocols/google-vertex-gemini.d.ts +59 -0
  19. package/dist/protocols/google-vertex-gemini.js +18 -0
  20. package/dist/protocols/index.d.ts +9 -0
  21. package/dist/protocols/index.js +9 -0
  22. package/dist/protocols/openai-chat.d.ts +391 -0
  23. package/dist/protocols/openai-chat.js +423 -0
  24. package/dist/protocols/openai-compatible-chat.d.ts +73 -0
  25. package/dist/protocols/openai-compatible-chat.js +20 -0
  26. package/dist/protocols/openai-compatible-responses.d.ts +85 -0
  27. package/dist/protocols/openai-compatible-responses.js +18 -0
  28. package/dist/protocols/openai-responses.d.ts +613 -0
  29. package/dist/protocols/openai-responses.js +828 -0
  30. package/dist/protocols/shared.d.ts +192 -0
  31. package/dist/protocols/shared.js +236 -0
  32. package/dist/protocols/utils/bedrock-auth.d.ts +18 -0
  33. package/dist/protocols/utils/bedrock-auth.js +41 -0
  34. package/dist/protocols/utils/bedrock-cache.d.ts +15 -0
  35. package/dist/protocols/utils/bedrock-cache.js +29 -0
  36. package/dist/protocols/utils/bedrock-media.d.ts +49 -0
  37. package/dist/protocols/utils/bedrock-media.js +68 -0
  38. package/dist/protocols/utils/cache.d.ts +6 -0
  39. package/dist/protocols/utils/cache.js +8 -0
  40. package/dist/protocols/utils/gemini-tool-schema.d.ts +2 -0
  41. package/dist/protocols/utils/gemini-tool-schema.js +83 -0
  42. package/dist/protocols/utils/lifecycle.d.ts +19 -0
  43. package/dist/protocols/utils/lifecycle.js +65 -0
  44. package/dist/protocols/utils/openai-options.d.ts +22 -0
  45. package/dist/protocols/utils/openai-options.js +64 -0
  46. package/dist/protocols/utils/tool-schema.d.ts +7 -0
  47. package/dist/protocols/utils/tool-schema.js +79 -0
  48. package/dist/protocols/utils/tool-stream.d.ts +155 -0
  49. package/dist/protocols/utils/tool-stream.js +136 -0
  50. package/dist/protocols.d.ts +1 -0
  51. package/dist/protocols.js +1 -0
  52. package/dist/provider-error.d.ts +13 -0
  53. package/dist/provider-error.js +122 -0
  54. package/dist/provider-package.d.ts +13 -0
  55. package/dist/provider-package.js +1 -0
  56. package/dist/provider.d.ts +23 -0
  57. package/dist/provider.js +2 -0
  58. package/dist/providers/amazon-bedrock.d.ts +150 -0
  59. package/dist/providers/amazon-bedrock.js +41 -0
  60. package/dist/providers/anthropic-compatible.d.ts +183 -0
  61. package/dist/providers/anthropic-compatible.js +44 -0
  62. package/dist/providers/anthropic.d.ts +181 -0
  63. package/dist/providers/anthropic.js +39 -0
  64. package/dist/providers/azure/chat.d.ts +2 -0
  65. package/dist/providers/azure/chat.js +1 -0
  66. package/dist/providers/azure/responses.d.ts +2 -0
  67. package/dist/providers/azure/responses.js +1 -0
  68. package/dist/providers/azure.d.ts +183 -0
  69. package/dist/providers/azure.js +89 -0
  70. package/dist/providers/cloudflare.d.ts +232 -0
  71. package/dist/providers/cloudflare.js +87 -0
  72. package/dist/providers/github-copilot.d.ts +167 -0
  73. package/dist/providers/github-copilot.js +47 -0
  74. package/dist/providers/google-vertex/anthropic.d.ts +2 -0
  75. package/dist/providers/google-vertex/anthropic.js +1 -0
  76. package/dist/providers/google-vertex-anthropic.d.ts +181 -0
  77. package/dist/providers/google-vertex-anthropic.js +46 -0
  78. package/dist/providers/google-vertex-shared.d.ts +22 -0
  79. package/dist/providers/google-vertex-shared.js +61 -0
  80. package/dist/providers/google-vertex.d.ts +93 -0
  81. package/dist/providers/google-vertex.js +49 -0
  82. package/dist/providers/google.d.ts +85 -0
  83. package/dist/providers/google.js +33 -0
  84. package/dist/providers/index.d.ts +15 -0
  85. package/dist/providers/index.js +15 -0
  86. package/dist/providers/openai/chat.d.ts +2 -0
  87. package/dist/providers/openai/chat.js +1 -0
  88. package/dist/providers/openai/responses.d.ts +2 -0
  89. package/dist/providers/openai/responses.js +1 -0
  90. package/dist/providers/openai-compatible/responses.d.ts +1 -0
  91. package/dist/providers/openai-compatible/responses.js +1 -0
  92. package/dist/providers/openai-compatible-profile.d.ts +43 -0
  93. package/dist/providers/openai-compatible-profile.js +12 -0
  94. package/dist/providers/openai-compatible-responses.d.ts +107 -0
  95. package/dist/providers/openai-compatible-responses.js +33 -0
  96. package/dist/providers/openai-compatible.d.ts +130 -0
  97. package/dist/providers/openai-compatible.js +55 -0
  98. package/dist/providers/openai-options.d.ts +30 -0
  99. package/dist/providers/openai-options.js +42 -0
  100. package/dist/providers/openai.d.ts +264 -0
  101. package/dist/providers/openai.js +62 -0
  102. package/dist/providers/openrouter.d.ts +321 -0
  103. package/dist/providers/openrouter.js +65 -0
  104. package/dist/providers/xai.d.ts +169 -0
  105. package/dist/providers/xai.js +43 -0
  106. package/dist/providers.d.ts +1 -0
  107. package/dist/providers.js +1 -0
  108. package/dist/route/auth-options.d.ts +34 -0
  109. package/dist/route/auth-options.js +14 -0
  110. package/dist/route/auth.d.ts +49 -0
  111. package/dist/route/auth.js +89 -0
  112. package/dist/route/client.d.ts +330 -0
  113. package/dist/route/client.js +211 -0
  114. package/dist/route/endpoint.d.ts +28 -0
  115. package/dist/route/endpoint.js +21 -0
  116. package/dist/route/executor.d.ts +12 -0
  117. package/dist/route/executor.js +229 -0
  118. package/dist/route/framing.d.ts +23 -0
  119. package/dist/route/framing.js +4 -0
  120. package/dist/route/index.d.ts +16 -0
  121. package/dist/route/index.js +9 -0
  122. package/dist/route/protocol.d.ts +76 -0
  123. package/dist/route/protocol.js +17 -0
  124. package/dist/route/transport/http.d.ts +33 -0
  125. package/dist/route/transport/http.js +98 -0
  126. package/dist/route/transport/index.d.ts +27 -0
  127. package/dist/route/transport/index.js +4 -0
  128. package/dist/route/transport/websocket.d.ts +56 -0
  129. package/dist/route/transport/websocket.js +181 -0
  130. package/dist/route.d.ts +1 -0
  131. package/dist/route.js +1 -0
  132. package/dist/schema/errors.d.ts +153 -0
  133. package/dist/schema/errors.js +150 -0
  134. package/dist/schema/events.d.ts +4292 -0
  135. package/dist/schema/events.js +482 -0
  136. package/dist/schema/ids.d.ts +30 -0
  137. package/dist/schema/ids.js +18 -0
  138. package/dist/schema/index.d.ts +5 -0
  139. package/dist/schema/index.js +5 -0
  140. package/dist/schema/messages.d.ts +385 -0
  141. package/dist/schema/messages.js +246 -0
  142. package/dist/schema/options.d.ts +143 -0
  143. package/dist/schema/options.js +195 -0
  144. package/dist/tool-runtime.d.ts +15 -0
  145. package/dist/tool-runtime.js +35 -0
  146. package/dist/tool.d.ts +134 -0
  147. package/dist/tool.js +66 -0
  148. package/dist/utils/record.d.ts +2 -0
  149. package/dist/utils/record.js +2 -0
  150. package/package.json +36 -9
  151. package/index.js +0 -1
@@ -0,0 +1,423 @@
1
+ import { Effect, Schema } from "effect";
2
+ import { Route } from "../route/client";
3
+ import { Auth } from "../route/auth";
4
+ import { Endpoint } from "../route/endpoint";
5
+ import { HttpTransport } from "../route/transport";
6
+ import { Protocol } from "../route/protocol";
7
+ import { LLMEvent, Usage, } from "../schema";
8
+ import { isRecord, JsonObject, optionalArray, optionalNull, ProviderShared } from "./shared";
9
+ import { OpenAIOptions } from "./utils/openai-options";
10
+ import { Lifecycle } from "./utils/lifecycle";
11
+ import { ToolSchemaProjection } from "./utils/tool-schema";
12
+ import { ToolStream } from "./utils/tool-stream";
13
+ const ADAPTER = "openai-chat";
14
+ const IMAGE_MIMES = new Set(ProviderShared.IMAGE_MIMES);
15
+ export const DEFAULT_BASE_URL = "https://api.openai.com/v1";
16
+ export const PATH = "/chat/completions";
17
+ // =============================================================================
18
+ // Request Body Schema
19
+ // =============================================================================
20
+ // The body schema is the provider-native JSON body. `fromRequest` below builds
21
+ // this shape from the common `LLMRequest`, then `Route.make` validates and
22
+ // JSON-encodes it before transport.
23
+ const OpenAIChatFunction = Schema.Struct({
24
+ name: Schema.String,
25
+ description: Schema.String,
26
+ parameters: JsonObject,
27
+ });
28
+ const OpenAIChatTool = Schema.Struct({
29
+ type: Schema.tag("function"),
30
+ function: OpenAIChatFunction,
31
+ });
32
+ const OpenAIChatAssistantToolCall = Schema.Struct({
33
+ id: Schema.String,
34
+ type: Schema.tag("function"),
35
+ function: Schema.Struct({
36
+ name: Schema.String,
37
+ arguments: Schema.String,
38
+ }),
39
+ });
40
+ const OpenAIChatUserContent = Schema.Union([
41
+ Schema.Struct({ type: Schema.Literal("text"), text: Schema.String }),
42
+ Schema.Struct({
43
+ type: Schema.Literal("image_url"),
44
+ image_url: Schema.Struct({ url: Schema.String }),
45
+ }),
46
+ ]);
47
+ const OpenAIChatMessage = Schema.Union([
48
+ Schema.Struct({ role: Schema.Literal("system"), content: Schema.String }),
49
+ Schema.Struct({
50
+ role: Schema.Literal("user"),
51
+ content: Schema.Union([Schema.String, Schema.Array(OpenAIChatUserContent)]),
52
+ }),
53
+ Schema.Struct({
54
+ role: Schema.Literal("assistant"),
55
+ content: Schema.NullOr(Schema.String),
56
+ tool_calls: optionalArray(OpenAIChatAssistantToolCall),
57
+ reasoning_content: Schema.optional(Schema.String),
58
+ }),
59
+ Schema.Struct({ role: Schema.Literal("tool"), tool_call_id: Schema.String, content: Schema.String }),
60
+ ]).pipe(Schema.toTaggedUnion("role"));
61
+ const OpenAIChatToolChoice = Schema.Union([
62
+ Schema.Literals(["auto", "none", "required"]),
63
+ Schema.Struct({
64
+ type: Schema.tag("function"),
65
+ function: Schema.Struct({ name: Schema.String }),
66
+ }),
67
+ ]);
68
+ export const bodyFields = {
69
+ model: Schema.String,
70
+ messages: Schema.Array(OpenAIChatMessage),
71
+ tools: optionalArray(OpenAIChatTool),
72
+ tool_choice: Schema.optional(OpenAIChatToolChoice),
73
+ stream: Schema.Literal(true),
74
+ stream_options: Schema.optional(Schema.Struct({ include_usage: Schema.Boolean })),
75
+ store: Schema.optional(Schema.Boolean),
76
+ reasoning_effort: Schema.optional(OpenAIOptions.OpenAIReasoningEffort),
77
+ max_tokens: Schema.optional(Schema.Number),
78
+ temperature: Schema.optional(Schema.Number),
79
+ top_p: Schema.optional(Schema.Number),
80
+ frequency_penalty: Schema.optional(Schema.Number),
81
+ presence_penalty: Schema.optional(Schema.Number),
82
+ seed: Schema.optional(Schema.Number),
83
+ stop: optionalArray(Schema.String),
84
+ };
85
+ const OpenAIChatBody = Schema.Struct(bodyFields);
86
+ // =============================================================================
87
+ // Streaming Event Schema
88
+ // =============================================================================
89
+ // The event schema is one decoded SSE `data:` payload. `Framing.sse` splits the
90
+ // byte stream into strings, then `Protocol.jsonEvent` decodes each string into
91
+ // this provider-native event shape.
92
+ const OpenAIChatUsage = Schema.Struct({
93
+ prompt_tokens: Schema.optional(Schema.Number),
94
+ completion_tokens: Schema.optional(Schema.Number),
95
+ total_tokens: Schema.optional(Schema.Number),
96
+ prompt_tokens_details: optionalNull(Schema.Struct({
97
+ cached_tokens: Schema.optional(Schema.Number),
98
+ })),
99
+ completion_tokens_details: optionalNull(Schema.Struct({
100
+ reasoning_tokens: Schema.optional(Schema.Number),
101
+ })),
102
+ });
103
+ const OpenAIChatToolCallDeltaFunction = Schema.Struct({
104
+ name: optionalNull(Schema.String),
105
+ arguments: optionalNull(Schema.String),
106
+ });
107
+ const OpenAIChatToolCallDelta = Schema.Struct({
108
+ index: Schema.Number,
109
+ id: optionalNull(Schema.String),
110
+ function: optionalNull(OpenAIChatToolCallDeltaFunction),
111
+ });
112
+ const OpenAIChatDelta = Schema.Struct({
113
+ content: optionalNull(Schema.String),
114
+ reasoning_content: optionalNull(Schema.String),
115
+ tool_calls: optionalNull(Schema.Array(OpenAIChatToolCallDelta)),
116
+ });
117
+ const OpenAIChatChoice = Schema.Struct({
118
+ delta: optionalNull(OpenAIChatDelta),
119
+ finish_reason: optionalNull(Schema.String),
120
+ });
121
+ export const OpenAIChatEvent = Schema.Struct({
122
+ choices: Schema.Array(OpenAIChatChoice),
123
+ usage: optionalNull(OpenAIChatUsage),
124
+ });
125
+ // =============================================================================
126
+ // Request Lowering
127
+ // =============================================================================
128
+ // Lowering is the only place that knows how common LLM messages map onto the
129
+ // OpenAI Chat wire format. Keep provider quirks here instead of leaking native
130
+ // fields into `LLMRequest`.
131
+ const lowerTool = (tool, inputSchema) => ({
132
+ type: "function",
133
+ function: {
134
+ name: tool.name,
135
+ description: tool.description,
136
+ parameters: ToolSchemaProjection.openAI(inputSchema),
137
+ },
138
+ });
139
+ const lowerToolChoice = (toolChoice) => ProviderShared.matchToolChoice("OpenAI Chat", toolChoice, {
140
+ auto: () => "auto",
141
+ none: () => "none",
142
+ required: () => "required",
143
+ tool: (name) => ({ type: "function", function: { name } }),
144
+ });
145
+ const lowerToolCall = (part) => ({
146
+ id: part.id,
147
+ type: "function",
148
+ function: {
149
+ name: part.name,
150
+ arguments: ProviderShared.encodeJson(part.input),
151
+ },
152
+ });
153
+ const lowerMedia = Effect.fn("OpenAIChat.lowerMedia")(function* (part) {
154
+ const media = yield* ProviderShared.validateMedia("OpenAI Chat", part, IMAGE_MIMES);
155
+ return { type: "image_url", image_url: { url: media.dataUrl } };
156
+ });
157
+ const openAICompatibleReasoningContent = (native) => isRecord(native) && typeof native.reasoning_content === "string" ? native.reasoning_content : undefined;
158
+ const lowerUserMessage = Effect.fn("OpenAIChat.lowerUserMessage")(function* (message) {
159
+ const content = [];
160
+ for (const part of message.content) {
161
+ if (part.type === "text") {
162
+ content.push({ type: "text", text: part.text });
163
+ continue;
164
+ }
165
+ if (part.type === "media") {
166
+ content.push(yield* lowerMedia(part));
167
+ continue;
168
+ }
169
+ return yield* ProviderShared.unsupportedContent("OpenAI Chat", "user", ["text", "media"]);
170
+ }
171
+ if (content.every((part) => part.type === "text"))
172
+ return { role: "user", content: content.map((part) => part.text).join("") };
173
+ return { role: "user", content };
174
+ });
175
+ const lowerAssistantMessage = Effect.fn("OpenAIChat.lowerAssistantMessage")(function* (message) {
176
+ const content = [];
177
+ const reasoning = [];
178
+ const toolCalls = [];
179
+ for (const part of message.content) {
180
+ if (!ProviderShared.supportsContent(part, ["text", "reasoning", "tool-call"]))
181
+ return yield* ProviderShared.unsupportedContent("OpenAI Chat", "assistant", ["text", "reasoning", "tool-call"]);
182
+ if (part.type === "text") {
183
+ content.push(part);
184
+ continue;
185
+ }
186
+ if (part.type === "reasoning") {
187
+ reasoning.push(part);
188
+ continue;
189
+ }
190
+ if (part.type === "tool-call") {
191
+ toolCalls.push(lowerToolCall(part));
192
+ continue;
193
+ }
194
+ }
195
+ return {
196
+ role: "assistant",
197
+ content: content.length === 0 ? null : ProviderShared.joinText(content),
198
+ tool_calls: toolCalls.length === 0 ? undefined : toolCalls,
199
+ reasoning_content: reasoning.length > 0
200
+ ? reasoning.map((part) => part.text).join("")
201
+ : openAICompatibleReasoningContent(message.native?.openaiCompatible),
202
+ };
203
+ });
204
+ const lowerToolMessages = Effect.fn("OpenAIChat.lowerToolMessages")(function* (message) {
205
+ const messages = [];
206
+ const images = [];
207
+ for (const part of message.content) {
208
+ if (!ProviderShared.supportsContent(part, ["tool-result"]))
209
+ return yield* ProviderShared.unsupportedContent("OpenAI Chat", "tool", ["tool-result"]);
210
+ if (part.result.type !== "content") {
211
+ messages.push({ role: "tool", tool_call_id: part.id, content: ProviderShared.toolResultText(part) });
212
+ continue;
213
+ }
214
+ const content = part.result.value;
215
+ const text = content.filter((item) => item.type === "text").map((item) => item.text);
216
+ messages.push({ role: "tool", tool_call_id: part.id, content: text.join("\n") });
217
+ const files = content.filter((item) => item.type === "file");
218
+ images.push(...(yield* Effect.forEach(files, (item) => lowerMedia({ type: "media", mediaType: item.mime, data: item.uri, filename: item.name }))));
219
+ }
220
+ return { messages, images };
221
+ });
222
+ const lowerMessage = Effect.fn("OpenAIChat.lowerMessage")(function* (message) {
223
+ if (message.role === "user")
224
+ return [yield* lowerUserMessage(message)];
225
+ if (message.role === "assistant")
226
+ return [yield* lowerAssistantMessage(message)];
227
+ return (yield* lowerToolMessages(message)).messages;
228
+ });
229
+ const lowerMessages = Effect.fn("OpenAIChat.lowerMessages")(function* (request) {
230
+ const system = request.system.length === 0 ? [] : [{ role: "system", content: ProviderShared.joinText(request.system) }];
231
+ const messages = [...system];
232
+ const pendingImages = [];
233
+ const flushImages = () => {
234
+ if (pendingImages.length === 0)
235
+ return;
236
+ messages.push({ role: "user", content: pendingImages.splice(0) });
237
+ };
238
+ for (const message of request.messages) {
239
+ if (message.role === "system") {
240
+ const part = yield* ProviderShared.wrappedSystemUpdate("OpenAI Chat", message);
241
+ if (pendingImages.length > 0) {
242
+ messages.push({ role: "user", content: [...pendingImages.splice(0), { type: "text", text: part.text }] });
243
+ continue;
244
+ }
245
+ const previous = messages.at(-1);
246
+ if (previous?.role === "user" && typeof previous.content === "string")
247
+ messages[messages.length - 1] = { role: "user", content: `${previous.content}\n${part.text}` };
248
+ else if (previous?.role === "user" && Array.isArray(previous.content))
249
+ messages[messages.length - 1] = {
250
+ role: "user",
251
+ content: [...previous.content, { type: "text", text: part.text }],
252
+ };
253
+ else
254
+ messages.push({ role: "user", content: part.text });
255
+ continue;
256
+ }
257
+ if (message.role === "tool") {
258
+ const lowered = yield* lowerToolMessages(message);
259
+ messages.push(...lowered.messages);
260
+ pendingImages.push(...lowered.images);
261
+ continue;
262
+ }
263
+ flushImages();
264
+ messages.push(...(yield* lowerMessage(message)));
265
+ }
266
+ flushImages();
267
+ return messages;
268
+ });
269
+ const lowerOptions = Effect.fn("OpenAIChat.lowerOptions")(function* (request) {
270
+ const store = OpenAIOptions.store(request);
271
+ const reasoningEffort = OpenAIOptions.reasoningEffort(request);
272
+ return {
273
+ ...(store !== undefined ? { store } : {}),
274
+ ...(reasoningEffort ? { reasoning_effort: reasoningEffort } : {}),
275
+ };
276
+ });
277
+ const fromRequest = Effect.fn("OpenAIChat.fromRequest")(function* (request) {
278
+ // `fromRequest` returns the provider body only. Endpoint, auth, framing,
279
+ // validation, and HTTP execution are composed by `Route.make`.
280
+ const generation = request.generation;
281
+ const toolSchemaCompatibility = request.model.compatibility?.toolSchema;
282
+ return {
283
+ model: request.model.id,
284
+ messages: yield* lowerMessages(request),
285
+ tools: request.tools.length === 0
286
+ ? undefined
287
+ : request.tools.map((tool) => lowerTool(tool, ToolSchemaProjection.modelCompatibility(tool.inputSchema, toolSchemaCompatibility))),
288
+ tool_choice: request.toolChoice ? yield* lowerToolChoice(request.toolChoice) : undefined,
289
+ stream: true,
290
+ stream_options: { include_usage: true },
291
+ max_tokens: generation?.maxTokens,
292
+ temperature: generation?.temperature,
293
+ top_p: generation?.topP,
294
+ frequency_penalty: generation?.frequencyPenalty,
295
+ presence_penalty: generation?.presencePenalty,
296
+ seed: generation?.seed,
297
+ stop: generation?.stop,
298
+ ...(yield* lowerOptions(request)),
299
+ };
300
+ });
301
+ // =============================================================================
302
+ // Stream Parsing
303
+ // =============================================================================
304
+ // Streaming parsers are small state machines: every event returns a new state
305
+ // plus the common `LLMEvent`s produced by that event. Tool calls are accumulated
306
+ // because OpenAI streams JSON arguments across multiple deltas.
307
+ const mapFinishReason = (reason) => {
308
+ if (reason === "stop")
309
+ return "stop";
310
+ if (reason === "length")
311
+ return "length";
312
+ if (reason === "content_filter")
313
+ return "content-filter";
314
+ if (reason === "function_call" || reason === "tool_calls")
315
+ return "tool-calls";
316
+ return "unknown";
317
+ };
318
+ // OpenAI Chat reports `prompt_tokens` (inclusive total) with a
319
+ // `cached_tokens` subset, and `completion_tokens` (inclusive total) with
320
+ // a `reasoning_tokens` subset. We pass the inclusive totals through and
321
+ // derive the non-cached breakdown so the `LLM.Usage` contract is
322
+ // satisfied on both sides.
323
+ const mapUsage = (usage) => {
324
+ if (!usage)
325
+ return undefined;
326
+ const cached = usage.prompt_tokens_details?.cached_tokens;
327
+ const reasoning = usage.completion_tokens_details?.reasoning_tokens;
328
+ const nonCached = ProviderShared.subtractTokens(usage.prompt_tokens, cached);
329
+ return new Usage({
330
+ inputTokens: usage.prompt_tokens,
331
+ outputTokens: usage.completion_tokens,
332
+ nonCachedInputTokens: nonCached,
333
+ cacheReadInputTokens: cached,
334
+ reasoningTokens: reasoning,
335
+ totalTokens: ProviderShared.totalTokens(usage.prompt_tokens, usage.completion_tokens, usage.total_tokens),
336
+ providerMetadata: { openai: usage },
337
+ });
338
+ };
339
+ const step = (state, event) => Effect.gen(function* () {
340
+ const events = [];
341
+ const usage = mapUsage(event.usage) ?? state.usage;
342
+ const choice = event.choices[0];
343
+ const finishReason = choice?.finish_reason ? mapFinishReason(choice.finish_reason) : state.finishReason;
344
+ const delta = choice?.delta;
345
+ const toolDeltas = delta?.tool_calls ?? [];
346
+ let tools = state.tools;
347
+ let lifecycle = state.lifecycle;
348
+ if (delta?.reasoning_content)
349
+ lifecycle = Lifecycle.reasoningDelta(lifecycle, events, "reasoning-0", delta.reasoning_content);
350
+ if (delta?.content) {
351
+ lifecycle = Lifecycle.reasoningEnd(lifecycle, events, "reasoning-0");
352
+ lifecycle = Lifecycle.textDelta(lifecycle, events, "text-0", delta.content);
353
+ }
354
+ if (toolDeltas.length)
355
+ lifecycle = Lifecycle.reasoningEnd(lifecycle, events, "reasoning-0");
356
+ for (const tool of toolDeltas) {
357
+ const result = ToolStream.appendOrStart(ADAPTER, tools, tool.index, { id: tool.id ?? undefined, name: tool.function?.name ?? undefined, text: tool.function?.arguments ?? "" }, "OpenAI Chat tool call delta is missing id or name");
358
+ if (ToolStream.isError(result))
359
+ return yield* result;
360
+ tools = result.tools;
361
+ if (result.events.length)
362
+ lifecycle = Lifecycle.stepStart(lifecycle, events);
363
+ events.push(...result.events);
364
+ }
365
+ // Finalize accumulated tool inputs eagerly when finish_reason arrives so
366
+ // JSON parse failures fail the stream at the boundary rather than at halt.
367
+ const finished = finishReason !== undefined && state.finishReason === undefined && Object.keys(tools).length > 0
368
+ ? yield* ToolStream.finishAll(ADAPTER, tools)
369
+ : undefined;
370
+ return [
371
+ {
372
+ tools: finished?.tools ?? tools,
373
+ toolCallEvents: finished?.events ?? state.toolCallEvents,
374
+ usage,
375
+ finishReason,
376
+ lifecycle,
377
+ },
378
+ events,
379
+ ];
380
+ });
381
+ const finishEvents = (state) => {
382
+ const events = [];
383
+ const hasToolCalls = state.toolCallEvents.length > 0;
384
+ const reason = state.finishReason === "stop" && hasToolCalls ? "tool-calls" : state.finishReason;
385
+ const lifecycle = state.toolCallEvents.length ? Lifecycle.stepStart(state.lifecycle, events) : state.lifecycle;
386
+ events.push(...state.toolCallEvents);
387
+ if (reason)
388
+ Lifecycle.finish(lifecycle, events, { reason, usage: state.usage });
389
+ return events;
390
+ };
391
+ // =============================================================================
392
+ // Protocol And OpenAI Route
393
+ // =============================================================================
394
+ /**
395
+ * The OpenAI Chat protocol — request body construction, body schema, and the
396
+ * streaming-event state machine. Reused by every route that speaks OpenAI Chat
397
+ * over HTTP+SSE: native OpenAI, DeepSeek, TogetherAI, Cerebras, Baseten,
398
+ * Fireworks, DeepInfra, and (once added) Azure OpenAI Chat.
399
+ */
400
+ export const protocol = Protocol.make({
401
+ id: ADAPTER,
402
+ body: {
403
+ schema: OpenAIChatBody,
404
+ from: fromRequest,
405
+ },
406
+ stream: {
407
+ event: Protocol.jsonEvent(OpenAIChatEvent),
408
+ initial: () => ({ tools: ToolStream.empty(), toolCallEvents: [], lifecycle: Lifecycle.initial() }),
409
+ step,
410
+ onHalt: finishEvents,
411
+ },
412
+ });
413
+ export const httpTransport = HttpTransport.sseJson.with();
414
+ export const route = Route.make({
415
+ id: ADAPTER,
416
+ provider: "openai",
417
+ providerMetadataKey: "openai",
418
+ protocol,
419
+ endpoint: Endpoint.path(PATH, { baseURL: DEFAULT_BASE_URL }),
420
+ auth: Auth.none,
421
+ transport: httpTransport,
422
+ });
423
+ export * as OpenAIChat from "./openai-chat";
@@ -0,0 +1,73 @@
1
+ import { Route, type RouteRoutedModelInput } from "../route/client";
2
+ export type OpenAICompatibleChatModelInput = RouteRoutedModelInput;
3
+ /**
4
+ * Route for non-OpenAI providers that expose an OpenAI Chat-compatible
5
+ * `/chat/completions` endpoint. Reuses `OpenAIChat.protocol` end-to-end and
6
+ * overrides only the route id so providers can be resolved per-family without
7
+ * colliding with native OpenAI. Provider helpers configure the route endpoint
8
+ * before model selection.
9
+ */
10
+ export declare const route: Route<{
11
+ readonly model: string;
12
+ readonly messages: readonly ({
13
+ readonly role: "system";
14
+ readonly content: string;
15
+ } | {
16
+ readonly role: "user";
17
+ readonly content: string | readonly ({
18
+ readonly type: "text";
19
+ readonly text: string;
20
+ } | {
21
+ readonly type: "image_url";
22
+ readonly image_url: {
23
+ readonly url: string;
24
+ };
25
+ })[];
26
+ } | {
27
+ readonly role: "assistant";
28
+ readonly content: string | null;
29
+ readonly tool_calls?: readonly {
30
+ readonly id: string;
31
+ readonly type: "function";
32
+ readonly function: {
33
+ readonly name: string;
34
+ readonly arguments: string;
35
+ };
36
+ }[] | undefined;
37
+ readonly reasoning_content?: string | undefined;
38
+ } | {
39
+ readonly role: "tool";
40
+ readonly tool_call_id: string;
41
+ readonly content: string;
42
+ })[];
43
+ readonly stream: true;
44
+ readonly tools?: readonly {
45
+ readonly type: "function";
46
+ readonly function: {
47
+ readonly name: string;
48
+ readonly description: string;
49
+ readonly parameters: {
50
+ readonly [x: string]: unknown;
51
+ };
52
+ };
53
+ }[] | undefined;
54
+ readonly tool_choice?: "required" | "none" | "auto" | {
55
+ readonly type: "function";
56
+ readonly function: {
57
+ readonly name: string;
58
+ };
59
+ } | undefined;
60
+ readonly stream_options?: {
61
+ readonly include_usage: boolean;
62
+ } | undefined;
63
+ readonly store?: boolean | undefined;
64
+ readonly reasoning_effort?: string | undefined;
65
+ readonly max_tokens?: number | undefined;
66
+ readonly temperature?: number | undefined;
67
+ readonly top_p?: number | undefined;
68
+ readonly frequency_penalty?: number | undefined;
69
+ readonly presence_penalty?: number | undefined;
70
+ readonly seed?: number | undefined;
71
+ readonly stop?: readonly string[] | undefined;
72
+ }, import("../route/transport/http").HttpPrepared<string>>;
73
+ export * as OpenAICompatibleChat from "./openai-compatible-chat";
@@ -0,0 +1,20 @@
1
+ import { Route } from "../route/client";
2
+ import { Endpoint } from "../route/endpoint";
3
+ import { Framing } from "../route/framing";
4
+ import * as OpenAIChat from "./openai-chat";
5
+ const ADAPTER = "openai-compatible-chat";
6
+ /**
7
+ * Route for non-OpenAI providers that expose an OpenAI Chat-compatible
8
+ * `/chat/completions` endpoint. Reuses `OpenAIChat.protocol` end-to-end and
9
+ * overrides only the route id so providers can be resolved per-family without
10
+ * colliding with native OpenAI. Provider helpers configure the route endpoint
11
+ * before model selection.
12
+ */
13
+ export const route = Route.make({
14
+ id: ADAPTER,
15
+ providerMetadataKey: "openai",
16
+ protocol: OpenAIChat.protocol,
17
+ endpoint: Endpoint.path("/chat/completions"),
18
+ framing: Framing.sse,
19
+ });
20
+ export * as OpenAICompatibleChat from "./openai-compatible-chat";
@@ -0,0 +1,85 @@
1
+ import { Route, type RouteRoutedModelInput } from "../route/client";
2
+ export type OpenAICompatibleResponsesModelInput = RouteRoutedModelInput;
3
+ /**
4
+ * Route for providers that expose an OpenAI Responses-compatible `/responses`
5
+ * endpoint. Provider helpers configure identity, endpoint, and auth before
6
+ * model selection while this route reuses the OpenAI Responses protocol.
7
+ */
8
+ export declare const route: Route<{
9
+ readonly stream: true;
10
+ readonly model: string;
11
+ readonly input: readonly ({
12
+ readonly role: "system";
13
+ readonly content: string;
14
+ } | {
15
+ readonly role: "user";
16
+ readonly content: readonly ({
17
+ readonly type: "input_text";
18
+ readonly text: string;
19
+ } | {
20
+ readonly type: "input_image";
21
+ readonly image_url: string;
22
+ })[];
23
+ } | {
24
+ readonly role: "assistant";
25
+ readonly content: readonly {
26
+ readonly type: "output_text";
27
+ readonly text: string;
28
+ }[];
29
+ } | {
30
+ readonly type: "reasoning";
31
+ readonly summary: readonly {
32
+ readonly type: "summary_text";
33
+ readonly text: string;
34
+ }[];
35
+ readonly id?: string | undefined;
36
+ readonly encrypted_content?: string | null | undefined;
37
+ } | {
38
+ readonly type: "item_reference";
39
+ readonly id: string;
40
+ } | {
41
+ readonly type: "function_call";
42
+ readonly call_id: string;
43
+ readonly name: string;
44
+ readonly arguments: string;
45
+ } | {
46
+ readonly type: "function_call_output";
47
+ readonly call_id: string;
48
+ readonly output: string | readonly ({
49
+ readonly type: "input_text";
50
+ readonly text: string;
51
+ } | {
52
+ readonly type: "input_image";
53
+ readonly image_url: string;
54
+ })[];
55
+ })[];
56
+ readonly instructions?: string | undefined;
57
+ readonly tools?: readonly {
58
+ readonly type: "function";
59
+ readonly name: string;
60
+ readonly description: string;
61
+ readonly parameters: {
62
+ readonly [x: string]: unknown;
63
+ };
64
+ readonly strict?: boolean | undefined;
65
+ }[] | undefined;
66
+ readonly tool_choice?: "required" | "none" | "auto" | {
67
+ readonly type: "function";
68
+ readonly name: string;
69
+ } | undefined;
70
+ readonly store?: boolean | undefined;
71
+ readonly service_tier?: "auto" | "default" | "flex" | "priority" | undefined;
72
+ readonly prompt_cache_key?: string | undefined;
73
+ readonly include?: readonly ("file_search_call.results" | "web_search_call.results" | "web_search_call.action.sources" | "message.input_image.image_url" | "computer_call_output.output.image_url" | "code_interpreter_call.outputs" | "reasoning.encrypted_content" | "message.output_text.logprobs")[] | undefined;
74
+ readonly reasoning?: {
75
+ readonly effort?: string | undefined;
76
+ readonly summary?: "auto" | undefined;
77
+ } | undefined;
78
+ readonly text?: {
79
+ readonly verbosity?: "low" | "medium" | "high" | undefined;
80
+ } | undefined;
81
+ readonly max_output_tokens?: number | undefined;
82
+ readonly temperature?: number | undefined;
83
+ readonly top_p?: number | undefined;
84
+ }, import("../route/transport/http").HttpPrepared<string>>;
85
+ export * as OpenAICompatibleResponses from "./openai-compatible-responses";
@@ -0,0 +1,18 @@
1
+ import { Route } from "../route/client";
2
+ import { Endpoint } from "../route/endpoint";
3
+ import { OpenAIResponses } from "./openai-responses";
4
+ const ADAPTER = "openai-compatible-responses";
5
+ /**
6
+ * Route for providers that expose an OpenAI Responses-compatible `/responses`
7
+ * endpoint. Provider helpers configure identity, endpoint, and auth before
8
+ * model selection while this route reuses the OpenAI Responses protocol.
9
+ */
10
+ export const route = Route.make({
11
+ id: ADAPTER,
12
+ providerMetadataKey: "openai",
13
+ protocol: OpenAIResponses.protocol,
14
+ endpoint: Endpoint.path(OpenAIResponses.PATH),
15
+ transport: OpenAIResponses.httpTransport,
16
+ defaults: { providerOptions: { openai: { store: false } } },
17
+ });
18
+ export * as OpenAICompatibleResponses from "./openai-compatible-responses";