@oh-my-pi/pi-ai 16.3.11 → 16.3.13

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 (36) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/dist/types/auth-broker/discover.d.ts +1 -1
  3. package/dist/types/auth-retry.d.ts +3 -1
  4. package/dist/types/auth-storage.d.ts +13 -8
  5. package/dist/types/providers/anthropic.d.ts +1 -0
  6. package/dist/types/providers/cursor.d.ts +5 -0
  7. package/dist/types/providers/openai-shared.d.ts +6 -3
  8. package/dist/types/providers/register-builtins.d.ts +5 -0
  9. package/dist/types/registry/oauth/callback-server.d.ts +2 -0
  10. package/dist/types/registry/oauth/xai-oauth.d.ts +1 -7
  11. package/dist/types/registry/registry.d.ts +2 -1
  12. package/dist/types/registry/xai-oauth.d.ts +2 -1
  13. package/dist/types/types.d.ts +2 -0
  14. package/dist/types/utils/event-stream.d.ts +7 -0
  15. package/dist/types/utils/idle-iterator.d.ts +10 -0
  16. package/package.json +4 -4
  17. package/src/auth-broker/discover.ts +20 -16
  18. package/src/auth-broker/remote-store.ts +55 -5
  19. package/src/auth-gateway/server.ts +1 -0
  20. package/src/auth-retry.ts +7 -2
  21. package/src/auth-storage.ts +210 -46
  22. package/src/providers/anthropic.ts +78 -8
  23. package/src/providers/cursor.ts +17 -10
  24. package/src/providers/openai-codex-responses.ts +37 -6
  25. package/src/providers/openai-responses.ts +1 -1
  26. package/src/providers/openai-shared.ts +30 -14
  27. package/src/providers/register-builtins.ts +15 -0
  28. package/src/registry/oauth/__tests__/xai-oauth.test.ts +55 -0
  29. package/src/registry/oauth/callback-server.ts +40 -12
  30. package/src/registry/oauth/xai-oauth.ts +6 -10
  31. package/src/registry/xai-oauth.ts +2 -1
  32. package/src/stream.ts +7 -1
  33. package/src/types.ts +2 -0
  34. package/src/usage/openai-codex.ts +3 -2
  35. package/src/utils/event-stream.ts +26 -0
  36. package/src/utils/idle-iterator.ts +65 -9
@@ -135,6 +135,16 @@ export interface IdleTimeoutIteratorOptions {
135
135
  * keepalive/no-op events from keeping a stalled tool call alive forever.
136
136
  */
137
137
  isProgressItem?: (item: unknown) => boolean;
138
+ /**
139
+ * Reports consumer-side local work in flight for the stream: the provider
140
+ * transport is waiting on a server-requested local tool bridge (e.g. the
141
+ * Cursor exec channel) before anything can flow upstream again. While it
142
+ * returns true, an expired idle / first-item deadline slides forward
143
+ * instead of aborting — the silence is ours, not a provider stall. The
144
+ * watchdog re-arms with a full budget once the local work completes, so a
145
+ * provider that stalls afterwards is still caught.
146
+ */
147
+ hasPendingLocalWork?: () => boolean;
138
148
  /**
139
149
  * Cancel iteration as soon as this signal aborts. Required for caller-driven
140
150
  * cancellation (ESC) when the underlying transport does not surface signal
@@ -157,7 +167,7 @@ export async function* iterateWithIdleTimeout<T>(
157
167
  options: IdleTimeoutIteratorOptions,
158
168
  ): AsyncGenerator<T> {
159
169
  const firstItemTimeoutMs = options.firstItemTimeoutMs ?? options.idleTimeoutMs;
160
- const firstItemDeadlineMs =
170
+ let firstItemDeadlineMs =
161
171
  firstItemTimeoutMs !== undefined && firstItemTimeoutMs > 0 ? Date.now() + firstItemTimeoutMs : undefined;
162
172
  const abortSignal = options.abortSignal;
163
173
  const iterator = iterable[Symbol.asyncIterator]();
@@ -197,6 +207,28 @@ export async function* iterateWithIdleTimeout<T>(
197
207
  };
198
208
  let lastProgressAt = Date.now();
199
209
 
210
+ const hasPendingLocalWork = (): boolean => {
211
+ if (!options.hasPendingLocalWork) return false;
212
+ try {
213
+ return options.hasPendingLocalWork();
214
+ } catch {
215
+ return false;
216
+ }
217
+ };
218
+ // Local work means the current gap is attributable to the consumer side,
219
+ // not the provider: slide the active deadline a full budget past now
220
+ // instead of aborting. Once the work completes the watchdog resumes from
221
+ // the last extension, so a provider that stalls afterwards is still caught.
222
+ const extendDeadlineForLocalWork = (): void => {
223
+ if (awaitingFirstItem) {
224
+ if (firstItemDeadlineMs !== undefined && firstItemTimeoutMs !== undefined) {
225
+ firstItemDeadlineMs = Date.now() + firstItemTimeoutMs;
226
+ }
227
+ } else {
228
+ lastProgressAt = Date.now();
229
+ }
230
+ };
231
+
200
232
  const noTimeoutEnforced =
201
233
  (firstItemTimeoutMs === undefined || firstItemTimeoutMs <= 0) &&
202
234
  (options.idleTimeoutMs === undefined || options.idleTimeoutMs <= 0);
@@ -271,6 +303,12 @@ export async function* iterateWithIdleTimeout<T>(
271
303
  timer = setTimeout(onTimerFire, Math.max(0, deadlineMs - Date.now()));
272
304
  };
273
305
 
306
+ // The in-flight iterator.next() promise, persisted across loop iterations:
307
+ // a deadline extension for pending local work loops without consuming it,
308
+ // and issuing a second next() while one is outstanding would drop an item.
309
+ let pendingNext:
310
+ | Promise<{ kind: "next"; result: IteratorResult<T> } | { kind: "error"; error: unknown }>
311
+ | undefined;
274
312
  try {
275
313
  let raceCount = 0;
276
314
  while (true) {
@@ -291,21 +329,29 @@ export async function* iterateWithIdleTimeout<T>(
291
329
  if (firstItemDeadlineMs !== undefined) {
292
330
  activeTimeoutMs = firstItemDeadlineMs - Date.now();
293
331
  if (activeTimeoutMs <= 0) {
294
- options.onFirstItemTimeout?.();
295
- closeIterator();
296
- throw new AIError.StreamTimeoutError(options.firstItemErrorMessage ?? options.errorMessage);
332
+ if (!hasPendingLocalWork()) {
333
+ options.onFirstItemTimeout?.();
334
+ closeIterator();
335
+ throw new AIError.StreamTimeoutError(options.firstItemErrorMessage ?? options.errorMessage);
336
+ }
337
+ extendDeadlineForLocalWork();
338
+ activeTimeoutMs = firstItemDeadlineMs! - Date.now();
297
339
  }
298
340
  }
299
341
  } else if (options.idleTimeoutMs !== undefined && options.idleTimeoutMs > 0) {
300
342
  activeTimeoutMs = options.idleTimeoutMs - (Date.now() - lastProgressAt);
301
343
  if (activeTimeoutMs <= 0) {
302
- options.onIdle?.();
303
- closeIterator();
304
- throw new AIError.StreamTimeoutError(options.errorMessage);
344
+ if (!hasPendingLocalWork()) {
345
+ options.onIdle?.();
346
+ closeIterator();
347
+ throw new AIError.StreamTimeoutError(options.errorMessage);
348
+ }
349
+ extendDeadlineForLocalWork();
350
+ activeTimeoutMs = options.idleTimeoutMs;
305
351
  }
306
352
  }
307
353
 
308
- const nextResultPromise = withRacy(iterator.next());
354
+ pendingNext ??= withRacy(iterator.next());
309
355
 
310
356
  const racers: Array<
311
357
  Promise<
@@ -314,7 +360,7 @@ export async function* iterateWithIdleTimeout<T>(
314
360
  | { kind: "timeout" }
315
361
  | { kind: "abort" }
316
362
  >
317
- > = [nextResultPromise];
363
+ > = [pendingNext];
318
364
 
319
365
  const enforceTimeout = !noTimeoutEnforced && activeTimeoutMs !== undefined && activeTimeoutMs > 0;
320
366
  if (enforceTimeout) {
@@ -333,11 +379,21 @@ export async function* iterateWithIdleTimeout<T>(
333
379
  let continuing = false;
334
380
  try {
335
381
  const outcome = await Promise.race(racers);
382
+ if (outcome.kind === "next" || outcome.kind === "error") {
383
+ pendingNext = undefined;
384
+ }
336
385
  if (outcome.kind === "abort") {
337
386
  closeIterator();
338
387
  throw abortReason(abortSignal!);
339
388
  }
340
389
  if (outcome.kind === "timeout") {
390
+ if (hasPendingLocalWork()) {
391
+ // A local tool is still running; the provider cannot make
392
+ // progress until we hand its result back. Keep waiting.
393
+ extendDeadlineForLocalWork();
394
+ continuing = true;
395
+ continue;
396
+ }
341
397
  if (!awaitingFirstItem) {
342
398
  options.onIdle?.();
343
399
  } else {