@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.
|
|
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
|
-
* `
|
|
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
|
-
*
|
|
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.
|
|
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.
|
|
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) {
|
package/src/usage/claude.ts
CHANGED
|
@@ -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
|
|
164
|
+
if (Number.isFinite(seconds)) return clampRetryDelay(baseline, seconds * 1000);
|
|
148
165
|
const dateDelay = Date.parse(retryAfter) - Date.now();
|
|
149
|
-
return Number.isFinite(dateDelay) ?
|
|
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
|
-
* `
|
|
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(
|
|
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
|
-
*
|
|
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
|
-
> = [
|
|
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") {
|