@gajae-code/ai 0.11.10 → 0.11.11

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.
package/CHANGELOG.md CHANGED
@@ -2,7 +2,12 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
- ## [0.11.10] - 2026-07-25
5
+ ## [0.11.11] - 2026-07-26
6
+
7
+ ### Fixed
8
+
9
+ - Anthropic `ping` keepalives no longer reset stream progress, so responses that stop producing content now reach the idle timeout instead of hanging indefinitely.
10
+ - The documented `GJC_OPENAI_STREAM_IDLE_TIMEOUT_MS` environment variable now takes effect: the stream-watchdog idle-timeout helpers resolve it GJC-first before the legacy `PI_OPENAI_STREAM_IDLE_TIMEOUT_MS` / `PI_STREAM_IDLE_TIMEOUT_MS` aliases (previously only the `PI_`-prefixed names were read, so setting the documented GJC name was a silent no-op).
6
11
 
7
12
  ## [0.11.9] - 2026-07-24
8
13
  ### Fixed
@@ -2,7 +2,7 @@ export declare function getProviderFirstEventTimeoutFallbackMs(provider: string)
2
2
  /**
3
3
  * Returns the idle timeout used for provider streaming transports.
4
4
  *
5
- * `PI_OPENAI_STREAM_IDLE_TIMEOUT_MS` is accepted as a backward-compatible alias.
5
+ * `GJC_OPENAI_STREAM_IDLE_TIMEOUT_MS` is honored first; `PI_OPENAI_STREAM_IDLE_TIMEOUT_MS` is a backward-compatible alias.
6
6
  * Set `PI_STREAM_IDLE_TIMEOUT_MS=0` to disable the watchdog.
7
7
  *
8
8
  * Providers that legitimately stream much slower than the global default can pass
@@ -13,7 +13,7 @@ export declare function getStreamIdleTimeoutMs(fallbackMs?: number): number | un
13
13
  /**
14
14
  * Returns the idle timeout used for OpenAI-family streaming transports.
15
15
  *
16
- * Set `PI_OPENAI_STREAM_IDLE_TIMEOUT_MS=0` to disable the watchdog.
16
+ * Honors `GJC_OPENAI_STREAM_IDLE_TIMEOUT_MS` first (`PI_OPENAI_STREAM_IDLE_TIMEOUT_MS` is the legacy alias). Set `=0` to disable.
17
17
  */
18
18
  export declare function getOpenAIStreamIdleTimeoutMs(): number | undefined;
19
19
  /**
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@gajae-code/ai",
4
- "version": "0.11.10",
4
+ "version": "0.11.11",
5
5
  "description": "Unified LLM API with automatic model discovery and provider configuration",
6
6
  "homepage": "https://gajae-code.com",
7
7
  "author": "Yeachan-Heo and Gajae Code Contributors",
@@ -40,7 +40,7 @@
40
40
  "dependencies": {
41
41
  "@anthropic-ai/sdk": "^0.94.0",
42
42
  "@bufbuild/protobuf": "^2.12.0",
43
- "@gajae-code/utils": "0.11.10",
43
+ "@gajae-code/utils": "0.11.11",
44
44
  "openai": "^6.36.0",
45
45
  "partial-json": "^0.1.7",
46
46
  "zod": "4.4.3"
@@ -1182,6 +1182,40 @@ function shouldIgnoreAnthropicPreambleEvent(eventType: unknown): boolean {
1182
1182
  return !ANTHROPIC_PRE_MESSAGE_START_EVENT_TYPES.has(eventType);
1183
1183
  }
1184
1184
 
1185
+ function createAnthropicStreamProgressPredicate(): (event: unknown) => boolean {
1186
+ let outputTokens = -1;
1187
+
1188
+ return event => {
1189
+ if (!isRecord(event) || typeof event.type !== "string") return false;
1190
+ if (
1191
+ event.type === "message_start" ||
1192
+ event.type === "content_block_start" ||
1193
+ event.type === "content_block_stop" ||
1194
+ event.type === "message_stop"
1195
+ ) {
1196
+ return true;
1197
+ }
1198
+ if (event.type === "content_block_delta") {
1199
+ if (!isRecord(event.delta)) return false;
1200
+ const delta = event.delta;
1201
+ return (
1202
+ (typeof delta.text === "string" && delta.text.length > 0) ||
1203
+ (typeof delta.thinking === "string" && delta.thinking.length > 0) ||
1204
+ (typeof delta.partial_json === "string" && delta.partial_json.length > 0) ||
1205
+ (typeof delta.signature === "string" && delta.signature.length > 0)
1206
+ );
1207
+ }
1208
+ if (event.type === "message_delta") {
1209
+ if (isRecord(event.delta) && event.delta.stop_reason != null) return true;
1210
+ if (!isRecord(event.usage) || typeof event.usage.output_tokens !== "number") return false;
1211
+ if (event.usage.output_tokens <= outputTokens) return false;
1212
+ outputTokens = event.usage.output_tokens;
1213
+ return true;
1214
+ }
1215
+ return false;
1216
+ };
1217
+ }
1218
+
1185
1219
  function isTransientStreamEnvelopeError(error: unknown): boolean {
1186
1220
  if (!(error instanceof Error)) return false;
1187
1221
  return (
@@ -1457,6 +1491,7 @@ export const streamAnthropic: StreamFunction<"anthropic-messages"> = (
1457
1491
  let sawEvent = false;
1458
1492
  let sawMessageStart = false;
1459
1493
  let sawTerminalEnvelope = false;
1494
+ const isProgressEvent = createAnthropicStreamProgressPredicate();
1460
1495
 
1461
1496
  for await (const event of iterateWithIdleTimeout(anthropicStream, {
1462
1497
  idleTimeoutMs,
@@ -1466,6 +1501,7 @@ export const streamAnthropic: StreamFunction<"anthropic-messages"> = (
1466
1501
  onIdle: () => activeAbortTracker.abortLocally(idleTimeoutAbortError),
1467
1502
  onFirstItemTimeout: () => activeAbortTracker.abortLocally(firstEventTimeoutAbortError),
1468
1503
  abortSignal: options?.signal,
1504
+ isProgressItem: isProgressEvent,
1469
1505
  })) {
1470
1506
  sawEvent = true;
1471
1507
  if (sawProviderSafetyStop) {
@@ -17,6 +17,13 @@ const FIVE_HOURS_MS = 5 * 60 * 60 * 1000;
17
17
  const SEVEN_DAYS_MS = 7 * 24 * 60 * 60 * 1000;
18
18
  const MAX_ATTEMPTS = 3;
19
19
  const BASE_RETRY_DELAY_MS = 500;
20
+ /**
21
+ * Ceiling for a server-supplied `Retry-After`. Matches `OPENAI_RETRY_DELAY_CAP_MS`
22
+ * and `fetchWithRetry`'s `DEFAULT_MAX_DELAY_MS`. Without it a hostile or
23
+ * misconfigured endpoint stalls the usage fetch for as long as it likes
24
+ * (`Retry-After: 86400` previously produced a 24h sleep).
25
+ */
26
+ const MAX_RETRY_DELAY_MS = 60_000;
20
27
 
21
28
  const CLAUDE_HEADERS = {
22
29
  accept: "application/json, text/plain, */*",
@@ -140,13 +147,23 @@ function isAbortError(error: unknown, signal?: AbortSignal): boolean {
140
147
  return error.name === "AbortError" || error.name === "TimeoutError";
141
148
  }
142
149
 
150
+ /**
151
+ * Honour the server hint but never exceed `MAX_RETRY_DELAY_MS`, and never
152
+ * return a negative/non-finite delay. Keeps the sleep bounded so an abort has
153
+ * an upper bound to fire within.
154
+ */
155
+ function clampRetryDelay(baseline: number, hintMs: number): number {
156
+ const hint = Number.isFinite(hintMs) ? Math.max(0, hintMs) : 0;
157
+ return Math.min(Math.max(baseline, hint), MAX_RETRY_DELAY_MS);
158
+ }
159
+
143
160
  function retryDelayMs(attempt: number, retryAfter: string | null): number {
144
161
  const baseline = BASE_RETRY_DELAY_MS * 2 ** attempt;
145
162
  if (!retryAfter?.trim()) return baseline;
146
163
  const seconds = Number.parseFloat(retryAfter);
147
- if (Number.isFinite(seconds)) return Math.max(baseline, Math.max(0, seconds * 1000));
164
+ if (Number.isFinite(seconds)) return clampRetryDelay(baseline, seconds * 1000);
148
165
  const dateDelay = Date.parse(retryAfter) - Date.now();
149
- return Number.isFinite(dateDelay) ? Math.max(baseline, Math.max(0, dateDelay)) : baseline;
166
+ return Number.isFinite(dateDelay) ? clampRetryDelay(baseline, dateDelay) : baseline;
150
167
  }
151
168
 
152
169
  async function waitBeforeRetry(
@@ -19,7 +19,7 @@ function normalizeIdleTimeoutMs(value: string | undefined, fallback: number): nu
19
19
  /**
20
20
  * Returns the idle timeout used for provider streaming transports.
21
21
  *
22
- * `PI_OPENAI_STREAM_IDLE_TIMEOUT_MS` is accepted as a backward-compatible alias.
22
+ * `GJC_OPENAI_STREAM_IDLE_TIMEOUT_MS` is honored first; `PI_OPENAI_STREAM_IDLE_TIMEOUT_MS` is a backward-compatible alias.
23
23
  * Set `PI_STREAM_IDLE_TIMEOUT_MS=0` to disable the watchdog.
24
24
  *
25
25
  * Providers that legitimately stream much slower than the global default can pass
@@ -27,17 +27,20 @@ function normalizeIdleTimeoutMs(value: string | undefined, fallback: number): nu
27
27
  * Caller options still take precedence; env overrides still trump the fallback.
28
28
  */
29
29
  export function getStreamIdleTimeoutMs(fallbackMs: number = DEFAULT_STREAM_IDLE_TIMEOUT_MS): number | undefined {
30
- return normalizeIdleTimeoutMs($env.PI_STREAM_IDLE_TIMEOUT_MS ?? $env.PI_OPENAI_STREAM_IDLE_TIMEOUT_MS, fallbackMs);
30
+ return normalizeIdleTimeoutMs(
31
+ $env.GJC_OPENAI_STREAM_IDLE_TIMEOUT_MS ?? $env.PI_STREAM_IDLE_TIMEOUT_MS ?? $env.PI_OPENAI_STREAM_IDLE_TIMEOUT_MS,
32
+ fallbackMs,
33
+ );
31
34
  }
32
35
 
33
36
  /**
34
37
  * Returns the idle timeout used for OpenAI-family streaming transports.
35
38
  *
36
- * Set `PI_OPENAI_STREAM_IDLE_TIMEOUT_MS=0` to disable the watchdog.
39
+ * Honors `GJC_OPENAI_STREAM_IDLE_TIMEOUT_MS` first (`PI_OPENAI_STREAM_IDLE_TIMEOUT_MS` is the legacy alias). Set `=0` to disable.
37
40
  */
38
41
  export function getOpenAIStreamIdleTimeoutMs(): number | undefined {
39
42
  return normalizeIdleTimeoutMs(
40
- $env.PI_OPENAI_STREAM_IDLE_TIMEOUT_MS ?? $env.PI_STREAM_IDLE_TIMEOUT_MS,
43
+ $env.GJC_OPENAI_STREAM_IDLE_TIMEOUT_MS ?? $env.PI_OPENAI_STREAM_IDLE_TIMEOUT_MS ?? $env.PI_STREAM_IDLE_TIMEOUT_MS,
41
44
  DEFAULT_STREAM_IDLE_TIMEOUT_MS,
42
45
  );
43
46
  }
@@ -173,8 +176,6 @@ export async function* iterateWithIdleTimeout<T>(
173
176
  }
174
177
  }
175
178
 
176
- const nextResultPromise = withRacy(iterator.next());
177
-
178
179
  const racers: Array<
179
180
  Promise<
180
181
  | { kind: "next"; result: IteratorResult<T> }
@@ -182,7 +183,7 @@ export async function* iterateWithIdleTimeout<T>(
182
183
  | { kind: "timeout" }
183
184
  | { kind: "abort" }
184
185
  >
185
- > = [nextResultPromise];
186
+ > = [];
186
187
 
187
188
  let timer: NodeJS.Timeout | undefined;
188
189
  let resolveTimeout: ((value: { kind: "timeout" }) => void) | undefined;
@@ -207,6 +208,13 @@ export async function* iterateWithIdleTimeout<T>(
207
208
  racers.push(promise);
208
209
  }
209
210
 
211
+ // Arm timeout/abort races before asking the source for its next item. A
212
+ // periodic keepalive iterator commonly registers its own timer inside
213
+ // `next()`; registering that first lets equal-deadline keepalives win every
214
+ // race and extend the idle window forever. Already-buffered items still
215
+ // settle as microtasks before a 0ms watchdog.
216
+ racers.unshift(withRacy(iterator.next()));
217
+
210
218
  try {
211
219
  const outcome = await Promise.race(racers);
212
220
  if (outcome.kind === "abort") {