@gajae-code/ai 0.18.1 → 0.18.6

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 (38) hide show
  1. package/CHANGELOG.md +74 -0
  2. package/dist/types/auth-storage.d.ts +8 -1
  3. package/dist/types/index.d.ts +0 -1
  4. package/dist/types/providers/cursor.d.ts +6 -3
  5. package/dist/types/providers/kiro-api-key.d.ts +1 -0
  6. package/dist/types/providers/openai-codex-responses.d.ts +2 -0
  7. package/dist/types/types.d.ts +1 -1
  8. package/dist/types/utils/fallback-transport.d.ts +6 -0
  9. package/dist/types/utils/oauth/glm-zcode.d.ts +5 -0
  10. package/dist/types/utils/oauth/index.d.ts +4 -0
  11. package/package.json +3 -3
  12. package/src/auth-storage.ts +115 -39
  13. package/src/index.ts +0 -1
  14. package/src/model-pricing.ts +10 -0
  15. package/src/model-thinking.ts +8 -0
  16. package/src/models.json +210 -0
  17. package/src/providers/anthropic.ts +25 -1
  18. package/src/providers/aws-eventstream.ts +2 -1
  19. package/src/providers/cursor.d.ts +6 -3
  20. package/src/providers/cursor.ts +73 -64
  21. package/src/providers/kiro-api-key.d.ts +1 -0
  22. package/src/providers/kiro-api-key.ts +1 -1
  23. package/src/providers/kiro-codewhisperer.ts +142 -34
  24. package/src/providers/openai-codex-responses.d.ts +2 -0
  25. package/src/providers/openai-codex-responses.ts +438 -40
  26. package/src/providers/openai-completions.ts +19 -1
  27. package/src/stream.ts +20 -0
  28. package/src/types.d.ts +1 -1
  29. package/src/types.ts +3 -0
  30. package/src/utils/fallback-transport.d.ts +6 -0
  31. package/src/utils/fallback-transport.ts +6 -0
  32. package/src/utils/idle-iterator.ts +10 -3
  33. package/src/utils/oauth/glm-zcode.d.ts +5 -0
  34. package/src/utils/oauth/glm-zcode.ts +8 -1
  35. package/src/utils/oauth/index.ts +11 -1
  36. package/src/utils/validation.ts +5 -0
  37. package/dist/types/utils/h2-fetch.d.ts +0 -35
  38. package/src/utils/h2-fetch.ts +0 -133
@@ -1,6 +1,6 @@
1
1
  import { scheduler } from "node:timers/promises";
2
2
  import { $credentialEnv, $env, extractHttpStatusFromError, logger } from "@gajae-code/utils";
3
- import OpenAI, { APIConnectionTimeoutError } from "openai";
3
+ import OpenAI, { APIConnectionTimeoutError, type ClientOptions as OpenAIClientOptions } from "openai";
4
4
  import type {
5
5
  ChatCompletionAssistantMessageParam,
6
6
  ChatCompletionChunk,
@@ -1509,6 +1509,22 @@ export const streamOpenAICompletions: StreamFunction<"openai-completions"> = (
1509
1509
  return stream;
1510
1510
  };
1511
1511
 
1512
+ /**
1513
+ * The OpenAI SDK retries pre-header timeouts and connection failures silently
1514
+ * (its retry lines log at `info`, below the default `warn`, and go to console,
1515
+ * which corrupts the TUI). Route them into the gjc log so a request stuck
1516
+ * before response headers — where our stream watchdogs are not armed yet —
1517
+ * leaves a trace instead of looking like a hung turn.
1518
+ */
1519
+ const OPENAI_SDK_LOGGER: NonNullable<OpenAIClientOptions["logger"]> = {
1520
+ error: message => logger.warn(`openai-sdk: ${message}`),
1521
+ warn: message => logger.warn(`openai-sdk: ${message}`),
1522
+ info: message => {
1523
+ if (/retrying|timed out|connection failed/.test(message)) logger.warn(`openai-sdk: ${message}`);
1524
+ },
1525
+ debug: () => {},
1526
+ };
1527
+
1512
1528
  async function createClient(
1513
1529
  model: Model<"openai-completions">,
1514
1530
  context: Context,
@@ -1680,6 +1696,8 @@ async function createClient(
1680
1696
  defaultHeaders: headers,
1681
1697
  fetch: debugFetch,
1682
1698
  ...(sdkTimeoutMs !== undefined ? { timeout: sdkTimeoutMs } : {}),
1699
+ logLevel: "info",
1700
+ logger: OPENAI_SDK_LOGGER,
1683
1701
  }),
1684
1702
  copilotPremiumRequests,
1685
1703
  baseUrl,
package/src/stream.ts CHANGED
@@ -1022,8 +1022,28 @@ function mapOptionsForApi<TApi extends Api>(
1022
1022
  // For older models: use budget-based thinking
1023
1023
  if (model.thinking?.mode === "anthropic-adaptive") {
1024
1024
  const effort = mapEffortToAnthropicAdaptiveEffort(model, reasoning);
1025
+
1026
+ // Adaptive mode sends effort directly (no explicit budget_tokens).
1027
+ // However, the thinking consumes tokens from the overall max_tokens budget.
1028
+ // If max_tokens is capped at DEFAULT_REQUEST_MAX_TOKENS (32k),
1029
+ // high/xhigh/max reasoning consumes all 32k, leaving no output tokens.
1030
+ // Solution: when caller did not explicitly set maxTokens,
1031
+ // increase the cap to model.maxTokens to allow room for output.
1032
+ let adaptiveMaxTokens = base.maxTokens ?? model.maxTokens;
1033
+ const hasExplicitMaxTokens = Number.isSafeInteger(options?.maxTokens) && (options?.maxTokens as number) > 0;
1034
+ if (
1035
+ !hasExplicitMaxTokens &&
1036
+ adaptiveMaxTokens === DEFAULT_REQUEST_MAX_TOKENS &&
1037
+ Number.isSafeInteger(model.maxTokens) &&
1038
+ model.maxTokens > DEFAULT_REQUEST_MAX_TOKENS
1039
+ ) {
1040
+ // Increase to model.maxTokens to leave room for both thinking and output
1041
+ adaptiveMaxTokens = model.maxTokens;
1042
+ }
1043
+
1025
1044
  return castApi<"anthropic-messages">({
1026
1045
  ...base,
1046
+ maxTokens: adaptiveMaxTokens,
1027
1047
  thinkingEnabled: true,
1028
1048
  effort,
1029
1049
  toolChoice: mapAnthropicToolChoice(options?.toolChoice),
package/src/types.d.ts CHANGED
@@ -746,7 +746,7 @@ export type TSchema = ZodType | TJsonSchema;
746
746
  export type Static<S> = S extends ZodType ? z.infer<S> : S extends {
747
747
  static: infer T;
748
748
  } ? T : unknown;
749
- export type RawArgumentRejectionCode = "ask-deep-interview-question-body-required" | "ask-intent-review-requires-positive-round" | "ask-intent-contract-requires-non-empty-authority" | "ask-deep-interview-metadata-requires-deep-interview-gate" | "ask-round-zero-metadata-requires-full-topology-fields" | "todo-write-unknown-root-key" | "todo-write-unknown-op-entry-key" | "todo-write-unknown-op-value" | "todo-write-done-drop-requires-target" | "todo-write-unknown-init-entry-key";
749
+ export type RawArgumentRejectionCode = "ask-question-body-required" | "ask-deep-interview-metadata-required" | "ask-deep-interview-single-question-required" | "ask-deep-interview-question-body-required" | "ask-intent-review-requires-positive-round" | "ask-intent-contract-requires-non-empty-authority" | "ask-deep-interview-metadata-requires-deep-interview-gate" | "ask-round-zero-metadata-requires-full-topology-fields" | "todo-write-unknown-root-key" | "todo-write-unknown-op-entry-key" | "todo-write-unknown-op-value" | "todo-write-done-drop-requires-target" | "todo-write-unknown-init-entry-key";
750
750
  /**
751
751
  * Optional structured detail attached to a raw-argument rejection. The fixed
752
752
  * per-code guidance in `RAW_ARGUMENT_REJECTION_MESSAGES` explains the shape;
package/src/types.ts CHANGED
@@ -1019,6 +1019,9 @@ export type TSchema = ZodType | TJsonSchema;
1019
1019
  export type Static<S> = S extends ZodType ? z.infer<S> : S extends { static: infer T } ? T : unknown;
1020
1020
 
1021
1021
  export type RawArgumentRejectionCode =
1022
+ | "ask-question-body-required"
1023
+ | "ask-deep-interview-metadata-required"
1024
+ | "ask-deep-interview-single-question-required"
1022
1025
  | "ask-deep-interview-question-body-required"
1023
1026
  | "ask-intent-review-requires-positive-round"
1024
1027
  | "ask-intent-contract-requires-non-empty-authority"
@@ -23,6 +23,12 @@ export interface FallbackTrigger {
23
23
  export declare const STREAM_FIRST_EVENT_TIMEOUT_PROVIDER_CODE = "stream_first_event_timeout";
24
24
  /** Stable code for a nominally successful response with no content or token usage. */
25
25
  export declare const EMPTY_RESPONSE_PROVIDER_CODE = "empty_response";
26
+ /**
27
+ * Terminal error code for a successful HTTP response whose body is not the protocol the
28
+ * provider speaks (for example a JSON or HTML error returned with 200). Deterministic for
29
+ * the request, so session retry and managed fallback must surface it instead of replaying.
30
+ */
31
+ export declare const PROVIDER_PROTOCOL_MISMATCH_ERROR_CODE = "provider_protocol_mismatch";
26
32
  /**
27
33
  * OpenAI's typed capacity-overload code. It arrives without an HTTP status —
28
34
  * inside an HTTP 200 terminal Responses envelope or a Codex error event — so the
@@ -26,6 +26,12 @@ export interface FallbackTrigger {
26
26
  export const STREAM_FIRST_EVENT_TIMEOUT_PROVIDER_CODE = "stream_first_event_timeout";
27
27
  /** Stable code for a nominally successful response with no content or token usage. */
28
28
  export const EMPTY_RESPONSE_PROVIDER_CODE = "empty_response";
29
+ /**
30
+ * Terminal error code for a successful HTTP response whose body is not the protocol the
31
+ * provider speaks (for example a JSON or HTML error returned with 200). Deterministic for
32
+ * the request, so session retry and managed fallback must surface it instead of replaying.
33
+ */
34
+ export const PROVIDER_PROTOCOL_MISMATCH_ERROR_CODE = "provider_protocol_mismatch";
29
35
  /**
30
36
  * OpenAI's typed capacity-overload code. It arrives without an HTTP status —
31
37
  * inside an HTTP 200 terminal Responses envelope or a Codex error event — so the
@@ -202,6 +202,13 @@ export class FirstEventTimeoutError extends Error {
202
202
  }
203
203
  }
204
204
 
205
+ /**
206
+ * Longest delay setTimeout honors. Bun, like Node, fires any larger delay after 1 ms,
207
+ * so a very long timeout (for example an env value meant as "effectively never")
208
+ * would otherwise abort the stream immediately.
209
+ */
210
+ const MAX_TIMER_DELAY_MS = 2 ** 31 - 1;
211
+
205
212
  const dummyWatchdog = setTimeout(() => {}, 1);
206
213
  clearTimeout(dummyWatchdog);
207
214
 
@@ -211,7 +218,7 @@ clearTimeout(dummyWatchdog);
211
218
  */
212
219
  export function createWatchdog(timeoutMs: number | undefined, onTimeout: () => void): Watchdog {
213
220
  if (timeoutMs !== undefined && timeoutMs > 0) {
214
- return setTimeout(onTimeout, timeoutMs);
221
+ return setTimeout(onTimeout, Math.min(timeoutMs, MAX_TIMER_DELAY_MS));
215
222
  }
216
223
  return undefined;
217
224
  }
@@ -335,10 +342,10 @@ export async function* iterateWithIdleTimeout<T>(
335
342
  let timer: NodeJS.Timeout | undefined;
336
343
  let resolveTimeout: ((value: { kind: "timeout" }) => void) | undefined;
337
344
  const enforceTimeout = !noTimeoutEnforced && activeTimeoutMs !== undefined && activeTimeoutMs >= 0;
338
- if (enforceTimeout) {
345
+ if (enforceTimeout && activeTimeoutMs !== undefined) {
339
346
  const { promise, resolve } = Promise.withResolvers<{ kind: "timeout" }>();
340
347
  resolveTimeout = resolve;
341
- timer = setTimeout(() => resolve({ kind: "timeout" }), activeTimeoutMs);
348
+ timer = setTimeout(() => resolve({ kind: "timeout" }), Math.min(activeTimeoutMs, MAX_TIMER_DELAY_MS));
342
349
  racers.push(promise);
343
350
  }
344
351
 
@@ -57,6 +57,11 @@ export declare class GlmZcodeOAuthFlow extends OAuthCallbackFlow {
57
57
  }>;
58
58
  exchangeToken(code: string, state: string, redirectUri: string): Promise<OAuthCredentials>;
59
59
  }
60
+ /**
61
+ * Manual-input prompt for the paste fallback. Repeats the redirect-URL shape and
62
+ * the DevTools pointer so a re-prompt after a rejected paste stays self-contained.
63
+ */
64
+ export declare const GLM_ZCODE_MANUAL_INPUT_PROMPT = "Paste the full zcode://oauth/callback?code=\u2026&state=\u2026 URL (copy it from browser DevTools \u2192 Network tab; the address bar never shows it) or the authorization code:";
60
65
  export declare function loginGlmZcode(ctrl: OAuthController, options?: GlmZcodeOAuthFlowOptions): Promise<OAuthCredentials>;
61
66
  export interface GlmZcodeRefreshOptions {
62
67
  signal?: AbortSignal;
@@ -387,7 +387,7 @@ export class GlmZcodeOAuthFlow extends OAuthCallbackFlow {
387
387
  return {
388
388
  url: `${authorizeUrl}?${params.toString()}`,
389
389
  instructions:
390
- "Complete Z.AI login in your browser. This is an UNOFFICIAL ZCode-based login — use at your own risk; it may stop working or violate ZCode/Z.AI Terms of Service. Because this CLI cannot receive the zcode:// redirect, paste the final redirect URL or authorization code when prompted. If the ZCode desktop app is installed, cancel the browser's prompt to open it: the app exchanges the single-use code itself and the pasted code is then rejected (broker error 2007).",
390
+ "Complete Z.AI login in your browser. This is an UNOFFICIAL ZCode-based login — use at your own risk; it may stop working or violate ZCode/Z.AI Terms of Service. If the ZCode desktop app is installed, cancel the browser's prompt to open it: the app exchanges the single-use code itself and the pasted code is then rejected (broker error 2007). After login the browser redirects to a zcode:// address, which never appears in the address bar — copy it from DevTools instead: open the browser DevTools Network tab (filter: zcode), select the zcode://oauth/callback?code=…&state=… request, and paste that full URL (or just the code) at the prompt.",
391
391
  };
392
392
  }
393
393
 
@@ -396,6 +396,13 @@ export class GlmZcodeOAuthFlow extends OAuthCallbackFlow {
396
396
  }
397
397
  }
398
398
 
399
+ /**
400
+ * Manual-input prompt for the paste fallback. Repeats the redirect-URL shape and
401
+ * the DevTools pointer so a re-prompt after a rejected paste stays self-contained.
402
+ */
403
+ export const GLM_ZCODE_MANUAL_INPUT_PROMPT =
404
+ "Paste the full zcode://oauth/callback?code=…&state=… URL (copy it from browser DevTools → Network tab; the address bar never shows it) or the authorization code:";
405
+
399
406
  export async function loginGlmZcode(
400
407
  ctrl: OAuthController,
401
408
  options?: GlmZcodeOAuthFlowOptions,
@@ -9,6 +9,16 @@ import type {
9
9
  OAuthProviderInterface,
10
10
  } from "./types";
11
11
 
12
+ export class UnknownOAuthProviderError extends Error {
13
+ readonly provider: string;
14
+
15
+ constructor(provider: string) {
16
+ super(`Unknown OAuth provider: ${provider}`);
17
+ this.name = "UnknownOAuthProviderError";
18
+ this.provider = provider;
19
+ }
20
+ }
21
+
12
22
  const builtInOAuthProviders: OAuthProviderInfo[] = [
13
23
  {
14
24
  id: "anthropic",
@@ -442,7 +452,7 @@ export async function refreshOAuthToken(
442
452
  newCredentials = credentials;
443
453
  break;
444
454
  default:
445
- throw new Error(`Unknown OAuth provider: ${provider}`);
455
+ throw new UnknownOAuthProviderError(provider);
446
456
  }
447
457
  return newCredentials;
448
458
  }
@@ -960,6 +960,11 @@ export function validateToolCall(tools: Tool[], toolCall: ToolCall): ToolCall["a
960
960
  }
961
961
 
962
962
  const RAW_ARGUMENT_REJECTION_MESSAGES: Record<RawArgumentRejectionCode, string> = {
963
+ "ask-question-body-required": "ask question bodies must contain non-whitespace text; remove empty extra questions",
964
+ "ask-deep-interview-metadata-required":
965
+ "active deep-interview questions require structured deepInterview metadata; question prose and workflowGate do not record an answer",
966
+ "ask-deep-interview-single-question-required":
967
+ "active deep-interview asks require exactly one question; include metadata on that question, never as an extra question",
963
968
  "ask-deep-interview-question-body-required":
964
969
  "deep-interview question bodies must contain a specific question, not a placeholder",
965
970
  "ask-intent-review-requires-positive-round":
@@ -1,35 +0,0 @@
1
- /**
2
- * Patch `globalThis.fetch` to advertise HTTP/2 in TLS ALPN, with transparent
3
- * HTTP/1.1 fallback when the server doesn't negotiate `h2`.
4
- *
5
- * Bun's HTTP/2 client is gated on `BUN_FEATURE_FLAG_EXPERIMENTAL_HTTP2_CLIENT`,
6
- * read by the native runtime before any JS executes; assigning to
7
- * `process.env` from inside JS is a no-op. Per-request `protocol: "http2"`
8
- * activates h2 over TLS ALPN and rejects with `error.code === "HTTP2Unsupported"`
9
- * if the server picks anything else, so we catch and retry without the hint.
10
- *
11
- * Some HTTPS endpoints (e.g. corporate API gateways behind reverse proxies)
12
- * advertise h2 via ALPN but then refuse or reset the connection at the HTTP/2
13
- * framing layer. Bun surfaces these as `ConnectionRefused`, `ConnectionReset`,
14
- * `ConnectionClosed`, or `HTTP2StreamReset` rather than `HTTP2Unsupported`.
15
- * `ConnectionRefused` is raised before the request is written, while
16
- * `HTTP2RefusedStream` is the explicit HTTP/2 promise that a stream was never
17
- * processed (including a stream above a graceful GOAWAY last-stream-id).
18
- * `ConnectionReset`, `ConnectionClosed`, and a generic `HTTP2StreamReset` do
19
- * not carry that promise: the peer may have consumed the body before the
20
- * connection failed. Retrying those codes would duplicate non-idempotent side
21
- * effects, so they preserve the original error instead of falling back.
22
- *
23
- * ALPN-refusing hosts (notably zcode.z.ai, the GLM ZCode OAuth broker) abort
24
- * the TLS handshake entirely when the client offers ALPN h2. Bun reports that
25
- * abort as `UNKNOWN_CERTIFICATE_VERIFICATION_ERROR` even though the host's
26
- * certificate chain verifies fine over h1 (issue #5178), so that code is a
27
- * fallback trigger too — never a reason to accept a bad certificate: the h1
28
- * attempt below performs full verification on its own.
29
- *
30
- * Bun negotiates h2 via ALPN over TLS only (no h2c), so plain `http://` URLs
31
- * skip the attempt entirely — avoids the throw/retry round-trip for localhost.
32
- *
33
- * Idempotent.
34
- */
35
- export declare function installH2Fetch(): void;
@@ -1,133 +0,0 @@
1
- /**
2
- * Patch `globalThis.fetch` to advertise HTTP/2 in TLS ALPN, with transparent
3
- * HTTP/1.1 fallback when the server doesn't negotiate `h2`.
4
- *
5
- * Bun's HTTP/2 client is gated on `BUN_FEATURE_FLAG_EXPERIMENTAL_HTTP2_CLIENT`,
6
- * read by the native runtime before any JS executes; assigning to
7
- * `process.env` from inside JS is a no-op. Per-request `protocol: "http2"`
8
- * activates h2 over TLS ALPN and rejects with `error.code === "HTTP2Unsupported"`
9
- * if the server picks anything else, so we catch and retry without the hint.
10
- *
11
- * Some HTTPS endpoints (e.g. corporate API gateways behind reverse proxies)
12
- * advertise h2 via ALPN but then refuse or reset the connection at the HTTP/2
13
- * framing layer. Bun surfaces these as `ConnectionRefused`, `ConnectionReset`,
14
- * `ConnectionClosed`, or `HTTP2StreamReset` rather than `HTTP2Unsupported`.
15
- * `ConnectionRefused` is raised before the request is written, while
16
- * `HTTP2RefusedStream` is the explicit HTTP/2 promise that a stream was never
17
- * processed (including a stream above a graceful GOAWAY last-stream-id).
18
- * `ConnectionReset`, `ConnectionClosed`, and a generic `HTTP2StreamReset` do
19
- * not carry that promise: the peer may have consumed the body before the
20
- * connection failed. Retrying those codes would duplicate non-idempotent side
21
- * effects, so they preserve the original error instead of falling back.
22
- *
23
- * ALPN-refusing hosts (notably zcode.z.ai, the GLM ZCode OAuth broker) abort
24
- * the TLS handshake entirely when the client offers ALPN h2. Bun reports that
25
- * abort as `UNKNOWN_CERTIFICATE_VERIFICATION_ERROR` even though the host's
26
- * certificate chain verifies fine over h1 (issue #5178), so that code is a
27
- * fallback trigger too — never a reason to accept a bad certificate: the h1
28
- * attempt below performs full verification on its own.
29
- *
30
- * Bun negotiates h2 via ALPN over TLS only (no h2c), so plain `http://` URLs
31
- * skip the attempt entirely — avoids the throw/retry round-trip for localhost.
32
- *
33
- * Idempotent.
34
- */
35
-
36
- const installed: unique symbol = Symbol.for("gajae-code.h2fetch.installed");
37
-
38
- interface PatchedFetch {
39
- [installed]?: true;
40
- }
41
-
42
- export function installH2Fetch(): void {
43
- const original = globalThis.fetch as typeof fetch & PatchedFetch;
44
- if (original[installed]) return;
45
-
46
- /** Error codes that indicate h2 negotiation/transport failure (not an application error). */
47
- const h2FallbackCodes: ReadonlySet<string> = new Set([
48
- "HTTP2Unsupported", // Server selected h1 in ALPN
49
- "ConnectionRefused", // Server refused the h2 connection
50
- "HTTP2RefusedStream", // REFUSED_STREAM / never-processed h2 stream
51
- // Bun's h2 client reports an ALPN-refusing host's TLS abort with this
52
- // code; the h1 fallback below re-verifies the certificate itself.
53
- "UNKNOWN_CERTIFICATE_VERIFICATION_ERROR",
54
- ]);
55
- const wrapper = async function h2fetch(input: string | URL | Request, init?: RequestInit): Promise<Response> {
56
- if (!isHttps(input)) return original(input, init);
57
- const snapshot = snapshotRequest(input, init);
58
- try {
59
- return await original(snapshot.input, { ...snapshot.init, protocol: "http2" });
60
- } catch (err) {
61
- const code = (err as { code?: string }).code ?? "";
62
- if (!h2FallbackCodes.has(code)) throw err;
63
- if (code === "HTTP2RefusedStream" && !isReplayableRequest(snapshot.input, snapshot.init)) throw err;
64
- return original(snapshot.input, snapshot.init);
65
- }
66
- } as typeof fetch & PatchedFetch;
67
-
68
- // Preserve `fetch.preconnect` and any other statics SDK code might poke at.
69
- Object.assign(wrapper, original);
70
- wrapper[installed] = true;
71
- globalThis.fetch = wrapper;
72
- }
73
-
74
- function snapshotRequest(
75
- input: string | URL | Request,
76
- init?: RequestInit,
77
- ): { input: string | URL | Request; init?: RequestInit } {
78
- const snapshotInput = input instanceof URL ? new URL(input.href) : input;
79
- const requestLike = typeof input !== "string" && !(input instanceof URL);
80
- if (init === undefined && !requestLike) {
81
- return { input: snapshotInput };
82
- }
83
-
84
- const snapshotInit: RequestInit = { ...(init ?? {}) };
85
- if (init?.headers !== undefined) {
86
- snapshotInit.headers = snapshotHeaders(init.headers);
87
- } else if (requestLike) {
88
- snapshotInit.headers = new Headers(input.headers);
89
- }
90
- return { input: snapshotInput, init: snapshotInit };
91
- }
92
-
93
- function snapshotHeaders(headers: NonNullable<RequestInit["headers"]>): NonNullable<RequestInit["headers"]> {
94
- if (headers instanceof Headers) return new Headers(headers);
95
- if (Array.isArray(headers)) {
96
- return headers.map(([name, value]) => [name, value] as [string, string]);
97
- }
98
- return { ...headers };
99
- }
100
-
101
- function isHttps(input: string | URL | Request): boolean {
102
- if (typeof input === "string") return input.startsWith("https:");
103
- if (input instanceof URL) return input.protocol === "https:";
104
- return input.url.startsWith("https:");
105
- }
106
-
107
- /**
108
- * Whether the request body can be handed to fetch a second time. This is only
109
- * used after `HTTP2RefusedStream`, which proves that the peer did not consume
110
- * request bytes; a one-shot ReadableStream is still not reusable locally.
111
- */
112
- function isReplayableRequest(input: string | URL | Request, init?: RequestInit): boolean {
113
- try {
114
- if (init?.body !== undefined) return isReplayableBody(init.body);
115
- if (typeof input === "string" || input instanceof URL) return true;
116
- // Request bodies are exposed as ReadableStreams. The same Request object
117
- // cannot be used for the second fetch once the first attempt touched it.
118
- return input.body === null;
119
- } catch {
120
- // Cross-realm or proxy Request objects may throw while exposing their
121
- // method/body. Do not retry when replayability cannot be established.
122
- return false;
123
- }
124
- }
125
-
126
- function isReplayableBody(body: RequestInit["body"]): boolean {
127
- if (body === null || typeof body === "string") return true;
128
- if (typeof Blob !== "undefined" && body instanceof Blob) return true;
129
- // ArrayBuffer/views, FormData, and URLSearchParams are mutable caller-owned
130
- // values. Fail closed instead of replaying a potentially changed payload if
131
- // the asynchronous refusal arrives after the caller mutates one.
132
- return false;
133
- }