@lenylvt/pi-ai 0.64.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 (167) hide show
  1. package/README.md +203 -0
  2. package/dist/api-registry.d.ts +20 -0
  3. package/dist/api-registry.d.ts.map +1 -0
  4. package/dist/api-registry.js +44 -0
  5. package/dist/api-registry.js.map +1 -0
  6. package/dist/cli.d.ts +3 -0
  7. package/dist/cli.d.ts.map +1 -0
  8. package/dist/cli.js +119 -0
  9. package/dist/cli.js.map +1 -0
  10. package/dist/env-api-keys.d.ts +7 -0
  11. package/dist/env-api-keys.d.ts.map +1 -0
  12. package/dist/env-api-keys.js +13 -0
  13. package/dist/env-api-keys.js.map +1 -0
  14. package/dist/index.d.ts +20 -0
  15. package/dist/index.d.ts.map +1 -0
  16. package/dist/index.js +14 -0
  17. package/dist/index.js.map +1 -0
  18. package/dist/models.d.ts +24 -0
  19. package/dist/models.d.ts.map +1 -0
  20. package/dist/models.generated.d.ts +2332 -0
  21. package/dist/models.generated.d.ts.map +1 -0
  22. package/dist/models.generated.js +2186 -0
  23. package/dist/models.generated.js.map +1 -0
  24. package/dist/models.js +60 -0
  25. package/dist/models.js.map +1 -0
  26. package/dist/oauth.d.ts +2 -0
  27. package/dist/oauth.d.ts.map +1 -0
  28. package/dist/oauth.js +2 -0
  29. package/dist/oauth.js.map +1 -0
  30. package/dist/providers/anthropic.d.ts +40 -0
  31. package/dist/providers/anthropic.d.ts.map +1 -0
  32. package/dist/providers/anthropic.js +749 -0
  33. package/dist/providers/anthropic.js.map +1 -0
  34. package/dist/providers/faux.d.ts +56 -0
  35. package/dist/providers/faux.d.ts.map +1 -0
  36. package/dist/providers/faux.js +367 -0
  37. package/dist/providers/faux.js.map +1 -0
  38. package/dist/providers/github-copilot-headers.d.ts +8 -0
  39. package/dist/providers/github-copilot-headers.d.ts.map +1 -0
  40. package/dist/providers/github-copilot-headers.js +29 -0
  41. package/dist/providers/github-copilot-headers.js.map +1 -0
  42. package/dist/providers/openai-codex-responses.d.ts +9 -0
  43. package/dist/providers/openai-codex-responses.d.ts.map +1 -0
  44. package/dist/providers/openai-codex-responses.js +741 -0
  45. package/dist/providers/openai-codex-responses.js.map +1 -0
  46. package/dist/providers/openai-completions.d.ts +15 -0
  47. package/dist/providers/openai-completions.d.ts.map +1 -0
  48. package/dist/providers/openai-completions.js +687 -0
  49. package/dist/providers/openai-completions.js.map +1 -0
  50. package/dist/providers/openai-responses-shared.d.ts +17 -0
  51. package/dist/providers/openai-responses-shared.d.ts.map +1 -0
  52. package/dist/providers/openai-responses-shared.js +458 -0
  53. package/dist/providers/openai-responses-shared.js.map +1 -0
  54. package/dist/providers/openai-responses.d.ts +13 -0
  55. package/dist/providers/openai-responses.d.ts.map +1 -0
  56. package/dist/providers/openai-responses.js +190 -0
  57. package/dist/providers/openai-responses.js.map +1 -0
  58. package/dist/providers/register-builtins.d.ts +16 -0
  59. package/dist/providers/register-builtins.d.ts.map +1 -0
  60. package/dist/providers/register-builtins.js +140 -0
  61. package/dist/providers/register-builtins.js.map +1 -0
  62. package/dist/providers/simple-options.d.ts +8 -0
  63. package/dist/providers/simple-options.d.ts.map +1 -0
  64. package/dist/providers/simple-options.js +35 -0
  65. package/dist/providers/simple-options.js.map +1 -0
  66. package/dist/providers/transform-messages.d.ts +8 -0
  67. package/dist/providers/transform-messages.d.ts.map +1 -0
  68. package/dist/providers/transform-messages.js +155 -0
  69. package/dist/providers/transform-messages.js.map +1 -0
  70. package/dist/stream.d.ts +8 -0
  71. package/dist/stream.d.ts.map +1 -0
  72. package/dist/stream.js +27 -0
  73. package/dist/stream.js.map +1 -0
  74. package/dist/types.d.ts +283 -0
  75. package/dist/types.d.ts.map +1 -0
  76. package/dist/types.js +2 -0
  77. package/dist/types.js.map +1 -0
  78. package/dist/utils/event-stream.d.ts +21 -0
  79. package/dist/utils/event-stream.d.ts.map +1 -0
  80. package/dist/utils/event-stream.js +81 -0
  81. package/dist/utils/event-stream.js.map +1 -0
  82. package/dist/utils/hash.d.ts +3 -0
  83. package/dist/utils/hash.d.ts.map +1 -0
  84. package/dist/utils/hash.js +14 -0
  85. package/dist/utils/hash.js.map +1 -0
  86. package/dist/utils/json-parse.d.ts +9 -0
  87. package/dist/utils/json-parse.d.ts.map +1 -0
  88. package/dist/utils/json-parse.js +29 -0
  89. package/dist/utils/json-parse.js.map +1 -0
  90. package/dist/utils/oauth/anthropic.d.ts +25 -0
  91. package/dist/utils/oauth/anthropic.d.ts.map +1 -0
  92. package/dist/utils/oauth/anthropic.js +335 -0
  93. package/dist/utils/oauth/anthropic.js.map +1 -0
  94. package/dist/utils/oauth/github-copilot.d.ts +30 -0
  95. package/dist/utils/oauth/github-copilot.d.ts.map +1 -0
  96. package/dist/utils/oauth/github-copilot.js +292 -0
  97. package/dist/utils/oauth/github-copilot.js.map +1 -0
  98. package/dist/utils/oauth/index.d.ts +36 -0
  99. package/dist/utils/oauth/index.d.ts.map +1 -0
  100. package/dist/utils/oauth/index.js +92 -0
  101. package/dist/utils/oauth/index.js.map +1 -0
  102. package/dist/utils/oauth/oauth-page.d.ts +3 -0
  103. package/dist/utils/oauth/oauth-page.d.ts.map +1 -0
  104. package/dist/utils/oauth/oauth-page.js +105 -0
  105. package/dist/utils/oauth/oauth-page.js.map +1 -0
  106. package/dist/utils/oauth/openai-codex.d.ts +34 -0
  107. package/dist/utils/oauth/openai-codex.d.ts.map +1 -0
  108. package/dist/utils/oauth/openai-codex.js +373 -0
  109. package/dist/utils/oauth/openai-codex.js.map +1 -0
  110. package/dist/utils/oauth/pkce.d.ts +13 -0
  111. package/dist/utils/oauth/pkce.d.ts.map +1 -0
  112. package/dist/utils/oauth/pkce.js +31 -0
  113. package/dist/utils/oauth/pkce.js.map +1 -0
  114. package/dist/utils/oauth/types.d.ts +47 -0
  115. package/dist/utils/oauth/types.d.ts.map +1 -0
  116. package/dist/utils/oauth/types.js +2 -0
  117. package/dist/utils/oauth/types.js.map +1 -0
  118. package/dist/utils/overflow.d.ts +53 -0
  119. package/dist/utils/overflow.d.ts.map +1 -0
  120. package/dist/utils/overflow.js +119 -0
  121. package/dist/utils/overflow.js.map +1 -0
  122. package/dist/utils/sanitize-unicode.d.ts +22 -0
  123. package/dist/utils/sanitize-unicode.d.ts.map +1 -0
  124. package/dist/utils/sanitize-unicode.js +26 -0
  125. package/dist/utils/sanitize-unicode.js.map +1 -0
  126. package/dist/utils/typebox-helpers.d.ts +17 -0
  127. package/dist/utils/typebox-helpers.d.ts.map +1 -0
  128. package/dist/utils/typebox-helpers.js +21 -0
  129. package/dist/utils/typebox-helpers.js.map +1 -0
  130. package/dist/utils/validation.d.ts +18 -0
  131. package/dist/utils/validation.d.ts.map +1 -0
  132. package/dist/utils/validation.js +80 -0
  133. package/dist/utils/validation.js.map +1 -0
  134. package/package.json +89 -0
  135. package/src/api-registry.ts +98 -0
  136. package/src/cli.ts +136 -0
  137. package/src/env-api-keys.ts +22 -0
  138. package/src/index.ts +29 -0
  139. package/src/models.generated.ts +2188 -0
  140. package/src/models.ts +82 -0
  141. package/src/oauth.ts +1 -0
  142. package/src/providers/anthropic.ts +905 -0
  143. package/src/providers/faux.ts +498 -0
  144. package/src/providers/github-copilot-headers.ts +37 -0
  145. package/src/providers/openai-codex-responses.ts +929 -0
  146. package/src/providers/openai-completions.ts +811 -0
  147. package/src/providers/openai-responses-shared.ts +513 -0
  148. package/src/providers/openai-responses.ts +251 -0
  149. package/src/providers/register-builtins.ts +232 -0
  150. package/src/providers/simple-options.ts +46 -0
  151. package/src/providers/transform-messages.ts +172 -0
  152. package/src/stream.ts +59 -0
  153. package/src/types.ts +294 -0
  154. package/src/utils/event-stream.ts +87 -0
  155. package/src/utils/hash.ts +13 -0
  156. package/src/utils/json-parse.ts +28 -0
  157. package/src/utils/oauth/anthropic.ts +402 -0
  158. package/src/utils/oauth/github-copilot.ts +396 -0
  159. package/src/utils/oauth/index.ts +123 -0
  160. package/src/utils/oauth/oauth-page.ts +109 -0
  161. package/src/utils/oauth/openai-codex.ts +450 -0
  162. package/src/utils/oauth/pkce.ts +34 -0
  163. package/src/utils/oauth/types.ts +59 -0
  164. package/src/utils/overflow.ts +125 -0
  165. package/src/utils/sanitize-unicode.ts +25 -0
  166. package/src/utils/typebox-helpers.ts +24 -0
  167. package/src/utils/validation.ts +93 -0
@@ -0,0 +1,513 @@
1
+ import type OpenAI from "openai";
2
+ import type {
3
+ Tool as OpenAITool,
4
+ ResponseCreateParamsStreaming,
5
+ ResponseFunctionCallOutputItemList,
6
+ ResponseFunctionToolCall,
7
+ ResponseInput,
8
+ ResponseInputContent,
9
+ ResponseInputImage,
10
+ ResponseInputText,
11
+ ResponseOutputMessage,
12
+ ResponseReasoningItem,
13
+ ResponseStreamEvent,
14
+ } from "openai/resources/responses/responses.js";
15
+ import { calculateCost } from "../models.js";
16
+ import type {
17
+ Api,
18
+ AssistantMessage,
19
+ Context,
20
+ ImageContent,
21
+ Model,
22
+ StopReason,
23
+ TextContent,
24
+ TextSignatureV1,
25
+ ThinkingContent,
26
+ Tool,
27
+ ToolCall,
28
+ Usage,
29
+ } from "../types.js";
30
+ import type { AssistantMessageEventStream } from "../utils/event-stream.js";
31
+ import { shortHash } from "../utils/hash.js";
32
+ import { parseStreamingJson } from "../utils/json-parse.js";
33
+ import { sanitizeSurrogates } from "../utils/sanitize-unicode.js";
34
+ import { transformMessages } from "./transform-messages.js";
35
+
36
+ // =============================================================================
37
+ // Utilities
38
+ // =============================================================================
39
+
40
+ function encodeTextSignatureV1(id: string, phase?: TextSignatureV1["phase"]): string {
41
+ const payload: TextSignatureV1 = { v: 1, id };
42
+ if (phase) payload.phase = phase;
43
+ return JSON.stringify(payload);
44
+ }
45
+
46
+ function parseTextSignature(
47
+ signature: string | undefined,
48
+ ): { id: string; phase?: TextSignatureV1["phase"] } | undefined {
49
+ if (!signature) return undefined;
50
+ if (signature.startsWith("{")) {
51
+ try {
52
+ const parsed = JSON.parse(signature) as Partial<TextSignatureV1>;
53
+ if (parsed.v === 1 && typeof parsed.id === "string") {
54
+ if (parsed.phase === "commentary" || parsed.phase === "final_answer") {
55
+ return { id: parsed.id, phase: parsed.phase };
56
+ }
57
+ return { id: parsed.id };
58
+ }
59
+ } catch {
60
+ // Fall through to legacy plain-string handling.
61
+ }
62
+ }
63
+ return { id: signature };
64
+ }
65
+
66
+ export interface OpenAIResponsesStreamOptions {
67
+ serviceTier?: ResponseCreateParamsStreaming["service_tier"];
68
+ applyServiceTierPricing?: (
69
+ usage: Usage,
70
+ serviceTier: ResponseCreateParamsStreaming["service_tier"] | undefined,
71
+ ) => void;
72
+ }
73
+
74
+ export interface ConvertResponsesMessagesOptions {
75
+ includeSystemPrompt?: boolean;
76
+ }
77
+
78
+ export interface ConvertResponsesToolsOptions {
79
+ strict?: boolean | null;
80
+ }
81
+
82
+ // =============================================================================
83
+ // Message conversion
84
+ // =============================================================================
85
+
86
+ export function convertResponsesMessages<TApi extends Api>(
87
+ model: Model<TApi>,
88
+ context: Context,
89
+ allowedToolCallProviders: ReadonlySet<string>,
90
+ options?: ConvertResponsesMessagesOptions,
91
+ ): ResponseInput {
92
+ const messages: ResponseInput = [];
93
+
94
+ const normalizeIdPart = (part: string): string => {
95
+ const sanitized = part.replace(/[^a-zA-Z0-9_-]/g, "_");
96
+ const normalized = sanitized.length > 64 ? sanitized.slice(0, 64) : sanitized;
97
+ return normalized.replace(/_+$/, "");
98
+ };
99
+
100
+ const buildForeignResponsesItemId = (itemId: string): string => {
101
+ const normalized = `fc_${shortHash(itemId)}`;
102
+ return normalized.length > 64 ? normalized.slice(0, 64) : normalized;
103
+ };
104
+
105
+ const normalizeToolCallId = (id: string, _targetModel: Model<TApi>, source: AssistantMessage): string => {
106
+ if (!allowedToolCallProviders.has(model.provider)) return normalizeIdPart(id);
107
+ if (!id.includes("|")) return normalizeIdPart(id);
108
+ const [callId, itemId] = id.split("|");
109
+ const normalizedCallId = normalizeIdPart(callId);
110
+ const isForeignToolCall = source.provider !== model.provider || source.api !== model.api;
111
+ let normalizedItemId = isForeignToolCall ? buildForeignResponsesItemId(itemId) : normalizeIdPart(itemId);
112
+ // OpenAI Responses API requires item id to start with "fc"
113
+ if (!normalizedItemId.startsWith("fc_")) {
114
+ normalizedItemId = normalizeIdPart(`fc_${normalizedItemId}`);
115
+ }
116
+ return `${normalizedCallId}|${normalizedItemId}`;
117
+ };
118
+
119
+ const transformedMessages = transformMessages(context.messages, model, normalizeToolCallId);
120
+
121
+ const includeSystemPrompt = options?.includeSystemPrompt ?? true;
122
+ if (includeSystemPrompt && context.systemPrompt) {
123
+ const role = model.reasoning ? "developer" : "system";
124
+ messages.push({
125
+ role,
126
+ content: sanitizeSurrogates(context.systemPrompt),
127
+ });
128
+ }
129
+
130
+ let msgIndex = 0;
131
+ for (const msg of transformedMessages) {
132
+ if (msg.role === "user") {
133
+ if (typeof msg.content === "string") {
134
+ messages.push({
135
+ role: "user",
136
+ content: [{ type: "input_text", text: sanitizeSurrogates(msg.content) }],
137
+ });
138
+ } else {
139
+ const content: ResponseInputContent[] = msg.content.map((item): ResponseInputContent => {
140
+ if (item.type === "text") {
141
+ return {
142
+ type: "input_text",
143
+ text: sanitizeSurrogates(item.text),
144
+ } satisfies ResponseInputText;
145
+ }
146
+ return {
147
+ type: "input_image",
148
+ detail: "auto",
149
+ image_url: `data:${item.mimeType};base64,${item.data}`,
150
+ } satisfies ResponseInputImage;
151
+ });
152
+ const filteredContent = !model.input.includes("image")
153
+ ? content.filter((c) => c.type !== "input_image")
154
+ : content;
155
+ if (filteredContent.length === 0) continue;
156
+ messages.push({
157
+ role: "user",
158
+ content: filteredContent,
159
+ });
160
+ }
161
+ } else if (msg.role === "assistant") {
162
+ const output: ResponseInput = [];
163
+ const assistantMsg = msg as AssistantMessage;
164
+ const isDifferentModel =
165
+ assistantMsg.model !== model.id &&
166
+ assistantMsg.provider === model.provider &&
167
+ assistantMsg.api === model.api;
168
+
169
+ for (const block of msg.content) {
170
+ if (block.type === "thinking") {
171
+ if (block.thinkingSignature) {
172
+ const reasoningItem = JSON.parse(block.thinkingSignature) as ResponseReasoningItem;
173
+ output.push(reasoningItem);
174
+ }
175
+ } else if (block.type === "text") {
176
+ const textBlock = block as TextContent;
177
+ const parsedSignature = parseTextSignature(textBlock.textSignature);
178
+ // OpenAI requires id to be max 64 characters
179
+ let msgId = parsedSignature?.id;
180
+ if (!msgId) {
181
+ msgId = `msg_${msgIndex}`;
182
+ } else if (msgId.length > 64) {
183
+ msgId = `msg_${shortHash(msgId)}`;
184
+ }
185
+ output.push({
186
+ type: "message",
187
+ role: "assistant",
188
+ content: [{ type: "output_text", text: sanitizeSurrogates(textBlock.text), annotations: [] }],
189
+ status: "completed",
190
+ id: msgId,
191
+ phase: parsedSignature?.phase,
192
+ } satisfies ResponseOutputMessage);
193
+ } else if (block.type === "toolCall") {
194
+ const toolCall = block as ToolCall;
195
+ const [callId, itemIdRaw] = toolCall.id.split("|");
196
+ let itemId: string | undefined = itemIdRaw;
197
+
198
+ // For different-model messages, set id to undefined to avoid pairing validation.
199
+ // OpenAI tracks which fc_xxx IDs were paired with rs_xxx reasoning items.
200
+ // By omitting the id, we avoid triggering that validation (like cross-provider does).
201
+ if (isDifferentModel && itemId?.startsWith("fc_")) {
202
+ itemId = undefined;
203
+ }
204
+
205
+ output.push({
206
+ type: "function_call",
207
+ id: itemId,
208
+ call_id: callId,
209
+ name: toolCall.name,
210
+ arguments: JSON.stringify(toolCall.arguments),
211
+ });
212
+ }
213
+ }
214
+ if (output.length === 0) continue;
215
+ messages.push(...output);
216
+ } else if (msg.role === "toolResult") {
217
+ const textResult = msg.content
218
+ .filter((c): c is TextContent => c.type === "text")
219
+ .map((c) => c.text)
220
+ .join("\n");
221
+ const hasImages = msg.content.some((c): c is ImageContent => c.type === "image");
222
+ const hasText = textResult.length > 0;
223
+ const [callId] = msg.toolCallId.split("|");
224
+
225
+ let output: string | ResponseFunctionCallOutputItemList;
226
+ if (hasImages && model.input.includes("image")) {
227
+ const contentParts: ResponseFunctionCallOutputItemList = [];
228
+
229
+ if (hasText) {
230
+ contentParts.push({
231
+ type: "input_text",
232
+ text: sanitizeSurrogates(textResult),
233
+ });
234
+ }
235
+
236
+ for (const block of msg.content) {
237
+ if (block.type === "image") {
238
+ contentParts.push({
239
+ type: "input_image",
240
+ detail: "auto",
241
+ image_url: `data:${block.mimeType};base64,${block.data}`,
242
+ });
243
+ }
244
+ }
245
+
246
+ output = contentParts;
247
+ } else {
248
+ output = sanitizeSurrogates(hasText ? textResult : "(see attached image)");
249
+ }
250
+
251
+ messages.push({
252
+ type: "function_call_output",
253
+ call_id: callId,
254
+ output,
255
+ });
256
+ }
257
+ msgIndex++;
258
+ }
259
+
260
+ return messages;
261
+ }
262
+
263
+ // =============================================================================
264
+ // Tool conversion
265
+ // =============================================================================
266
+
267
+ export function convertResponsesTools(tools: Tool[], options?: ConvertResponsesToolsOptions): OpenAITool[] {
268
+ const strict = options?.strict === undefined ? false : options.strict;
269
+ return tools.map((tool) => ({
270
+ type: "function",
271
+ name: tool.name,
272
+ description: tool.description,
273
+ parameters: tool.parameters as any, // TypeBox already generates JSON Schema
274
+ strict,
275
+ }));
276
+ }
277
+
278
+ // =============================================================================
279
+ // Stream processing
280
+ // =============================================================================
281
+
282
+ export async function processResponsesStream<TApi extends Api>(
283
+ openaiStream: AsyncIterable<ResponseStreamEvent>,
284
+ output: AssistantMessage,
285
+ stream: AssistantMessageEventStream,
286
+ model: Model<TApi>,
287
+ options?: OpenAIResponsesStreamOptions,
288
+ ): Promise<void> {
289
+ let currentItem: ResponseReasoningItem | ResponseOutputMessage | ResponseFunctionToolCall | null = null;
290
+ let currentBlock: ThinkingContent | TextContent | (ToolCall & { partialJson: string }) | null = null;
291
+ const blocks = output.content;
292
+ const blockIndex = () => blocks.length - 1;
293
+
294
+ for await (const event of openaiStream) {
295
+ if (event.type === "response.created") {
296
+ output.responseId = event.response.id;
297
+ } else if (event.type === "response.output_item.added") {
298
+ const item = event.item;
299
+ if (item.type === "reasoning") {
300
+ currentItem = item;
301
+ currentBlock = { type: "thinking", thinking: "" };
302
+ output.content.push(currentBlock);
303
+ stream.push({ type: "thinking_start", contentIndex: blockIndex(), partial: output });
304
+ } else if (item.type === "message") {
305
+ currentItem = item;
306
+ currentBlock = { type: "text", text: "" };
307
+ output.content.push(currentBlock);
308
+ stream.push({ type: "text_start", contentIndex: blockIndex(), partial: output });
309
+ } else if (item.type === "function_call") {
310
+ currentItem = item;
311
+ currentBlock = {
312
+ type: "toolCall",
313
+ id: `${item.call_id}|${item.id}`,
314
+ name: item.name,
315
+ arguments: {},
316
+ partialJson: item.arguments || "",
317
+ };
318
+ output.content.push(currentBlock);
319
+ stream.push({ type: "toolcall_start", contentIndex: blockIndex(), partial: output });
320
+ }
321
+ } else if (event.type === "response.reasoning_summary_part.added") {
322
+ if (currentItem && currentItem.type === "reasoning") {
323
+ currentItem.summary = currentItem.summary || [];
324
+ currentItem.summary.push(event.part);
325
+ }
326
+ } else if (event.type === "response.reasoning_summary_text.delta") {
327
+ if (currentItem?.type === "reasoning" && currentBlock?.type === "thinking") {
328
+ currentItem.summary = currentItem.summary || [];
329
+ const lastPart = currentItem.summary[currentItem.summary.length - 1];
330
+ if (lastPart) {
331
+ currentBlock.thinking += event.delta;
332
+ lastPart.text += event.delta;
333
+ stream.push({
334
+ type: "thinking_delta",
335
+ contentIndex: blockIndex(),
336
+ delta: event.delta,
337
+ partial: output,
338
+ });
339
+ }
340
+ }
341
+ } else if (event.type === "response.reasoning_summary_part.done") {
342
+ if (currentItem?.type === "reasoning" && currentBlock?.type === "thinking") {
343
+ currentItem.summary = currentItem.summary || [];
344
+ const lastPart = currentItem.summary[currentItem.summary.length - 1];
345
+ if (lastPart) {
346
+ currentBlock.thinking += "\n\n";
347
+ lastPart.text += "\n\n";
348
+ stream.push({
349
+ type: "thinking_delta",
350
+ contentIndex: blockIndex(),
351
+ delta: "\n\n",
352
+ partial: output,
353
+ });
354
+ }
355
+ }
356
+ } else if (event.type === "response.content_part.added") {
357
+ if (currentItem?.type === "message") {
358
+ currentItem.content = currentItem.content || [];
359
+ // Filter out ReasoningText, only accept output_text and refusal
360
+ if (event.part.type === "output_text" || event.part.type === "refusal") {
361
+ currentItem.content.push(event.part);
362
+ }
363
+ }
364
+ } else if (event.type === "response.output_text.delta") {
365
+ if (currentItem?.type === "message" && currentBlock?.type === "text") {
366
+ if (!currentItem.content || currentItem.content.length === 0) {
367
+ continue;
368
+ }
369
+ const lastPart = currentItem.content[currentItem.content.length - 1];
370
+ if (lastPart?.type === "output_text") {
371
+ currentBlock.text += event.delta;
372
+ lastPart.text += event.delta;
373
+ stream.push({
374
+ type: "text_delta",
375
+ contentIndex: blockIndex(),
376
+ delta: event.delta,
377
+ partial: output,
378
+ });
379
+ }
380
+ }
381
+ } else if (event.type === "response.refusal.delta") {
382
+ if (currentItem?.type === "message" && currentBlock?.type === "text") {
383
+ if (!currentItem.content || currentItem.content.length === 0) {
384
+ continue;
385
+ }
386
+ const lastPart = currentItem.content[currentItem.content.length - 1];
387
+ if (lastPart?.type === "refusal") {
388
+ currentBlock.text += event.delta;
389
+ lastPart.refusal += event.delta;
390
+ stream.push({
391
+ type: "text_delta",
392
+ contentIndex: blockIndex(),
393
+ delta: event.delta,
394
+ partial: output,
395
+ });
396
+ }
397
+ }
398
+ } else if (event.type === "response.function_call_arguments.delta") {
399
+ if (currentItem?.type === "function_call" && currentBlock?.type === "toolCall") {
400
+ currentBlock.partialJson += event.delta;
401
+ currentBlock.arguments = parseStreamingJson(currentBlock.partialJson);
402
+ stream.push({
403
+ type: "toolcall_delta",
404
+ contentIndex: blockIndex(),
405
+ delta: event.delta,
406
+ partial: output,
407
+ });
408
+ }
409
+ } else if (event.type === "response.function_call_arguments.done") {
410
+ if (currentItem?.type === "function_call" && currentBlock?.type === "toolCall") {
411
+ currentBlock.partialJson = event.arguments;
412
+ currentBlock.arguments = parseStreamingJson(currentBlock.partialJson);
413
+ }
414
+ } else if (event.type === "response.output_item.done") {
415
+ const item = event.item;
416
+
417
+ if (item.type === "reasoning" && currentBlock?.type === "thinking") {
418
+ currentBlock.thinking = item.summary?.map((s) => s.text).join("\n\n") || "";
419
+ currentBlock.thinkingSignature = JSON.stringify(item);
420
+ stream.push({
421
+ type: "thinking_end",
422
+ contentIndex: blockIndex(),
423
+ content: currentBlock.thinking,
424
+ partial: output,
425
+ });
426
+ currentBlock = null;
427
+ } else if (item.type === "message" && currentBlock?.type === "text") {
428
+ currentBlock.text = item.content.map((c) => (c.type === "output_text" ? c.text : c.refusal)).join("");
429
+ currentBlock.textSignature = encodeTextSignatureV1(item.id, item.phase ?? undefined);
430
+ stream.push({
431
+ type: "text_end",
432
+ contentIndex: blockIndex(),
433
+ content: currentBlock.text,
434
+ partial: output,
435
+ });
436
+ currentBlock = null;
437
+ } else if (item.type === "function_call") {
438
+ const args =
439
+ currentBlock?.type === "toolCall" && currentBlock.partialJson
440
+ ? parseStreamingJson(currentBlock.partialJson)
441
+ : parseStreamingJson(item.arguments || "{}");
442
+ const toolCall: ToolCall = {
443
+ type: "toolCall",
444
+ id: `${item.call_id}|${item.id}`,
445
+ name: item.name,
446
+ arguments: args,
447
+ };
448
+
449
+ currentBlock = null;
450
+ stream.push({ type: "toolcall_end", contentIndex: blockIndex(), toolCall, partial: output });
451
+ }
452
+ } else if (event.type === "response.completed") {
453
+ const response = event.response;
454
+ if (response?.id) {
455
+ output.responseId = response.id;
456
+ }
457
+ if (response?.usage) {
458
+ const cachedTokens = response.usage.input_tokens_details?.cached_tokens || 0;
459
+ output.usage = {
460
+ // OpenAI includes cached tokens in input_tokens, so subtract to get non-cached input
461
+ input: (response.usage.input_tokens || 0) - cachedTokens,
462
+ output: response.usage.output_tokens || 0,
463
+ cacheRead: cachedTokens,
464
+ cacheWrite: 0,
465
+ totalTokens: response.usage.total_tokens || 0,
466
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
467
+ };
468
+ }
469
+ calculateCost(model, output.usage);
470
+ if (options?.applyServiceTierPricing) {
471
+ const serviceTier = response?.service_tier ?? options.serviceTier;
472
+ options.applyServiceTierPricing(output.usage, serviceTier);
473
+ }
474
+ // Map status to stop reason
475
+ output.stopReason = mapStopReason(response?.status);
476
+ if (output.content.some((b) => b.type === "toolCall") && output.stopReason === "stop") {
477
+ output.stopReason = "toolUse";
478
+ }
479
+ } else if (event.type === "error") {
480
+ throw new Error(`Error Code ${event.code}: ${event.message}` || "Unknown error");
481
+ } else if (event.type === "response.failed") {
482
+ const error = event.response?.error;
483
+ const details = event.response?.incomplete_details;
484
+ const msg = error
485
+ ? `${error.code || "unknown"}: ${error.message || "no message"}`
486
+ : details?.reason
487
+ ? `incomplete: ${details.reason}`
488
+ : "Unknown error (no error details in response)";
489
+ throw new Error(msg);
490
+ }
491
+ }
492
+ }
493
+
494
+ function mapStopReason(status: OpenAI.Responses.ResponseStatus | undefined): StopReason {
495
+ if (!status) return "stop";
496
+ switch (status) {
497
+ case "completed":
498
+ return "stop";
499
+ case "incomplete":
500
+ return "length";
501
+ case "failed":
502
+ case "cancelled":
503
+ return "error";
504
+ // These two are wonky ...
505
+ case "in_progress":
506
+ case "queued":
507
+ return "stop";
508
+ default: {
509
+ const _exhaustive: never = status;
510
+ throw new Error(`Unhandled stop reason: ${_exhaustive}`);
511
+ }
512
+ }
513
+ }