@psnext/slingcli 2.5.20260623-1 → 2.5.20260629-1

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 (108) hide show
  1. package/node_modules/@earendil-works/pi-agent-core/dist/harness/compaction/compaction.js +5 -2
  2. package/node_modules/@earendil-works/pi-agent-core/dist/harness/session/session.js +2 -1
  3. package/node_modules/@earendil-works/pi-agent-core/dist/harness/utils/shell-output.js +1 -1
  4. package/node_modules/@earendil-works/pi-agent-core/package.json +2 -2
  5. package/node_modules/@earendil-works/pi-ai/dist/api/anthropic-messages.js +51 -47
  6. package/node_modules/@earendil-works/pi-ai/dist/api/azure-openai-responses.js +9 -3
  7. package/node_modules/@earendil-works/pi-ai/dist/api/bedrock-converse-stream.js +9 -6
  8. package/node_modules/@earendil-works/pi-ai/dist/api/cloudflare.js +0 -18
  9. package/node_modules/@earendil-works/pi-ai/dist/api/google-generative-ai.js +6 -3
  10. package/node_modules/@earendil-works/pi-ai/dist/api/google-vertex.js +6 -3
  11. package/node_modules/@earendil-works/pi-ai/dist/api/mistral-conversations.js +1 -1
  12. package/node_modules/@earendil-works/pi-ai/dist/api/openai-codex-responses.js +15 -25
  13. package/node_modules/@earendil-works/pi-ai/dist/api/openai-completions.js +30 -26
  14. package/node_modules/@earendil-works/pi-ai/dist/api/openai-responses-shared.js +189 -204
  15. package/node_modules/@earendil-works/pi-ai/dist/api/openai-responses.js +24 -21
  16. package/node_modules/@earendil-works/pi-ai/dist/api/openrouter-images.js +2 -5
  17. package/node_modules/@earendil-works/pi-ai/dist/api/simple-options.js +11 -2
  18. package/node_modules/@earendil-works/pi-ai/dist/auth/helpers.js +2 -2
  19. package/node_modules/@earendil-works/pi-ai/dist/auth/resolve.js +19 -4
  20. package/node_modules/@earendil-works/pi-ai/dist/compat.js +81 -7
  21. package/node_modules/@earendil-works/pi-ai/dist/image-models.generated.js +45 -0
  22. package/node_modules/@earendil-works/pi-ai/dist/images-models.js +7 -3
  23. package/node_modules/@earendil-works/pi-ai/dist/index.js +1 -0
  24. package/node_modules/@earendil-works/pi-ai/dist/legacy-api-aliases.js +49 -0
  25. package/node_modules/@earendil-works/pi-ai/dist/models.js +7 -3
  26. package/node_modules/@earendil-works/pi-ai/dist/providers/amazon-bedrock.models.js +25 -8
  27. package/node_modules/@earendil-works/pi-ai/dist/providers/ant-ling.models.js +3 -3
  28. package/node_modules/@earendil-works/pi-ai/dist/providers/anthropic.models.js +0 -34
  29. package/node_modules/@earendil-works/pi-ai/dist/providers/cerebras.models.js +2 -0
  30. package/node_modules/@earendil-works/pi-ai/dist/providers/cloudflare-ai-gateway.js +2 -2
  31. package/node_modules/@earendil-works/pi-ai/dist/providers/cloudflare-ai-gateway.models.js +21 -9
  32. package/node_modules/@earendil-works/pi-ai/dist/providers/cloudflare-auth.js +85 -0
  33. package/node_modules/@earendil-works/pi-ai/dist/providers/cloudflare-workers-ai.js +2 -2
  34. package/node_modules/@earendil-works/pi-ai/dist/providers/cloudflare-workers-ai.models.js +13 -13
  35. package/node_modules/@earendil-works/pi-ai/dist/providers/deepseek.models.js +2 -2
  36. package/node_modules/@earendil-works/pi-ai/dist/providers/faux.js +1 -20
  37. package/node_modules/@earendil-works/pi-ai/dist/providers/fireworks.models.js +19 -1
  38. package/node_modules/@earendil-works/pi-ai/dist/providers/huggingface.models.js +72 -0
  39. package/node_modules/@earendil-works/pi-ai/dist/providers/minimax-cn.models.js +4 -4
  40. package/node_modules/@earendil-works/pi-ai/dist/providers/minimax.models.js +4 -4
  41. package/node_modules/@earendil-works/pi-ai/dist/providers/nvidia.models.js +19 -0
  42. package/node_modules/@earendil-works/pi-ai/dist/providers/opencode-go.models.js +11 -11
  43. package/node_modules/@earendil-works/pi-ai/dist/providers/opencode.models.js +15 -15
  44. package/node_modules/@earendil-works/pi-ai/dist/providers/openrouter.models.js +310 -39
  45. package/node_modules/@earendil-works/pi-ai/dist/providers/together.models.js +37 -18
  46. package/node_modules/@earendil-works/pi-ai/dist/providers/vercel-ai-gateway.models.js +349 -77
  47. package/node_modules/@earendil-works/pi-ai/dist/providers/xai.models.js +7 -0
  48. package/node_modules/@earendil-works/pi-ai/dist/providers/zai-coding-cn.models.js +6 -6
  49. package/node_modules/@earendil-works/pi-ai/dist/providers/zai.models.js +6 -6
  50. package/node_modules/@earendil-works/pi-ai/dist/utils/estimate.js +95 -0
  51. package/node_modules/@earendil-works/pi-ai/dist/utils/headers.js +10 -0
  52. package/node_modules/@earendil-works/pi-ai/dist/utils/oauth/openai-codex.js +2 -2
  53. package/node_modules/@earendil-works/pi-ai/dist/utils/retry.js +86 -0
  54. package/node_modules/@earendil-works/pi-ai/package.json +1 -33
  55. package/node_modules/@earendil-works/pi-coding-agent/dist/cli/file-processor.js +13 -25
  56. package/node_modules/@earendil-works/pi-coding-agent/dist/core/agent-session.js +15 -30
  57. package/node_modules/@earendil-works/pi-coding-agent/dist/core/compaction/compaction.js +6 -3
  58. package/node_modules/@earendil-works/pi-coding-agent/dist/core/extensions/loader.js +5 -1
  59. package/node_modules/@earendil-works/pi-coding-agent/dist/core/model-registry.js +2 -4
  60. package/node_modules/@earendil-works/pi-coding-agent/dist/core/model-resolver.js +1 -1
  61. package/node_modules/@earendil-works/pi-coding-agent/dist/core/resource-loader.js +2 -0
  62. package/node_modules/@earendil-works/pi-coding-agent/dist/core/session-manager.js +9 -5
  63. package/node_modules/@earendil-works/pi-coding-agent/dist/core/settings-manager.js +11 -0
  64. package/node_modules/@earendil-works/pi-coding-agent/dist/core/timings.js +24 -14
  65. package/node_modules/@earendil-works/pi-coding-agent/dist/core/tools/read.js +12 -25
  66. package/node_modules/@earendil-works/pi-coding-agent/dist/main.js +20 -4
  67. package/node_modules/@earendil-works/pi-coding-agent/dist/modes/interactive/components/assistant-message.js +9 -9
  68. package/node_modules/@earendil-works/pi-coding-agent/dist/modes/interactive/components/extension-editor.js +12 -3
  69. package/node_modules/@earendil-works/pi-coding-agent/dist/modes/interactive/components/session-selector.js +14 -3
  70. package/node_modules/@earendil-works/pi-coding-agent/dist/modes/interactive/components/user-message.js +1 -1
  71. package/node_modules/@earendil-works/pi-coding-agent/dist/modes/interactive/interactive-mode.js +37 -25
  72. package/node_modules/@earendil-works/pi-coding-agent/dist/modes/rpc/rpc-client.js +14 -0
  73. package/node_modules/@earendil-works/pi-coding-agent/dist/modes/rpc/rpc-mode.js +16 -0
  74. package/node_modules/@earendil-works/pi-coding-agent/dist/rpc-entry.js +10 -0
  75. package/node_modules/@earendil-works/pi-coding-agent/dist/utils/image-convert.js +21 -15
  76. package/node_modules/@earendil-works/pi-coding-agent/dist/utils/image-process.js +83 -0
  77. package/node_modules/@earendil-works/pi-coding-agent/dist/utils/mime.js +41 -0
  78. package/node_modules/@earendil-works/pi-coding-agent/npm-shrinkwrap.json +12 -12
  79. package/node_modules/@earendil-works/pi-coding-agent/package.json +8 -5
  80. package/node_modules/@earendil-works/pi-orchestrator/dist/cli.js +136 -0
  81. package/node_modules/@earendil-works/pi-orchestrator/dist/config.js +55 -0
  82. package/node_modules/@earendil-works/pi-orchestrator/dist/handler.js +105 -0
  83. package/node_modules/@earendil-works/pi-orchestrator/dist/index.js +11 -0
  84. package/node_modules/@earendil-works/pi-orchestrator/dist/ipc/client.js +56 -0
  85. package/node_modules/@earendil-works/pi-orchestrator/dist/ipc/protocol.js +12 -0
  86. package/node_modules/@earendil-works/pi-orchestrator/dist/ipc/server.js +145 -0
  87. package/node_modules/@earendil-works/pi-orchestrator/dist/radius.js +365 -0
  88. package/node_modules/@earendil-works/pi-orchestrator/dist/rpc-process.js +168 -0
  89. package/node_modules/@earendil-works/pi-orchestrator/dist/serve.js +70 -0
  90. package/node_modules/@earendil-works/pi-orchestrator/dist/storage.js +58 -0
  91. package/node_modules/@earendil-works/pi-orchestrator/dist/supervisor.js +290 -0
  92. package/node_modules/@earendil-works/pi-orchestrator/dist/types.js +2 -0
  93. package/node_modules/@earendil-works/pi-orchestrator/package.json +45 -0
  94. package/node_modules/@earendil-works/pi-tui/dist/components/markdown.js +3 -0
  95. package/node_modules/@earendil-works/pi-tui/package.json +1 -1
  96. package/node_modules/@types/node/README.md +1 -1
  97. package/node_modules/@types/node/diagnostics_channel.d.ts +17 -20
  98. package/node_modules/@types/node/package.json +2 -2
  99. package/node_modules/brace-expansion/dist/commonjs/index.js +24 -14
  100. package/node_modules/brace-expansion/dist/commonjs/index.js.map +1 -1
  101. package/node_modules/brace-expansion/dist/esm/index.js +24 -14
  102. package/node_modules/brace-expansion/dist/esm/index.js.map +1 -1
  103. package/node_modules/brace-expansion/package.json +2 -2
  104. package/node_modules/google-auth-library/build/src/auth/googleauth.js +1 -1
  105. package/node_modules/google-auth-library/package.json +4 -4
  106. package/package.json +7 -6
  107. package/slingshot/index.js +302 -304
  108. package/node_modules/@earendil-works/pi-ai/dist/api-registry.js +0 -44
@@ -5,7 +5,6 @@ import { headersToRecord } from "../utils/headers.js";
5
5
  import { parseStreamingJson } from "../utils/json-parse.js";
6
6
  import { getProviderEnvValue } from "../utils/provider-env.js";
7
7
  import { sanitizeSurrogates } from "../utils/sanitize-unicode.js";
8
- import { isCloudflareProvider, resolveCloudflareBaseUrl } from "./cloudflare.js";
9
8
  import { buildCopilotDynamicHeaders, hasCopilotVisionInput } from "./github-copilot-headers.js";
10
9
  import { clampOpenAIPromptCacheKey } from "./openai-prompt-cache.js";
11
10
  import { buildBaseOptions } from "./simple-options.js";
@@ -15,6 +14,23 @@ import { transformMessages } from "./transform-messages.js";
15
14
  * This is needed because Anthropic (via proxy) requires the tools param
16
15
  * to be present when messages include tool_calls or tool role messages.
17
16
  */
17
+ function hasHeader(headers, name) {
18
+ if (!headers)
19
+ return false;
20
+ const expected = name.toLowerCase();
21
+ for (const [key, value] of Object.entries(headers)) {
22
+ if (key.toLowerCase() === expected && value !== null && value.trim().length > 0)
23
+ return true;
24
+ }
25
+ return false;
26
+ }
27
+ function getClientApiKey(provider, apiKey, headers) {
28
+ if (apiKey)
29
+ return apiKey;
30
+ if (hasHeader(headers, "authorization") || hasHeader(headers, "cf-aig-authorization"))
31
+ return "unused";
32
+ throw new Error(`No API key for provider: ${provider}`);
33
+ }
18
34
  function hasToolHistory(messages) {
19
35
  for (const msg of messages) {
20
36
  if (msg.role === "toolResult") {
@@ -81,14 +97,11 @@ export const stream = (model, context, options) => {
81
97
  timestamp: Date.now(),
82
98
  };
83
99
  try {
84
- const apiKey = options?.apiKey;
85
- if (!apiKey) {
86
- throw new Error(`No API key for provider: ${model.provider}`);
87
- }
100
+ const apiKey = getClientApiKey(model.provider, options?.apiKey, options?.headers);
88
101
  const compat = getCompat(model);
89
102
  const cacheRetention = resolveCacheRetention(options?.cacheRetention, options?.env);
90
103
  const cacheSessionId = cacheRetention === "none" ? undefined : options?.sessionId;
91
- const client = createClient(model, context, apiKey, options?.headers, cacheSessionId, compat, options?.env);
104
+ const client = createClient(model, context, apiKey, options?.headers, cacheSessionId, compat);
92
105
  let params = buildParams(model, context, options, compat, cacheRetention);
93
106
  const nextParams = await options?.onPayload?.(params, model);
94
107
  if (nextParams !== undefined) {
@@ -365,11 +378,8 @@ export const stream = (model, context, options) => {
365
378
  return stream;
366
379
  };
367
380
  export const streamSimple = (model, context, options) => {
368
- const apiKey = options?.apiKey;
369
- if (!apiKey) {
370
- throw new Error(`No API key for provider: ${model.provider}`);
371
- }
372
- const base = buildBaseOptions(model, options, apiKey);
381
+ getClientApiKey(model.provider, options?.apiKey, options?.headers);
382
+ const base = buildBaseOptions(model, context, options, options?.apiKey);
373
383
  const clampedReasoning = options?.reasoning ? clampThinkingLevel(model, options.reasoning) : undefined;
374
384
  const reasoningEffort = clampedReasoning === "off" ? undefined : clampedReasoning;
375
385
  const toolChoice = options?.toolChoice;
@@ -379,7 +389,7 @@ export const streamSimple = (model, context, options) => {
379
389
  toolChoice,
380
390
  });
381
391
  };
382
- function createClient(model, context, apiKey, optionsHeaders, sessionId, compat = getCompat(model), env) {
392
+ function createClient(model, context, apiKey, optionsHeaders, sessionId, compat = getCompat(model)) {
383
393
  const headers = { ...model.headers };
384
394
  if (model.provider === "github-copilot") {
385
395
  const hasImages = hasCopilotVisionInput(context.messages);
@@ -398,18 +408,11 @@ function createClient(model, context, apiKey, optionsHeaders, sessionId, compat
398
408
  if (optionsHeaders) {
399
409
  Object.assign(headers, optionsHeaders);
400
410
  }
401
- const defaultHeaders = model.provider === "cloudflare-ai-gateway"
402
- ? {
403
- ...headers,
404
- Authorization: headers.Authorization ?? null,
405
- "cf-aig-authorization": `Bearer ${apiKey}`,
406
- }
407
- : headers;
408
411
  return new OpenAI({
409
412
  apiKey,
410
- baseURL: isCloudflareProvider(model.provider) ? resolveCloudflareBaseUrl(model, env) : model.baseUrl,
413
+ baseURL: model.baseUrl,
411
414
  dangerouslyAllowBrowser: true,
412
- defaultHeaders,
415
+ defaultHeaders: headers,
413
416
  });
414
417
  }
415
418
  function buildParams(model, context, options, compat = getCompat(model), cacheRetention = resolveCacheRetention(options?.cacheRetention, options?.env)) {
@@ -545,7 +548,7 @@ function buildParams(model, context, options, compat = getCompat(model), cacheRe
545
548
  params.provider = model.compat.openRouterRouting;
546
549
  }
547
550
  // Vercel AI Gateway provider routing preferences
548
- if (model.baseUrl.includes("ai-gateway.vercel.sh") && model.compat?.vercelGatewayRouting) {
551
+ if (model.compat?.vercelGatewayRouting) {
549
552
  const routing = model.compat.vercelGatewayRouting;
550
553
  if (routing.only || routing.order) {
551
554
  const gatewayOptions = {};
@@ -913,6 +916,7 @@ function parseChunkUsage(rawUsage, model) {
913
916
  output: outputTokens,
914
917
  cacheRead: cacheReadTokens,
915
918
  cacheWrite: cacheWriteTokens,
919
+ reasoning: rawUsage.completion_tokens_details?.reasoning_tokens || 0,
916
920
  totalTokens: input + outputTokens + cacheReadTokens + cacheWriteTokens,
917
921
  cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
918
922
  };
@@ -943,9 +947,9 @@ function mapStopReason(reason) {
943
947
  }
944
948
  }
945
949
  /**
946
- * Detect compatibility settings from provider and baseUrl for known providers.
947
- * Provider takes precedence over URL-based detection since it's explicitly configured.
948
- * Returns a fully resolved OpenAICompletionsCompat object with all fields set.
950
+ * Auto-detect compatibility settings from provider name and baseUrl.
951
+ * Used as the base when model.compat is not set; explicit model.compat
952
+ * entries override these detected values.
949
953
  */
950
954
  function detectCompat(model) {
951
955
  const provider = model.provider;
@@ -1018,7 +1022,7 @@ function detectCompat(model) {
1018
1022
  }
1019
1023
  /**
1020
1024
  * Get resolved compatibility settings for a model.
1021
- * Uses explicit model.compat if provided, otherwise auto-detects from provider/URL.
1025
+ * Auto-detects from provider/URL then overrides with explicit model.compat.
1022
1026
  */
1023
1027
  function getCompat(model) {
1024
1028
  const detected = detectCompat(model);
@@ -214,254 +214,236 @@ export function convertResponsesTools(tools, options) {
214
214
  strict,
215
215
  }));
216
216
  }
217
- // =============================================================================
218
- // Stream processing
219
- // =============================================================================
220
217
  export async function processResponsesStream(openaiStream, output, stream, model, options) {
221
- let currentItem = null;
222
- let currentBlock = null;
223
- const blocks = output.content;
224
- const blockIndex = () => blocks.length - 1;
218
+ let sawTerminalResponseEvent = false;
219
+ const outputSlots = new Map();
220
+ const getSlot = (outputIndex, type) => {
221
+ const slot = outputSlots.get(outputIndex);
222
+ return slot?.type === type ? slot : undefined;
223
+ };
224
+ const createSlot = (outputIndex, item) => {
225
+ if (item.type === "reasoning") {
226
+ const block = { type: "thinking", thinking: "" };
227
+ output.content.push(block);
228
+ const slot = {
229
+ type: "thinking",
230
+ block,
231
+ contentIndex: output.content.length - 1,
232
+ };
233
+ outputSlots.set(outputIndex, slot);
234
+ stream.push({ type: "thinking_start", contentIndex: slot.contentIndex, partial: output });
235
+ return slot;
236
+ }
237
+ if (item.type === "message") {
238
+ const block = { type: "text", text: "" };
239
+ output.content.push(block);
240
+ const slot = { type: "text", block, contentIndex: output.content.length - 1 };
241
+ outputSlots.set(outputIndex, slot);
242
+ stream.push({ type: "text_start", contentIndex: slot.contentIndex, partial: output });
243
+ return slot;
244
+ }
245
+ if (item.type === "function_call") {
246
+ const block = {
247
+ type: "toolCall",
248
+ id: `${item.call_id}|${item.id}`,
249
+ name: item.name,
250
+ arguments: {},
251
+ partialJson: item.arguments || "",
252
+ };
253
+ output.content.push(block);
254
+ const slot = {
255
+ type: "toolCall",
256
+ block,
257
+ contentIndex: output.content.length - 1,
258
+ };
259
+ outputSlots.set(outputIndex, slot);
260
+ stream.push({ type: "toolcall_start", contentIndex: slot.contentIndex, partial: output });
261
+ return slot;
262
+ }
263
+ return undefined;
264
+ };
265
+ const getOrCreateSlot = (outputIndex, item) => {
266
+ return outputSlots.get(outputIndex) ?? createSlot(outputIndex, item);
267
+ };
268
+ const finalizeResponse = (response) => {
269
+ sawTerminalResponseEvent = true;
270
+ if (response?.id) {
271
+ output.responseId = response.id;
272
+ }
273
+ if (response?.usage) {
274
+ const cachedTokens = response.usage.input_tokens_details?.cached_tokens || 0;
275
+ output.usage = {
276
+ // OpenAI includes cached tokens in input_tokens, so subtract to get non-cached input
277
+ input: (response.usage.input_tokens || 0) - cachedTokens,
278
+ output: response.usage.output_tokens || 0,
279
+ cacheRead: cachedTokens,
280
+ cacheWrite: 0,
281
+ reasoning: response.usage.output_tokens_details?.reasoning_tokens || 0,
282
+ totalTokens: response.usage.total_tokens || 0,
283
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
284
+ };
285
+ }
286
+ calculateCost(model, output.usage);
287
+ if (options?.applyServiceTierPricing) {
288
+ const serviceTier = options.resolveServiceTier
289
+ ? options.resolveServiceTier(response?.service_tier, options.serviceTier)
290
+ : (response?.service_tier ?? options.serviceTier);
291
+ options.applyServiceTierPricing(output.usage, serviceTier);
292
+ }
293
+ // Map status to stop reason
294
+ output.stopReason = mapStopReason(response?.status);
295
+ if (output.content.some((b) => b.type === "toolCall") && output.stopReason === "stop") {
296
+ output.stopReason = "toolUse";
297
+ }
298
+ };
225
299
  for await (const event of openaiStream) {
226
300
  if (event.type === "response.created") {
227
301
  output.responseId = event.response.id;
228
302
  }
229
303
  else if (event.type === "response.output_item.added") {
230
- const item = event.item;
231
- if (item.type === "reasoning") {
232
- currentItem = item;
233
- currentBlock = { type: "thinking", thinking: "" };
234
- output.content.push(currentBlock);
235
- stream.push({ type: "thinking_start", contentIndex: blockIndex(), partial: output });
236
- }
237
- else if (item.type === "message") {
238
- currentItem = item;
239
- currentBlock = { type: "text", text: "" };
240
- output.content.push(currentBlock);
241
- stream.push({ type: "text_start", contentIndex: blockIndex(), partial: output });
242
- }
243
- else if (item.type === "function_call") {
244
- currentItem = item;
245
- currentBlock = {
246
- type: "toolCall",
247
- id: `${item.call_id}|${item.id}`,
248
- name: item.name,
249
- arguments: {},
250
- partialJson: item.arguments || "",
251
- };
252
- output.content.push(currentBlock);
253
- stream.push({ type: "toolcall_start", contentIndex: blockIndex(), partial: output });
254
- }
255
- }
256
- else if (event.type === "response.reasoning_summary_part.added") {
257
- if (currentItem && currentItem.type === "reasoning") {
258
- currentItem.summary = currentItem.summary || [];
259
- currentItem.summary.push(event.part);
260
- }
304
+ createSlot(event.output_index, event.item);
261
305
  }
262
306
  else if (event.type === "response.reasoning_summary_text.delta") {
263
- if (currentItem?.type === "reasoning" && currentBlock?.type === "thinking") {
264
- currentItem.summary = currentItem.summary || [];
265
- const lastPart = currentItem.summary[currentItem.summary.length - 1];
266
- if (lastPart) {
267
- currentBlock.thinking += event.delta;
268
- lastPart.text += event.delta;
269
- stream.push({
270
- type: "thinking_delta",
271
- contentIndex: blockIndex(),
272
- delta: event.delta,
273
- partial: output,
274
- });
275
- }
276
- }
307
+ const slot = getSlot(event.output_index, "thinking");
308
+ if (!slot)
309
+ continue;
310
+ slot.block.thinking += event.delta;
311
+ stream.push({
312
+ type: "thinking_delta",
313
+ contentIndex: slot.contentIndex,
314
+ delta: event.delta,
315
+ partial: output,
316
+ });
277
317
  }
278
318
  else if (event.type === "response.reasoning_summary_part.done") {
279
- if (currentItem?.type === "reasoning" && currentBlock?.type === "thinking") {
280
- currentItem.summary = currentItem.summary || [];
281
- const lastPart = currentItem.summary[currentItem.summary.length - 1];
282
- if (lastPart) {
283
- currentBlock.thinking += "\n\n";
284
- lastPart.text += "\n\n";
285
- stream.push({
286
- type: "thinking_delta",
287
- contentIndex: blockIndex(),
288
- delta: "\n\n",
289
- partial: output,
290
- });
291
- }
292
- }
319
+ const slot = getSlot(event.output_index, "thinking");
320
+ if (!slot)
321
+ continue;
322
+ slot.block.thinking += "\n\n";
323
+ stream.push({
324
+ type: "thinking_delta",
325
+ contentIndex: slot.contentIndex,
326
+ delta: "\n\n",
327
+ partial: output,
328
+ });
293
329
  }
294
330
  else if (event.type === "response.reasoning_text.delta") {
295
- if (currentItem?.type === "reasoning" && currentBlock?.type === "thinking") {
296
- currentBlock.thinking += event.delta;
297
- stream.push({
298
- type: "thinking_delta",
299
- contentIndex: blockIndex(),
300
- delta: event.delta,
301
- partial: output,
302
- });
303
- }
304
- }
305
- else if (event.type === "response.content_part.added") {
306
- if (currentItem?.type === "message") {
307
- currentItem.content = currentItem.content || [];
308
- // Filter out ReasoningText, only accept output_text and refusal
309
- if (event.part.type === "output_text" || event.part.type === "refusal") {
310
- currentItem.content.push(event.part);
311
- }
312
- }
331
+ const slot = getSlot(event.output_index, "thinking");
332
+ if (!slot)
333
+ continue;
334
+ slot.block.thinking += event.delta;
335
+ stream.push({
336
+ type: "thinking_delta",
337
+ contentIndex: slot.contentIndex,
338
+ delta: event.delta,
339
+ partial: output,
340
+ });
313
341
  }
314
342
  else if (event.type === "response.output_text.delta") {
315
- if (currentItem?.type === "message" && currentBlock?.type === "text") {
316
- if (!currentItem.content || currentItem.content.length === 0) {
317
- continue;
318
- }
319
- const lastPart = currentItem.content[currentItem.content.length - 1];
320
- if (lastPart?.type === "output_text") {
321
- currentBlock.text += event.delta;
322
- lastPart.text += event.delta;
323
- stream.push({
324
- type: "text_delta",
325
- contentIndex: blockIndex(),
326
- delta: event.delta,
327
- partial: output,
328
- });
329
- }
330
- }
343
+ const slot = getSlot(event.output_index, "text");
344
+ if (!slot)
345
+ continue;
346
+ slot.block.text += event.delta;
347
+ stream.push({
348
+ type: "text_delta",
349
+ contentIndex: slot.contentIndex,
350
+ delta: event.delta,
351
+ partial: output,
352
+ });
331
353
  }
332
354
  else if (event.type === "response.refusal.delta") {
333
- if (currentItem?.type === "message" && currentBlock?.type === "text") {
334
- if (!currentItem.content || currentItem.content.length === 0) {
335
- continue;
336
- }
337
- const lastPart = currentItem.content[currentItem.content.length - 1];
338
- if (lastPart?.type === "refusal") {
339
- currentBlock.text += event.delta;
340
- lastPart.refusal += event.delta;
341
- stream.push({
342
- type: "text_delta",
343
- contentIndex: blockIndex(),
344
- delta: event.delta,
345
- partial: output,
346
- });
347
- }
348
- }
355
+ const slot = getSlot(event.output_index, "text");
356
+ if (!slot)
357
+ continue;
358
+ slot.block.text += event.delta;
359
+ stream.push({
360
+ type: "text_delta",
361
+ contentIndex: slot.contentIndex,
362
+ delta: event.delta,
363
+ partial: output,
364
+ });
349
365
  }
350
366
  else if (event.type === "response.function_call_arguments.delta") {
351
- if (currentItem?.type === "function_call" && currentBlock?.type === "toolCall") {
352
- currentBlock.partialJson += event.delta;
353
- currentBlock.arguments = parseStreamingJson(currentBlock.partialJson);
354
- stream.push({
355
- type: "toolcall_delta",
356
- contentIndex: blockIndex(),
357
- delta: event.delta,
358
- partial: output,
359
- });
360
- }
367
+ const slot = getSlot(event.output_index, "toolCall");
368
+ if (!slot)
369
+ continue;
370
+ slot.block.partialJson += event.delta;
371
+ slot.block.arguments = parseStreamingJson(slot.block.partialJson);
372
+ stream.push({
373
+ type: "toolcall_delta",
374
+ contentIndex: slot.contentIndex,
375
+ delta: event.delta,
376
+ partial: output,
377
+ });
361
378
  }
362
379
  else if (event.type === "response.function_call_arguments.done") {
363
- if (currentItem?.type === "function_call" && currentBlock?.type === "toolCall") {
364
- const previousPartialJson = currentBlock.partialJson;
365
- currentBlock.partialJson = event.arguments;
366
- currentBlock.arguments = parseStreamingJson(currentBlock.partialJson);
367
- if (event.arguments.startsWith(previousPartialJson)) {
368
- const delta = event.arguments.slice(previousPartialJson.length);
369
- if (delta.length > 0) {
370
- stream.push({
371
- type: "toolcall_delta",
372
- contentIndex: blockIndex(),
373
- delta,
374
- partial: output,
375
- });
376
- }
380
+ const slot = getSlot(event.output_index, "toolCall");
381
+ if (!slot)
382
+ continue;
383
+ const previousPartialJson = slot.block.partialJson;
384
+ slot.block.partialJson = event.arguments;
385
+ slot.block.arguments = parseStreamingJson(slot.block.partialJson);
386
+ if (event.arguments.startsWith(previousPartialJson)) {
387
+ const delta = event.arguments.slice(previousPartialJson.length);
388
+ if (delta.length > 0) {
389
+ stream.push({
390
+ type: "toolcall_delta",
391
+ contentIndex: slot.contentIndex,
392
+ delta,
393
+ partial: output,
394
+ });
377
395
  }
378
396
  }
379
397
  }
380
398
  else if (event.type === "response.output_item.done") {
381
399
  const item = event.item;
382
- if (item.type === "reasoning" && currentBlock?.type === "thinking") {
400
+ const slot = getOrCreateSlot(event.output_index, item);
401
+ if (item.type === "reasoning" && slot?.type === "thinking") {
383
402
  const summaryText = item.summary?.map((s) => s.text).join("\n\n") || "";
384
403
  const contentText = item.content?.map((c) => c.text).join("\n\n") || "";
385
- currentBlock.thinking = summaryText || contentText || currentBlock.thinking;
386
- currentBlock.thinkingSignature = JSON.stringify(item);
404
+ slot.block.thinking = summaryText || contentText || slot.block.thinking;
405
+ slot.block.thinkingSignature = JSON.stringify(item);
387
406
  stream.push({
388
407
  type: "thinking_end",
389
- contentIndex: blockIndex(),
390
- content: currentBlock.thinking,
408
+ contentIndex: slot.contentIndex,
409
+ content: slot.block.thinking,
391
410
  partial: output,
392
411
  });
393
- currentBlock = null;
412
+ outputSlots.delete(event.output_index);
394
413
  }
395
- else if (item.type === "message" && currentBlock?.type === "text") {
396
- currentBlock.text =
397
- item.content?.map((c) => (c.type === "output_text" ? c.text : c.refusal)).join("") || "";
398
- currentBlock.textSignature = encodeTextSignatureV1(item.id, item.phase ?? undefined);
414
+ else if (item.type === "message" && slot?.type === "text") {
415
+ slot.block.text = item.content?.map((c) => (c.type === "output_text" ? c.text : c.refusal)).join("") || "";
416
+ slot.block.textSignature = encodeTextSignatureV1(item.id, item.phase ?? undefined);
399
417
  stream.push({
400
418
  type: "text_end",
401
- contentIndex: blockIndex(),
402
- content: currentBlock.text,
419
+ contentIndex: slot.contentIndex,
420
+ content: slot.block.text,
403
421
  partial: output,
404
422
  });
405
- currentBlock = null;
423
+ outputSlots.delete(event.output_index);
406
424
  }
407
- else if (item.type === "function_call") {
408
- const args = currentBlock?.type === "toolCall" && currentBlock.partialJson
409
- ? parseStreamingJson(currentBlock.partialJson)
410
- : parseStreamingJson(item.arguments || "{}");
411
- let toolCall;
412
- if (currentBlock?.type === "toolCall") {
413
- // Finalize in-place and strip the scratch buffer so replay only
414
- // carries parsed arguments.
415
- currentBlock.arguments = args;
416
- delete currentBlock.partialJson;
417
- toolCall = currentBlock;
418
- }
419
- else {
420
- toolCall = {
421
- type: "toolCall",
422
- id: `${item.call_id}|${item.id}`,
423
- name: item.name,
424
- arguments: args,
425
- };
426
- }
427
- currentBlock = null;
428
- stream.push({ type: "toolcall_end", contentIndex: blockIndex(), toolCall, partial: output });
425
+ else if (item.type === "function_call" && slot?.type === "toolCall") {
426
+ slot.block.arguments = parseStreamingJson(item.arguments || slot.block.partialJson || "{}");
427
+ // Finalize in-place and strip the scratch buffer so replay only
428
+ // carries parsed arguments.
429
+ delete slot.block.partialJson;
430
+ stream.push({
431
+ type: "toolcall_end",
432
+ contentIndex: slot.contentIndex,
433
+ toolCall: slot.block,
434
+ partial: output,
435
+ });
436
+ outputSlots.delete(event.output_index);
429
437
  }
430
438
  }
431
- else if (event.type === "response.completed") {
432
- const response = event.response;
433
- if (response?.id) {
434
- output.responseId = response.id;
435
- }
436
- if (response?.usage) {
437
- const cachedTokens = response.usage.input_tokens_details?.cached_tokens || 0;
438
- output.usage = {
439
- // OpenAI includes cached tokens in input_tokens, so subtract to get non-cached input
440
- input: (response.usage.input_tokens || 0) - cachedTokens,
441
- output: response.usage.output_tokens || 0,
442
- cacheRead: cachedTokens,
443
- cacheWrite: 0,
444
- totalTokens: response.usage.total_tokens || 0,
445
- cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
446
- };
447
- }
448
- calculateCost(model, output.usage);
449
- if (options?.applyServiceTierPricing) {
450
- const serviceTier = options.resolveServiceTier
451
- ? options.resolveServiceTier(response?.service_tier, options.serviceTier)
452
- : (response?.service_tier ?? options.serviceTier);
453
- options.applyServiceTierPricing(output.usage, serviceTier);
454
- }
455
- // Map status to stop reason
456
- output.stopReason = mapStopReason(response?.status);
457
- if (output.content.some((b) => b.type === "toolCall") && output.stopReason === "stop") {
458
- output.stopReason = "toolUse";
459
- }
439
+ else if (event.type === "response.completed" || event.type === "response.incomplete") {
440
+ finalizeResponse(event.response);
460
441
  }
461
442
  else if (event.type === "error") {
462
443
  throw new Error(`Error Code ${event.code}: ${event.message}` || "Unknown error");
463
444
  }
464
445
  else if (event.type === "response.failed") {
446
+ sawTerminalResponseEvent = true;
465
447
  const error = event.response?.error;
466
448
  const details = event.response?.incomplete_details;
467
449
  const msg = error
@@ -472,6 +454,9 @@ export async function processResponsesStream(openaiStream, output, stream, model
472
454
  throw new Error(msg);
473
455
  }
474
456
  }
457
+ if (!sawTerminalResponseEvent) {
458
+ throw new Error("OpenAI Responses stream ended before a terminal response event");
459
+ }
475
460
  }
476
461
  function mapStopReason(status) {
477
462
  if (!status)