@psnext/slingcli 2.4.20260601-1 → 2.5.20260602-2

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 (96) hide show
  1. package/node_modules/@aws-sdk/credential-provider-http/node_modules/@smithy/node-http-handler/package.json +4 -4
  2. package/node_modules/@aws-sdk/credential-provider-node/dist-cjs/index.js +12 -1
  3. package/node_modules/@aws-sdk/credential-provider-node/dist-es/runtime/memoize-chain.js +12 -1
  4. package/node_modules/@aws-sdk/credential-provider-node/package.json +1 -1
  5. package/node_modules/@aws-sdk/nested-clients/node_modules/@smithy/node-http-handler/package.json +4 -4
  6. package/node_modules/@earendil-works/pi-agent-core/dist/harness/agent-harness.js +81 -18
  7. package/node_modules/@earendil-works/pi-agent-core/dist/harness/compaction/branch-summarization.js +1 -0
  8. package/node_modules/@earendil-works/pi-agent-core/dist/harness/compaction/compaction.js +1 -0
  9. package/node_modules/@earendil-works/pi-agent-core/dist/harness/session/session.js +14 -1
  10. package/node_modules/@earendil-works/pi-agent-core/package.json +3 -3
  11. package/node_modules/@earendil-works/pi-ai/dist/models.generated.js +197 -190
  12. package/node_modules/@earendil-works/pi-ai/dist/providers/amazon-bedrock.js +84 -21
  13. package/node_modules/@earendil-works/pi-ai/dist/providers/anthropic.js +24 -14
  14. package/node_modules/@earendil-works/pi-ai/dist/providers/azure-openai-responses.js +5 -9
  15. package/node_modules/@earendil-works/pi-ai/dist/providers/google-vertex.js +1 -1
  16. package/node_modules/@earendil-works/pi-ai/dist/providers/google.js +5 -3
  17. package/node_modules/@earendil-works/pi-ai/dist/providers/images/openrouter.js +2 -3
  18. package/node_modules/@earendil-works/pi-ai/dist/providers/mistral.js +2 -3
  19. package/node_modules/@earendil-works/pi-ai/dist/providers/openai-codex-responses.js +130 -62
  20. package/node_modules/@earendil-works/pi-ai/dist/providers/openai-completions.js +18 -12
  21. package/node_modules/@earendil-works/pi-ai/dist/providers/openai-responses-shared.js +4 -1
  22. package/node_modules/@earendil-works/pi-ai/dist/providers/openai-responses.js +5 -9
  23. package/node_modules/@earendil-works/pi-ai/dist/providers/simple-options.js +1 -0
  24. package/node_modules/@earendil-works/pi-ai/dist/stream.js +14 -2
  25. package/node_modules/@earendil-works/pi-ai/dist/utils/abort-signals.js +34 -0
  26. package/node_modules/@earendil-works/pi-ai/dist/utils/oauth/device-code.js +8 -7
  27. package/node_modules/@earendil-works/pi-ai/dist/utils/oauth/github-copilot.js +1 -1
  28. package/node_modules/@earendil-works/pi-ai/dist/utils/oauth/index.js +1 -1
  29. package/node_modules/@earendil-works/pi-ai/dist/utils/oauth/openai-codex.js +179 -79
  30. package/node_modules/@earendil-works/pi-ai/package.json +2 -2
  31. package/node_modules/@earendil-works/pi-coding-agent/dist/cli/args.js +27 -0
  32. package/node_modules/@earendil-works/pi-coding-agent/dist/config.js +9 -1
  33. package/node_modules/@earendil-works/pi-coding-agent/dist/core/agent-session-services.js +1 -0
  34. package/node_modules/@earendil-works/pi-coding-agent/dist/core/agent-session.js +29 -5
  35. package/node_modules/@earendil-works/pi-coding-agent/dist/core/extensions/runner.js +21 -3
  36. package/node_modules/@earendil-works/pi-coding-agent/dist/core/footer-data-provider.js +29 -1
  37. package/node_modules/@earendil-works/pi-coding-agent/dist/core/model-registry.js +65 -13
  38. package/node_modules/@earendil-works/pi-coding-agent/dist/core/model-resolver.js +1 -1
  39. package/node_modules/@earendil-works/pi-coding-agent/dist/core/resolve-config-value.js +134 -11
  40. package/node_modules/@earendil-works/pi-coding-agent/dist/core/sdk.js +12 -6
  41. package/node_modules/@earendil-works/pi-coding-agent/dist/core/session-manager.js +167 -96
  42. package/node_modules/@earendil-works/pi-coding-agent/dist/core/settings-manager.js +14 -9
  43. package/node_modules/@earendil-works/pi-coding-agent/dist/core/system-prompt.js +0 -3
  44. package/node_modules/@earendil-works/pi-coding-agent/dist/core/tools/edit.js +7 -10
  45. package/node_modules/@earendil-works/pi-coding-agent/dist/core/tools/ls.js +5 -7
  46. package/node_modules/@earendil-works/pi-coding-agent/dist/core/tools/read.js +6 -7
  47. package/node_modules/@earendil-works/pi-coding-agent/dist/core/tools/render-utils.js +17 -1
  48. package/node_modules/@earendil-works/pi-coding-agent/dist/core/tools/write.js +5 -6
  49. package/node_modules/@earendil-works/pi-coding-agent/dist/index.js +2 -0
  50. package/node_modules/@earendil-works/pi-coding-agent/dist/main.js +69 -16
  51. package/node_modules/@earendil-works/pi-coding-agent/dist/migrations.js +118 -1
  52. package/node_modules/@earendil-works/pi-coding-agent/dist/modes/interactive/components/config-selector.js +1 -1
  53. package/node_modules/@earendil-works/pi-coding-agent/dist/modes/interactive/components/login-dialog.js +2 -4
  54. package/node_modules/@earendil-works/pi-coding-agent/dist/modes/interactive/interactive-mode.js +61 -6
  55. package/node_modules/@earendil-works/pi-coding-agent/dist/modes/interactive/theme/theme.js +10 -0
  56. package/node_modules/@earendil-works/pi-coding-agent/dist/modes/print-mode.js +1 -0
  57. package/node_modules/@earendil-works/pi-coding-agent/dist/modes/rpc/rpc-mode.js +4 -1
  58. package/node_modules/@earendil-works/pi-coding-agent/dist/utils/deprecation.js +13 -0
  59. package/node_modules/@earendil-works/pi-coding-agent/dist/utils/json.js +7 -0
  60. package/node_modules/@earendil-works/pi-coding-agent/npm-shrinkwrap.json +71 -56
  61. package/node_modules/@earendil-works/pi-coding-agent/package.json +7 -7
  62. package/node_modules/@earendil-works/pi-tui/dist/components/editor.js +9 -53
  63. package/node_modules/@earendil-works/pi-tui/dist/components/input.js +6 -54
  64. package/node_modules/@earendil-works/pi-tui/dist/terminal-image.js +34 -7
  65. package/node_modules/@earendil-works/pi-tui/dist/terminal.js +172 -37
  66. package/node_modules/@earendil-works/pi-tui/dist/tui.js +166 -22
  67. package/node_modules/@earendil-works/pi-tui/dist/utils.js +8 -3
  68. package/node_modules/@earendil-works/pi-tui/dist/word-navigation.js +96 -0
  69. package/node_modules/@earendil-works/pi-tui/package.json +2 -2
  70. package/node_modules/@mariozechner/clipboard/Cargo.toml +3 -3
  71. package/node_modules/@mariozechner/clipboard/index.d.ts +34 -20
  72. package/node_modules/@mariozechner/clipboard/index.js +546 -257
  73. package/node_modules/@mariozechner/clipboard/package.json +14 -14
  74. package/node_modules/@mariozechner/clipboard/src/lib.rs +4 -9
  75. package/node_modules/@mariozechner/clipboard-linux-x64-gnu/clipboard.linux-x64-gnu.node +0 -0
  76. package/node_modules/@mariozechner/clipboard-linux-x64-gnu/package.json +2 -2
  77. package/node_modules/@smithy/core/dist-cjs/submodules/retry/index.browser.js +39 -22
  78. package/node_modules/@smithy/core/dist-cjs/submodules/retry/index.js +39 -22
  79. package/node_modules/@smithy/core/dist-es/submodules/retry/middleware-retry/configurations.js +19 -6
  80. package/node_modules/@smithy/core/dist-es/submodules/retry/middleware-retry/retryMiddleware.js +4 -5
  81. package/node_modules/@smithy/core/dist-es/submodules/retry/service-error-classification/constants.js +1 -1
  82. package/node_modules/@smithy/core/dist-es/submodules/retry/util-retry/ConfiguredRetryStrategy.js +4 -5
  83. package/node_modules/@smithy/core/dist-es/submodules/retry/util-retry/DefaultRetryToken.js +3 -0
  84. package/node_modules/@smithy/core/dist-es/submodules/retry/util-retry/StandardRetryStrategy.js +9 -5
  85. package/node_modules/@smithy/core/dist-types/submodules/retry/middleware-retry/configurations.d.ts +4 -1
  86. package/node_modules/@smithy/core/dist-types/submodules/retry/util-retry/ConfiguredRetryStrategy.d.ts +1 -2
  87. package/node_modules/@smithy/core/dist-types/submodules/retry/util-retry/DefaultRetryToken.d.ts +3 -0
  88. package/node_modules/@smithy/core/dist-types/submodules/retry/util-retry/StandardRetryStrategy.d.ts +1 -1
  89. package/node_modules/@smithy/core/package.json +2 -2
  90. package/node_modules/@smithy/credential-provider-imds/package.json +3 -3
  91. package/node_modules/@smithy/fetch-http-handler/package.json +4 -4
  92. package/node_modules/@smithy/signature-v4/package.json +3 -3
  93. package/node_modules/@smithy/types/dist-types/retry.d.ts +25 -0
  94. package/node_modules/@smithy/types/package.json +1 -1
  95. package/package.json +6 -6
  96. package/slingshot/index.js +231 -230
@@ -15,9 +15,9 @@ if (typeof process !== "undefined" && (process.versions?.node || process.version
15
15
  _os = m;
16
16
  });
17
17
  }
18
- import { getEnvApiKey } from "../env-api-keys.js";
19
18
  import { clampThinkingLevel } from "../models.js";
20
19
  import { registerSessionResourceCleanup } from "../session-resources.js";
20
+ import { combineAbortSignals } from "../utils/abort-signals.js";
21
21
  import { appendAssistantMessageDiagnostic, createAssistantMessageDiagnostic, formatThrownValue, } from "../utils/diagnostics.js";
22
22
  import { AssistantMessageEventStream } from "../utils/event-stream.js";
23
23
  import { headersToRecord } from "../utils/headers.js";
@@ -32,6 +32,8 @@ const JWT_CLAIM_PATH = "https://api.openai.com/auth";
32
32
  const DEFAULT_MAX_RETRIES = 0;
33
33
  const BASE_DELAY_MS = 1000;
34
34
  const DEFAULT_MAX_RETRY_DELAY_MS = 60_000;
35
+ const DEFAULT_SSE_HEADER_TIMEOUT_MS = 10_000;
36
+ const DEFAULT_WEBSOCKET_CONNECT_TIMEOUT_MS = 15_000;
35
37
  const CODEX_TOOL_CALL_PROVIDERS = new Set(["openai", "openai-codex", "opencode"]);
36
38
  const WEBSOCKET_MESSAGE_TOO_BIG_CLOSE_CODE = 1009;
37
39
  const CODEX_RESPONSE_STATUSES = new Set([
@@ -96,6 +98,27 @@ function sleep(ms, signal) {
96
98
  });
97
99
  });
98
100
  }
101
+ function normalizeTimeoutMs(value) {
102
+ if (value === undefined)
103
+ return undefined;
104
+ if (!Number.isFinite(value) || value < 0) {
105
+ throw new Error(`Invalid timeoutMs: ${String(value)}`);
106
+ }
107
+ return Math.floor(value);
108
+ }
109
+ function createSSEHeaderTimeout() {
110
+ const controller = new AbortController();
111
+ let error;
112
+ const timeout = setTimeout(() => {
113
+ error = new Error(`Codex SSE response headers timed out after ${DEFAULT_SSE_HEADER_TIMEOUT_MS}ms`);
114
+ controller.abort(error);
115
+ }, DEFAULT_SSE_HEADER_TIMEOUT_MS);
116
+ return {
117
+ signal: controller.signal,
118
+ clear: () => clearTimeout(timeout),
119
+ error: () => error,
120
+ };
121
+ }
99
122
  // ============================================================================
100
123
  // Main Stream Function
101
124
  // ============================================================================
@@ -120,11 +143,11 @@ export const streamOpenAICodexResponses = (model, context, options) => {
120
143
  timestamp: Date.now(),
121
144
  };
122
145
  try {
123
- const apiKey = options?.apiKey || getEnvApiKey(model.provider) || "";
146
+ const apiKey = options?.apiKey;
124
147
  if (!apiKey) {
125
148
  throw new Error(`No API key for provider: ${model.provider}`);
126
149
  }
127
- const accountId = extractAccountId(apiKey) || "";
150
+ const accountId = extractAccountId(apiKey);
128
151
  let body = buildRequestBody(model, context, options);
129
152
  const nextBody = await options?.onPayload?.(body, model);
130
153
  if (nextBody !== undefined) {
@@ -134,6 +157,8 @@ export const streamOpenAICodexResponses = (model, context, options) => {
134
157
  const sseHeaders = buildSSEHeaders(model.headers, options?.headers, accountId, apiKey, options?.sessionId);
135
158
  const websocketHeaders = buildWebSocketHeaders(model.headers, options?.headers, accountId, apiKey, websocketRequestId);
136
159
  const bodyJson = JSON.stringify(body);
160
+ const idleTimeoutMs = normalizeTimeoutMs(options?.timeoutMs);
161
+ const websocketConnectTimeoutMs = normalizeTimeoutMs(options?.websocketConnectTimeoutMs);
137
162
  const transport = options?.transport || "auto";
138
163
  const websocketDisabledForSession = transport !== "sse" && isWebSocketSseFallbackActive(options?.sessionId);
139
164
  if (websocketDisabledForSession) {
@@ -144,7 +169,7 @@ export const streamOpenAICodexResponses = (model, context, options) => {
144
169
  try {
145
170
  await processWebSocketStream(resolveCodexWebSocketUrl(model.baseUrl), body, websocketHeaders, output, stream, model, () => {
146
171
  websocketStarted = true;
147
- }, options);
172
+ }, idleTimeoutMs, websocketConnectTimeoutMs, options);
148
173
  if (options?.signal?.aborted) {
149
174
  throw new Error("Request was aborted");
150
175
  }
@@ -184,12 +209,24 @@ export const streamOpenAICodexResponses = (model, context, options) => {
184
209
  throw new Error("Request was aborted");
185
210
  }
186
211
  try {
187
- response = await fetch(resolveCodexUrl(model.baseUrl), {
188
- method: "POST",
189
- headers: sseHeaders,
190
- body: bodyJson,
191
- signal: options?.signal,
192
- });
212
+ const headerTimeout = createSSEHeaderTimeout();
213
+ const combinedSignal = combineAbortSignals([options?.signal, headerTimeout.signal]);
214
+ try {
215
+ response = await fetch(resolveCodexUrl(model.baseUrl), {
216
+ method: "POST",
217
+ headers: sseHeaders,
218
+ body: bodyJson,
219
+ signal: combinedSignal.signal,
220
+ });
221
+ }
222
+ catch (error) {
223
+ const timeoutError = headerTimeout.error();
224
+ throw timeoutError && !options?.signal?.aborted ? timeoutError : error;
225
+ }
226
+ finally {
227
+ combinedSignal.cleanup();
228
+ headerTimeout.clear();
229
+ }
193
230
  await options?.onResponse?.({ status: response.status, headers: headersToRecord(response.headers) }, model);
194
231
  if (response.ok) {
195
232
  break;
@@ -257,7 +294,7 @@ export const streamOpenAICodexResponses = (model, context, options) => {
257
294
  return stream;
258
295
  };
259
296
  export const streamSimpleOpenAICodexResponses = (model, context, options) => {
260
- const apiKey = options?.apiKey || getEnvApiKey(model.provider);
297
+ const apiKey = options?.apiKey;
261
298
  if (!apiKey) {
262
299
  throw new Error(`No API key for provider: ${model.provider}`);
263
300
  }
@@ -282,7 +319,7 @@ function buildRequestBody(model, context, options) {
282
319
  stream: true,
283
320
  instructions: context.systemPrompt || "You are a helpful assistant.",
284
321
  input: messages,
285
- text: { verbosity: options?.textVerbosity || "medium" },
322
+ text: { verbosity: options?.textVerbosity || "low" },
286
323
  include: ["reasoning.encrypted_content"],
287
324
  prompt_cache_key: clampOpenAIPromptCacheKey(options?.sessionId),
288
325
  tool_choice: "auto",
@@ -339,7 +376,11 @@ function resolveCodexServiceTier(responseServiceTier, requestServiceTier) {
339
376
  function resolveCodexUrl(baseUrl) {
340
377
  const raw = baseUrl && baseUrl.trim().length > 0 ? baseUrl : DEFAULT_CODEX_BASE_URL;
341
378
  const normalized = raw.replace(/\/+$/, "");
342
- return `${normalized}/responses`;
379
+ if (normalized.endsWith("/codex/responses"))
380
+ return normalized;
381
+ if (normalized.endsWith("/codex"))
382
+ return `${normalized}/responses`;
383
+ return `${normalized}/codex/responses`;
343
384
  }
344
385
  function resolveCodexWebSocketUrl(baseUrl) {
345
386
  const url = new URL(resolveCodexUrl(baseUrl));
@@ -353,7 +394,7 @@ function resolveCodexWebSocketUrl(baseUrl) {
353
394
  // Response Processing
354
395
  // ============================================================================
355
396
  async function processStream(response, output, stream, model, options) {
356
- await processResponsesStream(mapCodexEvents(parseSSE(response)), output, stream, model, {
397
+ await processResponsesStream(mapCodexEvents(parseSSE(response, options?.signal)), output, stream, model, {
357
398
  serviceTier: options?.serviceTier,
358
399
  resolveServiceTier: resolveCodexServiceTier,
359
400
  applyServiceTierPricing: (usage, serviceTier) => applyServiceTierPricing(usage, serviceTier, model),
@@ -420,15 +461,25 @@ function normalizeCodexStatus(status) {
420
461
  // ============================================================================
421
462
  // SSE Parsing
422
463
  // ============================================================================
423
- async function* parseSSE(response) {
464
+ async function* parseSSE(response, signal) {
424
465
  if (!response.body)
425
466
  return;
426
467
  const reader = response.body.getReader();
427
468
  const decoder = new TextDecoder();
428
469
  let buffer = "";
470
+ const onAbort = () => {
471
+ void reader.cancel().catch(() => { });
472
+ };
473
+ signal?.addEventListener("abort", onAbort, { once: true });
429
474
  try {
430
475
  while (true) {
476
+ if (signal?.aborted) {
477
+ throw new Error("Request was aborted");
478
+ }
431
479
  const { done, value } = await reader.read();
480
+ if (signal?.aborted) {
481
+ throw new Error("Request was aborted");
482
+ }
432
483
  if (done)
433
484
  break;
434
485
  buffer += decoder.decode(value, { stream: true });
@@ -459,6 +510,7 @@ async function* parseSSE(response) {
459
510
  }
460
511
  }
461
512
  finally {
513
+ signal?.removeEventListener("abort", onAbort);
462
514
  try {
463
515
  await reader.cancel();
464
516
  }
@@ -615,7 +667,7 @@ function scheduleSessionWebSocketExpiry(sessionId, entry) {
615
667
  websocketSessionCache.delete(sessionId);
616
668
  }, SESSION_WEBSOCKET_CACHE_TTL_MS);
617
669
  }
618
- async function connectWebSocket(url, headers, signal) {
670
+ async function connectWebSocket(url, headers, signal, connectTimeoutMs = DEFAULT_WEBSOCKET_CONNECT_TIMEOUT_MS) {
619
671
  const WebSocketCtor = await getWebSocketConstructor();
620
672
  if (!WebSocketCtor) {
621
673
  throw new Error("WebSocket transport is not available in this runtime");
@@ -624,6 +676,7 @@ async function connectWebSocket(url, headers, signal) {
624
676
  delete wsHeaders["OpenAI-Beta"];
625
677
  return new Promise((resolve, reject) => {
626
678
  let settled = false;
679
+ let timeout;
627
680
  let socket;
628
681
  try {
629
682
  socket = new WebSocketCtor(url, { headers: wsHeaders });
@@ -632,62 +685,63 @@ async function connectWebSocket(url, headers, signal) {
632
685
  reject(error instanceof Error ? error : new Error(String(error)));
633
686
  return;
634
687
  }
635
- const onOpen = () => {
636
- if (settled)
637
- return;
638
- settled = true;
639
- cleanup();
640
- resolve(socket);
688
+ const cleanup = () => {
689
+ if (timeout) {
690
+ clearTimeout(timeout);
691
+ timeout = undefined;
692
+ }
693
+ socket.removeEventListener("open", onOpen);
694
+ socket.removeEventListener("error", onError);
695
+ socket.removeEventListener("close", onClose);
696
+ signal?.removeEventListener("abort", onAbort);
641
697
  };
642
- const onError = (event) => {
643
- const error = extractWebSocketError(event);
698
+ const fail = (error, closeReason) => {
644
699
  if (settled)
645
700
  return;
646
701
  settled = true;
647
702
  cleanup();
703
+ if (closeReason) {
704
+ closeWebSocketSilently(socket, 1000, closeReason);
705
+ }
648
706
  reject(error);
649
707
  };
650
- const onClose = (event) => {
651
- const error = extractWebSocketCloseError(event);
708
+ const onOpen = () => {
652
709
  if (settled)
653
710
  return;
654
711
  settled = true;
655
712
  cleanup();
656
- reject(error);
713
+ resolve(socket);
657
714
  };
658
- const onAbort = () => {
659
- if (settled)
660
- return;
661
- settled = true;
662
- cleanup();
663
- socket.close(1000, "aborted");
664
- reject(new Error("Request was aborted"));
715
+ const onError = (event) => {
716
+ fail(extractWebSocketError(event));
665
717
  };
666
- const cleanup = () => {
667
- socket.removeEventListener("open", onOpen);
668
- socket.removeEventListener("error", onError);
669
- socket.removeEventListener("close", onClose);
670
- signal?.removeEventListener("abort", onAbort);
718
+ const onClose = (event) => {
719
+ fail(extractWebSocketCloseError(event));
720
+ };
721
+ const onAbort = () => {
722
+ fail(new Error("Request was aborted"), "aborted");
671
723
  };
672
724
  socket.addEventListener("open", onOpen);
673
725
  socket.addEventListener("error", onError);
674
726
  socket.addEventListener("close", onClose);
675
727
  signal?.addEventListener("abort", onAbort);
728
+ if (connectTimeoutMs > 0) {
729
+ timeout = setTimeout(() => {
730
+ fail(new Error(`WebSocket connect timeout after ${connectTimeoutMs}ms`), "connect_timeout");
731
+ }, connectTimeoutMs);
732
+ }
733
+ if (signal?.aborted) {
734
+ onAbort();
735
+ }
676
736
  });
677
737
  }
678
- async function acquireWebSocket(url, headers, sessionId, signal) {
738
+ async function acquireWebSocket(url, headers, sessionId, signal, connectTimeoutMs) {
679
739
  if (!sessionId) {
680
- const socket = await connectWebSocket(url, headers, signal);
740
+ const socket = await connectWebSocket(url, headers, signal, connectTimeoutMs);
681
741
  return {
682
742
  socket,
683
743
  reused: false,
684
- release: ({ keep } = {}) => {
685
- if (keep === false) {
686
- closeWebSocketSilently(socket);
687
- return;
688
- }
689
- closeWebSocketSilently(socket);
690
- },
744
+ release: () => closeWebSocketSilently(socket),
691
745
  };
692
746
  }
693
747
  const cached = websocketSessionCache.get(sessionId);
@@ -714,7 +768,7 @@ async function acquireWebSocket(url, headers, sessionId, signal) {
714
768
  };
715
769
  }
716
770
  if (cached.busy) {
717
- const socket = await connectWebSocket(url, headers, signal);
771
+ const socket = await connectWebSocket(url, headers, signal, connectTimeoutMs);
718
772
  return {
719
773
  socket,
720
774
  reused: false,
@@ -728,7 +782,7 @@ async function acquireWebSocket(url, headers, sessionId, signal) {
728
782
  websocketSessionCache.delete(sessionId);
729
783
  }
730
784
  }
731
- const socket = await connectWebSocket(url, headers, signal);
785
+ const socket = await connectWebSocket(url, headers, signal, connectTimeoutMs);
732
786
  const entry = { socket, busy: true };
733
787
  websocketSessionCache.set(sessionId, entry);
734
788
  return {
@@ -804,7 +858,7 @@ async function decodeWebSocketData(data) {
804
858
  }
805
859
  return null;
806
860
  }
807
- async function* parseWebSocket(socket, signal) {
861
+ async function* parseWebSocket(socket, signal, idleTimeoutMs) {
808
862
  const queue = [];
809
863
  let pending = null;
810
864
  let done = false;
@@ -882,8 +936,23 @@ async function* parseWebSocket(socket, signal) {
882
936
  }
883
937
  if (done)
884
938
  break;
885
- await new Promise((resolve) => {
939
+ let timeout;
940
+ await new Promise((resolve, reject) => {
886
941
  pending = resolve;
942
+ if (idleTimeoutMs !== undefined && idleTimeoutMs > 0) {
943
+ timeout = setTimeout(() => {
944
+ const error = new Error(`WebSocket idle timeout after ${idleTimeoutMs}ms`);
945
+ failed = error;
946
+ done = true;
947
+ pending = null;
948
+ closeWebSocketSilently(socket, 1000, "idle_timeout");
949
+ reject(error);
950
+ }, idleTimeoutMs);
951
+ }
952
+ }).finally(() => {
953
+ if (timeout) {
954
+ clearTimeout(timeout);
955
+ }
887
956
  });
888
957
  }
889
958
  if (failed) {
@@ -952,8 +1021,8 @@ async function* startWebSocketOutputOnFirstEvent(events, output, stream, onStart
952
1021
  yield event;
953
1022
  }
954
1023
  }
955
- async function processWebSocketStream(url, body, headers, output, stream, model, onStart, options) {
956
- const { socket, entry, reused, release } = await acquireWebSocket(url, headers, options?.sessionId, options?.signal);
1024
+ async function processWebSocketStream(url, body, headers, output, stream, model, onStart, idleTimeoutMs, websocketConnectTimeoutMs, options) {
1025
+ const { socket, entry, reused, release } = await acquireWebSocket(url, headers, options?.sessionId, options?.signal, websocketConnectTimeoutMs);
957
1026
  let keepConnection = true;
958
1027
  const useCachedContext = options?.transport === "websocket-cached" || options?.transport === "auto";
959
1028
  // ChatGPT Codex Responses rejects `store: true` ("Store must be set to false").
@@ -985,7 +1054,7 @@ async function processWebSocketStream(url, body, headers, output, stream, model,
985
1054
  }
986
1055
  try {
987
1056
  socket.send(JSON.stringify({ type: "response.create", ...requestBody }));
988
- await processResponsesStream(startWebSocketOutputOnFirstEvent(mapCodexEvents(parseWebSocket(socket, options?.signal)), output, stream, onStart), output, stream, model, {
1057
+ await processResponsesStream(startWebSocketOutputOnFirstEvent(mapCodexEvents(parseWebSocket(socket, options?.signal, idleTimeoutMs)), output, stream, onStart), output, stream, model, {
989
1058
  serviceTier: options?.serviceTier,
990
1059
  resolveServiceTier: resolveCodexServiceTier,
991
1060
  applyServiceTierPricing: (usage, serviceTier) => applyServiceTierPricing(usage, serviceTier, model),
@@ -1048,13 +1117,15 @@ function extractAccountId(token) {
1048
1117
  try {
1049
1118
  const parts = token.split(".");
1050
1119
  if (parts.length !== 3)
1051
- return null;
1120
+ throw new Error("Invalid token");
1052
1121
  const payload = JSON.parse(atob(parts[1]));
1053
1122
  const accountId = payload?.[JWT_CLAIM_PATH]?.chatgpt_account_id;
1054
- return accountId || null;
1123
+ if (!accountId)
1124
+ throw new Error("No account ID in token");
1125
+ return accountId;
1055
1126
  }
1056
1127
  catch {
1057
- return null;
1128
+ throw new Error("Failed to extract accountId from token");
1058
1129
  }
1059
1130
  }
1060
1131
  function createCodexRequestId() {
@@ -1069,10 +1140,7 @@ function buildBaseCodexHeaders(initHeaders, additionalHeaders, accountId, token)
1069
1140
  headers.set(key, value);
1070
1141
  }
1071
1142
  headers.set("Authorization", `Bearer ${token}`);
1072
- // Only set chatgpt-account-id if we have one (OpenAI-specific)
1073
- if (accountId) {
1074
- headers.set("chatgpt-account-id", accountId);
1075
- }
1143
+ headers.set("chatgpt-account-id", accountId);
1076
1144
  headers.set("originator", "pi");
1077
1145
  const userAgent = _os ? `pi (${_os.platform()} ${_os.release()}; ${_os.arch()})` : "pi (browser)";
1078
1146
  headers.set("User-Agent", userAgent);
@@ -1,5 +1,4 @@
1
1
  import OpenAI from "openai";
2
- import { getEnvApiKey } from "../env-api-keys.js";
3
2
  import { calculateCost, clampThinkingLevel } from "../models.js";
4
3
  import { AssistantMessageEventStream } from "../utils/event-stream.js";
5
4
  import { headersToRecord } from "../utils/headers.js";
@@ -70,7 +69,10 @@ export const streamOpenAICompletions = (model, context, options) => {
70
69
  timestamp: Date.now(),
71
70
  };
72
71
  try {
73
- const apiKey = options?.apiKey || getEnvApiKey(model.provider) || "";
72
+ const apiKey = options?.apiKey;
73
+ if (!apiKey) {
74
+ throw new Error(`No API key for provider: ${model.provider}`);
75
+ }
74
76
  const compat = getCompat(model);
75
77
  const cacheRetention = resolveCacheRetention(options?.cacheRetention);
76
78
  const cacheSessionId = cacheRetention === "none" ? undefined : options?.sessionId;
@@ -335,7 +337,7 @@ export const streamOpenAICompletions = (model, context, options) => {
335
337
  return stream;
336
338
  };
337
339
  export const streamSimpleOpenAICompletions = (model, context, options) => {
338
- const apiKey = options?.apiKey || getEnvApiKey(model.provider);
340
+ const apiKey = options?.apiKey;
339
341
  if (!apiKey) {
340
342
  throw new Error(`No API key for provider: ${model.provider}`);
341
343
  }
@@ -350,12 +352,6 @@ export const streamSimpleOpenAICompletions = (model, context, options) => {
350
352
  });
351
353
  };
352
354
  function createClient(model, context, apiKey, optionsHeaders, sessionId, compat = getCompat(model)) {
353
- if (!apiKey) {
354
- if (!process.env.OPENAI_API_KEY) {
355
- throw new Error("OpenAI API key is required. Set OPENAI_API_KEY environment variable or pass it as an argument.");
356
- }
357
- apiKey = process.env.OPENAI_API_KEY;
358
- }
359
355
  const headers = { ...model.headers };
360
356
  if (model.provider === "github-copilot") {
361
357
  const hasImages = hasCopilotVisionInput(context.messages);
@@ -448,7 +444,7 @@ function buildParams(model, context, options, compat = getCompat(model), cacheRe
448
444
  }
449
445
  else if (compat.thinkingFormat === "deepseek" && model.reasoning) {
450
446
  params.thinking = { type: options?.reasoningEffort ? "enabled" : "disabled" };
451
- if (options?.reasoningEffort) {
447
+ if (options?.reasoningEffort && compat.supportsReasoningEffort) {
452
448
  params.reasoning_effort =
453
449
  model.thinkingLevelMap?.[options.reasoningEffort] ?? options.reasoningEffort;
454
450
  }
@@ -472,6 +468,15 @@ function buildParams(model, context, options, compat = getCompat(model), cacheRe
472
468
  togetherParams.reasoning_effort = model.thinkingLevelMap?.[options.reasoningEffort] ?? options.reasoningEffort;
473
469
  }
474
470
  }
471
+ else if (compat.thinkingFormat === "string-thinking" && model.reasoning) {
472
+ const stringThinkingParams = params;
473
+ if (options?.reasoningEffort) {
474
+ stringThinkingParams.thinking = model.thinkingLevelMap?.[options.reasoningEffort] ?? options.reasoningEffort;
475
+ }
476
+ else if (model.thinkingLevelMap?.off !== null) {
477
+ stringThinkingParams.thinking = model.thinkingLevelMap?.off ?? "none";
478
+ }
479
+ }
475
480
  else if (options?.reasoningEffort && model.reasoning && compat.supportsReasoningEffort) {
476
481
  // OpenAI-style reasoning_effort
477
482
  params.reasoning_effort = model.thinkingLevelMap?.[options.reasoningEffort] ?? options.reasoningEffort;
@@ -871,6 +876,7 @@ function detectCompat(model) {
871
876
  const isZai = provider === "zai" || baseUrl.includes("api.z.ai");
872
877
  const isTogether = provider === "together" || baseUrl.includes("api.together.ai") || baseUrl.includes("api.together.xyz");
873
878
  const isMoonshot = provider === "moonshotai" || provider === "moonshotai-cn" || baseUrl.includes("api.moonshot.");
879
+ const isOpenRouter = provider === "openrouter" || baseUrl.includes("openrouter.ai");
874
880
  const isCloudflareWorkersAI = provider === "cloudflare-workers-ai" || baseUrl.includes("api.cloudflare.com");
875
881
  const isCloudflareAiGateway = provider === "cloudflare-ai-gateway" || baseUrl.includes("gateway.ai.cloudflare.com");
876
882
  const isNonStandard = provider === "cerebras" ||
@@ -892,7 +898,7 @@ function detectCompat(model) {
892
898
  const cacheControlFormat = provider === "openrouter" && model.id.startsWith("anthropic/") ? "anthropic" : undefined;
893
899
  return {
894
900
  supportsStore: !isNonStandard,
895
- supportsDeveloperRole: !isNonStandard,
901
+ supportsDeveloperRole: !isNonStandard && !isOpenRouter,
896
902
  supportsReasoningEffort: !isGrok && !isZai && !isMoonshot && !isTogether && !isCloudflareAiGateway,
897
903
  supportsUsageInStreaming: true,
898
904
  maxTokensField: useMaxTokens ? "max_tokens" : "max_completion_tokens",
@@ -906,7 +912,7 @@ function detectCompat(model) {
906
912
  ? "zai"
907
913
  : isTogether
908
914
  ? "together"
909
- : provider === "openrouter" || baseUrl.includes("openrouter.ai")
915
+ : isOpenRouter
910
916
  ? "openrouter"
911
917
  : "openai",
912
918
  openRouterRouting: {},
@@ -106,6 +106,7 @@ export function convertResponsesMessages(model, context, allowedToolCallProvider
106
106
  const isDifferentModel = assistantMsg.model !== model.id &&
107
107
  assistantMsg.provider === model.provider &&
108
108
  assistantMsg.api === model.api;
109
+ let textBlockIndex = 0;
109
110
  for (const block of msg.content) {
110
111
  if (block.type === "thinking") {
111
112
  if (block.thinkingSignature) {
@@ -116,10 +117,12 @@ export function convertResponsesMessages(model, context, allowedToolCallProvider
116
117
  else if (block.type === "text") {
117
118
  const textBlock = block;
118
119
  const parsedSignature = parseTextSignature(textBlock.textSignature);
120
+ const fallbackMessageId = textBlockIndex === 0 ? `msg_pi_${msgIndex}` : `msg_pi_${msgIndex}_${textBlockIndex}`;
121
+ textBlockIndex++;
119
122
  // OpenAI requires id to be max 64 characters
120
123
  let msgId = parsedSignature?.id;
121
124
  if (!msgId) {
122
- msgId = `msg_${msgIndex}`;
125
+ msgId = fallbackMessageId;
123
126
  }
124
127
  else if (msgId.length > 64) {
125
128
  msgId = `msg_${shortHash(msgId)}`;
@@ -1,5 +1,4 @@
1
1
  import OpenAI from "openai";
2
- import { getEnvApiKey } from "../env-api-keys.js";
3
2
  import { clampThinkingLevel } from "../models.js";
4
3
  import { AssistantMessageEventStream } from "../utils/event-stream.js";
5
4
  import { headersToRecord } from "../utils/headers.js";
@@ -73,7 +72,10 @@ export const streamOpenAIResponses = (model, context, options) => {
73
72
  };
74
73
  try {
75
74
  // Create OpenAI client
76
- const apiKey = options?.apiKey || getEnvApiKey(model.provider) || "";
75
+ const apiKey = options?.apiKey;
76
+ if (!apiKey) {
77
+ throw new Error(`No API key for provider: ${model.provider}`);
78
+ }
77
79
  const cacheRetention = resolveCacheRetention(options?.cacheRetention);
78
80
  const cacheSessionId = cacheRetention === "none" ? undefined : options?.sessionId;
79
81
  const client = createClient(model, context, apiKey, options?.headers, cacheSessionId);
@@ -118,7 +120,7 @@ export const streamOpenAIResponses = (model, context, options) => {
118
120
  return stream;
119
121
  };
120
122
  export const streamSimpleOpenAIResponses = (model, context, options) => {
121
- const apiKey = options?.apiKey || getEnvApiKey(model.provider);
123
+ const apiKey = options?.apiKey;
122
124
  if (!apiKey) {
123
125
  throw new Error(`No API key for provider: ${model.provider}`);
124
126
  }
@@ -131,12 +133,6 @@ export const streamSimpleOpenAIResponses = (model, context, options) => {
131
133
  });
132
134
  };
133
135
  function createClient(model, context, apiKey, optionsHeaders, sessionId) {
134
- if (!apiKey) {
135
- if (!process.env.OPENAI_API_KEY) {
136
- throw new Error("OpenAI API key is required. Set OPENAI_API_KEY environment variable or pass it as an argument.");
137
- }
138
- apiKey = process.env.OPENAI_API_KEY;
139
- }
140
136
  const compat = getCompat(model);
141
137
  const headers = { ...model.headers };
142
138
  if (model.provider === "github-copilot") {
@@ -11,6 +11,7 @@ export function buildBaseOptions(_model, options, apiKey) {
11
11
  onPayload: options?.onPayload,
12
12
  onResponse: options?.onResponse,
13
13
  timeoutMs: options?.timeoutMs,
14
+ websocketConnectTimeoutMs: options?.websocketConnectTimeoutMs,
14
15
  maxRetries: options?.maxRetries,
15
16
  maxRetryDelayMs: options?.maxRetryDelayMs,
16
17
  metadata: options?.metadata,
@@ -1,6 +1,18 @@
1
1
  import "./providers/register-builtins.js";
2
2
  import { getApiProvider } from "./api-registry.js";
3
+ import { getEnvApiKey } from "./env-api-keys.js";
3
4
  export { getEnvApiKey } from "./env-api-keys.js";
5
+ function hasExplicitApiKey(apiKey) {
6
+ return typeof apiKey === "string" && apiKey.trim().length > 0;
7
+ }
8
+ function withEnvApiKey(model, options) {
9
+ if (hasExplicitApiKey(options?.apiKey))
10
+ return options;
11
+ const apiKey = getEnvApiKey(model.provider);
12
+ if (!apiKey)
13
+ return options;
14
+ return { ...options, apiKey };
15
+ }
4
16
  function resolveApiProvider(api) {
5
17
  const provider = getApiProvider(api);
6
18
  if (!provider) {
@@ -10,7 +22,7 @@ function resolveApiProvider(api) {
10
22
  }
11
23
  export function stream(model, context, options) {
12
24
  const provider = resolveApiProvider(model.api);
13
- return provider.stream(model, context, options);
25
+ return provider.stream(model, context, withEnvApiKey(model, options));
14
26
  }
15
27
  export async function complete(model, context, options) {
16
28
  const s = stream(model, context, options);
@@ -18,7 +30,7 @@ export async function complete(model, context, options) {
18
30
  }
19
31
  export function streamSimple(model, context, options) {
20
32
  const provider = resolveApiProvider(model.api);
21
- return provider.streamSimple(model, context, options);
33
+ return provider.streamSimple(model, context, withEnvApiKey(model, options));
22
34
  }
23
35
  export async function completeSimple(model, context, options) {
24
36
  const s = streamSimple(model, context, options);
@@ -0,0 +1,34 @@
1
+ export function combineAbortSignals(signals) {
2
+ const activeSignals = signals.filter((signal) => signal !== undefined);
3
+ if (activeSignals.length === 0) {
4
+ return { cleanup: () => { } };
5
+ }
6
+ if (activeSignals.length === 1) {
7
+ return { signal: activeSignals[0], cleanup: () => { } };
8
+ }
9
+ const controller = new AbortController();
10
+ const listeners = [];
11
+ const abort = (signal) => {
12
+ if (!controller.signal.aborted) {
13
+ controller.abort(signal.reason);
14
+ }
15
+ };
16
+ for (const signal of activeSignals) {
17
+ if (signal.aborted) {
18
+ abort(signal);
19
+ break;
20
+ }
21
+ const listener = () => abort(signal);
22
+ signal.addEventListener("abort", listener, { once: true });
23
+ listeners.push({ signal, listener });
24
+ }
25
+ return {
26
+ signal: controller.signal,
27
+ cleanup: () => {
28
+ for (const { signal, listener } of listeners) {
29
+ signal.removeEventListener("abort", listener);
30
+ }
31
+ },
32
+ };
33
+ }
34
+ //# sourceMappingURL=abort-signals.js.map
@@ -33,22 +33,23 @@ export async function pollOAuthDeviceCodeFlow(options) {
33
33
  if (options.signal?.aborted) {
34
34
  throw new Error(CANCEL_MESSAGE);
35
35
  }
36
- const remainingMs = deadline - Date.now();
37
- await abortableSleep(Math.min(intervalMs, remainingMs), options.signal, CANCEL_MESSAGE);
38
36
  const result = await options.poll();
39
37
  if (result.status === "complete") {
40
- return result.accessToken;
38
+ return result.value;
41
39
  }
42
- if (result.status === "pending") {
43
- continue;
40
+ if (result.status === "failed") {
41
+ throw new Error(result.message);
44
42
  }
45
43
  if (result.status === "slow_down") {
46
44
  slowDownResponses += 1;
47
45
  // RFC 8628 section 3.5: apply this increase to this and all subsequent requests.
48
46
  intervalMs = Math.max(MINIMUM_INTERVAL_MS, intervalMs + SLOW_DOWN_INTERVAL_INCREMENT_MS);
49
- continue;
50
47
  }
51
- throw new Error(result.message);
48
+ const remainingMs = deadline - Date.now();
49
+ if (remainingMs <= 0) {
50
+ break;
51
+ }
52
+ await abortableSleep(Math.min(intervalMs, remainingMs), options.signal, CANCEL_MESSAGE);
52
53
  }
53
54
  throw new Error(slowDownResponses > 0 ? SLOW_DOWN_TIMEOUT_MESSAGE : TIMEOUT_MESSAGE);
54
55
  }