@gajae-code/ai 0.12.0 → 0.12.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.
- package/CHANGELOG.md +23 -0
- package/dist/types/model-thinking.d.ts +15 -0
- package/dist/types/providers/azure-openai-responses.d.ts +2 -0
- package/dist/types/providers/google-vertex.d.ts +2 -0
- package/dist/types/utils/fallback-transport.d.ts +2 -0
- package/dist/types/utils/http-inspector.d.ts +10 -0
- package/dist/types/utils/idle-iterator.d.ts +4 -0
- package/dist/types/utils/oauth/kimi.d.ts +2 -0
- package/dist/types/utils/oauth/perplexity.d.ts +2 -7
- package/package.json +2 -2
- package/src/auth-storage.ts +92 -15
- package/src/model-thinking.ts +19 -0
- package/src/providers/amazon-bedrock.ts +5 -20
- package/src/providers/anthropic.ts +95 -44
- package/src/providers/azure-openai-responses.ts +28 -16
- package/src/providers/google-vertex.ts +35 -4
- package/src/providers/ollama.ts +32 -4
- package/src/providers/openai-codex-responses.ts +48 -31
- package/src/providers/openai-completions.ts +13 -12
- package/src/providers/openai-responses.ts +9 -11
- package/src/providers/register-builtins.ts +12 -2
- package/src/utils/fallback-transport.ts +17 -10
- package/src/utils/http-inspector.ts +47 -1
- package/src/utils/idle-iterator.ts +12 -3
- package/src/utils/oauth/kimi.ts +17 -2
- package/src/utils/oauth/perplexity.ts +21 -2
- package/src/utils/schema/adapt.ts +2 -2
- package/src/utils/tool-choice-capability.ts +2 -1
|
@@ -48,9 +48,13 @@ import {
|
|
|
48
48
|
sanitizeOpenAIResponsesHistoryItemsForReplay,
|
|
49
49
|
} from "../utils";
|
|
50
50
|
import { AssistantMessageEventStream } from "../utils/event-stream";
|
|
51
|
-
import { transportFailureFacts } from "../utils/fallback-transport";
|
|
51
|
+
import { STREAM_FIRST_EVENT_TIMEOUT_PROVIDER_CODE, transportFailureFacts } from "../utils/fallback-transport";
|
|
52
52
|
import { finalizeErrorMessage, type RawHttpRequestDump } from "../utils/http-inspector";
|
|
53
|
-
import {
|
|
53
|
+
import {
|
|
54
|
+
getOpenAIStreamIdleTimeoutMs,
|
|
55
|
+
getStreamFirstEventTimeoutMs,
|
|
56
|
+
iterateWithIdleTimeout,
|
|
57
|
+
} from "../utils/idle-iterator";
|
|
54
58
|
import { parseStreamingJson } from "../utils/json-parse";
|
|
55
59
|
import { resolveRetryBudget } from "../utils/retry-budget";
|
|
56
60
|
import {
|
|
@@ -103,7 +107,6 @@ const CODEX_MAX_RETRIES = 5;
|
|
|
103
107
|
const CODEX_RETRY_DELAY_MS = 500;
|
|
104
108
|
const CODEX_WEBSOCKET_CONNECT_TIMEOUT_MS = 10000;
|
|
105
109
|
const CODEX_WEBSOCKET_IDLE_TIMEOUT_MS = 300000;
|
|
106
|
-
const CODEX_WEBSOCKET_FIRST_EVENT_TIMEOUT_MS = 15000;
|
|
107
110
|
const CODEX_WEBSOCKET_RETRY_BUDGET = CODEX_MAX_RETRIES;
|
|
108
111
|
const CODEX_WEBSOCKET_TRANSPORT_ERROR_PREFIX = "Codex websocket transport error";
|
|
109
112
|
const CODEX_PREVIOUS_RESPONSE_STALE_CODES = new Set(["previous_response_not_found", "codex_previous_response_stale"]);
|
|
@@ -274,6 +277,7 @@ async function retryCodexInitialTransportWithoutToolChoice(
|
|
|
274
277
|
|
|
275
278
|
interface CodexRequestSetup {
|
|
276
279
|
requestSignal: AbortSignal;
|
|
280
|
+
firstEventTimeoutMs: number | undefined;
|
|
277
281
|
wrapCodexSseStream: (source: AsyncGenerator<Record<string, unknown>>) => AsyncGenerator<Record<string, unknown>>;
|
|
278
282
|
requestAbortController: AbortController;
|
|
279
283
|
}
|
|
@@ -355,16 +359,6 @@ function getCodexWebSocketIdleTimeoutMs(overrideMs?: number): number {
|
|
|
355
359
|
);
|
|
356
360
|
}
|
|
357
361
|
|
|
358
|
-
function getCodexWebSocketFirstEventTimeoutMs(idleTimeoutMs: number, overrideMs?: number): number {
|
|
359
|
-
return (
|
|
360
|
-
overrideMs ??
|
|
361
|
-
parseCodexPositiveInteger(
|
|
362
|
-
$env.PI_CODEX_WEBSOCKET_FIRST_EVENT_TIMEOUT_MS,
|
|
363
|
-
Math.min(CODEX_WEBSOCKET_FIRST_EVENT_TIMEOUT_MS, idleTimeoutMs),
|
|
364
|
-
)
|
|
365
|
-
);
|
|
366
|
-
}
|
|
367
|
-
|
|
368
362
|
function createCodexProviderSessionState(): CodexProviderSessionState {
|
|
369
363
|
const state: CodexProviderSessionState = {
|
|
370
364
|
webSocketSessions: new Map(),
|
|
@@ -391,8 +385,12 @@ function getCodexProviderSessionState(
|
|
|
391
385
|
return created;
|
|
392
386
|
}
|
|
393
387
|
|
|
394
|
-
function createCodexWebSocketTransportError(message: string): Error {
|
|
395
|
-
|
|
388
|
+
function createCodexWebSocketTransportError(message: string, providerCode?: string): Error & { providerCode?: string } {
|
|
389
|
+
const error = new Error(`${CODEX_WEBSOCKET_TRANSPORT_ERROR_PREFIX}: ${message}`) as Error & {
|
|
390
|
+
providerCode?: string;
|
|
391
|
+
};
|
|
392
|
+
error.providerCode = providerCode;
|
|
393
|
+
return error;
|
|
396
394
|
}
|
|
397
395
|
|
|
398
396
|
function isCodexWebSocketFatalError(error: Error): boolean {
|
|
@@ -405,6 +403,13 @@ function isCodexWebSocketTransportError(error: unknown): boolean {
|
|
|
405
403
|
return error.message.startsWith(CODEX_WEBSOCKET_TRANSPORT_ERROR_PREFIX);
|
|
406
404
|
}
|
|
407
405
|
|
|
406
|
+
function isCodexFirstEventTimeout(error: unknown): boolean {
|
|
407
|
+
return (
|
|
408
|
+
error instanceof Error &&
|
|
409
|
+
(error as { providerCode?: unknown }).providerCode === STREAM_FIRST_EVENT_TIMEOUT_PROVIDER_CODE
|
|
410
|
+
);
|
|
411
|
+
}
|
|
412
|
+
|
|
408
413
|
function isCodexWebSocketRetryableStreamError(error: unknown): boolean {
|
|
409
414
|
if (!(error instanceof Error) || !isCodexWebSocketTransportError(error)) return false;
|
|
410
415
|
const message = error.message.toLowerCase();
|
|
@@ -607,17 +612,22 @@ function createRequestSetup(options: OpenAICodexResponsesOptions | undefined): C
|
|
|
607
612
|
const requestSignal = options?.signal
|
|
608
613
|
? AbortSignal.any([options.signal, requestAbortController.signal])
|
|
609
614
|
: requestAbortController.signal;
|
|
615
|
+
const idleTimeoutMs = options?.streamIdleTimeoutMs ?? getOpenAIStreamIdleTimeoutMs();
|
|
616
|
+
const firstEventTimeoutMs = options?.streamFirstEventTimeoutMs ?? getStreamFirstEventTimeoutMs(idleTimeoutMs);
|
|
610
617
|
const wrapCodexSseStream = (
|
|
611
618
|
source: AsyncGenerator<Record<string, unknown>>,
|
|
612
619
|
): AsyncGenerator<Record<string, unknown>> =>
|
|
613
620
|
iterateWithIdleTimeout(source, {
|
|
614
|
-
idleTimeoutMs
|
|
621
|
+
idleTimeoutMs,
|
|
622
|
+
firstItemTimeoutMs: firstEventTimeoutMs,
|
|
623
|
+
firstItemErrorMessage: "OpenAI Codex SSE stream timed out while waiting for the first event",
|
|
615
624
|
errorMessage: "OpenAI Codex SSE stream stalled while waiting for the next event",
|
|
616
625
|
onIdle: () => requestAbortController.abort(),
|
|
626
|
+
onFirstItemTimeout: () => requestAbortController.abort(),
|
|
617
627
|
abortSignal: options?.signal,
|
|
618
628
|
isProgressItem: isCodexStreamProgressEvent,
|
|
619
629
|
});
|
|
620
|
-
return { requestAbortController, requestSignal, wrapCodexSseStream };
|
|
630
|
+
return { requestAbortController, requestSignal, firstEventTimeoutMs, wrapCodexSseStream };
|
|
621
631
|
}
|
|
622
632
|
|
|
623
633
|
async function buildCodexRequestContext(
|
|
@@ -838,6 +848,7 @@ async function openCodexWebSocketTransport(
|
|
|
838
848
|
websocketState,
|
|
839
849
|
requestSetup.requestSignal,
|
|
840
850
|
options,
|
|
851
|
+
requestSetup.firstEventTimeoutMs,
|
|
841
852
|
);
|
|
842
853
|
return { eventStream, requestBodyForState, transport: "websocket" };
|
|
843
854
|
}
|
|
@@ -1478,7 +1489,7 @@ async function recoverCodexStreamError(
|
|
|
1478
1489
|
runtime: CodexStreamRuntime,
|
|
1479
1490
|
error: unknown,
|
|
1480
1491
|
): Promise<boolean> {
|
|
1481
|
-
if (
|
|
1492
|
+
if (isCodexFirstEventTimeout(error)) return false;
|
|
1482
1493
|
if (await tryRetryWithoutForcedToolChoice(context, runtime, error)) {
|
|
1483
1494
|
return true;
|
|
1484
1495
|
}
|
|
@@ -2196,7 +2207,6 @@ function headersToRecord(headers: Headers): Record<string, string> {
|
|
|
2196
2207
|
|
|
2197
2208
|
interface CodexWebSocketConnectionOptions {
|
|
2198
2209
|
idleTimeoutMs: number;
|
|
2199
|
-
firstEventTimeoutMs: number;
|
|
2200
2210
|
onHandshakeHeaders?: (headers: Headers) => void;
|
|
2201
2211
|
}
|
|
2202
2212
|
|
|
@@ -2204,7 +2214,6 @@ class CodexWebSocketConnection {
|
|
|
2204
2214
|
#url: string;
|
|
2205
2215
|
#headers: Record<string, string>;
|
|
2206
2216
|
#idleTimeoutMs: number;
|
|
2207
|
-
#firstEventTimeoutMs: number;
|
|
2208
2217
|
#onHandshakeHeaders?: (headers: Headers) => void;
|
|
2209
2218
|
#socket: Bun.WebSocket | null = null;
|
|
2210
2219
|
#queue: Array<Record<string, unknown> | Error | null> = [];
|
|
@@ -2216,7 +2225,6 @@ class CodexWebSocketConnection {
|
|
|
2216
2225
|
this.#url = url;
|
|
2217
2226
|
this.#headers = headers;
|
|
2218
2227
|
this.#idleTimeoutMs = options.idleTimeoutMs;
|
|
2219
|
-
this.#firstEventTimeoutMs = options.firstEventTimeoutMs;
|
|
2220
2228
|
this.#onHandshakeHeaders = options.onHandshakeHeaders;
|
|
2221
2229
|
}
|
|
2222
2230
|
|
|
@@ -2346,6 +2354,7 @@ class CodexWebSocketConnection {
|
|
|
2346
2354
|
async *streamRequest(
|
|
2347
2355
|
request: Record<string, unknown>,
|
|
2348
2356
|
signal?: AbortSignal,
|
|
2357
|
+
firstEventTimeoutMs?: number,
|
|
2349
2358
|
): AsyncGenerator<Record<string, unknown>> {
|
|
2350
2359
|
if (!this.#socket || this.#socket.readyState !== WebSocket.OPEN) {
|
|
2351
2360
|
throw createCodexWebSocketTransportError("websocket connection is unavailable");
|
|
@@ -2368,11 +2377,11 @@ class CodexWebSocketConnection {
|
|
|
2368
2377
|
|
|
2369
2378
|
try {
|
|
2370
2379
|
this.#socket.send(JSON.stringify(request));
|
|
2371
|
-
let
|
|
2380
|
+
let sawFirstProgress = false;
|
|
2372
2381
|
let lastProgressAt = Date.now();
|
|
2373
2382
|
while (true) {
|
|
2374
|
-
let timeoutMs =
|
|
2375
|
-
if (
|
|
2383
|
+
let timeoutMs = firstEventTimeoutMs;
|
|
2384
|
+
if (sawFirstProgress) {
|
|
2376
2385
|
timeoutMs = this.#idleTimeoutMs - (Date.now() - lastProgressAt);
|
|
2377
2386
|
if (timeoutMs <= 0) {
|
|
2378
2387
|
throw createCodexWebSocketTransportError("idle timeout waiting for websocket");
|
|
@@ -2380,7 +2389,8 @@ class CodexWebSocketConnection {
|
|
|
2380
2389
|
}
|
|
2381
2390
|
const next = await this.#nextMessage(
|
|
2382
2391
|
timeoutMs,
|
|
2383
|
-
|
|
2392
|
+
sawFirstProgress ? "idle timeout waiting for websocket" : "timeout waiting for first websocket event",
|
|
2393
|
+
sawFirstProgress ? undefined : STREAM_FIRST_EVENT_TIMEOUT_PROVIDER_CODE,
|
|
2384
2394
|
);
|
|
2385
2395
|
if (next instanceof Error) {
|
|
2386
2396
|
throw next;
|
|
@@ -2388,8 +2398,8 @@ class CodexWebSocketConnection {
|
|
|
2388
2398
|
if (next === null) {
|
|
2389
2399
|
throw createCodexWebSocketTransportError("websocket closed before response completion");
|
|
2390
2400
|
}
|
|
2391
|
-
sawFirstEvent = true;
|
|
2392
2401
|
if (isCodexStreamProgressEvent(next)) {
|
|
2402
|
+
sawFirstProgress = true;
|
|
2393
2403
|
lastProgressAt = Date.now();
|
|
2394
2404
|
}
|
|
2395
2405
|
yield next;
|
|
@@ -2425,13 +2435,17 @@ class CodexWebSocketConnection {
|
|
|
2425
2435
|
if (waiter) waiter();
|
|
2426
2436
|
}
|
|
2427
2437
|
|
|
2428
|
-
async #nextMessage(
|
|
2438
|
+
async #nextMessage(
|
|
2439
|
+
timeoutMs: number | undefined,
|
|
2440
|
+
timeoutReason: string,
|
|
2441
|
+
providerCode?: string,
|
|
2442
|
+
): Promise<Record<string, unknown> | Error | null> {
|
|
2429
2443
|
while (this.#queue.length === 0) {
|
|
2430
2444
|
const { promise, resolve } = Promise.withResolvers<void>();
|
|
2431
2445
|
this.#waiters.push(resolve);
|
|
2432
2446
|
let timedOut = false;
|
|
2433
2447
|
let timeout: NodeJS.Timeout | undefined;
|
|
2434
|
-
if (timeoutMs > 0) {
|
|
2448
|
+
if (timeoutMs !== undefined && timeoutMs > 0) {
|
|
2435
2449
|
timeout = setTimeout(() => {
|
|
2436
2450
|
timedOut = true;
|
|
2437
2451
|
const waiterIndex = this.#waiters.indexOf(resolve);
|
|
@@ -2444,7 +2458,10 @@ class CodexWebSocketConnection {
|
|
|
2444
2458
|
await promise;
|
|
2445
2459
|
if (timeout) clearTimeout(timeout);
|
|
2446
2460
|
if (timedOut && this.#queue.length === 0) {
|
|
2447
|
-
|
|
2461
|
+
if (providerCode === STREAM_FIRST_EVENT_TIMEOUT_PROVIDER_CODE) {
|
|
2462
|
+
this.close("first-event-timeout");
|
|
2463
|
+
}
|
|
2464
|
+
return createCodexWebSocketTransportError(timeoutReason, providerCode);
|
|
2448
2465
|
}
|
|
2449
2466
|
}
|
|
2450
2467
|
return this.#queue.shift() ?? null;
|
|
@@ -2473,7 +2490,6 @@ async function getOrCreateCodexWebSocketConnection(
|
|
|
2473
2490
|
const idleTimeoutMs = getCodexWebSocketIdleTimeoutMs(options?.streamIdleTimeoutMs);
|
|
2474
2491
|
state.connection = new CodexWebSocketConnection(url, headerRecord, {
|
|
2475
2492
|
idleTimeoutMs,
|
|
2476
|
-
firstEventTimeoutMs: getCodexWebSocketFirstEventTimeoutMs(idleTimeoutMs, options?.streamFirstEventTimeoutMs),
|
|
2477
2493
|
onHandshakeHeaders: handshakeHeaders => {
|
|
2478
2494
|
updateCodexSessionMetadataFromHeaders(state, handshakeHeaders);
|
|
2479
2495
|
},
|
|
@@ -2544,9 +2560,10 @@ async function openCodexWebSocketEventStream(
|
|
|
2544
2560
|
state: CodexWebSocketSessionState,
|
|
2545
2561
|
signal?: AbortSignal,
|
|
2546
2562
|
options?: Pick<OpenAICodexResponsesOptions, "streamFirstEventTimeoutMs" | "streamIdleTimeoutMs">,
|
|
2563
|
+
firstEventTimeoutMs?: number,
|
|
2547
2564
|
): Promise<AsyncGenerator<Record<string, unknown>>> {
|
|
2548
2565
|
const connection = await getOrCreateCodexWebSocketConnection(state, url, headers, signal, options);
|
|
2549
|
-
return connection.streamRequest(request, signal);
|
|
2566
|
+
return connection.streamRequest(request, signal, firstEventTimeoutMs);
|
|
2550
2567
|
}
|
|
2551
2568
|
|
|
2552
2569
|
function createCodexHeaders(
|
|
@@ -47,7 +47,6 @@ import {
|
|
|
47
47
|
rewriteCopilotError,
|
|
48
48
|
} from "../utils/http-inspector";
|
|
49
49
|
import {
|
|
50
|
-
createWatchdog,
|
|
51
50
|
getOpenAIStreamIdleTimeoutMs,
|
|
52
51
|
getProviderFirstEventTimeoutFallbackMs,
|
|
53
52
|
getStreamFirstEventTimeoutMs,
|
|
@@ -457,7 +456,6 @@ export const streamOpenAICompletions: StreamFunction<"openai-completions"> = (
|
|
|
457
456
|
const output: AssistantMessage = createInitialResponsesAssistantMessage(model.api, model.provider, model.id);
|
|
458
457
|
let rawRequestDump: RawHttpRequestDump | undefined;
|
|
459
458
|
const abortTracker = createAbortSourceTracker(options?.signal);
|
|
460
|
-
const firstEventTimeoutAbortError = new Error(OPENAI_COMPLETIONS_FIRST_EVENT_TIMEOUT_MESSAGE);
|
|
461
459
|
const { requestAbortController, requestSignal } = abortTracker;
|
|
462
460
|
|
|
463
461
|
try {
|
|
@@ -579,10 +577,8 @@ export const streamOpenAICompletions: StreamFunction<"openai-completions"> = (
|
|
|
579
577
|
model.provider === "alibaba-token-plan"
|
|
580
578
|
? ALIBABA_TOKEN_PLAN_FIRST_EVENT_TIMEOUT_MS
|
|
581
579
|
: getProviderFirstEventTimeoutFallbackMs(model.provider);
|
|
582
|
-
const
|
|
583
|
-
options?.streamFirstEventTimeoutMs ?? getStreamFirstEventTimeoutMs(idleTimeoutMs, firstEventFallbackMs)
|
|
584
|
-
() => abortTracker.abortLocally(firstEventTimeoutAbortError),
|
|
585
|
-
);
|
|
580
|
+
const firstEventTimeoutMs =
|
|
581
|
+
options?.streamFirstEventTimeoutMs ?? getStreamFirstEventTimeoutMs(idleTimeoutMs, firstEventFallbackMs);
|
|
586
582
|
if (premiumRequestsTotal !== undefined) {
|
|
587
583
|
output.usage.premiumRequests = premiumRequestsTotal;
|
|
588
584
|
}
|
|
@@ -769,10 +765,12 @@ export const streamOpenAICompletions: StreamFunction<"openai-completions"> = (
|
|
|
769
765
|
};
|
|
770
766
|
|
|
771
767
|
for await (const chunk of iterateWithIdleTimeout(openaiStream, {
|
|
772
|
-
|
|
768
|
+
firstItemTimeoutMs: firstEventTimeoutMs,
|
|
769
|
+
firstItemErrorMessage: OPENAI_COMPLETIONS_FIRST_EVENT_TIMEOUT_MESSAGE,
|
|
773
770
|
idleTimeoutMs,
|
|
774
771
|
errorMessage: "OpenAI completions stream stalled while waiting for the next event",
|
|
775
772
|
onIdle: () => requestAbortController.abort(),
|
|
773
|
+
onFirstItemTimeout: () => requestAbortController.abort(),
|
|
776
774
|
abortSignal: options?.signal,
|
|
777
775
|
isProgressItem: isOpenAICompletionsProgressChunk,
|
|
778
776
|
})) {
|
|
@@ -983,14 +981,17 @@ export const streamOpenAICompletions: StreamFunction<"openai-completions"> = (
|
|
|
983
981
|
stream.end();
|
|
984
982
|
} catch (error) {
|
|
985
983
|
for (const block of output.content) delete (block as any).index;
|
|
986
|
-
const
|
|
984
|
+
const localAbortReason = abortTracker.getLocalAbortReason();
|
|
987
985
|
const capturedErrorResponse = getCapturedErrorResponse?.();
|
|
988
986
|
output.stopReason = abortTracker.wasCallerAbort() ? "aborted" : "error";
|
|
989
|
-
output.errorStatus =
|
|
990
|
-
|
|
987
|
+
output.errorStatus =
|
|
988
|
+
extractHttpStatusFromError(localAbortReason ?? error) ??
|
|
989
|
+
(localAbortReason ? undefined : capturedErrorResponse?.status);
|
|
990
|
+
output.transportFailure = localAbortReason
|
|
991
|
+
? transportFailureFacts(localAbortReason)
|
|
992
|
+
: transportFailureFacts(error, capturedErrorResponse);
|
|
991
993
|
output.errorMessage =
|
|
992
|
-
|
|
993
|
-
(await finalizeErrorMessage(error, rawRequestDump, capturedErrorResponse));
|
|
994
|
+
localAbortReason?.message ?? (await finalizeErrorMessage(error, rawRequestDump, capturedErrorResponse));
|
|
994
995
|
// Some providers via OpenRouter include extra details here.
|
|
995
996
|
const rawMetadata = (error as { error?: { metadata?: { raw?: string } } })?.error?.metadata?.raw;
|
|
996
997
|
if (rawMetadata) output.errorMessage += `\n${rawMetadata}`;
|
|
@@ -37,7 +37,6 @@ import { AssistantMessageEventStream } from "../utils/event-stream";
|
|
|
37
37
|
import { transportFailureFacts } from "../utils/fallback-transport";
|
|
38
38
|
import { finalizeErrorMessage, type RawHttpRequestDump, rewriteCopilotError } from "../utils/http-inspector";
|
|
39
39
|
import {
|
|
40
|
-
createWatchdog,
|
|
41
40
|
getOpenAIStreamIdleTimeoutMs,
|
|
42
41
|
getStreamFirstEventTimeoutMs,
|
|
43
42
|
iterateWithIdleTimeout,
|
|
@@ -265,7 +264,6 @@ export const streamOpenAIResponses: StreamFunction<"openai-responses"> = (
|
|
|
265
264
|
);
|
|
266
265
|
let rawRequestDump: RawHttpRequestDump | undefined;
|
|
267
266
|
const abortTracker = createAbortSourceTracker(options?.signal);
|
|
268
|
-
const firstEventTimeoutAbortError = new Error(OPENAI_RESPONSES_FIRST_EVENT_TIMEOUT_MESSAGE);
|
|
269
267
|
const { requestAbortController, requestSignal } = abortTracker;
|
|
270
268
|
|
|
271
269
|
try {
|
|
@@ -337,10 +335,8 @@ export const streamOpenAIResponses: StreamFunction<"openai-responses"> = (
|
|
|
337
335
|
});
|
|
338
336
|
const firstEventFallbackMs =
|
|
339
337
|
model.provider === "alibaba-token-plan" ? ALIBABA_TOKEN_PLAN_FIRST_EVENT_TIMEOUT_MS : undefined;
|
|
340
|
-
const
|
|
341
|
-
options?.streamFirstEventTimeoutMs ?? getStreamFirstEventTimeoutMs(idleTimeoutMs, firstEventFallbackMs)
|
|
342
|
-
() => abortTracker.abortLocally(firstEventTimeoutAbortError),
|
|
343
|
-
);
|
|
338
|
+
const firstEventTimeoutMs =
|
|
339
|
+
options?.streamFirstEventTimeoutMs ?? getStreamFirstEventTimeoutMs(idleTimeoutMs, firstEventFallbackMs);
|
|
344
340
|
if (premiumRequestsTotal !== undefined) output.usage.premiumRequests = premiumRequestsTotal;
|
|
345
341
|
stream.push({ type: "start", partial: output });
|
|
346
342
|
|
|
@@ -348,9 +344,11 @@ export const streamOpenAIResponses: StreamFunction<"openai-responses"> = (
|
|
|
348
344
|
await processResponsesStream(
|
|
349
345
|
iterateWithIdleTimeout(openaiStream, {
|
|
350
346
|
idleTimeoutMs,
|
|
351
|
-
|
|
347
|
+
firstItemTimeoutMs: firstEventTimeoutMs,
|
|
348
|
+
firstItemErrorMessage: OPENAI_RESPONSES_FIRST_EVENT_TIMEOUT_MESSAGE,
|
|
352
349
|
errorMessage: "OpenAI responses stream stalled while waiting for the next event",
|
|
353
350
|
onIdle: () => requestAbortController.abort(),
|
|
351
|
+
onFirstItemTimeout: () => requestAbortController.abort(),
|
|
354
352
|
abortSignal: options?.signal,
|
|
355
353
|
isProgressItem: isOpenAIResponsesProgressEvent,
|
|
356
354
|
}),
|
|
@@ -389,11 +387,11 @@ export const streamOpenAIResponses: StreamFunction<"openai-responses"> = (
|
|
|
389
387
|
stream.end();
|
|
390
388
|
} catch (error) {
|
|
391
389
|
for (const block of output.content) delete (block as { index?: number }).index;
|
|
392
|
-
const
|
|
390
|
+
const localAbortReason = abortTracker.getLocalAbortReason();
|
|
393
391
|
output.stopReason = abortTracker.wasCallerAbort() ? "aborted" : "error";
|
|
394
|
-
output.errorStatus = extractHttpStatusFromError(error);
|
|
395
|
-
output.transportFailure = transportFailureFacts(error);
|
|
396
|
-
output.errorMessage =
|
|
392
|
+
output.errorStatus = extractHttpStatusFromError(localAbortReason ?? error);
|
|
393
|
+
output.transportFailure = transportFailureFacts(localAbortReason ?? error);
|
|
394
|
+
output.errorMessage = localAbortReason?.message ?? (await finalizeErrorMessage(error, rawRequestDump));
|
|
397
395
|
output.errorMessage = rewriteCopilotError(output.errorMessage, error, model.provider);
|
|
398
396
|
// Explicitly mark the poisoned-history rejection so the shared
|
|
399
397
|
// `invalid_prompt` contract is present even when the SDK error surfaces
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
* lazy wrappers below), so this file IS the main streaming path's provider
|
|
11
11
|
* loader: heavy SDKs stay out of the CLI startup parse graph.
|
|
12
12
|
*/
|
|
13
|
+
|
|
13
14
|
import type {
|
|
14
15
|
Api,
|
|
15
16
|
AssistantMessage,
|
|
@@ -21,7 +22,13 @@ import type {
|
|
|
21
22
|
} from "../types";
|
|
22
23
|
import { type AbortSourceTracker, createAbortSourceTracker } from "../utils/abort";
|
|
23
24
|
import { AssistantMessageEventStream as EventStreamImpl } from "../utils/event-stream";
|
|
24
|
-
import {
|
|
25
|
+
import { transportFailureFacts } from "../utils/fallback-transport";
|
|
26
|
+
import {
|
|
27
|
+
FirstEventTimeoutError,
|
|
28
|
+
getStreamFirstEventTimeoutMs,
|
|
29
|
+
getStreamIdleTimeoutMs,
|
|
30
|
+
iterateWithIdleTimeout,
|
|
31
|
+
} from "../utils/idle-iterator";
|
|
25
32
|
import type { BedrockOptions } from "./amazon-bedrock";
|
|
26
33
|
import type { AnthropicOptions } from "./anthropic";
|
|
27
34
|
import type { AzureOpenAIResponsesOptions } from "./azure-openai-responses";
|
|
@@ -229,7 +236,8 @@ function forwardStream<TApi extends Api>(
|
|
|
229
236
|
errorMessage: LAZY_STREAM_IDLE_TIMEOUT_ERROR,
|
|
230
237
|
firstItemErrorMessage: LAZY_STREAM_FIRST_EVENT_TIMEOUT_ERROR,
|
|
231
238
|
onIdle: () => abortTracker.abortLocally(new Error(LAZY_STREAM_IDLE_TIMEOUT_ERROR)),
|
|
232
|
-
onFirstItemTimeout: () =>
|
|
239
|
+
onFirstItemTimeout: () =>
|
|
240
|
+
abortTracker.abortLocally(new FirstEventTimeoutError(LAZY_STREAM_FIRST_EVENT_TIMEOUT_ERROR)),
|
|
233
241
|
abortSignal: options.signal,
|
|
234
242
|
// The synthetic `start` event is yielded immediately by every provider before
|
|
235
243
|
// the upstream model has emitted any tokens. Treating it as the first "real"
|
|
@@ -261,6 +269,7 @@ function createLazyLoadErrorMessage<TApi extends Api>(
|
|
|
261
269
|
error: unknown,
|
|
262
270
|
stopReason: Extract<AssistantMessage["stopReason"], "aborted" | "error"> = "error",
|
|
263
271
|
): AssistantMessage {
|
|
272
|
+
const transportFailure = transportFailureFacts(error);
|
|
264
273
|
return {
|
|
265
274
|
role: "assistant",
|
|
266
275
|
content: [],
|
|
@@ -278,6 +287,7 @@ function createLazyLoadErrorMessage<TApi extends Api>(
|
|
|
278
287
|
stopReason,
|
|
279
288
|
errorMessage:
|
|
280
289
|
stopReason === "aborted" ? "Request was aborted" : error instanceof Error ? error.message : String(error),
|
|
290
|
+
...(transportFailure ? { transportFailure } : {}),
|
|
281
291
|
timestamp: Date.now(),
|
|
282
292
|
};
|
|
283
293
|
}
|
|
@@ -5,6 +5,9 @@ export interface FallbackTrigger {
|
|
|
5
5
|
retryAfterMs?: number;
|
|
6
6
|
}
|
|
7
7
|
|
|
8
|
+
/** Stable code for streams that time out before producing semantic progress. */
|
|
9
|
+
export const STREAM_FIRST_EVENT_TIMEOUT_PROVIDER_CODE = "stream_first_event_timeout";
|
|
10
|
+
|
|
8
11
|
export type TransportHeaders = Headers | Record<string, string | undefined>;
|
|
9
12
|
|
|
10
13
|
/**
|
|
@@ -185,7 +188,8 @@ export function transportFailureFacts(
|
|
|
185
188
|
!isQuotaCode(normalizedCode) &&
|
|
186
189
|
!isAuthCode(normalizedCode) &&
|
|
187
190
|
!isRateLimitCode(normalizedCode) &&
|
|
188
|
-
!isContextOverflowCode(normalizedCode)
|
|
191
|
+
!isContextOverflowCode(normalizedCode) &&
|
|
192
|
+
normalizedCode !== STREAM_FIRST_EVENT_TIMEOUT_PROVIDER_CODE
|
|
189
193
|
) {
|
|
190
194
|
return undefined;
|
|
191
195
|
}
|
|
@@ -256,14 +260,17 @@ export function classifyFallbackTrigger(
|
|
|
256
260
|
parseRetryAfterMilliseconds(headers?.get("retry-after-ms") ?? null) ??
|
|
257
261
|
parseRetryAfterSeconds(headers?.get("retry-after") ?? null);
|
|
258
262
|
const code = (facts.openaiErrorCode ?? facts.anthropicErrorType ?? facts.providerCode)?.toLowerCase();
|
|
259
|
-
const triggerClass: FallbackTriggerClass =
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
263
|
+
const triggerClass: FallbackTriggerClass =
|
|
264
|
+
code === STREAM_FIRST_EVENT_TIMEOUT_PROVIDER_CODE
|
|
265
|
+
? "server"
|
|
266
|
+
: isQuotaCode(code)
|
|
267
|
+
? "quota"
|
|
268
|
+
: facts.status === 401 || facts.status === 403 || isAuthCode(code)
|
|
269
|
+
? "auth"
|
|
270
|
+
: facts.status === 429 || isRateLimitCode(code)
|
|
271
|
+
? "rate_limit"
|
|
272
|
+
: facts.status !== undefined && facts.status >= 500 && facts.status <= 599
|
|
273
|
+
? "server"
|
|
274
|
+
: "other";
|
|
268
275
|
return retryAfterMs === undefined ? { class: triggerClass } : { class: triggerClass, retryAfterMs };
|
|
269
276
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import * as fs from "node:fs/promises";
|
|
1
2
|
import * as path from "node:path";
|
|
2
3
|
import { APP_NAME, extractHttpStatusFromError, getLogsDir } from "@gajae-code/utils";
|
|
3
4
|
import { isCopilotTransientModelError } from "./retry.js";
|
|
@@ -94,6 +95,49 @@ export function formatModelUnavailableGuidance(dump: RawHttpRequestDump | undefi
|
|
|
94
95
|
].join("\n");
|
|
95
96
|
}
|
|
96
97
|
|
|
98
|
+
/**
|
|
99
|
+
* Cap on retained HTTP 400 request dumps.
|
|
100
|
+
*
|
|
101
|
+
* Each dump carries the full sanitized request body, so they are large: a
|
|
102
|
+
* developer machine accumulated 27,249 files totalling 7.0 GB, averaging 264 KB
|
|
103
|
+
* each, because nothing ever removed them. The rotating application log already
|
|
104
|
+
* bounds itself (`maxSize: 10m`, `maxFiles: 5`); these diagnostics get the same
|
|
105
|
+
* treatment so the newest failures stay available without unbounded growth.
|
|
106
|
+
*/
|
|
107
|
+
const MAX_RETAINED_DUMPS = 50;
|
|
108
|
+
|
|
109
|
+
/** Directory holding the retained HTTP 400 dumps. */
|
|
110
|
+
export function httpRequestDumpDir(): string {
|
|
111
|
+
return path.join(getLogsDir(), "http-400-requests");
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Drop the oldest dumps beyond the cap. Best-effort: diagnostics must never turn
|
|
116
|
+
* a request failure into a second failure, so every step swallows its error.
|
|
117
|
+
*
|
|
118
|
+
* File names are `${Date.now()}-${hash}.json`, so a lexical sort is chronological
|
|
119
|
+
* for the millisecond timestamps this writer produces.
|
|
120
|
+
*/
|
|
121
|
+
export async function pruneHttpRequestDumps(dir: string = httpRequestDumpDir()): Promise<number> {
|
|
122
|
+
const entries = await fs.readdir(dir).catch(() => undefined);
|
|
123
|
+
if (!entries) return 0;
|
|
124
|
+
|
|
125
|
+
const dumps = entries.filter(name => name.endsWith(".json")).sort();
|
|
126
|
+
if (dumps.length <= MAX_RETAINED_DUMPS) return 0;
|
|
127
|
+
|
|
128
|
+
let removed = 0;
|
|
129
|
+
for (const name of dumps.slice(0, dumps.length - MAX_RETAINED_DUMPS)) {
|
|
130
|
+
if (
|
|
131
|
+
await fs.rm(path.join(dir, name), { force: true }).then(
|
|
132
|
+
() => true,
|
|
133
|
+
() => false,
|
|
134
|
+
)
|
|
135
|
+
)
|
|
136
|
+
removed++;
|
|
137
|
+
}
|
|
138
|
+
return removed;
|
|
139
|
+
}
|
|
140
|
+
|
|
97
141
|
export async function appendRawHttpRequestDumpFor400(
|
|
98
142
|
message: string,
|
|
99
143
|
error: unknown,
|
|
@@ -105,10 +149,12 @@ export async function appendRawHttpRequestDumpFor400(
|
|
|
105
149
|
|
|
106
150
|
const sanitizedDump = sanitizeDump(dump);
|
|
107
151
|
const fileName = `${Date.now()}-${Bun.hash(JSON.stringify(sanitizedDump)).toString(36)}.json`;
|
|
108
|
-
const
|
|
152
|
+
const dumpDir = httpRequestDumpDir();
|
|
153
|
+
const filePath = path.join(dumpDir, fileName);
|
|
109
154
|
|
|
110
155
|
try {
|
|
111
156
|
await Bun.write(filePath, `${JSON.stringify(sanitizedDump, null, 2)}\n`);
|
|
157
|
+
await pruneHttpRequestDumps(dumpDir);
|
|
112
158
|
return `${message}\nraw-http-request=${filePath}\n${RAW_HTTP_REQUEST_PRIVACY_NOTE}`;
|
|
113
159
|
} catch (writeError) {
|
|
114
160
|
const writeMessage = writeError instanceof Error ? writeError.message : String(writeError);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { $env } from "@gajae-code/utils";
|
|
2
|
+
import { STREAM_FIRST_EVENT_TIMEOUT_PROVIDER_CODE } from "./fallback-transport";
|
|
2
3
|
|
|
3
4
|
const DEFAULT_STREAM_IDLE_TIMEOUT_MS = 120_000;
|
|
4
5
|
const DEFAULT_STREAM_FIRST_EVENT_TIMEOUT_MS = 100_000;
|
|
@@ -66,6 +67,14 @@ export function getStreamFirstEventTimeoutMs(
|
|
|
66
67
|
}
|
|
67
68
|
|
|
68
69
|
export type Watchdog = NodeJS.Timeout | undefined;
|
|
70
|
+
export class FirstEventTimeoutError extends Error {
|
|
71
|
+
readonly providerCode = STREAM_FIRST_EVENT_TIMEOUT_PROVIDER_CODE;
|
|
72
|
+
|
|
73
|
+
constructor(message: string) {
|
|
74
|
+
super(message);
|
|
75
|
+
this.name = "FirstEventTimeoutError";
|
|
76
|
+
}
|
|
77
|
+
}
|
|
69
78
|
|
|
70
79
|
const dummyWatchdog = setTimeout(() => {}, 1);
|
|
71
80
|
clearTimeout(dummyWatchdog);
|
|
@@ -228,9 +237,9 @@ export async function* iterateWithIdleTimeout<T>(
|
|
|
228
237
|
options.onFirstItemTimeout?.();
|
|
229
238
|
}
|
|
230
239
|
closeIterator();
|
|
231
|
-
throw
|
|
232
|
-
|
|
233
|
-
|
|
240
|
+
throw awaitingFirstItem
|
|
241
|
+
? new FirstEventTimeoutError(options.firstItemErrorMessage ?? options.errorMessage)
|
|
242
|
+
: new Error(options.errorMessage);
|
|
234
243
|
}
|
|
235
244
|
if (outcome.kind === "error") {
|
|
236
245
|
throw outcome.error;
|
package/src/utils/oauth/kimi.ts
CHANGED
|
@@ -7,7 +7,7 @@ import * as fs from "node:fs";
|
|
|
7
7
|
import * as os from "node:os";
|
|
8
8
|
import * as path from "node:path";
|
|
9
9
|
import { scheduler } from "node:timers/promises";
|
|
10
|
-
import { $
|
|
10
|
+
import { $pickCredentialEnv, getAgentDir, isEnoent } from "@gajae-code/utils";
|
|
11
11
|
import packageJson from "../../../package.json" with { type: "json" };
|
|
12
12
|
import type { OAuthController, OAuthCredentials } from "./types";
|
|
13
13
|
|
|
@@ -38,8 +38,23 @@ interface TokenResponse {
|
|
|
38
38
|
interval?: number;
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
+
/**
|
|
42
|
+
* OAuth host for the device flow, from trusted environment sources only.
|
|
43
|
+
*
|
|
44
|
+
* This host receives the device-authorization request, the authorization-code
|
|
45
|
+
* exchange, and the refresh call that carries the existing refresh token, so
|
|
46
|
+
* whatever can set it can collect the user's Kimi credentials. `$env` merges the
|
|
47
|
+
* caller's `cwd/.env`, so reading it there would let repository content redirect
|
|
48
|
+
* the login flow. Resolve it the same way the credentials themselves are:
|
|
49
|
+
* launching shell plus GJC/user-owned `.env` files, never the project `.env`.
|
|
50
|
+
*/
|
|
41
51
|
function resolveOAuthHost(): string {
|
|
42
|
-
return $
|
|
52
|
+
return $pickCredentialEnv("KIMI_CODE_OAUTH_HOST", "KIMI_OAUTH_HOST") || DEFAULT_OAUTH_HOST;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Test seam: the OAuth host as resolved from trusted env. */
|
|
56
|
+
export function resolveKimiOAuthHostForTest(): string {
|
|
57
|
+
return resolveOAuthHost();
|
|
43
58
|
}
|
|
44
59
|
|
|
45
60
|
function formatDeviceModel(system: string, release: string, arch: string): string {
|
|
@@ -186,13 +186,32 @@ async function httpEmailLogin(ctrl: OAuthController): Promise<OAuthCredentials>
|
|
|
186
186
|
*
|
|
187
187
|
* No browser/manual token paste fallback is used.
|
|
188
188
|
*/
|
|
189
|
+
/**
|
|
190
|
+
* Whether the operator disabled borrowing a token from the native macOS app.
|
|
191
|
+
*
|
|
192
|
+
* `GJC_AUTH_NO_BORROW` is the documented name; `PI_AUTH_NO_BORROW` is the legacy
|
|
193
|
+
* one that was the only name actually read.
|
|
194
|
+
*/
|
|
195
|
+
function authBorrowDisabled(): boolean {
|
|
196
|
+
return Boolean($env.GJC_AUTH_NO_BORROW || $env.PI_AUTH_NO_BORROW);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** Test seam: the resolved native-app borrowing opt-out. */
|
|
200
|
+
export function authBorrowDisabledForTest(): boolean {
|
|
201
|
+
return authBorrowDisabled();
|
|
202
|
+
}
|
|
203
|
+
|
|
189
204
|
export async function loginPerplexity(ctrl: OAuthController): Promise<OAuthCredentials> {
|
|
190
205
|
if (!ctrl.onPrompt) {
|
|
191
206
|
throw new Error("Perplexity login requires onPrompt callback");
|
|
192
207
|
}
|
|
193
208
|
|
|
194
|
-
// Path 1: Native macOS app JWT
|
|
195
|
-
|
|
209
|
+
// Path 1: Native macOS app JWT, skipped when the operator opts out.
|
|
210
|
+
//
|
|
211
|
+
// Presence-based on purpose: this is a privacy opt-out, so any set value must
|
|
212
|
+
// disable borrowing. A boolean contract would let `GJC_AUTH_NO_BORROW=0`
|
|
213
|
+
// silently re-enable reading a token out of another application.
|
|
214
|
+
if (!authBorrowDisabled()) {
|
|
196
215
|
ctrl.onProgress?.("Checking for Perplexity desktop app...");
|
|
197
216
|
const nativeJwt = await extractFromNativeApp();
|
|
198
217
|
if (nativeJwt) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { $
|
|
1
|
+
import { $pickflag } from "@gajae-code/utils";
|
|
2
2
|
import { upgradeJsonSchemaTo202012 } from "./draft";
|
|
3
3
|
import { tryEnforceStrictSchema } from "./normalize";
|
|
4
4
|
|
|
@@ -11,7 +11,7 @@ import { tryEnforceStrictSchema } from "./normalize";
|
|
|
11
11
|
* see `openai-completions`, `openai-responses`, `OpenAI code provider-responses`, and
|
|
12
12
|
* the strict candidate selection in `anthropic`.
|
|
13
13
|
*/
|
|
14
|
-
export const NO_STRICT = $
|
|
14
|
+
export const NO_STRICT = $pickflag("GJC_NO_STRICT", "PI_NO_STRICT");
|
|
15
15
|
|
|
16
16
|
/**
|
|
17
17
|
* Consolidated helper for OpenAI-style strict schema enforcement.
|
|
@@ -175,7 +175,8 @@ export function isForcedToolChoiceUnsupportedError(error: unknown, sentForcedToo
|
|
|
175
175
|
message,
|
|
176
176
|
) ||
|
|
177
177
|
/forces?\s+tool\s+use.*?(not\s+compatible|incompatible|not\s+supported)/is.test(message) ||
|
|
178
|
-
/does\s+not\s+support\s+forced\s+tool[_\s-]?choices?/is.test(message)
|
|
178
|
+
/does\s+not\s+support\s+forced\s+tool[_\s-]?choices?/is.test(message) ||
|
|
179
|
+
/tool[_\s-]?choices?\s+['"`][^'"`\r\n]+['"`]\s+not\s+found\s+in\s+['"`]tools['"`]\s+parameter\b/is.test(message)
|
|
179
180
|
);
|
|
180
181
|
}
|
|
181
182
|
|