@gajae-code/ai 0.2.3 → 0.2.5
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 +6 -0
- package/dist/types/providers/anthropic.d.ts +1 -0
- package/dist/types/types.d.ts +10 -0
- package/dist/types/utils/oauth/callback-server.d.ts +3 -0
- package/dist/types/utils/retry-budget.d.ts +1 -0
- package/package.json +2 -2
- package/src/auth-broker/remote-store.ts +1 -1
- package/src/auth-storage.ts +2 -2
- package/src/providers/amazon-bedrock.ts +2 -0
- package/src/providers/anthropic.ts +10 -5
- package/src/providers/azure-openai-responses.ts +2 -1
- package/src/providers/google-gemini-cli.ts +5 -3
- package/src/providers/ollama.ts +2 -0
- package/src/providers/openai-codex-responses.ts +17 -9
- package/src/providers/openai-completions.ts +7 -9
- package/src/providers/openai-responses-server.ts +6 -6
- package/src/providers/openai-responses.ts +4 -1
- package/src/stream.ts +2 -0
- package/src/types.ts +10 -0
- package/src/utils/oauth/callback-server.ts +6 -1
- package/src/utils/retry-budget.ts +4 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.2.4] - 2026-06-02
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Added configurable provider request and stream retry budgets so hosts can bound transient upstream/server retry behavior separately from session-level retries.
|
|
10
|
+
|
|
5
11
|
## [0.2.2] - 2026-05-31
|
|
6
12
|
|
|
7
13
|
### Fixed
|
package/dist/types/types.d.ts
CHANGED
|
@@ -178,6 +178,16 @@ export interface StreamOptions {
|
|
|
178
178
|
* Default: 60000 (60 seconds). Set to 0 to disable the cap.
|
|
179
179
|
*/
|
|
180
180
|
maxRetryDelayMs?: number;
|
|
181
|
+
/**
|
|
182
|
+
* Maximum provider request retries for transports/SDKs that retry before a stream is established.
|
|
183
|
+
* Counts retries only, not the initial attempt. Providers keep their built-in default when unset.
|
|
184
|
+
*/
|
|
185
|
+
requestMaxRetries?: number;
|
|
186
|
+
/**
|
|
187
|
+
* Maximum provider stream replay retries after a replay-safe transient stream failure.
|
|
188
|
+
* Counts retries only, not the initial stream attempt. Providers keep their built-in default when unset.
|
|
189
|
+
*/
|
|
190
|
+
streamMaxRetries?: number;
|
|
181
191
|
/**
|
|
182
192
|
* Optional metadata to include in API requests.
|
|
183
193
|
* Providers extract the fields they understand and ignore the rest.
|
|
@@ -7,6 +7,8 @@ export interface OAuthCallbackFlowOptions {
|
|
|
7
7
|
preferredPort: number;
|
|
8
8
|
callbackPath?: string;
|
|
9
9
|
callbackHostname?: string;
|
|
10
|
+
/** Local listener hostname; defaults to callbackHostname when omitted. */
|
|
11
|
+
callbackBindHostname?: string;
|
|
10
12
|
/** Exact redirect URI advertised to the provider; disables port fallback. */
|
|
11
13
|
redirectUri?: string;
|
|
12
14
|
}
|
|
@@ -19,6 +21,7 @@ export declare abstract class OAuthCallbackFlow {
|
|
|
19
21
|
preferredPort: number;
|
|
20
22
|
callbackPath: string;
|
|
21
23
|
callbackHostname: string;
|
|
24
|
+
callbackBindHostname: string;
|
|
22
25
|
redirectUri?: string;
|
|
23
26
|
constructor(ctrl: OAuthController, preferredPortOrOptions: number | OAuthCallbackFlowOptions, callbackPath?: string);
|
|
24
27
|
/**
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function resolveRetryBudget(value: number | undefined, fallback: number): number;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@gajae-code/ai",
|
|
4
|
-
"version": "0.2.
|
|
4
|
+
"version": "0.2.5",
|
|
5
5
|
"description": "Unified LLM API with automatic model discovery and provider configuration",
|
|
6
6
|
"homepage": "https://gaebal-gajae.dev",
|
|
7
7
|
"author": "Yeachan-Heo",
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
"dependencies": {
|
|
44
44
|
"@anthropic-ai/sdk": "^0.94.0",
|
|
45
45
|
"@bufbuild/protobuf": "^2.12.0",
|
|
46
|
-
"@gajae-code/utils": "0.2.
|
|
46
|
+
"@gajae-code/utils": "0.2.5",
|
|
47
47
|
"openai": "^6.36.0",
|
|
48
48
|
"partial-json": "^0.1.7",
|
|
49
49
|
"zod": "4.4.3"
|
|
@@ -281,7 +281,7 @@ export class RemoteAuthCredentialStore implements AuthCredentialStore {
|
|
|
281
281
|
|
|
282
282
|
async prepareForRequest(credentialId: number, opts: { signal?: AbortSignal } = {}): Promise<boolean> {
|
|
283
283
|
const entry = this.#snapshot.credentials.find(candidate => candidate.id === credentialId);
|
|
284
|
-
if (
|
|
284
|
+
if (entry?.credential.type !== "oauth" || entry.rotatesInMs === null) return false;
|
|
285
285
|
const remainingMs = this.#snapshotReceivedAt + entry.rotatesInMs - Date.now();
|
|
286
286
|
if (remainingMs > WAIT_THRESHOLD_MS) return false;
|
|
287
287
|
return this.waitForFreshSnapshot(MAX_WAIT_MS, opts);
|
package/src/auth-storage.ts
CHANGED
|
@@ -2733,7 +2733,7 @@ export class AuthStorage {
|
|
|
2733
2733
|
if (!prepare) return true;
|
|
2734
2734
|
const stored = this.#getStoredCredentials(provider);
|
|
2735
2735
|
const selected = stored[selection.index];
|
|
2736
|
-
if (
|
|
2736
|
+
if (selected?.credential.type !== "oauth") return false;
|
|
2737
2737
|
|
|
2738
2738
|
const prepared = await prepare(selected.id, { signal: options?.signal });
|
|
2739
2739
|
if (!prepared) return true;
|
|
@@ -2745,7 +2745,7 @@ export class AuthStorage {
|
|
|
2745
2745
|
const latestIndex = latestRows.findIndex(row => row.id === selected.id);
|
|
2746
2746
|
if (latestIndex === -1) return false;
|
|
2747
2747
|
const latest = latestRows[latestIndex];
|
|
2748
|
-
if (
|
|
2748
|
+
if (latest?.credential.type !== "oauth") return false;
|
|
2749
2749
|
selection.index = latestIndex;
|
|
2750
2750
|
selection.credential = latest.credential;
|
|
2751
2751
|
return true;
|
|
@@ -31,6 +31,7 @@ import { normalizeToolCallId, resolveCacheRetention } from "../utils";
|
|
|
31
31
|
import { AssistantMessageEventStream } from "../utils/event-stream";
|
|
32
32
|
import { appendRawHttpRequestDumpFor400, type RawHttpRequestDump, withHttpStatus } from "../utils/http-inspector";
|
|
33
33
|
import { parseStreamingJson } from "../utils/json-parse";
|
|
34
|
+
import { resolveRetryBudget } from "../utils/retry-budget";
|
|
34
35
|
import { toolWireSchema } from "../utils/schema/wire";
|
|
35
36
|
import { resolveAwsCredentials } from "./aws-credentials";
|
|
36
37
|
import { decodeEventStream } from "./aws-eventstream";
|
|
@@ -259,6 +260,7 @@ export const streamBedrock: StreamFunction<"bedrock-converse-stream"> = (
|
|
|
259
260
|
headers: requestHeaders,
|
|
260
261
|
body,
|
|
261
262
|
signal: options.signal,
|
|
263
|
+
maxAttempts: resolveRetryBudget(options.requestMaxRetries, 4) + 1,
|
|
262
264
|
});
|
|
263
265
|
|
|
264
266
|
if (!response.ok) {
|
|
@@ -61,6 +61,7 @@ import { parseJsonWithRepair, parseStreamingJson } from "../utils/json-parse";
|
|
|
61
61
|
import { parseGitHubCopilotApiKey } from "../utils/oauth/github-copilot";
|
|
62
62
|
import { notifyProviderResponse } from "../utils/provider-response";
|
|
63
63
|
import { isCopilotTransientModelError } from "../utils/retry";
|
|
64
|
+
import { resolveRetryBudget } from "../utils/retry-budget";
|
|
64
65
|
import { COMBINATOR_KEYS, NO_STRICT, toolWireSchema } from "../utils/schema";
|
|
65
66
|
import { spillToDescription } from "../utils/schema/spill";
|
|
66
67
|
import { notifyRawSseEvent, wrapFetchForSseDebug } from "../utils/sse-debug";
|
|
@@ -183,12 +184,14 @@ export function buildAnthropicHeaders(options: AnthropicHeaderOptions): Record<s
|
|
|
183
184
|
"User-Agent": userAgent,
|
|
184
185
|
};
|
|
185
186
|
} else if (!isAnthropicApiBaseUrl(options.baseUrl)) {
|
|
187
|
+
const incomingUserAgent = getHeaderCaseInsensitive(options.modelHeaders, "User-Agent");
|
|
186
188
|
return {
|
|
187
189
|
...modelHeaders,
|
|
188
190
|
Accept: acceptHeader,
|
|
189
191
|
Authorization: `Bearer ${options.apiKey}`,
|
|
190
192
|
...sharedHeaders,
|
|
191
193
|
"Anthropic-Beta": betaHeader,
|
|
194
|
+
...(incomingUserAgent ? { "User-Agent": incomingUserAgent } : {}),
|
|
192
195
|
};
|
|
193
196
|
} else {
|
|
194
197
|
return {
|
|
@@ -626,6 +629,7 @@ export type AnthropicClientOptionsArgs = {
|
|
|
626
629
|
hasTools?: boolean;
|
|
627
630
|
onSseEvent?: AnthropicOptions["onSseEvent"];
|
|
628
631
|
fetch?: FetchImpl;
|
|
632
|
+
requestMaxRetries?: number;
|
|
629
633
|
};
|
|
630
634
|
|
|
631
635
|
export type AnthropicClientOptionsResult = {
|
|
@@ -1057,6 +1061,7 @@ export const streamAnthropic: StreamFunction<"anthropic-messages"> = (
|
|
|
1057
1061
|
hasTools: !!context.tools?.length,
|
|
1058
1062
|
onSseEvent: options?.onSseEvent,
|
|
1059
1063
|
fetch: options?.fetch,
|
|
1064
|
+
requestMaxRetries: options?.requestMaxRetries,
|
|
1060
1065
|
});
|
|
1061
1066
|
client = created.client;
|
|
1062
1067
|
isOAuthToken = created.isOAuthToken;
|
|
@@ -1445,7 +1450,7 @@ export const streamAnthropic: StreamFunction<"anthropic-messages"> = (
|
|
|
1445
1450
|
firstTokenTime === undefined && isProviderRetryableError(streamFailure, model.provider);
|
|
1446
1451
|
if (
|
|
1447
1452
|
activeAbortTracker.wasCallerAbort() ||
|
|
1448
|
-
providerRetryAttempt >= PROVIDER_MAX_RETRIES ||
|
|
1453
|
+
providerRetryAttempt >= resolveRetryBudget(options?.streamMaxRetries, PROVIDER_MAX_RETRIES) ||
|
|
1449
1454
|
(!canRetryTransientEnvelopeFailure && !canRetryProviderFailure)
|
|
1450
1455
|
) {
|
|
1451
1456
|
throw streamFailure;
|
|
@@ -1604,7 +1609,7 @@ export function buildAnthropicClientOptions(args: AnthropicClientOptionsArgs): A
|
|
|
1604
1609
|
apiKey: null,
|
|
1605
1610
|
authToken: copilotApiKey,
|
|
1606
1611
|
baseURL: baseUrl,
|
|
1607
|
-
maxRetries: 5,
|
|
1612
|
+
maxRetries: resolveRetryBudget(args.requestMaxRetries, 5),
|
|
1608
1613
|
dangerouslyAllowBrowser: true,
|
|
1609
1614
|
defaultHeaders,
|
|
1610
1615
|
logLevel: ANTHROPIC_SDK_LOG_LEVEL,
|
|
@@ -1637,7 +1642,7 @@ export function buildAnthropicClientOptions(args: AnthropicClientOptionsArgs): A
|
|
|
1637
1642
|
apiKey: null,
|
|
1638
1643
|
authToken: null,
|
|
1639
1644
|
baseURL: baseUrl,
|
|
1640
|
-
maxRetries: 5,
|
|
1645
|
+
maxRetries: resolveRetryBudget(args.requestMaxRetries, 5),
|
|
1641
1646
|
dangerouslyAllowBrowser: true,
|
|
1642
1647
|
defaultHeaders,
|
|
1643
1648
|
logLevel: ANTHROPIC_SDK_LOG_LEVEL,
|
|
@@ -1650,7 +1655,7 @@ export function buildAnthropicClientOptions(args: AnthropicClientOptionsArgs): A
|
|
|
1650
1655
|
apiKey: oauthToken ? null : apiKey,
|
|
1651
1656
|
authToken: oauthToken ? apiKey : undefined,
|
|
1652
1657
|
baseURL: baseUrl,
|
|
1653
|
-
maxRetries: 5,
|
|
1658
|
+
maxRetries: resolveRetryBudget(args.requestMaxRetries, 5),
|
|
1654
1659
|
dangerouslyAllowBrowser: true,
|
|
1655
1660
|
defaultHeaders,
|
|
1656
1661
|
logLevel: ANTHROPIC_SDK_LOG_LEVEL,
|
|
@@ -1679,7 +1684,7 @@ function disableThinkingIfToolChoiceForced(params: MessageCreateParamsStreaming)
|
|
|
1679
1684
|
|
|
1680
1685
|
function ensureMaxTokensForThinking(params: MessageCreateParamsStreaming, model: Model<"anthropic-messages">): void {
|
|
1681
1686
|
const thinking = params.thinking;
|
|
1682
|
-
if (
|
|
1687
|
+
if (thinking?.type !== "enabled") return;
|
|
1683
1688
|
|
|
1684
1689
|
const budgetTokens = thinking.budget_tokens ?? 0;
|
|
1685
1690
|
if (budgetTokens <= 0) return;
|
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
getStreamFirstEventTimeoutMs,
|
|
27
27
|
iterateWithIdleTimeout,
|
|
28
28
|
} from "../utils/idle-iterator";
|
|
29
|
+
import { resolveRetryBudget } from "../utils/retry-budget";
|
|
29
30
|
import { sanitizeSchemaForOpenAIResponses, toolWireSchema } from "../utils/schema";
|
|
30
31
|
import { wrapFetchForSseDebug } from "../utils/sse-debug";
|
|
31
32
|
import { mapToOpenAIResponsesToolChoice } from "../utils/tool-choice";
|
|
@@ -249,7 +250,7 @@ function createClient(model: Model<"azure-openai-responses">, apiKey: string, op
|
|
|
249
250
|
apiKey,
|
|
250
251
|
apiVersion,
|
|
251
252
|
dangerouslyAllowBrowser: true,
|
|
252
|
-
maxRetries: 5,
|
|
253
|
+
maxRetries: resolveRetryBudget(options?.requestMaxRetries, 5),
|
|
253
254
|
defaultHeaders: headers,
|
|
254
255
|
baseURL: baseUrl,
|
|
255
256
|
fetch: onSseEvent ? wrapFetchForSseDebug(baseFetch, event => onSseEvent(event, model)) : baseFetch,
|
|
@@ -21,6 +21,7 @@ import type {
|
|
|
21
21
|
import { normalizeSystemPrompts } from "../utils";
|
|
22
22
|
import { AssistantMessageEventStream } from "../utils/event-stream";
|
|
23
23
|
import { appendRawHttpRequestDumpFor400, type RawHttpRequestDump, withHttpStatus } from "../utils/http-inspector";
|
|
24
|
+
import { resolveRetryBudget } from "../utils/retry-budget";
|
|
24
25
|
// Refresh is the sole responsibility of AuthStorage (broker-aware, single-flighted);
|
|
25
26
|
// the stream provider trusts the access token threaded through `options.apiKey`.
|
|
26
27
|
import { normalizeSchemaForCCA } from "../utils/schema";
|
|
@@ -340,7 +341,7 @@ export const streamGoogleGeminiCli: StreamFunction<"google-gemini-cli"> = (
|
|
|
340
341
|
headers: requestHeaders,
|
|
341
342
|
body: requestBodyJson,
|
|
342
343
|
signal: options?.signal,
|
|
343
|
-
maxAttempts: MAX_RETRIES + 1,
|
|
344
|
+
maxAttempts: resolveRetryBudget(options?.requestMaxRetries, MAX_RETRIES) + 1,
|
|
344
345
|
defaultDelayMs: attempt => BASE_DELAY_MS * 2 ** attempt,
|
|
345
346
|
maxDelayMs: options?.maxRetryDelayMs ?? RATE_LIMIT_BUDGET_MS,
|
|
346
347
|
fetch: options?.fetch,
|
|
@@ -509,7 +510,8 @@ export const streamGoogleGeminiCli: StreamFunction<"google-gemini-cli"> = (
|
|
|
509
510
|
let receivedContent = false;
|
|
510
511
|
let currentResponse = response;
|
|
511
512
|
|
|
512
|
-
|
|
513
|
+
const emptyStreamRetryBudget = resolveRetryBudget(options?.streamMaxRetries, MAX_EMPTY_STREAM_RETRIES);
|
|
514
|
+
for (let emptyAttempt = 0; emptyAttempt <= emptyStreamRetryBudget; emptyAttempt++) {
|
|
513
515
|
if (options?.signal?.aborted) {
|
|
514
516
|
throw new Error("Request was aborted");
|
|
515
517
|
}
|
|
@@ -549,7 +551,7 @@ export const streamGoogleGeminiCli: StreamFunction<"google-gemini-cli"> = (
|
|
|
549
551
|
break;
|
|
550
552
|
}
|
|
551
553
|
|
|
552
|
-
if (emptyAttempt <
|
|
554
|
+
if (emptyAttempt < emptyStreamRetryBudget) {
|
|
553
555
|
resetOutput();
|
|
554
556
|
}
|
|
555
557
|
}
|
package/src/providers/ollama.ts
CHANGED
|
@@ -18,6 +18,7 @@ import { normalizeSystemPrompts } from "../utils";
|
|
|
18
18
|
import { AssistantMessageEventStream } from "../utils/event-stream";
|
|
19
19
|
import { finalizeErrorMessage, type RawHttpRequestDump } from "../utils/http-inspector";
|
|
20
20
|
import { parseStreamingJson } from "../utils/json-parse";
|
|
21
|
+
import { resolveRetryBudget } from "../utils/retry-budget";
|
|
21
22
|
import { toolWireSchema } from "../utils/schema/wire";
|
|
22
23
|
import { transformMessages } from "./transform-messages";
|
|
23
24
|
|
|
@@ -402,6 +403,7 @@ export const streamOllama: StreamFunction<"ollama-chat"> = (
|
|
|
402
403
|
},
|
|
403
404
|
body: JSON.stringify(body),
|
|
404
405
|
signal: options.signal,
|
|
406
|
+
maxAttempts: resolveRetryBudget(options.requestMaxRetries, 4) + 1,
|
|
405
407
|
defaultDelayMs: OLLAMA_RETRY_DELAYS_MS,
|
|
406
408
|
fetch: options.fetch,
|
|
407
409
|
});
|
|
@@ -49,6 +49,7 @@ import { AssistantMessageEventStream } from "../utils/event-stream";
|
|
|
49
49
|
import { finalizeErrorMessage, type RawHttpRequestDump } from "../utils/http-inspector";
|
|
50
50
|
import { getOpenAIStreamIdleTimeoutMs, iterateWithIdleTimeout } from "../utils/idle-iterator";
|
|
51
51
|
import { parseStreamingJson } from "../utils/json-parse";
|
|
52
|
+
import { resolveRetryBudget } from "../utils/retry-budget";
|
|
52
53
|
import { adaptSchemaForStrict, NO_STRICT, sanitizeSchemaForOpenAIResponses, toolWireSchema } from "../utils/schema";
|
|
53
54
|
import { compactGrammarDefinition } from "./grammar";
|
|
54
55
|
import { CODEX_BASE_URL, getCodexAccountId, OPENAI_HEADER_VALUES, OPENAI_HEADERS } from "./openai-codex/constants";
|
|
@@ -227,7 +228,10 @@ function isCodexWebSocketEnvEnabled(): boolean {
|
|
|
227
228
|
return $flag("PI_CODEX_WEBSOCKET");
|
|
228
229
|
}
|
|
229
230
|
|
|
230
|
-
function getCodexWebSocketRetryBudget(): number {
|
|
231
|
+
function getCodexWebSocketRetryBudget(options?: Pick<OpenAICodexResponsesOptions, "streamMaxRetries">): number {
|
|
232
|
+
if (options?.streamMaxRetries !== undefined) {
|
|
233
|
+
return resolveRetryBudget(options.streamMaxRetries, CODEX_WEBSOCKET_RETRY_BUDGET);
|
|
234
|
+
}
|
|
231
235
|
return parseCodexNonNegativeInteger($env.PI_CODEX_WEBSOCKET_RETRY_BUDGET, CODEX_WEBSOCKET_RETRY_BUDGET);
|
|
232
236
|
}
|
|
233
237
|
|
|
@@ -641,7 +645,7 @@ async function openInitialCodexEventStream(
|
|
|
641
645
|
}> {
|
|
642
646
|
const { transformedBody, websocketState } = requestContext;
|
|
643
647
|
if (websocketState && shouldUseCodexWebSocket(model, websocketState, options?.preferWebsockets)) {
|
|
644
|
-
const websocketRetryBudget = getCodexWebSocketRetryBudget();
|
|
648
|
+
const websocketRetryBudget = getCodexWebSocketRetryBudget(options);
|
|
645
649
|
let websocketRetries = 0;
|
|
646
650
|
while (true) {
|
|
647
651
|
try {
|
|
@@ -707,7 +711,7 @@ async function openCodexWebSocketTransport(
|
|
|
707
711
|
sentModelsEtagHeader: websocketHeaders.has(X_MODELS_ETAG_HEADER),
|
|
708
712
|
requestType: websocketRequest.type,
|
|
709
713
|
retry,
|
|
710
|
-
retryBudget: getCodexWebSocketRetryBudget(),
|
|
714
|
+
retryBudget: getCodexWebSocketRetryBudget(options),
|
|
711
715
|
});
|
|
712
716
|
const eventStream = await openCodexWebSocketEventStream(
|
|
713
717
|
toWebSocketUrl(requestContext.url),
|
|
@@ -744,6 +748,7 @@ async function openCodexSseTransport(
|
|
|
744
748
|
requestSetup.requestSignal,
|
|
745
749
|
event => options?.onSseEvent?.(event, model),
|
|
746
750
|
options?.fetch,
|
|
751
|
+
options,
|
|
747
752
|
),
|
|
748
753
|
);
|
|
749
754
|
return { eventStream, requestBodyForState: structuredCloneJSON(body), transport: "sse" };
|
|
@@ -1348,7 +1353,7 @@ async function tryRecoverCodexPreviousResponseNotFound(
|
|
|
1348
1353
|
runtime.transport !== "websocket" ||
|
|
1349
1354
|
context.output.content.length > 0 ||
|
|
1350
1355
|
context.options?.signal?.aborted ||
|
|
1351
|
-
runtime.providerRetryAttempt >= CODEX_MAX_RETRIES
|
|
1356
|
+
runtime.providerRetryAttempt >= resolveRetryBudget(context.options?.streamMaxRetries, CODEX_MAX_RETRIES)
|
|
1352
1357
|
) {
|
|
1353
1358
|
return false;
|
|
1354
1359
|
}
|
|
@@ -1390,12 +1395,14 @@ async function tryReplayWebsocketFailureOverSse(
|
|
|
1390
1395
|
const replayingBufferedOutputOverSse = context.output.content.length > 0;
|
|
1391
1396
|
const isFatal = isCodexWebSocketFatalError(streamError);
|
|
1392
1397
|
const activateFallback =
|
|
1393
|
-
replayingBufferedOutputOverSse ||
|
|
1398
|
+
replayingBufferedOutputOverSse ||
|
|
1399
|
+
isFatal ||
|
|
1400
|
+
runtime.websocketStreamRetries >= getCodexWebSocketRetryBudget(context.options);
|
|
1394
1401
|
recordCodexWebSocketFailure(state, activateFallback);
|
|
1395
1402
|
logCodexDebug("codex websocket stream fallback", {
|
|
1396
1403
|
error: streamError.message,
|
|
1397
1404
|
retry: runtime.websocketStreamRetries,
|
|
1398
|
-
retryBudget: getCodexWebSocketRetryBudget(),
|
|
1405
|
+
retryBudget: getCodexWebSocketRetryBudget(context.options),
|
|
1399
1406
|
activated: activateFallback,
|
|
1400
1407
|
fatal: isFatal,
|
|
1401
1408
|
replayedBufferedOutput: replayingBufferedOutputOverSse,
|
|
@@ -1431,7 +1438,7 @@ async function tryRetryCodexProviderError(
|
|
|
1431
1438
|
if (
|
|
1432
1439
|
!isRetryableCodexProviderError(error) ||
|
|
1433
1440
|
context.output.content.length > 0 ||
|
|
1434
|
-
runtime.providerRetryAttempt >= CODEX_MAX_RETRIES ||
|
|
1441
|
+
runtime.providerRetryAttempt >= resolveRetryBudget(context.options?.streamMaxRetries, CODEX_MAX_RETRIES) ||
|
|
1435
1442
|
context.options?.signal?.aborted
|
|
1436
1443
|
) {
|
|
1437
1444
|
return false;
|
|
@@ -1447,7 +1454,7 @@ async function tryRetryCodexProviderError(
|
|
|
1447
1454
|
logCodexDebug("retrying codex provider stream error", {
|
|
1448
1455
|
error: error instanceof Error ? error.message : String(error),
|
|
1449
1456
|
retry: runtime.providerRetryAttempt,
|
|
1450
|
-
retryBudget: CODEX_MAX_RETRIES,
|
|
1457
|
+
retryBudget: resolveRetryBudget(context.options?.streamMaxRetries, CODEX_MAX_RETRIES),
|
|
1451
1458
|
transport: runtime.transport,
|
|
1452
1459
|
});
|
|
1453
1460
|
|
|
@@ -2187,6 +2194,7 @@ async function openCodexSseEventStream(
|
|
|
2187
2194
|
signal?: AbortSignal,
|
|
2188
2195
|
onSseEvent?: OpenAICodexResponsesOptions["onSseEvent"],
|
|
2189
2196
|
fetchOverride?: FetchImpl,
|
|
2197
|
+
options?: Pick<OpenAICodexResponsesOptions, "requestMaxRetries">,
|
|
2190
2198
|
): Promise<AsyncGenerator<Record<string, unknown>>> {
|
|
2191
2199
|
const headers = createCodexHeaders(requestHeaders, accountId, apiKey, sessionId, "sse", state);
|
|
2192
2200
|
logCodexDebug("codex request", {
|
|
@@ -2201,7 +2209,7 @@ async function openCodexSseEventStream(
|
|
|
2201
2209
|
headers,
|
|
2202
2210
|
body: JSON.stringify(body),
|
|
2203
2211
|
signal,
|
|
2204
|
-
maxAttempts: CODEX_MAX_RETRIES + 1,
|
|
2212
|
+
maxAttempts: resolveRetryBudget(options?.requestMaxRetries, CODEX_MAX_RETRIES) + 1,
|
|
2205
2213
|
defaultDelayMs: attempt => CODEX_RETRY_DELAY_MS * (attempt + 1),
|
|
2206
2214
|
maxDelayMs: CODEX_RATE_LIMIT_BUDGET_MS,
|
|
2207
2215
|
fetch: fetchOverride,
|
|
@@ -56,6 +56,7 @@ import { parseGitHubCopilotApiKey } from "../utils/oauth/github-copilot";
|
|
|
56
56
|
import { getKimiCommonHeaders } from "../utils/oauth/kimi";
|
|
57
57
|
import { notifyProviderResponse } from "../utils/provider-response";
|
|
58
58
|
import { callWithCopilotModelRetry } from "../utils/retry";
|
|
59
|
+
import { resolveRetryBudget } from "../utils/retry-budget";
|
|
59
60
|
import { adaptSchemaForStrict, NO_STRICT, toolWireSchema } from "../utils/schema";
|
|
60
61
|
import { wrapFetchForSseDebug } from "../utils/sse-debug";
|
|
61
62
|
import { type HealedToolCall, modelMayLeakKimiToolCalls, ToolCallHealer } from "../utils/tool-call-healing";
|
|
@@ -444,6 +445,7 @@ export const streamOpenAICompletions: StreamFunction<"openai-completions"> = (
|
|
|
444
445
|
options?.fetch,
|
|
445
446
|
options?.streamFirstEventTimeoutMs,
|
|
446
447
|
options?.authCredentialType,
|
|
448
|
+
options?.requestMaxRetries,
|
|
447
449
|
);
|
|
448
450
|
const premiumRequestsTotal = copilotPremiumRequests;
|
|
449
451
|
getCapturedErrorResponse = captureErrorResponse;
|
|
@@ -552,7 +554,7 @@ export const streamOpenAICompletions: StreamFunction<"openai-completions"> = (
|
|
|
552
554
|
eventStream: AssistantMessageEventStream,
|
|
553
555
|
text: string,
|
|
554
556
|
): void => {
|
|
555
|
-
if (
|
|
557
|
+
if (currentBlock?.type !== "text") {
|
|
556
558
|
finishCurrentBlock(currentBlock);
|
|
557
559
|
currentBlock = { type: "text", text: "" };
|
|
558
560
|
message.content.push(currentBlock);
|
|
@@ -573,8 +575,7 @@ export const streamOpenAICompletions: StreamFunction<"openai-completions"> = (
|
|
|
573
575
|
signature?: string,
|
|
574
576
|
): void => {
|
|
575
577
|
if (
|
|
576
|
-
|
|
577
|
-
currentBlock.type !== "thinking" ||
|
|
578
|
+
currentBlock?.type !== "thinking" ||
|
|
578
579
|
(signature !== undefined && currentBlock.thinkingSignature !== signature)
|
|
579
580
|
) {
|
|
580
581
|
finishCurrentBlock(currentBlock);
|
|
@@ -784,11 +785,7 @@ export const streamOpenAICompletions: StreamFunction<"openai-completions"> = (
|
|
|
784
785
|
|
|
785
786
|
if (choice?.delta?.tool_calls && choice.delta.tool_calls.length > 0) {
|
|
786
787
|
for (const toolCall of choice.delta.tool_calls) {
|
|
787
|
-
if (
|
|
788
|
-
!currentBlock ||
|
|
789
|
-
currentBlock.type !== "toolCall" ||
|
|
790
|
-
(toolCall.id && currentBlock.id !== toolCall.id)
|
|
791
|
-
) {
|
|
788
|
+
if (currentBlock?.type !== "toolCall" || (toolCall.id && currentBlock.id !== toolCall.id)) {
|
|
792
789
|
finishCurrentBlock(currentBlock);
|
|
793
790
|
currentBlock = {
|
|
794
791
|
type: "toolCall",
|
|
@@ -920,6 +917,7 @@ async function createClient(
|
|
|
920
917
|
fetchOverride?: FetchImpl,
|
|
921
918
|
streamFirstEventTimeoutOverride?: number,
|
|
922
919
|
authCredentialType?: OpenAICompletionsOptions["authCredentialType"],
|
|
920
|
+
requestMaxRetries?: number,
|
|
923
921
|
): Promise<{
|
|
924
922
|
client: OpenAI;
|
|
925
923
|
copilotPremiumRequests: number | undefined;
|
|
@@ -1051,7 +1049,7 @@ async function createClient(
|
|
|
1051
1049
|
apiKey,
|
|
1052
1050
|
baseURL: baseUrl,
|
|
1053
1051
|
dangerouslyAllowBrowser: true,
|
|
1054
|
-
maxRetries: 5,
|
|
1052
|
+
maxRetries: resolveRetryBudget(requestMaxRetries, 5),
|
|
1055
1053
|
defaultHeaders: headers,
|
|
1056
1054
|
defaultQuery: azureDefaultQuery,
|
|
1057
1055
|
fetch: debugFetch,
|
|
@@ -954,7 +954,7 @@ export function encodeStream(
|
|
|
954
954
|
break;
|
|
955
955
|
}
|
|
956
956
|
case "text_delta": {
|
|
957
|
-
if (
|
|
957
|
+
if (state.open?.kind !== "message") break;
|
|
958
958
|
const cur: OpenMessage = state.open;
|
|
959
959
|
cur.currentPartText += ev.delta;
|
|
960
960
|
emit("response.output_text.delta", {
|
|
@@ -970,7 +970,7 @@ export function encodeStream(
|
|
|
970
970
|
break;
|
|
971
971
|
}
|
|
972
972
|
case "text_end": {
|
|
973
|
-
if (
|
|
973
|
+
if (state.open?.kind !== "message") break;
|
|
974
974
|
const cur: OpenMessage = state.open;
|
|
975
975
|
const text = ev.content ?? cur.currentPartText;
|
|
976
976
|
emit("response.output_text.done", {
|
|
@@ -997,7 +997,7 @@ export function encodeStream(
|
|
|
997
997
|
break;
|
|
998
998
|
}
|
|
999
999
|
case "thinking_delta": {
|
|
1000
|
-
if (
|
|
1000
|
+
if (state.open?.kind !== "reasoning") break;
|
|
1001
1001
|
const cur: OpenReasoning = state.open;
|
|
1002
1002
|
cur.reasoningText += ev.delta;
|
|
1003
1003
|
emit("response.reasoning_summary_text.delta", {
|
|
@@ -1009,7 +1009,7 @@ export function encodeStream(
|
|
|
1009
1009
|
break;
|
|
1010
1010
|
}
|
|
1011
1011
|
case "thinking_end": {
|
|
1012
|
-
if (
|
|
1012
|
+
if (state.open?.kind !== "reasoning") break;
|
|
1013
1013
|
const cur: OpenReasoning = state.open;
|
|
1014
1014
|
const text = ev.content ?? cur.reasoningText;
|
|
1015
1015
|
cur.reasoningText = text;
|
|
@@ -1034,7 +1034,7 @@ export function encodeStream(
|
|
|
1034
1034
|
break;
|
|
1035
1035
|
}
|
|
1036
1036
|
case "toolcall_delta": {
|
|
1037
|
-
if (
|
|
1037
|
+
if (state.open?.kind !== "function_call") break;
|
|
1038
1038
|
const cur: OpenFunctionCall = state.open;
|
|
1039
1039
|
cur.argsText += ev.delta;
|
|
1040
1040
|
if (cur.customWireName) {
|
|
@@ -1053,7 +1053,7 @@ export function encodeStream(
|
|
|
1053
1053
|
break;
|
|
1054
1054
|
}
|
|
1055
1055
|
case "toolcall_end": {
|
|
1056
|
-
if (
|
|
1056
|
+
if (state.open?.kind !== "function_call") break;
|
|
1057
1057
|
const cur: OpenFunctionCall = state.open;
|
|
1058
1058
|
// Promote possibly-late info from the canonical ToolCall.
|
|
1059
1059
|
const tc = ev.toolCall;
|
|
@@ -42,6 +42,7 @@ import {
|
|
|
42
42
|
import { parseGitHubCopilotApiKey } from "../utils/oauth/github-copilot";
|
|
43
43
|
import { notifyProviderResponse } from "../utils/provider-response";
|
|
44
44
|
import { callWithCopilotModelRetry } from "../utils/retry";
|
|
45
|
+
import { resolveRetryBudget } from "../utils/retry-budget";
|
|
45
46
|
import { adaptSchemaForStrict, NO_STRICT, sanitizeSchemaForOpenAIResponses, toolWireSchema } from "../utils/schema";
|
|
46
47
|
import { wrapFetchForSseDebug } from "../utils/sse-debug";
|
|
47
48
|
import { mapToOpenAIResponsesToolChoice, type OpenAIResponsesToolChoice } from "../utils/tool-choice";
|
|
@@ -253,6 +254,7 @@ export const streamOpenAIResponses: StreamFunction<"openai-responses"> = (
|
|
|
253
254
|
options?.onSseEvent,
|
|
254
255
|
options?.fetch,
|
|
255
256
|
options?.authCredentialType,
|
|
257
|
+
options?.requestMaxRetries,
|
|
256
258
|
);
|
|
257
259
|
const premiumRequestsTotal = copilotPremiumRequests;
|
|
258
260
|
const providerSessionState = getOpenAIResponsesProviderSessionState(model, options?.providerSessionState);
|
|
@@ -354,6 +356,7 @@ function createClient(
|
|
|
354
356
|
onSseEvent?: OpenAIResponsesOptions["onSseEvent"],
|
|
355
357
|
fetchOverride?: FetchImpl,
|
|
356
358
|
authCredentialType?: OpenAIResponsesOptions["authCredentialType"],
|
|
359
|
+
requestMaxRetries?: number,
|
|
357
360
|
): {
|
|
358
361
|
client: OpenAI;
|
|
359
362
|
copilotPremiumRequests: number | undefined;
|
|
@@ -410,7 +413,7 @@ function createClient(
|
|
|
410
413
|
apiKey,
|
|
411
414
|
baseURL: baseUrl,
|
|
412
415
|
dangerouslyAllowBrowser: true,
|
|
413
|
-
maxRetries: 5,
|
|
416
|
+
maxRetries: resolveRetryBudget(requestMaxRetries, 5),
|
|
414
417
|
defaultHeaders: headers,
|
|
415
418
|
fetch: onSseEvent
|
|
416
419
|
? wrapFetchForSseDebug(transformedFetch, event => onSseEvent(event, model))
|
package/src/stream.ts
CHANGED
|
@@ -563,6 +563,8 @@ function mapOptionsForApi<TApi extends Api>(
|
|
|
563
563
|
headers: options?.headers,
|
|
564
564
|
initiatorOverride: options?.initiatorOverride,
|
|
565
565
|
maxRetryDelayMs: options?.maxRetryDelayMs,
|
|
566
|
+
requestMaxRetries: options?.requestMaxRetries,
|
|
567
|
+
streamMaxRetries: options?.streamMaxRetries,
|
|
566
568
|
metadata: options?.metadata,
|
|
567
569
|
sessionId: options?.sessionId,
|
|
568
570
|
providerSessionState: options?.providerSessionState,
|
package/src/types.ts
CHANGED
|
@@ -310,6 +310,16 @@ export interface StreamOptions {
|
|
|
310
310
|
* Default: 60000 (60 seconds). Set to 0 to disable the cap.
|
|
311
311
|
*/
|
|
312
312
|
maxRetryDelayMs?: number;
|
|
313
|
+
/**
|
|
314
|
+
* Maximum provider request retries for transports/SDKs that retry before a stream is established.
|
|
315
|
+
* Counts retries only, not the initial attempt. Providers keep their built-in default when unset.
|
|
316
|
+
*/
|
|
317
|
+
requestMaxRetries?: number;
|
|
318
|
+
/**
|
|
319
|
+
* Maximum provider stream replay retries after a replay-safe transient stream failure.
|
|
320
|
+
* Counts retries only, not the initial stream attempt. Providers keep their built-in default when unset.
|
|
321
|
+
*/
|
|
322
|
+
streamMaxRetries?: number;
|
|
313
323
|
/**
|
|
314
324
|
* Optional metadata to include in API requests.
|
|
315
325
|
* Providers extract the fields they understand and ignore the rest.
|
|
@@ -23,6 +23,8 @@ export interface OAuthCallbackFlowOptions {
|
|
|
23
23
|
preferredPort: number;
|
|
24
24
|
callbackPath?: string;
|
|
25
25
|
callbackHostname?: string;
|
|
26
|
+
/** Local listener hostname; defaults to callbackHostname when omitted. */
|
|
27
|
+
callbackBindHostname?: string;
|
|
26
28
|
/** Exact redirect URI advertised to the provider; disables port fallback. */
|
|
27
29
|
redirectUri?: string;
|
|
28
30
|
}
|
|
@@ -35,6 +37,7 @@ export abstract class OAuthCallbackFlow {
|
|
|
35
37
|
preferredPort: number;
|
|
36
38
|
callbackPath: string;
|
|
37
39
|
callbackHostname: string;
|
|
40
|
+
callbackBindHostname: string;
|
|
38
41
|
redirectUri?: string;
|
|
39
42
|
#callbackResolve?: (result: CallbackResult) => void;
|
|
40
43
|
#callbackReject?: (error: string) => void;
|
|
@@ -49,12 +52,14 @@ export abstract class OAuthCallbackFlow {
|
|
|
49
52
|
this.preferredPort = preferredPortOrOptions;
|
|
50
53
|
this.callbackPath = callbackPath;
|
|
51
54
|
this.callbackHostname = DEFAULT_HOSTNAME;
|
|
55
|
+
this.callbackBindHostname = DEFAULT_HOSTNAME;
|
|
52
56
|
return;
|
|
53
57
|
}
|
|
54
58
|
|
|
55
59
|
this.preferredPort = preferredPortOrOptions.preferredPort;
|
|
56
60
|
this.callbackPath = preferredPortOrOptions.callbackPath ?? CALLBACK_PATH;
|
|
57
61
|
this.callbackHostname = preferredPortOrOptions.callbackHostname ?? DEFAULT_HOSTNAME;
|
|
62
|
+
this.callbackBindHostname = preferredPortOrOptions.callbackBindHostname ?? this.callbackHostname;
|
|
58
63
|
this.redirectUri = preferredPortOrOptions.redirectUri;
|
|
59
64
|
}
|
|
60
65
|
|
|
@@ -144,7 +149,7 @@ export abstract class OAuthCallbackFlow {
|
|
|
144
149
|
*/
|
|
145
150
|
#createServer(port: number, expectedState: string): Bun.Server<unknown> {
|
|
146
151
|
return Bun.serve({
|
|
147
|
-
hostname: this.
|
|
152
|
+
hostname: this.callbackBindHostname,
|
|
148
153
|
port,
|
|
149
154
|
reusePort: false,
|
|
150
155
|
fetch: req => this.#handleCallback(req, expectedState),
|