@earendil-works/pi-ai 0.84.4 → 0.85.1

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 (94) hide show
  1. package/README.md +36 -3
  2. package/dist/api/anthropic-messages.d.ts.map +1 -1
  3. package/dist/api/anthropic-messages.js +123 -37
  4. package/dist/api/anthropic-messages.js.map +1 -1
  5. package/dist/api/cloudflare-ai-binding.d.ts +77 -0
  6. package/dist/api/cloudflare-ai-binding.d.ts.map +1 -0
  7. package/dist/api/cloudflare-ai-binding.js +72 -0
  8. package/dist/api/cloudflare-ai-binding.js.map +1 -0
  9. package/dist/api/openai-codex-responses.d.ts.map +1 -1
  10. package/dist/api/openai-codex-responses.js +6 -3
  11. package/dist/api/openai-codex-responses.js.map +1 -1
  12. package/dist/api/openai-completions.d.ts +2 -1
  13. package/dist/api/openai-completions.d.ts.map +1 -1
  14. package/dist/api/openai-completions.js +4 -0
  15. package/dist/api/openai-completions.js.map +1 -1
  16. package/dist/api/openai-responses-shared.d.ts.map +1 -1
  17. package/dist/api/openai-responses-shared.js +4 -1
  18. package/dist/api/openai-responses-shared.js.map +1 -1
  19. package/dist/api/openai-responses.d.ts.map +1 -1
  20. package/dist/api/openai-responses.js +15 -4
  21. package/dist/api/openai-responses.js.map +1 -1
  22. package/dist/api/pi-messages.d.ts +2 -0
  23. package/dist/api/pi-messages.d.ts.map +1 -1
  24. package/dist/api/pi-messages.js +6 -0
  25. package/dist/api/pi-messages.js.map +1 -1
  26. package/dist/image-models.generated.d.ts +30 -0
  27. package/dist/image-models.generated.d.ts.map +1 -1
  28. package/dist/image-models.generated.js +30 -0
  29. package/dist/image-models.generated.js.map +1 -1
  30. package/dist/index.d.ts +1 -0
  31. package/dist/index.d.ts.map +1 -1
  32. package/dist/index.js +1 -0
  33. package/dist/index.js.map +1 -1
  34. package/dist/models.d.ts +1 -0
  35. package/dist/models.d.ts.map +1 -1
  36. package/dist/models.js +5 -2
  37. package/dist/models.js.map +1 -1
  38. package/dist/providers/cloudflare-ai-gateway.d.ts.map +1 -1
  39. package/dist/providers/cloudflare-ai-gateway.js +4 -0
  40. package/dist/providers/cloudflare-ai-gateway.js.map +1 -1
  41. package/dist/providers/data/.manifest.json +1 -1
  42. package/dist/providers/data/amazon-bedrock.json +1 -1
  43. package/dist/providers/data/anthropic.json +1 -1
  44. package/dist/providers/data/azure-openai-responses.json +1 -1
  45. package/dist/providers/data/baseten.json +1 -1
  46. package/dist/providers/data/cloudflare-ai-gateway.json +1 -1
  47. package/dist/providers/data/cloudflare-workers-ai.json +1 -1
  48. package/dist/providers/data/fireworks.json +1 -1
  49. package/dist/providers/data/github-copilot.json +1 -1
  50. package/dist/providers/data/google-vertex.json +1 -1
  51. package/dist/providers/data/google.json +1 -1
  52. package/dist/providers/data/groq.json +1 -1
  53. package/dist/providers/data/huggingface.json +1 -1
  54. package/dist/providers/data/nvidia.json +1 -1
  55. package/dist/providers/data/openai-codex.json +1 -1
  56. package/dist/providers/data/openai.json +1 -1
  57. package/dist/providers/data/opencode-go.json +1 -1
  58. package/dist/providers/data/opencode.json +1 -1
  59. package/dist/providers/data/openrouter.json +1 -1
  60. package/dist/providers/data/qwen-token-plan-cn.json +1 -1
  61. package/dist/providers/data/qwen-token-plan-individual.json +1 -1
  62. package/dist/providers/data/qwen-token-plan.json +1 -1
  63. package/dist/providers/data/together.json +1 -1
  64. package/dist/providers/data/vercel-ai-gateway.json +1 -1
  65. package/dist/providers/data/xai.json +1 -1
  66. package/dist/providers/faux.d.ts.map +1 -1
  67. package/dist/providers/faux.js +3 -3
  68. package/dist/providers/faux.js.map +1 -1
  69. package/dist/providers/openrouter.d.ts +1 -1
  70. package/dist/providers/openrouter.d.ts.map +1 -1
  71. package/dist/providers/openrouter.js +5 -1
  72. package/dist/providers/openrouter.js.map +1 -1
  73. package/dist/types.d.ts +28 -7
  74. package/dist/types.d.ts.map +1 -1
  75. package/dist/types.js.map +1 -1
  76. package/dist/utils/assistant-message-frame.d.ts +77 -0
  77. package/dist/utils/assistant-message-frame.d.ts.map +1 -0
  78. package/dist/utils/assistant-message-frame.js +425 -0
  79. package/dist/utils/assistant-message-frame.js.map +1 -0
  80. package/dist/utils/node-http-proxy.d.ts.map +1 -1
  81. package/dist/utils/node-http-proxy.js +53 -12
  82. package/dist/utils/node-http-proxy.js.map +1 -1
  83. package/dist/utils/retry.d.ts.map +1 -1
  84. package/dist/utils/retry.js +2 -1
  85. package/dist/utils/retry.js.map +1 -1
  86. package/dist/utils/uuid.d.ts +2 -2
  87. package/dist/utils/uuid.d.ts.map +1 -1
  88. package/dist/utils/uuid.js +35 -39
  89. package/dist/utils/uuid.js.map +1 -1
  90. package/package.json +7 -3
  91. package/dist/api/cloudflare-gateway-binding.d.ts +0 -69
  92. package/dist/api/cloudflare-gateway-binding.d.ts.map +0 -1
  93. package/dist/api/cloudflare-gateway-binding.js +0 -159
  94. package/dist/api/cloudflare-gateway-binding.js.map +0 -1
@@ -27,6 +27,38 @@ function parseProxyTargetUrl(targetUrl) {
27
27
  return undefined;
28
28
  }
29
29
  }
30
+ function stripBrackets(host) {
31
+ return host.startsWith("[") && host.endsWith("]") ? host.slice(1, -1) : host;
32
+ }
33
+ function parseNoProxyEntry(entry) {
34
+ const trimmed = entry.trim().toLowerCase();
35
+ if (!trimmed)
36
+ return undefined;
37
+ if (trimmed.startsWith("[")) {
38
+ const closingBracket = trimmed.indexOf("]");
39
+ if (closingBracket !== -1) {
40
+ const host = trimmed.slice(1, closingBracket);
41
+ const rest = trimmed.slice(closingBracket + 1);
42
+ if (rest.startsWith(":")) {
43
+ const port = Number.parseInt(rest.slice(1), 10);
44
+ return { host, port: Number.isNaN(port) ? 0 : port };
45
+ }
46
+ return { host, port: 0 };
47
+ }
48
+ }
49
+ if (trimmed.includes(":") && trimmed.split(":").length > 2) {
50
+ return { host: trimmed, port: 0 };
51
+ }
52
+ const colonIndex = trimmed.lastIndexOf(":");
53
+ if (colonIndex !== -1 && colonIndex === trimmed.indexOf(":")) {
54
+ const host = trimmed.slice(0, colonIndex);
55
+ const port = Number.parseInt(trimmed.slice(colonIndex + 1), 10);
56
+ if (!Number.isNaN(port)) {
57
+ return { host, port };
58
+ }
59
+ }
60
+ return { host: trimmed, port: 0 };
61
+ }
30
62
  function shouldProxyHostname(hostname, port, env) {
31
63
  const noProxy = getProxyEnv("no_proxy", env).toLowerCase();
32
64
  if (!noProxy) {
@@ -35,23 +67,32 @@ function shouldProxyHostname(hostname, port, env) {
35
67
  if (noProxy === "*") {
36
68
  return false;
37
69
  }
38
- return noProxy.split(/[,\s]/).every((proxy) => {
39
- if (!proxy) {
70
+ const normalizedTargetHost = stripBrackets(hostname.toLowerCase());
71
+ return noProxy.split(/[,\s]/).every((entry) => {
72
+ const parsed = parseNoProxyEntry(entry);
73
+ if (!parsed) {
74
+ return true;
75
+ }
76
+ if (parsed.port && parsed.port !== port) {
40
77
  return true;
41
78
  }
42
- const parsedProxy = proxy.match(/^(.+):(\d+)$/);
43
- let proxyHostname = parsedProxy ? parsedProxy[1] : proxy;
44
- const proxyPort = parsedProxy ? Number.parseInt(parsedProxy[2], 10) : 0;
45
- if (proxyPort && proxyPort !== port) {
79
+ let domain = stripBrackets(parsed.host);
80
+ if (domain.startsWith("*.")) {
81
+ domain = domain.slice(2);
82
+ }
83
+ else if (domain.startsWith(".") || domain.startsWith("*")) {
84
+ domain = domain.slice(1);
85
+ }
86
+ if (!domain) {
46
87
  return true;
47
88
  }
48
- if (!/^[.*]/.test(proxyHostname)) {
49
- return hostname !== proxyHostname;
89
+ if (normalizedTargetHost === domain) {
90
+ return false;
50
91
  }
51
- if (proxyHostname.startsWith("*")) {
52
- proxyHostname = proxyHostname.slice(1);
92
+ if (normalizedTargetHost.endsWith(`.${domain}`)) {
93
+ return false;
53
94
  }
54
- return !hostname.endsWith(proxyHostname);
95
+ return true;
55
96
  });
56
97
  }
57
98
  function getProxyForUrl(targetUrl, env) {
@@ -60,7 +101,7 @@ function getProxyForUrl(targetUrl, env) {
60
101
  return "";
61
102
  }
62
103
  const protocol = parsedUrl.protocol.split(":", 1)[0];
63
- const hostname = parsedUrl.host.replace(/:\d*$/, "");
104
+ const hostname = stripBrackets(parsedUrl.hostname || parsedUrl.host.replace(/:\d*$/, ""));
64
105
  const port = Number.parseInt(parsedUrl.port, 10) || DEFAULT_PROXY_PORTS[protocol] || 0;
65
106
  if (!shouldProxyHostname(hostname, port, env)) {
66
107
  return "";
@@ -1 +1 @@
1
- {"version":3,"file":"node-http-proxy.js","sourceRoot":"","sources":["../../src/utils/node-http-proxy.ts"],"names":[],"mappings":"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","sourcesContent":["import type { ProviderEnv } from \"../types.ts\";\nimport { getProviderEnvValue } from \"./provider-env.ts\";\n\nconst DEFAULT_PROXY_PORTS: Record<string, number> = {\n\tftp: 21,\n\tgopher: 70,\n\thttp: 80,\n\thttps: 443,\n\tws: 80,\n\twss: 443,\n};\n\nfunction getProxyEnv(key: string, env?: ProviderEnv): string {\n\tconst lowercaseKey = key.toLowerCase();\n\tconst uppercaseKey = key.toUpperCase();\n\treturn (\n\t\tenv?.[lowercaseKey] ||\n\t\tenv?.[uppercaseKey] ||\n\t\tgetProviderEnvValue(lowercaseKey) ||\n\t\tgetProviderEnvValue(uppercaseKey) ||\n\t\t\"\"\n\t);\n}\n\nfunction parseProxyTargetUrl(targetUrl: string | URL): URL | undefined {\n\tif (targetUrl instanceof URL) {\n\t\treturn targetUrl;\n\t}\n\n\ttry {\n\t\treturn new URL(targetUrl);\n\t} catch {\n\t\treturn undefined;\n\t}\n}\n\nfunction shouldProxyHostname(hostname: string, port: number, env?: ProviderEnv): boolean {\n\tconst noProxy = getProxyEnv(\"no_proxy\", env).toLowerCase();\n\tif (!noProxy) {\n\t\treturn true;\n\t}\n\tif (noProxy === \"*\") {\n\t\treturn false;\n\t}\n\n\treturn noProxy.split(/[,\\s]/).every((proxy) => {\n\t\tif (!proxy) {\n\t\t\treturn true;\n\t\t}\n\n\t\tconst parsedProxy = proxy.match(/^(.+):(\\d+)$/);\n\t\tlet proxyHostname = parsedProxy ? parsedProxy[1] : proxy;\n\t\tconst proxyPort = parsedProxy ? Number.parseInt(parsedProxy[2]!, 10) : 0;\n\t\tif (proxyPort && proxyPort !== port) {\n\t\t\treturn true;\n\t\t}\n\n\t\tif (!/^[.*]/.test(proxyHostname)) {\n\t\t\treturn hostname !== proxyHostname;\n\t\t}\n\n\t\tif (proxyHostname.startsWith(\"*\")) {\n\t\t\tproxyHostname = proxyHostname.slice(1);\n\t\t}\n\t\treturn !hostname.endsWith(proxyHostname);\n\t});\n}\n\nfunction getProxyForUrl(targetUrl: string | URL, env?: ProviderEnv): string {\n\tconst parsedUrl = parseProxyTargetUrl(targetUrl);\n\tif (!parsedUrl?.protocol || !parsedUrl.host) {\n\t\treturn \"\";\n\t}\n\n\tconst protocol = parsedUrl.protocol.split(\":\", 1)[0]!;\n\tconst hostname = parsedUrl.host.replace(/:\\d*$/, \"\");\n\tconst port = Number.parseInt(parsedUrl.port, 10) || DEFAULT_PROXY_PORTS[protocol] || 0;\n\tif (!shouldProxyHostname(hostname, port, env)) {\n\t\treturn \"\";\n\t}\n\n\tlet proxy = getProxyEnv(`${protocol}_proxy`, env) || getProxyEnv(\"all_proxy\", env);\n\tif (proxy && !proxy.includes(\"://\")) {\n\t\tproxy = `${protocol}://${proxy}`;\n\t}\n\treturn proxy;\n}\n\nexport const UNSUPPORTED_PROXY_PROTOCOL_MESSAGE =\n\t\"Unsupported proxy protocol. SOCKS and PAC proxy URLs are not supported; use an HTTP or HTTPS proxy URL.\";\n\nexport function resolveHttpProxyUrlForTarget(targetUrl: string | URL, env?: ProviderEnv): URL | undefined {\n\tconst proxy = getProxyForUrl(targetUrl, env);\n\tif (!proxy) {\n\t\treturn undefined;\n\t}\n\n\tlet proxyUrl: URL;\n\ttry {\n\t\tproxyUrl = new URL(proxy);\n\t} catch (error) {\n\t\tthrow new Error(\n\t\t\t`Invalid proxy URL ${JSON.stringify(proxy)}: ${error instanceof Error ? error.message : String(error)}`,\n\t\t);\n\t}\n\n\tif (proxyUrl.protocol !== \"http:\" && proxyUrl.protocol !== \"https:\") {\n\t\tthrow new Error(`${UNSUPPORTED_PROXY_PROTOCOL_MESSAGE} Got ${proxyUrl.protocol}`);\n\t}\n\n\treturn proxyUrl;\n}\n"]}
1
+ {"version":3,"file":"node-http-proxy.js","sourceRoot":"","sources":["../../src/utils/node-http-proxy.ts"],"names":[],"mappings":"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","sourcesContent":["import type { ProviderEnv } from \"../types.ts\";\nimport { getProviderEnvValue } from \"./provider-env.ts\";\n\nconst DEFAULT_PROXY_PORTS: Record<string, number> = {\n\tftp: 21,\n\tgopher: 70,\n\thttp: 80,\n\thttps: 443,\n\tws: 80,\n\twss: 443,\n};\n\nfunction getProxyEnv(key: string, env?: ProviderEnv): string {\n\tconst lowercaseKey = key.toLowerCase();\n\tconst uppercaseKey = key.toUpperCase();\n\treturn (\n\t\tenv?.[lowercaseKey] ||\n\t\tenv?.[uppercaseKey] ||\n\t\tgetProviderEnvValue(lowercaseKey) ||\n\t\tgetProviderEnvValue(uppercaseKey) ||\n\t\t\"\"\n\t);\n}\n\nfunction parseProxyTargetUrl(targetUrl: string | URL): URL | undefined {\n\tif (targetUrl instanceof URL) {\n\t\treturn targetUrl;\n\t}\n\n\ttry {\n\t\treturn new URL(targetUrl);\n\t} catch {\n\t\treturn undefined;\n\t}\n}\n\nfunction stripBrackets(host: string): string {\n\treturn host.startsWith(\"[\") && host.endsWith(\"]\") ? host.slice(1, -1) : host;\n}\n\nfunction parseNoProxyEntry(entry: string): { host: string; port: number } | undefined {\n\tconst trimmed = entry.trim().toLowerCase();\n\tif (!trimmed) return undefined;\n\n\tif (trimmed.startsWith(\"[\")) {\n\t\tconst closingBracket = trimmed.indexOf(\"]\");\n\t\tif (closingBracket !== -1) {\n\t\t\tconst host = trimmed.slice(1, closingBracket);\n\t\t\tconst rest = trimmed.slice(closingBracket + 1);\n\t\t\tif (rest.startsWith(\":\")) {\n\t\t\t\tconst port = Number.parseInt(rest.slice(1), 10);\n\t\t\t\treturn { host, port: Number.isNaN(port) ? 0 : port };\n\t\t\t}\n\t\t\treturn { host, port: 0 };\n\t\t}\n\t}\n\n\tif (trimmed.includes(\":\") && trimmed.split(\":\").length > 2) {\n\t\treturn { host: trimmed, port: 0 };\n\t}\n\n\tconst colonIndex = trimmed.lastIndexOf(\":\");\n\tif (colonIndex !== -1 && colonIndex === trimmed.indexOf(\":\")) {\n\t\tconst host = trimmed.slice(0, colonIndex);\n\t\tconst port = Number.parseInt(trimmed.slice(colonIndex + 1), 10);\n\t\tif (!Number.isNaN(port)) {\n\t\t\treturn { host, port };\n\t\t}\n\t}\n\n\treturn { host: trimmed, port: 0 };\n}\n\nfunction shouldProxyHostname(hostname: string, port: number, env?: ProviderEnv): boolean {\n\tconst noProxy = getProxyEnv(\"no_proxy\", env).toLowerCase();\n\tif (!noProxy) {\n\t\treturn true;\n\t}\n\tif (noProxy === \"*\") {\n\t\treturn false;\n\t}\n\n\tconst normalizedTargetHost = stripBrackets(hostname.toLowerCase());\n\n\treturn noProxy.split(/[,\\s]/).every((entry) => {\n\t\tconst parsed = parseNoProxyEntry(entry);\n\t\tif (!parsed) {\n\t\t\treturn true;\n\t\t}\n\n\t\tif (parsed.port && parsed.port !== port) {\n\t\t\treturn true;\n\t\t}\n\n\t\tlet domain = stripBrackets(parsed.host);\n\t\tif (domain.startsWith(\"*.\")) {\n\t\t\tdomain = domain.slice(2);\n\t\t} else if (domain.startsWith(\".\") || domain.startsWith(\"*\")) {\n\t\t\tdomain = domain.slice(1);\n\t\t}\n\n\t\tif (!domain) {\n\t\t\treturn true;\n\t\t}\n\n\t\tif (normalizedTargetHost === domain) {\n\t\t\treturn false;\n\t\t}\n\n\t\tif (normalizedTargetHost.endsWith(`.${domain}`)) {\n\t\t\treturn false;\n\t\t}\n\n\t\treturn true;\n\t});\n}\n\nfunction getProxyForUrl(targetUrl: string | URL, env?: ProviderEnv): string {\n\tconst parsedUrl = parseProxyTargetUrl(targetUrl);\n\tif (!parsedUrl?.protocol || !parsedUrl.host) {\n\t\treturn \"\";\n\t}\n\n\tconst protocol = parsedUrl.protocol.split(\":\", 1)[0]!;\n\tconst hostname = stripBrackets(parsedUrl.hostname || parsedUrl.host.replace(/:\\d*$/, \"\"));\n\tconst port = Number.parseInt(parsedUrl.port, 10) || DEFAULT_PROXY_PORTS[protocol] || 0;\n\tif (!shouldProxyHostname(hostname, port, env)) {\n\t\treturn \"\";\n\t}\n\n\tlet proxy = getProxyEnv(`${protocol}_proxy`, env) || getProxyEnv(\"all_proxy\", env);\n\tif (proxy && !proxy.includes(\"://\")) {\n\t\tproxy = `${protocol}://${proxy}`;\n\t}\n\treturn proxy;\n}\n\nexport const UNSUPPORTED_PROXY_PROTOCOL_MESSAGE =\n\t\"Unsupported proxy protocol. SOCKS and PAC proxy URLs are not supported; use an HTTP or HTTPS proxy URL.\";\n\nexport function resolveHttpProxyUrlForTarget(targetUrl: string | URL, env?: ProviderEnv): URL | undefined {\n\tconst proxy = getProxyForUrl(targetUrl, env);\n\tif (!proxy) {\n\t\treturn undefined;\n\t}\n\n\tlet proxyUrl: URL;\n\ttry {\n\t\tproxyUrl = new URL(proxy);\n\t} catch (error) {\n\t\tthrow new Error(\n\t\t\t`Invalid proxy URL ${JSON.stringify(proxy)}: ${error instanceof Error ? error.message : String(error)}`,\n\t\t);\n\t}\n\n\tif (proxyUrl.protocol !== \"http:\" && proxyUrl.protocol !== \"https:\") {\n\t\tthrow new Error(`${UNSUPPORTED_PROXY_PROTOCOL_MESSAGE} Got ${proxyUrl.protocol}`);\n\t}\n\n\treturn proxyUrl;\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"retry.d.ts","sourceRoot":"","sources":["../../src/utils/retry.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AA2FpD;;;;;GAKG;AACH,MAAM,WAAW,WAAW;IAC3B,OAAO,EAAE,OAAO,CAAC;IACjB,qFAAqF;IACrF,UAAU,EAAE,MAAM,CAAC;IACnB,0FAA0F;IAC1F,WAAW,EAAE,MAAM,CAAC;CACpB;AAED,kFAAkF;AAClF,MAAM,WAAW,cAAc;IAC9B,0EAA0E;IAC1E,gBAAgB,CAAC,EAAE,CAClB,OAAO,EAAE,MAAM,EACf,WAAW,EAAE,MAAM,EACnB,OAAO,EAAE,MAAM,EACf,YAAY,EAAE,MAAM,KAChB,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1B,mFAAmF;IACnF,mBAAmB,CAAC,EAAE,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACjD,mFAAmF;IACnF,eAAe,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACnG;AA0BD;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAsB,kBAAkB,CACvC,OAAO,EAAE,MAAM,OAAO,CAAC,gBAAgB,CAAC,EACxC,MAAM,EAAE,WAAW,GAAG,SAAS,EAC/B,MAAM,EAAE,WAAW,GAAG,SAAS,EAC/B,SAAS,CAAC,EAAE,cAAc,GACxB,OAAO,CAAC,gBAAgB,CAAC,CA4C3B;AAED;;;;;;;;GAQG;AACH,wBAAgB,yBAAyB,CAAC,OAAO,EAAE,gBAAgB,GAAG,OAAO,CAK5E","sourcesContent":["import type { AssistantMessage } from \"../types.ts\";\n\nfunction buildProviderErrorPattern(patterns: readonly string[]): RegExp {\n\treturn new RegExp(patterns.join(\"|\"), \"i\");\n}\n\nconst NON_RETRYABLE_PROVIDER_LIMIT_ERROR_PATTERN = buildProviderErrorPattern([\n\t// OpenCode Go/free-tier limits returned as 429 JSON error types by OpenCode's\n\t// Zen API. These are subscription/account limits, not transient throttles.\n\t\"GoUsageLimitError\",\n\t\"FreeUsageLimitError\",\n\n\t// OpenCode Go subscription-limit text asks users to enable available-balance\n\t// usage after rolling/weekly/monthly limits are reached.\n\t\"Monthly usage limit reached\",\n\t\"available balance\",\n\n\t// Generic quota/budget/billing exhaustion. `insufficient_quota` is OpenAI's\n\t// quota/billing error code; the other strings cover common gateway wording.\n\t\"insufficient_quota\",\n\t\"out of budget\",\n\t\"quota exceeded\",\n\t\"billing\",\n]);\n\nconst RETRYABLE_PROVIDER_ERROR_PATTERN = buildProviderErrorPattern([\n\t// Generic provider load, HTTP status, and server-side transient failures.\n\t\"overloaded\",\n\t\"rate.?limit\",\n\t\"too many requests\",\n\t\"429\",\n\t\"500\",\n\t\"502\",\n\t\"503\",\n\t\"504\",\n\t\"524\",\n\t\"service.?unavailable\",\n\t\"server.?error\",\n\t\"internal.?error\",\n\n\t// Wrapper/provider text for transient upstream failures, including OpenRouter\n\t// \"Provider returned error\" responses (#2264).\n\t\"provider.?returned.?error\",\n\t\"exceeded request buffer limit while retrying upstream\",\n\n\t// Network, proxy, and fetch transport failures. This includes OpenAI Codex\n\t// raw-fetch failures such as \"upstream connect\", \"connection refused\", and\n\t// \"reset before headers\" (#733), plus OpenRouter connection drops (#3317).\n\t\"network.?error\",\n\t\"connection.?error\",\n\t\"connection.?refused\",\n\t\"connection.?lost\",\n\t\"other side closed\",\n\t\"fetch failed\",\n\t\"getaddrinfo\",\n\t\"ENOTFOUND\",\n\t\"EAI_AGAIN\",\n\t\"upstream.?connect\",\n\t\"reset before headers\",\n\t\"socket hang up\",\n\t\"socket connection was closed\",\n\t\"timed? out\",\n\t\"timeout\",\n\t\"terminated\",\n\n\t// WebSocket transports can report close/error text instead of HTTP/fetch text.\n\t\"websocket.?closed\",\n\t\"websocket.?error\",\n\n\t// Premature stream endings from SDKs and transports. Anthropic can throw\n\t// \"stream ended without ...\" and \"Anthropic stream ended before message_stop\"\n\t// (#4433); Bedrock/Smithy can throw an HTTP/2 no-response error (#3594).\n\t\"ended without\",\n\t\"stream ended before message_stop\",\n\t\"stream ended before a terminal response event\",\n\t\"http2 request did not get a response\",\n\n\t// Provider-requested retry delay cap failures should flow through the outer\n\t// retry policy so callers can surface/abort the backoff (#1123).\n\t\"retry delay\",\n\n\t// Explicit retry guidance emitted mid-stream by OpenAI Responses and Bedrock\n\t// stream exceptions (#6019).\n\t\"you can retry your request\",\n\t\"try your request again\",\n\t\"please retry your request\",\n\n\t// gRPC based providers (e.g. NVIDIA NIM)\n\t\"ResourceExhausted\",\n]);\n\n/**\n * Retry policy: bounded attempts with exponential backoff (`baseDelayMs * 2^(attempt-1)`).\n * Matches `settings.retry` (`enabled`, `maxRetries`, `baseDelayMs`) in coding-agent; kept\n * here so the classifier and the policy-driven retry loop live together and stay reusable\n * by the SDK and other callers.\n */\nexport interface RetryPolicy {\n\tenabled: boolean;\n\t/** Max retry attempts (0 = no retries). The initial call never counts as a retry. */\n\tmaxRetries: number;\n\t/** Base delay in ms. Per-attempt delay is `baseDelayMs * 2^(attempt-1)` before jitter. */\n\tbaseDelayMs: number;\n}\n\n/** Optional callbacks emitted by {@link retryAssistantCall} around each retry. */\nexport interface RetryCallbacks {\n\t/** Emitted before the backoff sleep of each retry attempt (1-indexed). */\n\tonRetryScheduled?: (\n\t\tattempt: number,\n\t\tmaxAttempts: number,\n\t\tdelayMs: number,\n\t\terrorMessage: string,\n\t) => void | Promise<void>;\n\t/** Emitted after the backoff sleep, immediately before the retried call starts. */\n\tonRetryAttemptStart?: () => void | Promise<void>;\n\t/** Emitted once when the loop ends: success if a later call completed normally. */\n\tonRetryFinished?: (success: boolean, attempt: number, finalError?: string) => void | Promise<void>;\n}\n\nclass RetrySleepAbortError extends Error {\n\tconstructor() {\n\t\tsuper(\"Aborted\");\n\t}\n}\n\nfunction sleep(ms: number, signal?: AbortSignal): Promise<void> {\n\treturn new Promise((resolve, reject) => {\n\t\tif (signal?.aborted) {\n\t\t\treject(new RetrySleepAbortError());\n\t\t\treturn;\n\t\t}\n\t\tconst timeout = setTimeout(resolve, ms);\n\t\tsignal?.addEventListener(\n\t\t\t\"abort\",\n\t\t\t() => {\n\t\t\t\tclearTimeout(timeout);\n\t\t\t\treject(new RetrySleepAbortError());\n\t\t\t},\n\t\t\t{ once: true },\n\t\t);\n\t});\n}\n\n/**\n * Run a single assistant-producing call with bounded retry on transient errors.\n *\n * Behavior:\n * - A successful response is returned immediately. Aborts are terminal and never\n * retried, but reported as unsuccessful if they happen after a retry was scheduled.\n * Aborts during the backoff sleep are normalized to an aborted `AssistantMessage`\n * too, so callers do not need to care when cancellation happened.\n * - A non-retryable error (per {@link isRetryableAssistantError}, including quota/\n * billing exhaustion) is returned immediately so deterministic errors fail fast.\n * - Otherwise retries up to `maxRetries` times with exponential backoff, emitting\n * `onRetryScheduled` before each sleep, `onRetryAttemptStart` after each sleep before\n * the retried call starts, and `onRetryFinished` once at the end (whether the loop\n * ends in success, exhausted retries, or an aborted backoff).\n *\n * When `policy` is undefined or disabled, the first response is returned unchanged\n * (equivalent to calling `produce()` directly).\n */\nexport async function retryAssistantCall(\n\tproduce: () => Promise<AssistantMessage>,\n\tpolicy: RetryPolicy | undefined,\n\tsignal: AbortSignal | undefined,\n\tcallbacks?: RetryCallbacks,\n): Promise<AssistantMessage> {\n\tconst maxAttempts = policy?.enabled ? policy.maxRetries : 0;\n\n\tlet attempt = 0;\n\tlet lastRetry: { attempt: number; errorMessage: string } | undefined;\n\tfor (;;) {\n\t\tconst response = await produce();\n\n\t\t// Abort: terminal but not successful. Never retry an aborted message.\n\t\tif (response.stopReason === \"aborted\") {\n\t\t\tif (lastRetry) await callbacks?.onRetryFinished?.(false, lastRetry.attempt);\n\t\t\treturn response;\n\t\t}\n\n\t\t// Success: non-error, non-abort responses return as-is.\n\t\tif (response.stopReason !== \"error\") {\n\t\t\tif (lastRetry) await callbacks?.onRetryFinished?.(true, lastRetry.attempt);\n\t\t\treturn response;\n\t\t}\n\n\t\t// Non-retryable, or budget exhausted: return the final error message.\n\t\tif (attempt >= maxAttempts || !isRetryableAssistantError(response)) {\n\t\t\tif (lastRetry) await callbacks?.onRetryFinished?.(false, lastRetry.attempt, response.errorMessage);\n\t\t\treturn response;\n\t\t}\n\n\t\tattempt++;\n\t\tlastRetry = { attempt, errorMessage: response.errorMessage || \"Unknown error\" };\n\t\tconst delayMs = policy!.baseDelayMs * 2 ** (attempt - 1);\n\t\tawait callbacks?.onRetryScheduled?.(attempt, maxAttempts, delayMs, lastRetry.errorMessage);\n\n\t\t// Normalize aborts during retry backoff to the same AssistantMessage shape as\n\t\t// provider stream aborts, so callers do not need to care when cancellation happened.\n\t\ttry {\n\t\t\tawait sleep(delayMs, signal);\n\t\t} catch (error) {\n\t\t\tawait callbacks?.onRetryFinished?.(false, attempt, lastRetry.errorMessage);\n\t\t\tif (error instanceof RetrySleepAbortError) {\n\t\t\t\treturn { ...response, stopReason: \"aborted\", errorMessage: undefined };\n\t\t\t}\n\t\t\tthrow error;\n\t\t}\n\t\tawait callbacks?.onRetryAttemptStart?.();\n\t}\n}\n\n/**\n * Classifies whether a failed assistant message looks like a transient provider\n * or transport error, so callers can decide if the last assistant turn should be\n * restarted.\n *\n * This does not implement retry policy. Callers should first handle context\n * overflow separately, then apply their own retry budget, backoff, and reporting\n * before restarting the assistant turn.\n */\nexport function isRetryableAssistantError(message: AssistantMessage): boolean {\n\tif (message.stopReason !== \"error\" || !message.errorMessage) return false;\n\tconst errorMessage = message.errorMessage;\n\tif (NON_RETRYABLE_PROVIDER_LIMIT_ERROR_PATTERN.test(errorMessage)) return false;\n\treturn RETRYABLE_PROVIDER_ERROR_PATTERN.test(errorMessage);\n}\n"]}
1
+ {"version":3,"file":"retry.d.ts","sourceRoot":"","sources":["../../src/utils/retry.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AA2FpD;;;;;GAKG;AACH,MAAM,WAAW,WAAW;IAC3B,OAAO,EAAE,OAAO,CAAC;IACjB,qFAAqF;IACrF,UAAU,EAAE,MAAM,CAAC;IACnB,0FAA0F;IAC1F,WAAW,EAAE,MAAM,CAAC;CACpB;AAED,kFAAkF;AAClF,MAAM,WAAW,cAAc;IAC9B,0EAA0E;IAC1E,gBAAgB,CAAC,EAAE,CAClB,OAAO,EAAE,MAAM,EACf,WAAW,EAAE,MAAM,EACnB,OAAO,EAAE,MAAM,EACf,YAAY,EAAE,MAAM,KAChB,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1B,mFAAmF;IACnF,mBAAmB,CAAC,EAAE,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACjD,mFAAmF;IACnF,eAAe,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACnG;AA0BD;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAsB,kBAAkB,CACvC,OAAO,EAAE,MAAM,OAAO,CAAC,gBAAgB,CAAC,EACxC,MAAM,EAAE,WAAW,GAAG,SAAS,EAC/B,MAAM,EAAE,WAAW,GAAG,SAAS,EAC/B,SAAS,CAAC,EAAE,cAAc,GACxB,OAAO,CAAC,gBAAgB,CAAC,CA6C3B;AAED;;;;;;;;GAQG;AACH,wBAAgB,yBAAyB,CAAC,OAAO,EAAE,gBAAgB,GAAG,OAAO,CAK5E","sourcesContent":["import type { AssistantMessage } from \"../types.ts\";\n\nfunction buildProviderErrorPattern(patterns: readonly string[]): RegExp {\n\treturn new RegExp(patterns.join(\"|\"), \"i\");\n}\n\nconst NON_RETRYABLE_PROVIDER_LIMIT_ERROR_PATTERN = buildProviderErrorPattern([\n\t// OpenCode Go/free-tier limits returned as 429 JSON error types by OpenCode's\n\t// Zen API. These are subscription/account limits, not transient throttles.\n\t\"GoUsageLimitError\",\n\t\"FreeUsageLimitError\",\n\n\t// OpenCode Go subscription-limit text asks users to enable available-balance\n\t// usage after rolling/weekly/monthly limits are reached.\n\t\"Monthly usage limit reached\",\n\t\"available balance\",\n\n\t// Generic quota/budget/billing exhaustion. `insufficient_quota` is OpenAI's\n\t// quota/billing error code; the other strings cover common gateway wording.\n\t\"insufficient_quota\",\n\t\"out of budget\",\n\t\"quota exceeded\",\n\t\"billing\",\n]);\n\nconst RETRYABLE_PROVIDER_ERROR_PATTERN = buildProviderErrorPattern([\n\t// Generic provider load, HTTP status, and server-side transient failures.\n\t\"overloaded\",\n\t\"rate.?limit\",\n\t\"too many requests\",\n\t\"429\",\n\t\"500\",\n\t\"502\",\n\t\"503\",\n\t\"504\",\n\t\"524\",\n\t\"service.?unavailable\",\n\t\"server.?error\",\n\t\"internal.?error\",\n\n\t// Wrapper/provider text for transient upstream failures, including OpenRouter\n\t// \"Provider returned error\" responses (#2264).\n\t\"provider.?returned.?error\",\n\t\"exceeded request buffer limit while retrying upstream\",\n\n\t// Network, proxy, and fetch transport failures. This includes OpenAI Codex\n\t// raw-fetch failures such as \"upstream connect\", \"connection refused\", and\n\t// \"reset before headers\" (#733), plus OpenRouter connection drops (#3317).\n\t\"network.?error\",\n\t\"connection.?error\",\n\t\"connection.?refused\",\n\t\"connection.?lost\",\n\t\"other side closed\",\n\t\"fetch failed\",\n\t\"getaddrinfo\",\n\t\"ENOTFOUND\",\n\t\"EAI_AGAIN\",\n\t\"upstream.?connect\",\n\t\"reset before headers\",\n\t\"socket hang up\",\n\t\"socket connection was closed\",\n\t\"timed? out\",\n\t\"timeout\",\n\t\"terminated\",\n\n\t// WebSocket transports can report close/error text instead of HTTP/fetch text.\n\t\"websocket.?closed\",\n\t\"websocket.?error\",\n\n\t// Premature stream endings from SDKs and transports. Anthropic can throw\n\t// \"stream ended without ...\" and \"Anthropic stream ended before message_stop\"\n\t// (#4433); Bedrock/Smithy can throw an HTTP/2 no-response error (#3594).\n\t\"ended without\",\n\t\"stream ended before message_stop\",\n\t\"stream ended before a terminal response event\",\n\t\"http2 request did not get a response\",\n\n\t// Provider-requested retry delay cap failures should flow through the outer\n\t// retry policy so callers can surface/abort the backoff (#1123).\n\t\"retry delay\",\n\n\t// Explicit retry guidance emitted mid-stream by OpenAI Responses and Bedrock\n\t// stream exceptions (#6019).\n\t\"you can retry your request\",\n\t\"try your request again\",\n\t\"please retry your request\",\n\n\t// gRPC based providers (e.g. NVIDIA NIM)\n\t\"ResourceExhausted\",\n]);\n\n/**\n * Retry policy: bounded attempts with exponential backoff (`baseDelayMs * 2^(attempt-1)`).\n * Matches `settings.retry` (`enabled`, `maxRetries`, `baseDelayMs`) in coding-agent; kept\n * here so the classifier and the policy-driven retry loop live together and stay reusable\n * by the SDK and other callers.\n */\nexport interface RetryPolicy {\n\tenabled: boolean;\n\t/** Max retry attempts (0 = no retries). The initial call never counts as a retry. */\n\tmaxRetries: number;\n\t/** Base delay in ms. Per-attempt delay is `baseDelayMs * 2^(attempt-1)` before jitter. */\n\tbaseDelayMs: number;\n}\n\n/** Optional callbacks emitted by {@link retryAssistantCall} around each retry. */\nexport interface RetryCallbacks {\n\t/** Emitted before the backoff sleep of each retry attempt (1-indexed). */\n\tonRetryScheduled?: (\n\t\tattempt: number,\n\t\tmaxAttempts: number,\n\t\tdelayMs: number,\n\t\terrorMessage: string,\n\t) => void | Promise<void>;\n\t/** Emitted after the backoff sleep, immediately before the retried call starts. */\n\tonRetryAttemptStart?: () => void | Promise<void>;\n\t/** Emitted once when the loop ends: success if a later call completed normally. */\n\tonRetryFinished?: (success: boolean, attempt: number, finalError?: string) => void | Promise<void>;\n}\n\nclass RetrySleepAbortError extends Error {\n\tconstructor() {\n\t\tsuper(\"Aborted\");\n\t}\n}\n\nfunction sleep(ms: number, signal?: AbortSignal): Promise<void> {\n\treturn new Promise((resolve, reject) => {\n\t\tif (signal?.aborted) {\n\t\t\treject(new RetrySleepAbortError());\n\t\t\treturn;\n\t\t}\n\t\tconst timeout = setTimeout(resolve, ms);\n\t\tsignal?.addEventListener(\n\t\t\t\"abort\",\n\t\t\t() => {\n\t\t\t\tclearTimeout(timeout);\n\t\t\t\treject(new RetrySleepAbortError());\n\t\t\t},\n\t\t\t{ once: true },\n\t\t);\n\t});\n}\n\n/**\n * Run a single assistant-producing call with bounded retry on transient errors.\n *\n * Behavior:\n * - A successful response is returned immediately. Aborts are terminal and never\n * retried, but reported as unsuccessful if they happen after a retry was scheduled.\n * Aborts during the backoff sleep are normalized to an aborted `AssistantMessage`\n * too, so callers do not need to care when cancellation happened.\n * - A non-retryable error (per {@link isRetryableAssistantError}, including quota/\n * billing exhaustion) is returned immediately so deterministic errors fail fast.\n * - Otherwise retries up to `maxRetries` times with exponential backoff, emitting\n * `onRetryScheduled` before each sleep, `onRetryAttemptStart` after each sleep before\n * the retried call starts, and `onRetryFinished` once at the end (whether the loop\n * ends in success, exhausted retries, or an aborted backoff).\n *\n * When `policy` is undefined or disabled, the first response is returned unchanged\n * (equivalent to calling `produce()` directly).\n */\nexport async function retryAssistantCall(\n\tproduce: () => Promise<AssistantMessage>,\n\tpolicy: RetryPolicy | undefined,\n\tsignal: AbortSignal | undefined,\n\tcallbacks?: RetryCallbacks,\n): Promise<AssistantMessage> {\n\tconst maxAttempts = policy?.enabled ? policy.maxRetries : 0;\n\n\tlet attempt = 0;\n\tlet lastRetry: { attempt: number; errorMessage: string } | undefined;\n\tfor (;;) {\n\t\tconst response = await produce();\n\n\t\t// Abort: terminal but not successful. Never retry an aborted message.\n\t\tif (response.stopReason === \"aborted\") {\n\t\t\tif (lastRetry) await callbacks?.onRetryFinished?.(false, lastRetry.attempt);\n\t\t\treturn response;\n\t\t}\n\n\t\t// Success: non-error, non-abort responses return as-is.\n\t\tif (response.stopReason !== \"error\") {\n\t\t\tif (lastRetry) await callbacks?.onRetryFinished?.(true, lastRetry.attempt);\n\t\t\treturn response;\n\t\t}\n\n\t\t// Non-retryable, or budget exhausted: return the final error message.\n\t\tif (attempt >= maxAttempts || !isRetryableAssistantError(response)) {\n\t\t\tif (lastRetry) await callbacks?.onRetryFinished?.(false, lastRetry.attempt, response.errorMessage);\n\t\t\treturn response;\n\t\t}\n\n\t\tattempt++;\n\t\tlastRetry = { attempt, errorMessage: response.errorMessage || \"Unknown error\" };\n\t\tconst delayMs = policy!.baseDelayMs * 2 ** (attempt - 1);\n\t\tawait callbacks?.onRetryScheduled?.(attempt, maxAttempts, delayMs, lastRetry.errorMessage);\n\n\t\t// Normalize aborts during retry backoff to the same AssistantMessage shape as\n\t\t// provider stream aborts, so callers do not need to care when cancellation happened.\n\t\ttry {\n\t\t\tawait sleep(delayMs, signal);\n\t\t} catch (error) {\n\t\t\tawait callbacks?.onRetryFinished?.(false, attempt, lastRetry.errorMessage);\n\t\t\tif (error instanceof RetrySleepAbortError) {\n\t\t\t\tconst { errorMessage: _errorMessage, ...rest } = response;\n\t\t\t\treturn { ...rest, stopReason: \"aborted\" };\n\t\t\t}\n\t\t\tthrow error;\n\t\t}\n\t\tawait callbacks?.onRetryAttemptStart?.();\n\t}\n}\n\n/**\n * Classifies whether a failed assistant message looks like a transient provider\n * or transport error, so callers can decide if the last assistant turn should be\n * restarted.\n *\n * This does not implement retry policy. Callers should first handle context\n * overflow separately, then apply their own retry budget, backoff, and reporting\n * before restarting the assistant turn.\n */\nexport function isRetryableAssistantError(message: AssistantMessage): boolean {\n\tif (message.stopReason !== \"error\" || !message.errorMessage) return false;\n\tconst errorMessage = message.errorMessage;\n\tif (NON_RETRYABLE_PROVIDER_LIMIT_ERROR_PATTERN.test(errorMessage)) return false;\n\treturn RETRYABLE_PROVIDER_ERROR_PATTERN.test(errorMessage);\n}\n"]}
@@ -147,7 +147,8 @@ export async function retryAssistantCall(produce, policy, signal, callbacks) {
147
147
  catch (error) {
148
148
  await callbacks?.onRetryFinished?.(false, attempt, lastRetry.errorMessage);
149
149
  if (error instanceof RetrySleepAbortError) {
150
- return { ...response, stopReason: "aborted", errorMessage: undefined };
150
+ const { errorMessage: _errorMessage, ...rest } = response;
151
+ return { ...rest, stopReason: "aborted" };
151
152
  }
152
153
  throw error;
153
154
  }
@@ -1 +1 @@
1
- {"version":3,"file":"retry.js","sourceRoot":"","sources":["../../src/utils/retry.ts"],"names":[],"mappings":"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","sourcesContent":["import type { AssistantMessage } from \"../types.ts\";\n\nfunction buildProviderErrorPattern(patterns: readonly string[]): RegExp {\n\treturn new RegExp(patterns.join(\"|\"), \"i\");\n}\n\nconst NON_RETRYABLE_PROVIDER_LIMIT_ERROR_PATTERN = buildProviderErrorPattern([\n\t// OpenCode Go/free-tier limits returned as 429 JSON error types by OpenCode's\n\t// Zen API. These are subscription/account limits, not transient throttles.\n\t\"GoUsageLimitError\",\n\t\"FreeUsageLimitError\",\n\n\t// OpenCode Go subscription-limit text asks users to enable available-balance\n\t// usage after rolling/weekly/monthly limits are reached.\n\t\"Monthly usage limit reached\",\n\t\"available balance\",\n\n\t// Generic quota/budget/billing exhaustion. `insufficient_quota` is OpenAI's\n\t// quota/billing error code; the other strings cover common gateway wording.\n\t\"insufficient_quota\",\n\t\"out of budget\",\n\t\"quota exceeded\",\n\t\"billing\",\n]);\n\nconst RETRYABLE_PROVIDER_ERROR_PATTERN = buildProviderErrorPattern([\n\t// Generic provider load, HTTP status, and server-side transient failures.\n\t\"overloaded\",\n\t\"rate.?limit\",\n\t\"too many requests\",\n\t\"429\",\n\t\"500\",\n\t\"502\",\n\t\"503\",\n\t\"504\",\n\t\"524\",\n\t\"service.?unavailable\",\n\t\"server.?error\",\n\t\"internal.?error\",\n\n\t// Wrapper/provider text for transient upstream failures, including OpenRouter\n\t// \"Provider returned error\" responses (#2264).\n\t\"provider.?returned.?error\",\n\t\"exceeded request buffer limit while retrying upstream\",\n\n\t// Network, proxy, and fetch transport failures. This includes OpenAI Codex\n\t// raw-fetch failures such as \"upstream connect\", \"connection refused\", and\n\t// \"reset before headers\" (#733), plus OpenRouter connection drops (#3317).\n\t\"network.?error\",\n\t\"connection.?error\",\n\t\"connection.?refused\",\n\t\"connection.?lost\",\n\t\"other side closed\",\n\t\"fetch failed\",\n\t\"getaddrinfo\",\n\t\"ENOTFOUND\",\n\t\"EAI_AGAIN\",\n\t\"upstream.?connect\",\n\t\"reset before headers\",\n\t\"socket hang up\",\n\t\"socket connection was closed\",\n\t\"timed? out\",\n\t\"timeout\",\n\t\"terminated\",\n\n\t// WebSocket transports can report close/error text instead of HTTP/fetch text.\n\t\"websocket.?closed\",\n\t\"websocket.?error\",\n\n\t// Premature stream endings from SDKs and transports. Anthropic can throw\n\t// \"stream ended without ...\" and \"Anthropic stream ended before message_stop\"\n\t// (#4433); Bedrock/Smithy can throw an HTTP/2 no-response error (#3594).\n\t\"ended without\",\n\t\"stream ended before message_stop\",\n\t\"stream ended before a terminal response event\",\n\t\"http2 request did not get a response\",\n\n\t// Provider-requested retry delay cap failures should flow through the outer\n\t// retry policy so callers can surface/abort the backoff (#1123).\n\t\"retry delay\",\n\n\t// Explicit retry guidance emitted mid-stream by OpenAI Responses and Bedrock\n\t// stream exceptions (#6019).\n\t\"you can retry your request\",\n\t\"try your request again\",\n\t\"please retry your request\",\n\n\t// gRPC based providers (e.g. NVIDIA NIM)\n\t\"ResourceExhausted\",\n]);\n\n/**\n * Retry policy: bounded attempts with exponential backoff (`baseDelayMs * 2^(attempt-1)`).\n * Matches `settings.retry` (`enabled`, `maxRetries`, `baseDelayMs`) in coding-agent; kept\n * here so the classifier and the policy-driven retry loop live together and stay reusable\n * by the SDK and other callers.\n */\nexport interface RetryPolicy {\n\tenabled: boolean;\n\t/** Max retry attempts (0 = no retries). The initial call never counts as a retry. */\n\tmaxRetries: number;\n\t/** Base delay in ms. Per-attempt delay is `baseDelayMs * 2^(attempt-1)` before jitter. */\n\tbaseDelayMs: number;\n}\n\n/** Optional callbacks emitted by {@link retryAssistantCall} around each retry. */\nexport interface RetryCallbacks {\n\t/** Emitted before the backoff sleep of each retry attempt (1-indexed). */\n\tonRetryScheduled?: (\n\t\tattempt: number,\n\t\tmaxAttempts: number,\n\t\tdelayMs: number,\n\t\terrorMessage: string,\n\t) => void | Promise<void>;\n\t/** Emitted after the backoff sleep, immediately before the retried call starts. */\n\tonRetryAttemptStart?: () => void | Promise<void>;\n\t/** Emitted once when the loop ends: success if a later call completed normally. */\n\tonRetryFinished?: (success: boolean, attempt: number, finalError?: string) => void | Promise<void>;\n}\n\nclass RetrySleepAbortError extends Error {\n\tconstructor() {\n\t\tsuper(\"Aborted\");\n\t}\n}\n\nfunction sleep(ms: number, signal?: AbortSignal): Promise<void> {\n\treturn new Promise((resolve, reject) => {\n\t\tif (signal?.aborted) {\n\t\t\treject(new RetrySleepAbortError());\n\t\t\treturn;\n\t\t}\n\t\tconst timeout = setTimeout(resolve, ms);\n\t\tsignal?.addEventListener(\n\t\t\t\"abort\",\n\t\t\t() => {\n\t\t\t\tclearTimeout(timeout);\n\t\t\t\treject(new RetrySleepAbortError());\n\t\t\t},\n\t\t\t{ once: true },\n\t\t);\n\t});\n}\n\n/**\n * Run a single assistant-producing call with bounded retry on transient errors.\n *\n * Behavior:\n * - A successful response is returned immediately. Aborts are terminal and never\n * retried, but reported as unsuccessful if they happen after a retry was scheduled.\n * Aborts during the backoff sleep are normalized to an aborted `AssistantMessage`\n * too, so callers do not need to care when cancellation happened.\n * - A non-retryable error (per {@link isRetryableAssistantError}, including quota/\n * billing exhaustion) is returned immediately so deterministic errors fail fast.\n * - Otherwise retries up to `maxRetries` times with exponential backoff, emitting\n * `onRetryScheduled` before each sleep, `onRetryAttemptStart` after each sleep before\n * the retried call starts, and `onRetryFinished` once at the end (whether the loop\n * ends in success, exhausted retries, or an aborted backoff).\n *\n * When `policy` is undefined or disabled, the first response is returned unchanged\n * (equivalent to calling `produce()` directly).\n */\nexport async function retryAssistantCall(\n\tproduce: () => Promise<AssistantMessage>,\n\tpolicy: RetryPolicy | undefined,\n\tsignal: AbortSignal | undefined,\n\tcallbacks?: RetryCallbacks,\n): Promise<AssistantMessage> {\n\tconst maxAttempts = policy?.enabled ? policy.maxRetries : 0;\n\n\tlet attempt = 0;\n\tlet lastRetry: { attempt: number; errorMessage: string } | undefined;\n\tfor (;;) {\n\t\tconst response = await produce();\n\n\t\t// Abort: terminal but not successful. Never retry an aborted message.\n\t\tif (response.stopReason === \"aborted\") {\n\t\t\tif (lastRetry) await callbacks?.onRetryFinished?.(false, lastRetry.attempt);\n\t\t\treturn response;\n\t\t}\n\n\t\t// Success: non-error, non-abort responses return as-is.\n\t\tif (response.stopReason !== \"error\") {\n\t\t\tif (lastRetry) await callbacks?.onRetryFinished?.(true, lastRetry.attempt);\n\t\t\treturn response;\n\t\t}\n\n\t\t// Non-retryable, or budget exhausted: return the final error message.\n\t\tif (attempt >= maxAttempts || !isRetryableAssistantError(response)) {\n\t\t\tif (lastRetry) await callbacks?.onRetryFinished?.(false, lastRetry.attempt, response.errorMessage);\n\t\t\treturn response;\n\t\t}\n\n\t\tattempt++;\n\t\tlastRetry = { attempt, errorMessage: response.errorMessage || \"Unknown error\" };\n\t\tconst delayMs = policy!.baseDelayMs * 2 ** (attempt - 1);\n\t\tawait callbacks?.onRetryScheduled?.(attempt, maxAttempts, delayMs, lastRetry.errorMessage);\n\n\t\t// Normalize aborts during retry backoff to the same AssistantMessage shape as\n\t\t// provider stream aborts, so callers do not need to care when cancellation happened.\n\t\ttry {\n\t\t\tawait sleep(delayMs, signal);\n\t\t} catch (error) {\n\t\t\tawait callbacks?.onRetryFinished?.(false, attempt, lastRetry.errorMessage);\n\t\t\tif (error instanceof RetrySleepAbortError) {\n\t\t\t\treturn { ...response, stopReason: \"aborted\", errorMessage: undefined };\n\t\t\t}\n\t\t\tthrow error;\n\t\t}\n\t\tawait callbacks?.onRetryAttemptStart?.();\n\t}\n}\n\n/**\n * Classifies whether a failed assistant message looks like a transient provider\n * or transport error, so callers can decide if the last assistant turn should be\n * restarted.\n *\n * This does not implement retry policy. Callers should first handle context\n * overflow separately, then apply their own retry budget, backoff, and reporting\n * before restarting the assistant turn.\n */\nexport function isRetryableAssistantError(message: AssistantMessage): boolean {\n\tif (message.stopReason !== \"error\" || !message.errorMessage) return false;\n\tconst errorMessage = message.errorMessage;\n\tif (NON_RETRYABLE_PROVIDER_LIMIT_ERROR_PATTERN.test(errorMessage)) return false;\n\treturn RETRYABLE_PROVIDER_ERROR_PATTERN.test(errorMessage);\n}\n"]}
1
+ {"version":3,"file":"retry.js","sourceRoot":"","sources":["../../src/utils/retry.ts"],"names":[],"mappings":"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","sourcesContent":["import type { AssistantMessage } from \"../types.ts\";\n\nfunction buildProviderErrorPattern(patterns: readonly string[]): RegExp {\n\treturn new RegExp(patterns.join(\"|\"), \"i\");\n}\n\nconst NON_RETRYABLE_PROVIDER_LIMIT_ERROR_PATTERN = buildProviderErrorPattern([\n\t// OpenCode Go/free-tier limits returned as 429 JSON error types by OpenCode's\n\t// Zen API. These are subscription/account limits, not transient throttles.\n\t\"GoUsageLimitError\",\n\t\"FreeUsageLimitError\",\n\n\t// OpenCode Go subscription-limit text asks users to enable available-balance\n\t// usage after rolling/weekly/monthly limits are reached.\n\t\"Monthly usage limit reached\",\n\t\"available balance\",\n\n\t// Generic quota/budget/billing exhaustion. `insufficient_quota` is OpenAI's\n\t// quota/billing error code; the other strings cover common gateway wording.\n\t\"insufficient_quota\",\n\t\"out of budget\",\n\t\"quota exceeded\",\n\t\"billing\",\n]);\n\nconst RETRYABLE_PROVIDER_ERROR_PATTERN = buildProviderErrorPattern([\n\t// Generic provider load, HTTP status, and server-side transient failures.\n\t\"overloaded\",\n\t\"rate.?limit\",\n\t\"too many requests\",\n\t\"429\",\n\t\"500\",\n\t\"502\",\n\t\"503\",\n\t\"504\",\n\t\"524\",\n\t\"service.?unavailable\",\n\t\"server.?error\",\n\t\"internal.?error\",\n\n\t// Wrapper/provider text for transient upstream failures, including OpenRouter\n\t// \"Provider returned error\" responses (#2264).\n\t\"provider.?returned.?error\",\n\t\"exceeded request buffer limit while retrying upstream\",\n\n\t// Network, proxy, and fetch transport failures. This includes OpenAI Codex\n\t// raw-fetch failures such as \"upstream connect\", \"connection refused\", and\n\t// \"reset before headers\" (#733), plus OpenRouter connection drops (#3317).\n\t\"network.?error\",\n\t\"connection.?error\",\n\t\"connection.?refused\",\n\t\"connection.?lost\",\n\t\"other side closed\",\n\t\"fetch failed\",\n\t\"getaddrinfo\",\n\t\"ENOTFOUND\",\n\t\"EAI_AGAIN\",\n\t\"upstream.?connect\",\n\t\"reset before headers\",\n\t\"socket hang up\",\n\t\"socket connection was closed\",\n\t\"timed? out\",\n\t\"timeout\",\n\t\"terminated\",\n\n\t// WebSocket transports can report close/error text instead of HTTP/fetch text.\n\t\"websocket.?closed\",\n\t\"websocket.?error\",\n\n\t// Premature stream endings from SDKs and transports. Anthropic can throw\n\t// \"stream ended without ...\" and \"Anthropic stream ended before message_stop\"\n\t// (#4433); Bedrock/Smithy can throw an HTTP/2 no-response error (#3594).\n\t\"ended without\",\n\t\"stream ended before message_stop\",\n\t\"stream ended before a terminal response event\",\n\t\"http2 request did not get a response\",\n\n\t// Provider-requested retry delay cap failures should flow through the outer\n\t// retry policy so callers can surface/abort the backoff (#1123).\n\t\"retry delay\",\n\n\t// Explicit retry guidance emitted mid-stream by OpenAI Responses and Bedrock\n\t// stream exceptions (#6019).\n\t\"you can retry your request\",\n\t\"try your request again\",\n\t\"please retry your request\",\n\n\t// gRPC based providers (e.g. NVIDIA NIM)\n\t\"ResourceExhausted\",\n]);\n\n/**\n * Retry policy: bounded attempts with exponential backoff (`baseDelayMs * 2^(attempt-1)`).\n * Matches `settings.retry` (`enabled`, `maxRetries`, `baseDelayMs`) in coding-agent; kept\n * here so the classifier and the policy-driven retry loop live together and stay reusable\n * by the SDK and other callers.\n */\nexport interface RetryPolicy {\n\tenabled: boolean;\n\t/** Max retry attempts (0 = no retries). The initial call never counts as a retry. */\n\tmaxRetries: number;\n\t/** Base delay in ms. Per-attempt delay is `baseDelayMs * 2^(attempt-1)` before jitter. */\n\tbaseDelayMs: number;\n}\n\n/** Optional callbacks emitted by {@link retryAssistantCall} around each retry. */\nexport interface RetryCallbacks {\n\t/** Emitted before the backoff sleep of each retry attempt (1-indexed). */\n\tonRetryScheduled?: (\n\t\tattempt: number,\n\t\tmaxAttempts: number,\n\t\tdelayMs: number,\n\t\terrorMessage: string,\n\t) => void | Promise<void>;\n\t/** Emitted after the backoff sleep, immediately before the retried call starts. */\n\tonRetryAttemptStart?: () => void | Promise<void>;\n\t/** Emitted once when the loop ends: success if a later call completed normally. */\n\tonRetryFinished?: (success: boolean, attempt: number, finalError?: string) => void | Promise<void>;\n}\n\nclass RetrySleepAbortError extends Error {\n\tconstructor() {\n\t\tsuper(\"Aborted\");\n\t}\n}\n\nfunction sleep(ms: number, signal?: AbortSignal): Promise<void> {\n\treturn new Promise((resolve, reject) => {\n\t\tif (signal?.aborted) {\n\t\t\treject(new RetrySleepAbortError());\n\t\t\treturn;\n\t\t}\n\t\tconst timeout = setTimeout(resolve, ms);\n\t\tsignal?.addEventListener(\n\t\t\t\"abort\",\n\t\t\t() => {\n\t\t\t\tclearTimeout(timeout);\n\t\t\t\treject(new RetrySleepAbortError());\n\t\t\t},\n\t\t\t{ once: true },\n\t\t);\n\t});\n}\n\n/**\n * Run a single assistant-producing call with bounded retry on transient errors.\n *\n * Behavior:\n * - A successful response is returned immediately. Aborts are terminal and never\n * retried, but reported as unsuccessful if they happen after a retry was scheduled.\n * Aborts during the backoff sleep are normalized to an aborted `AssistantMessage`\n * too, so callers do not need to care when cancellation happened.\n * - A non-retryable error (per {@link isRetryableAssistantError}, including quota/\n * billing exhaustion) is returned immediately so deterministic errors fail fast.\n * - Otherwise retries up to `maxRetries` times with exponential backoff, emitting\n * `onRetryScheduled` before each sleep, `onRetryAttemptStart` after each sleep before\n * the retried call starts, and `onRetryFinished` once at the end (whether the loop\n * ends in success, exhausted retries, or an aborted backoff).\n *\n * When `policy` is undefined or disabled, the first response is returned unchanged\n * (equivalent to calling `produce()` directly).\n */\nexport async function retryAssistantCall(\n\tproduce: () => Promise<AssistantMessage>,\n\tpolicy: RetryPolicy | undefined,\n\tsignal: AbortSignal | undefined,\n\tcallbacks?: RetryCallbacks,\n): Promise<AssistantMessage> {\n\tconst maxAttempts = policy?.enabled ? policy.maxRetries : 0;\n\n\tlet attempt = 0;\n\tlet lastRetry: { attempt: number; errorMessage: string } | undefined;\n\tfor (;;) {\n\t\tconst response = await produce();\n\n\t\t// Abort: terminal but not successful. Never retry an aborted message.\n\t\tif (response.stopReason === \"aborted\") {\n\t\t\tif (lastRetry) await callbacks?.onRetryFinished?.(false, lastRetry.attempt);\n\t\t\treturn response;\n\t\t}\n\n\t\t// Success: non-error, non-abort responses return as-is.\n\t\tif (response.stopReason !== \"error\") {\n\t\t\tif (lastRetry) await callbacks?.onRetryFinished?.(true, lastRetry.attempt);\n\t\t\treturn response;\n\t\t}\n\n\t\t// Non-retryable, or budget exhausted: return the final error message.\n\t\tif (attempt >= maxAttempts || !isRetryableAssistantError(response)) {\n\t\t\tif (lastRetry) await callbacks?.onRetryFinished?.(false, lastRetry.attempt, response.errorMessage);\n\t\t\treturn response;\n\t\t}\n\n\t\tattempt++;\n\t\tlastRetry = { attempt, errorMessage: response.errorMessage || \"Unknown error\" };\n\t\tconst delayMs = policy!.baseDelayMs * 2 ** (attempt - 1);\n\t\tawait callbacks?.onRetryScheduled?.(attempt, maxAttempts, delayMs, lastRetry.errorMessage);\n\n\t\t// Normalize aborts during retry backoff to the same AssistantMessage shape as\n\t\t// provider stream aborts, so callers do not need to care when cancellation happened.\n\t\ttry {\n\t\t\tawait sleep(delayMs, signal);\n\t\t} catch (error) {\n\t\t\tawait callbacks?.onRetryFinished?.(false, attempt, lastRetry.errorMessage);\n\t\t\tif (error instanceof RetrySleepAbortError) {\n\t\t\t\tconst { errorMessage: _errorMessage, ...rest } = response;\n\t\t\t\treturn { ...rest, stopReason: \"aborted\" };\n\t\t\t}\n\t\t\tthrow error;\n\t\t}\n\t\tawait callbacks?.onRetryAttemptStart?.();\n\t}\n}\n\n/**\n * Classifies whether a failed assistant message looks like a transient provider\n * or transport error, so callers can decide if the last assistant turn should be\n * restarted.\n *\n * This does not implement retry policy. Callers should first handle context\n * overflow separately, then apply their own retry budget, backoff, and reporting\n * before restarting the assistant turn.\n */\nexport function isRetryableAssistantError(message: AssistantMessage): boolean {\n\tif (message.stopReason !== \"error\" || !message.errorMessage) return false;\n\tconst errorMessage = message.errorMessage;\n\tif (NON_RETRYABLE_PROVIDER_LIMIT_ERROR_PATTERN.test(errorMessage)) return false;\n\treturn RETRYABLE_PROVIDER_ERROR_PATTERN.test(errorMessage);\n}\n"]}
@@ -1,3 +1,3 @@
1
- /** Generate a time-ordered UUIDv7. */
2
- export declare function uuidv7(): string;
1
+ /** Generate a time-ordered UUIDv7. A supplied timestamp is preserved for follower ids. */
2
+ export declare function uuidv7(timestampMs?: number): string;
3
3
  //# sourceMappingURL=uuid.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"uuid.d.ts","sourceRoot":"","sources":["../../src/utils/uuid.ts"],"names":[],"mappings":"AAaA,sCAAsC;AACtC,wBAAgB,MAAM,IAAI,MAAM,CAiC/B","sourcesContent":["let lastTimestamp = -Infinity;\nlet sequence = 0;\n\nfunction fillRandomBytes(bytes: Uint8Array<ArrayBuffer>): void {\n\tif (globalThis.crypto?.getRandomValues) {\n\t\tglobalThis.crypto.getRandomValues(bytes);\n\t\treturn;\n\t}\n\tfor (let i = 0; i < bytes.length; i++) {\n\t\tbytes[i] = Math.floor(Math.random() * 256);\n\t}\n}\n\n/** Generate a time-ordered UUIDv7. */\nexport function uuidv7(): string {\n\tconst random = new Uint8Array(16);\n\tfillRandomBytes(random);\n\tconst timestamp = Date.now();\n\n\tif (timestamp > lastTimestamp) {\n\t\tsequence = random[6] * 0x1000000 + random[7] * 0x10000 + random[8] * 0x100 + random[9];\n\t\tlastTimestamp = timestamp;\n\t} else {\n\t\tsequence = (sequence + 1) >>> 0;\n\t\tif (sequence === 0) lastTimestamp++;\n\t}\n\n\tconst bytes = new Uint8Array(16);\n\tbytes[0] = (lastTimestamp / 0x10000000000) & 0xff;\n\tbytes[1] = (lastTimestamp / 0x100000000) & 0xff;\n\tbytes[2] = (lastTimestamp / 0x1000000) & 0xff;\n\tbytes[3] = (lastTimestamp / 0x10000) & 0xff;\n\tbytes[4] = (lastTimestamp / 0x100) & 0xff;\n\tbytes[5] = lastTimestamp & 0xff;\n\tbytes[6] = 0x70 | ((sequence >>> 28) & 0x0f);\n\tbytes[7] = (sequence >>> 20) & 0xff;\n\tbytes[8] = 0x80 | ((sequence >>> 14) & 0x3f);\n\tbytes[9] = (sequence >>> 6) & 0xff;\n\tbytes[10] = ((sequence & 0x3f) << 2) | (random[10] & 0x03);\n\tbytes[11] = random[11];\n\tbytes[12] = random[12];\n\tbytes[13] = random[13];\n\tbytes[14] = random[14];\n\tbytes[15] = random[15];\n\n\tconst hex = Array.from(bytes, (byte) => byte.toString(16).padStart(2, \"0\"));\n\treturn `${hex.slice(0, 4).join(\"\")}-${hex.slice(4, 6).join(\"\")}-${hex.slice(6, 8).join(\"\")}-${hex.slice(8, 10).join(\"\")}-${hex.slice(10, 16).join(\"\")}`;\n}\n"]}
1
+ {"version":3,"file":"uuid.d.ts","sourceRoot":"","sources":["../../src/utils/uuid.ts"],"names":[],"mappings":"AAMA,0FAA0F;AAC1F,wBAAgB,MAAM,CAAC,WAAW,CAAC,EAAE,MAAM,GAAG,MAAM,CAqCnD","sourcesContent":["const MAX_UUID_V7_TIMESTAMP = 0xffffffffffff;\nconst MAX_SEQUENCE = (1n << 41n) - 1n;\n\nlet lastOrdinaryTimestamp = -1;\nlet sequence: bigint | undefined;\n\n/** Generate a time-ordered UUIDv7. A supplied timestamp is preserved for follower ids. */\nexport function uuidv7(timestampMs?: number): string {\n\tconst requestedTimestamp = timestampMs ?? Date.now();\n\tif (!Number.isInteger(requestedTimestamp) || requestedTimestamp < 0 || requestedTimestamp > MAX_UUID_V7_TIMESTAMP) {\n\t\tthrow new RangeError(`UUIDv7 timestamp must be an integer between 0 and ${MAX_UUID_V7_TIMESTAMP}`);\n\t}\n\n\tconst effectiveTimestamp =\n\t\ttimestampMs === undefined ? Math.max(requestedTimestamp, lastOrdinaryTimestamp) : timestampMs;\n\tif (timestampMs === undefined) lastOrdinaryTimestamp = effectiveTimestamp;\n\n\tconst bytes = new Uint8Array(16);\n\tglobalThis.crypto.getRandomValues(bytes);\n\tif (sequence === undefined) {\n\t\tsequence =\n\t\t\t(BigInt(bytes[1]) << 32n) |\n\t\t\t(BigInt(bytes[2]) << 24n) |\n\t\t\t(BigInt(bytes[3]) << 16n) |\n\t\t\t(BigInt(bytes[4]) << 8n) |\n\t\t\tBigInt(bytes[5]);\n\t} else {\n\t\tif (sequence === MAX_SEQUENCE) throw new RangeError(\"UUIDv7 generator sequence exhausted\");\n\t\tsequence++;\n\t}\n\n\tconst timestamp = BigInt(effectiveTimestamp);\n\tfor (let index = 5; index >= 0; index--) {\n\t\tbytes[index] = Number(timestamp >> BigInt((5 - index) * 8)) & 0xff;\n\t}\n\tbytes[6] = 0x70 | Number((sequence >> 37n) & 0x0fn);\n\tbytes[7] = Number((sequence >> 29n) & 0xffn);\n\tbytes[8] = 0x80 | Number((sequence >> 23n) & 0x3fn);\n\tbytes[9] = Number((sequence >> 15n) & 0xffn);\n\tbytes[10] = Number((sequence >> 7n) & 0xffn);\n\tbytes[11] = Number((sequence & 0x7fn) << 1n) | (bytes[11] & 0x01);\n\n\tconst hex = Array.from(bytes, (byte) => byte.toString(16).padStart(2, \"0\"));\n\treturn `${hex.slice(0, 4).join(\"\")}-${hex.slice(4, 6).join(\"\")}-${hex.slice(6, 8).join(\"\")}-${hex.slice(8, 10).join(\"\")}-${hex.slice(10).join(\"\")}`;\n}\n"]}
@@ -1,46 +1,42 @@
1
- let lastTimestamp = -Infinity;
2
- let sequence = 0;
3
- function fillRandomBytes(bytes) {
4
- if (globalThis.crypto?.getRandomValues) {
5
- globalThis.crypto.getRandomValues(bytes);
6
- return;
1
+ const MAX_UUID_V7_TIMESTAMP = 0xffffffffffff;
2
+ const MAX_SEQUENCE = (1n << 41n) - 1n;
3
+ let lastOrdinaryTimestamp = -1;
4
+ let sequence;
5
+ /** Generate a time-ordered UUIDv7. A supplied timestamp is preserved for follower ids. */
6
+ export function uuidv7(timestampMs) {
7
+ const requestedTimestamp = timestampMs ?? Date.now();
8
+ if (!Number.isInteger(requestedTimestamp) || requestedTimestamp < 0 || requestedTimestamp > MAX_UUID_V7_TIMESTAMP) {
9
+ throw new RangeError(`UUIDv7 timestamp must be an integer between 0 and ${MAX_UUID_V7_TIMESTAMP}`);
7
10
  }
8
- for (let i = 0; i < bytes.length; i++) {
9
- bytes[i] = Math.floor(Math.random() * 256);
10
- }
11
- }
12
- /** Generate a time-ordered UUIDv7. */
13
- export function uuidv7() {
14
- const random = new Uint8Array(16);
15
- fillRandomBytes(random);
16
- const timestamp = Date.now();
17
- if (timestamp > lastTimestamp) {
18
- sequence = random[6] * 0x1000000 + random[7] * 0x10000 + random[8] * 0x100 + random[9];
19
- lastTimestamp = timestamp;
11
+ const effectiveTimestamp = timestampMs === undefined ? Math.max(requestedTimestamp, lastOrdinaryTimestamp) : timestampMs;
12
+ if (timestampMs === undefined)
13
+ lastOrdinaryTimestamp = effectiveTimestamp;
14
+ const bytes = new Uint8Array(16);
15
+ globalThis.crypto.getRandomValues(bytes);
16
+ if (sequence === undefined) {
17
+ sequence =
18
+ (BigInt(bytes[1]) << 32n) |
19
+ (BigInt(bytes[2]) << 24n) |
20
+ (BigInt(bytes[3]) << 16n) |
21
+ (BigInt(bytes[4]) << 8n) |
22
+ BigInt(bytes[5]);
20
23
  }
21
24
  else {
22
- sequence = (sequence + 1) >>> 0;
23
- if (sequence === 0)
24
- lastTimestamp++;
25
+ if (sequence === MAX_SEQUENCE)
26
+ throw new RangeError("UUIDv7 generator sequence exhausted");
27
+ sequence++;
25
28
  }
26
- const bytes = new Uint8Array(16);
27
- bytes[0] = (lastTimestamp / 0x10000000000) & 0xff;
28
- bytes[1] = (lastTimestamp / 0x100000000) & 0xff;
29
- bytes[2] = (lastTimestamp / 0x1000000) & 0xff;
30
- bytes[3] = (lastTimestamp / 0x10000) & 0xff;
31
- bytes[4] = (lastTimestamp / 0x100) & 0xff;
32
- bytes[5] = lastTimestamp & 0xff;
33
- bytes[6] = 0x70 | ((sequence >>> 28) & 0x0f);
34
- bytes[7] = (sequence >>> 20) & 0xff;
35
- bytes[8] = 0x80 | ((sequence >>> 14) & 0x3f);
36
- bytes[9] = (sequence >>> 6) & 0xff;
37
- bytes[10] = ((sequence & 0x3f) << 2) | (random[10] & 0x03);
38
- bytes[11] = random[11];
39
- bytes[12] = random[12];
40
- bytes[13] = random[13];
41
- bytes[14] = random[14];
42
- bytes[15] = random[15];
29
+ const timestamp = BigInt(effectiveTimestamp);
30
+ for (let index = 5; index >= 0; index--) {
31
+ bytes[index] = Number(timestamp >> BigInt((5 - index) * 8)) & 0xff;
32
+ }
33
+ bytes[6] = 0x70 | Number((sequence >> 37n) & 0x0fn);
34
+ bytes[7] = Number((sequence >> 29n) & 0xffn);
35
+ bytes[8] = 0x80 | Number((sequence >> 23n) & 0x3fn);
36
+ bytes[9] = Number((sequence >> 15n) & 0xffn);
37
+ bytes[10] = Number((sequence >> 7n) & 0xffn);
38
+ bytes[11] = Number((sequence & 0x7fn) << 1n) | (bytes[11] & 0x01);
43
39
  const hex = Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0"));
44
- return `${hex.slice(0, 4).join("")}-${hex.slice(4, 6).join("")}-${hex.slice(6, 8).join("")}-${hex.slice(8, 10).join("")}-${hex.slice(10, 16).join("")}`;
40
+ return `${hex.slice(0, 4).join("")}-${hex.slice(4, 6).join("")}-${hex.slice(6, 8).join("")}-${hex.slice(8, 10).join("")}-${hex.slice(10).join("")}`;
45
41
  }
46
42
  //# sourceMappingURL=uuid.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"uuid.js","sourceRoot":"","sources":["../../src/utils/uuid.ts"],"names":[],"mappings":"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","sourcesContent":["let lastTimestamp = -Infinity;\nlet sequence = 0;\n\nfunction fillRandomBytes(bytes: Uint8Array<ArrayBuffer>): void {\n\tif (globalThis.crypto?.getRandomValues) {\n\t\tglobalThis.crypto.getRandomValues(bytes);\n\t\treturn;\n\t}\n\tfor (let i = 0; i < bytes.length; i++) {\n\t\tbytes[i] = Math.floor(Math.random() * 256);\n\t}\n}\n\n/** Generate a time-ordered UUIDv7. */\nexport function uuidv7(): string {\n\tconst random = new Uint8Array(16);\n\tfillRandomBytes(random);\n\tconst timestamp = Date.now();\n\n\tif (timestamp > lastTimestamp) {\n\t\tsequence = random[6] * 0x1000000 + random[7] * 0x10000 + random[8] * 0x100 + random[9];\n\t\tlastTimestamp = timestamp;\n\t} else {\n\t\tsequence = (sequence + 1) >>> 0;\n\t\tif (sequence === 0) lastTimestamp++;\n\t}\n\n\tconst bytes = new Uint8Array(16);\n\tbytes[0] = (lastTimestamp / 0x10000000000) & 0xff;\n\tbytes[1] = (lastTimestamp / 0x100000000) & 0xff;\n\tbytes[2] = (lastTimestamp / 0x1000000) & 0xff;\n\tbytes[3] = (lastTimestamp / 0x10000) & 0xff;\n\tbytes[4] = (lastTimestamp / 0x100) & 0xff;\n\tbytes[5] = lastTimestamp & 0xff;\n\tbytes[6] = 0x70 | ((sequence >>> 28) & 0x0f);\n\tbytes[7] = (sequence >>> 20) & 0xff;\n\tbytes[8] = 0x80 | ((sequence >>> 14) & 0x3f);\n\tbytes[9] = (sequence >>> 6) & 0xff;\n\tbytes[10] = ((sequence & 0x3f) << 2) | (random[10] & 0x03);\n\tbytes[11] = random[11];\n\tbytes[12] = random[12];\n\tbytes[13] = random[13];\n\tbytes[14] = random[14];\n\tbytes[15] = random[15];\n\n\tconst hex = Array.from(bytes, (byte) => byte.toString(16).padStart(2, \"0\"));\n\treturn `${hex.slice(0, 4).join(\"\")}-${hex.slice(4, 6).join(\"\")}-${hex.slice(6, 8).join(\"\")}-${hex.slice(8, 10).join(\"\")}-${hex.slice(10, 16).join(\"\")}`;\n}\n"]}
1
+ {"version":3,"file":"uuid.js","sourceRoot":"","sources":["../../src/utils/uuid.ts"],"names":[],"mappings":"AAAA,MAAM,qBAAqB,GAAG,cAAc,CAAC;AAC7C,MAAM,YAAY,GAAG,CAAC,EAAE,IAAI,GAAG,CAAC,GAAG,EAAE,CAAC;AAEtC,IAAI,qBAAqB,GAAG,CAAC,CAAC,CAAC;AAC/B,IAAI,QAA4B,CAAC;AAEjC,0FAA0F;AAC1F,MAAM,UAAU,MAAM,CAAC,WAAoB,EAAU;IACpD,MAAM,kBAAkB,GAAG,WAAW,IAAI,IAAI,CAAC,GAAG,EAAE,CAAC;IACrD,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,kBAAkB,CAAC,IAAI,kBAAkB,GAAG,CAAC,IAAI,kBAAkB,GAAG,qBAAqB,EAAE,CAAC;QACnH,MAAM,IAAI,UAAU,CAAC,qDAAqD,qBAAqB,EAAE,CAAC,CAAC;IACpG,CAAC;IAED,MAAM,kBAAkB,GACvB,WAAW,KAAK,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,kBAAkB,EAAE,qBAAqB,CAAC,CAAC,CAAC,CAAC,WAAW,CAAC;IAC/F,IAAI,WAAW,KAAK,SAAS;QAAE,qBAAqB,GAAG,kBAAkB,CAAC;IAE1E,MAAM,KAAK,GAAG,IAAI,UAAU,CAAC,EAAE,CAAC,CAAC;IACjC,UAAU,CAAC,MAAM,CAAC,eAAe,CAAC,KAAK,CAAC,CAAC;IACzC,IAAI,QAAQ,KAAK,SAAS,EAAE,CAAC;QAC5B,QAAQ;YACP,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,GAAG,CAAC;gBACzB,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,GAAG,CAAC;gBACzB,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,GAAG,CAAC;gBACzB,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;gBACxB,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;IACnB,CAAC;SAAM,CAAC;QACP,IAAI,QAAQ,KAAK,YAAY;YAAE,MAAM,IAAI,UAAU,CAAC,qCAAqC,CAAC,CAAC;QAC3F,QAAQ,EAAE,CAAC;IACZ,CAAC;IAED,MAAM,SAAS,GAAG,MAAM,CAAC,kBAAkB,CAAC,CAAC;IAC7C,KAAK,IAAI,KAAK,GAAG,CAAC,EAAE,KAAK,IAAI,CAAC,EAAE,KAAK,EAAE,EAAE,CAAC;QACzC,KAAK,CAAC,KAAK,CAAC,GAAG,MAAM,CAAC,SAAS,IAAI,MAAM,CAAC,CAAC,CAAC,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC;IACpE,CAAC;IACD,KAAK,CAAC,CAAC,CAAC,GAAG,IAAI,GAAG,MAAM,CAAC,CAAC,QAAQ,IAAI,GAAG,CAAC,GAAG,KAAK,CAAC,CAAC;IACpD,KAAK,CAAC,CAAC,CAAC,GAAG,MAAM,CAAC,CAAC,QAAQ,IAAI,GAAG,CAAC,GAAG,KAAK,CAAC,CAAC;IAC7C,KAAK,CAAC,CAAC,CAAC,GAAG,IAAI,GAAG,MAAM,CAAC,CAAC,QAAQ,IAAI,GAAG,CAAC,GAAG,KAAK,CAAC,CAAC;IACpD,KAAK,CAAC,CAAC,CAAC,GAAG,MAAM,CAAC,CAAC,QAAQ,IAAI,GAAG,CAAC,GAAG,KAAK,CAAC,CAAC;IAC7C,KAAK,CAAC,EAAE,CAAC,GAAG,MAAM,CAAC,CAAC,QAAQ,IAAI,EAAE,CAAC,GAAG,KAAK,CAAC,CAAC;IAC7C,KAAK,CAAC,EAAE,CAAC,GAAG,MAAM,CAAC,CAAC,QAAQ,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC,GAAG,IAAI,CAAC,CAAC;IAElE,MAAM,GAAG,GAAG,KAAK,CAAC,IAAI,CAAC,KAAK,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC;IAC5E,OAAO,GAAG,GAAG,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,GAAG,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,GAAG,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,GAAG,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,EAAE,CAAC;AAAA,CACpJ","sourcesContent":["const MAX_UUID_V7_TIMESTAMP = 0xffffffffffff;\nconst MAX_SEQUENCE = (1n << 41n) - 1n;\n\nlet lastOrdinaryTimestamp = -1;\nlet sequence: bigint | undefined;\n\n/** Generate a time-ordered UUIDv7. A supplied timestamp is preserved for follower ids. */\nexport function uuidv7(timestampMs?: number): string {\n\tconst requestedTimestamp = timestampMs ?? Date.now();\n\tif (!Number.isInteger(requestedTimestamp) || requestedTimestamp < 0 || requestedTimestamp > MAX_UUID_V7_TIMESTAMP) {\n\t\tthrow new RangeError(`UUIDv7 timestamp must be an integer between 0 and ${MAX_UUID_V7_TIMESTAMP}`);\n\t}\n\n\tconst effectiveTimestamp =\n\t\ttimestampMs === undefined ? Math.max(requestedTimestamp, lastOrdinaryTimestamp) : timestampMs;\n\tif (timestampMs === undefined) lastOrdinaryTimestamp = effectiveTimestamp;\n\n\tconst bytes = new Uint8Array(16);\n\tglobalThis.crypto.getRandomValues(bytes);\n\tif (sequence === undefined) {\n\t\tsequence =\n\t\t\t(BigInt(bytes[1]) << 32n) |\n\t\t\t(BigInt(bytes[2]) << 24n) |\n\t\t\t(BigInt(bytes[3]) << 16n) |\n\t\t\t(BigInt(bytes[4]) << 8n) |\n\t\t\tBigInt(bytes[5]);\n\t} else {\n\t\tif (sequence === MAX_SEQUENCE) throw new RangeError(\"UUIDv7 generator sequence exhausted\");\n\t\tsequence++;\n\t}\n\n\tconst timestamp = BigInt(effectiveTimestamp);\n\tfor (let index = 5; index >= 0; index--) {\n\t\tbytes[index] = Number(timestamp >> BigInt((5 - index) * 8)) & 0xff;\n\t}\n\tbytes[6] = 0x70 | Number((sequence >> 37n) & 0x0fn);\n\tbytes[7] = Number((sequence >> 29n) & 0xffn);\n\tbytes[8] = 0x80 | Number((sequence >> 23n) & 0x3fn);\n\tbytes[9] = Number((sequence >> 15n) & 0xffn);\n\tbytes[10] = Number((sequence >> 7n) & 0xffn);\n\tbytes[11] = Number((sequence & 0x7fn) << 1n) | (bytes[11] & 0x01);\n\n\tconst hex = Array.from(bytes, (byte) => byte.toString(16).padStart(2, \"0\"));\n\treturn `${hex.slice(0, 4).join(\"\")}-${hex.slice(4, 6).join(\"\")}-${hex.slice(6, 8).join(\"\")}-${hex.slice(8, 10).join(\"\")}-${hex.slice(10).join(\"\")}`;\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@earendil-works/pi-ai",
3
- "version": "0.84.4",
3
+ "version": "0.85.1",
4
4
  "description": "Unified LLM API with automatic model discovery and provider configuration",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -27,6 +27,10 @@
27
27
  "types": "./dist/api/*.d.ts",
28
28
  "import": "./dist/api/*.js"
29
29
  },
30
+ "./utils/*": {
31
+ "types": "./dist/utils/*.d.ts",
32
+ "import": "./dist/utils/*.js"
33
+ },
30
34
  "./oauth": {
31
35
  "types": "./dist/oauth.d.ts",
32
36
  "import": "./dist/oauth.js"
@@ -60,9 +64,9 @@
60
64
  "prepublishOnly": "npm run clean && npm run build"
61
65
  },
62
66
  "dependencies": {
63
- "@anthropic-ai/sdk": "0.91.1",
67
+ "@anthropic-ai/sdk": "0.123.0",
64
68
  "@aws-sdk/client-bedrock-runtime": "3.1048.0",
65
- "@earendil-works/pi-telemetry": "^0.84.4",
69
+ "@earendil-works/pi-telemetry": "^0.85.1",
66
70
  "@google/genai": "1.52.0",
67
71
  "@smithy/node-http-handler": "4.7.3",
68
72
  "http-proxy-agent": "7.0.2",
@@ -1,69 +0,0 @@
1
- /**
2
- * AI Gateway transport over the Workers AI binding.
3
- *
4
- * pi's Cloudflare AI Gateway support speaks HTTPS
5
- * (`gateway.ai.cloudflare.com/v1/{account}/{gateway}/{provider}/...`, see `api/cloudflare.ts`),
6
- * which needs a Cloudflare API token even when the caller is a Worker in the gateway's own
7
- * account.
8
- *
9
- * In order to solve for this problem, `createGatewayBindingFetch` returns a {@link FetchFunction}
10
- * that translates requests under a gateway HTTPS prefix into calls to the Workers AI binding's
11
- * universal endpoint, `env.AI.gateway(id).run({provider, endpoint, headers, query})`.
12
- * Binding calls are pre-authenticated in-account and return the provider's native wire format as a
13
- * regular (streaming) `Response`, so API implementations behave identically over either
14
- * transport.
15
- *
16
- * The result is the transport for one gateway-bound client, not a general-purpose fetch:
17
- * requests it cannot serve — URLs outside the prefix, or in-prefix requests the universal
18
- * endpoint cannot express (non-POST, non-JSON body) — reject with a descriptive error.
19
- * Transport selection is the caller's job, per client: route such traffic over HTTPS with
20
- * real gateway auth instead of through this shim.
21
- */
22
- import type { FetchFunction } from "../types.ts";
23
- /**
24
- * Structural type for the Workers AI binding's gateway surface (`env.AI`), so this
25
- * module does not depend on `@cloudflare/workers-types`. Any real `Ai` binding satisfies it.
26
- */
27
- export interface AiGatewayBinding {
28
- gateway(id: string): AiGatewayBindingGateway;
29
- }
30
- export interface AiGatewayBindingGateway {
31
- run(data: AiGatewayUniversalRequestLike, options?: {
32
- signal?: AbortSignal;
33
- }): Promise<Response>;
34
- }
35
- /** One universal-endpoint request entry, as accepted by `AiGateway.run()`. */
36
- export interface AiGatewayUniversalRequestLike {
37
- provider: string;
38
- endpoint: string;
39
- headers: Record<string, string>;
40
- query: unknown;
41
- }
42
- /**
43
- * Placeholder value for auth headers on binding-routed requests. API implementations
44
- * require an API key or a recognized auth header (`authorization`, `x-api-key`,
45
- * `cf-aig-authorization`) before dispatch; binding calls are pre-authenticated, so pass
46
- * `cf-aig-authorization: Bearer ${CLOUDFLARE_GATEWAY_BINDING_AUTH_SENTINEL}` to satisfy
47
- * the check. The shim strips `cf-aig-authorization` before calling the binding. Pair it with
48
- * `Authorization: null` / `x-api-key: null` so the SDKs' placeholder auth headers never reach
49
- * the gateway, which would treat a request-supplied auth header as a BYOK provider key that
50
- * overrides its stored keys — the same as it would over HTTPS.
51
- */
52
- export declare const CLOUDFLARE_GATEWAY_BINDING_AUTH_SENTINEL = "cloudflare-gateway-binding";
53
- export interface GatewayBindingFetchOptions {
54
- /** The Workers AI binding (e.g. `env.AI`). */
55
- binding: AiGatewayBinding;
56
- /**
57
- * Gateway HTTPS prefix every request must fall under, without a trailing slash:
58
- * `https://gateway.ai.cloudflare.com/v1/{accountId}/{gatewayName}`.
59
- */
60
- baseUrl: string;
61
- /** Gateway name passed to `binding.gateway()`. Must match the `baseUrl` gateway. */
62
- gateway: string;
63
- }
64
- /**
65
- * Create a `fetch` that routes AI Gateway requests through the Workers AI binding.
66
- * See the module docs for behavior and composition notes.
67
- */
68
- export declare function createGatewayBindingFetch(options: GatewayBindingFetchOptions): FetchFunction;
69
- //# sourceMappingURL=cloudflare-gateway-binding.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"cloudflare-gateway-binding.d.ts","sourceRoot":"","sources":["../../src/api/cloudflare-gateway-binding.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAEjD;;;GAGG;AACH,MAAM,WAAW,gBAAgB;IAChC,OAAO,CAAC,EAAE,EAAE,MAAM,GAAG,uBAAuB,CAAC;CAC7C;AAED,MAAM,WAAW,uBAAuB;IACvC,GAAG,CAAC,IAAI,EAAE,6BAA6B,EAAE,OAAO,CAAC,EAAE;QAAE,MAAM,CAAC,EAAE,WAAW,CAAA;KAAE,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;CAChG;AAED,8EAA8E;AAC9E,MAAM,WAAW,6BAA6B;IAC7C,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAChC,KAAK,EAAE,OAAO,CAAC;CACf;AAED;;;;;;;;;GASG;AACH,eAAO,MAAM,wCAAwC,+BAA+B,CAAC;AAErF,MAAM,WAAW,0BAA0B;IAC1C,8CAA8C;IAC9C,OAAO,EAAE,gBAAgB,CAAC;IAC1B;;;OAGG;IACH,OAAO,EAAE,MAAM,CAAC;IAChB,oFAAoF;IACpF,OAAO,EAAE,MAAM,CAAC;CAChB;AAQD;;;GAGG;AACH,wBAAgB,yBAAyB,CAAC,OAAO,EAAE,0BAA0B,GAAG,aAAa,CAgE5F","sourcesContent":["/**\n * AI Gateway transport over the Workers AI binding.\n *\n * pi's Cloudflare AI Gateway support speaks HTTPS\n * (`gateway.ai.cloudflare.com/v1/{account}/{gateway}/{provider}/...`, see `api/cloudflare.ts`),\n * which needs a Cloudflare API token even when the caller is a Worker in the gateway's own\n * account.\n *\n * In order to solve for this problem, `createGatewayBindingFetch` returns a {@link FetchFunction}\n * that translates requests under a gateway HTTPS prefix into calls to the Workers AI binding's\n * universal endpoint, `env.AI.gateway(id).run({provider, endpoint, headers, query})`.\n * Binding calls are pre-authenticated in-account and return the provider's native wire format as a\n * regular (streaming) `Response`, so API implementations behave identically over either\n * transport.\n *\n * The result is the transport for one gateway-bound client, not a general-purpose fetch:\n * requests it cannot serve — URLs outside the prefix, or in-prefix requests the universal\n * endpoint cannot express (non-POST, non-JSON body) — reject with a descriptive error.\n * Transport selection is the caller's job, per client: route such traffic over HTTPS with\n * real gateway auth instead of through this shim.\n */\n\nimport type { FetchFunction } from \"../types.ts\";\n\n/**\n * Structural type for the Workers AI binding's gateway surface (`env.AI`), so this\n * module does not depend on `@cloudflare/workers-types`. Any real `Ai` binding satisfies it.\n */\nexport interface AiGatewayBinding {\n\tgateway(id: string): AiGatewayBindingGateway;\n}\n\nexport interface AiGatewayBindingGateway {\n\trun(data: AiGatewayUniversalRequestLike, options?: { signal?: AbortSignal }): Promise<Response>;\n}\n\n/** One universal-endpoint request entry, as accepted by `AiGateway.run()`. */\nexport interface AiGatewayUniversalRequestLike {\n\tprovider: string;\n\tendpoint: string;\n\theaders: Record<string, string>;\n\tquery: unknown;\n}\n\n/**\n * Placeholder value for auth headers on binding-routed requests. API implementations\n * require an API key or a recognized auth header (`authorization`, `x-api-key`,\n * `cf-aig-authorization`) before dispatch; binding calls are pre-authenticated, so pass\n * `cf-aig-authorization: Bearer ${CLOUDFLARE_GATEWAY_BINDING_AUTH_SENTINEL}` to satisfy\n * the check. The shim strips `cf-aig-authorization` before calling the binding. Pair it with\n * `Authorization: null` / `x-api-key: null` so the SDKs' placeholder auth headers never reach\n * the gateway, which would treat a request-supplied auth header as a BYOK provider key that\n * overrides its stored keys — the same as it would over HTTPS.\n */\nexport const CLOUDFLARE_GATEWAY_BINDING_AUTH_SENTINEL = \"cloudflare-gateway-binding\";\n\nexport interface GatewayBindingFetchOptions {\n\t/** The Workers AI binding (e.g. `env.AI`). */\n\tbinding: AiGatewayBinding;\n\t/**\n\t * Gateway HTTPS prefix every request must fall under, without a trailing slash:\n\t * `https://gateway.ai.cloudflare.com/v1/{accountId}/{gatewayName}`.\n\t */\n\tbaseUrl: string;\n\t/** Gateway name passed to `binding.gateway()`. Must match the `baseUrl` gateway. */\n\tgateway: string;\n}\n\n// Never forwarded to the binding: hop-by-hop/derived headers, and gateway auth\n// (binding calls are pre-authenticated; the sentinel must not reach the wire).\nconst STRIP_HEADERS = new Set([\"content-length\", \"host\", \"cf-aig-authorization\"]);\n\ntype FetchInput = Parameters<FetchFunction>[0];\n\n/**\n * Create a `fetch` that routes AI Gateway requests through the Workers AI binding.\n * See the module docs for behavior and composition notes.\n */\nexport function createGatewayBindingFetch(options: GatewayBindingFetchOptions): FetchFunction {\n\tconst { binding, gateway } = options;\n\t// Prefix matching runs on URL-normalized components (origin + pathname), not raw strings:\n\t// dot segments resolve away and fragments drop, matching what real fetch would put on the\n\t// wire, so a lexical variant can't split provider/endpoint differently than HTTPS would.\n\tconst base = new URL(options.baseUrl);\n\tconst basePath = base.pathname.endsWith(\"/\") ? base.pathname : `${base.pathname}/`;\n\n\treturn async (input: FetchInput, init?: RequestInit): Promise<Response> => {\n\t\tconst request = input instanceof Request ? input : undefined;\n\t\tconst url = request ? request.url : input.toString();\n\t\tconst method = (init?.method ?? request?.method ?? \"GET\").toUpperCase();\n\t\tlet parsed: URL | undefined;\n\t\ttry {\n\t\t\tparsed = new URL(url);\n\t\t} catch {\n\t\t\tparsed = undefined;\n\t\t}\n\t\t// Out-of-prefix URLs are a configuration bug, not passthrough traffic: silently\n\t\t// forwarding would ship the auth sentinel to whatever host the URL names.\n\t\tif (parsed === undefined || parsed.origin !== base.origin || !parsed.pathname.startsWith(basePath)) {\n\t\t\tthrow new Error(\n\t\t\t\t`createGatewayBindingFetch: ${method} ${url} is outside the configured gateway ` +\n\t\t\t\t\t`prefix (${base.origin}${basePath}); this fetch only serves its gateway-bound client`,\n\t\t\t);\n\t\t}\n\n\t\t// In-prefix requests the universal endpoint cannot express always reject: forwarding\n\t\t// them over HTTPS would send the sentinel to the gateway and fail with a misleading\n\t\t// auth error instead of naming the real problem. Callers that need such endpoints\n\t\t// route them over HTTPS with real gateway auth themselves.\n\t\tconst unexpressible = (reason: string): never => {\n\t\t\tthrow new Error(\n\t\t\t\t`createGatewayBindingFetch: cannot express ${method} ${url} as a universal ` +\n\t\t\t\t\t`gateway request (${reason}); route it over HTTPS with gateway auth instead`,\n\t\t\t);\n\t\t};\n\t\tif (method !== \"POST\") return unexpressible(\"only POST is supported\");\n\n\t\tconst rest = parsed.pathname.slice(basePath.length);\n\t\tconst slash = rest.indexOf(\"/\");\n\t\tif (slash <= 0) {\n\t\t\treturn unexpressible(\"missing provider/endpoint path\");\n\t\t}\n\t\tconst provider = rest.slice(0, slash);\n\t\t// Keep the query string on the endpoint — it's part of what HTTPS would have sent.\n\t\tconst endpoint = rest.slice(slash + 1) + parsed.search;\n\n\t\tconst bodyText = await readBodyText(request, init);\n\t\tlet query: unknown;\n\t\ttry {\n\t\t\tquery = bodyText === undefined ? undefined : JSON.parse(bodyText);\n\t\t} catch {\n\t\t\treturn unexpressible(\"non-JSON body\");\n\t\t}\n\t\tif (query === undefined) {\n\t\t\treturn unexpressible(\"missing body\");\n\t\t}\n\n\t\tconst headers = collectHeaders(request, init);\n\t\t// Per the fetch spec an explicit `signal: null` in init clears a Request input's signal.\n\t\tconst signal = init?.signal ?? (init && \"signal\" in init && init.signal === null ? undefined : request?.signal);\n\t\treturn binding.gateway(gateway).run({ provider, endpoint, headers, query }, signal ? { signal } : {});\n\t};\n}\n\nasync function readBodyText(request: Request | undefined, init?: RequestInit): Promise<string | undefined> {\n\tconst body = init?.body;\n\tif (body === undefined || body === null) {\n\t\t// Per the fetch spec an explicit `body: null` in init clears a Request input's body.\n\t\tif (init && \"body\" in init && body === null) return undefined;\n\t\tif (request && request.body !== null) return request.clone().text();\n\t\treturn undefined;\n\t}\n\tif (typeof body === \"string\") return body;\n\tif (body instanceof Uint8Array) return new TextDecoder().decode(body);\n\tif (body instanceof ArrayBuffer) return new TextDecoder().decode(new Uint8Array(body));\n\t// URLSearchParams, FormData, Blob, ReadableStream in init: read via a Request wrapper.\n\t// Consuming a one-shot stream here is fine — unexpressible requests reject rather than\n\t// replay, so nothing downstream needs the body again.\n\treturn new Request(\"http://body.local\", {\n\t\tmethod: \"POST\",\n\t\tbody,\n\t\t// The fetch spec requires `duplex: \"half\"` to construct a Request with a stream body\n\t\t// (Node's undici enforces it; it is ignored for the replayable body types). TypeScript's\n\t\t// RequestInit does not declare the field yet, hence the cast.\n\t\tduplex: \"half\",\n\t} as RequestInit).text();\n}\n\n// Entry header names are lowercased so case-variant duplicates collapse and stripping is\n// uniform. Per the fetch spec, `init.headers` replaces a Request input's headers entirely.\nfunction collectHeaders(request: Request | undefined, init?: RequestInit): Record<string, string> {\n\tconst result: Record<string, string> = {};\n\tconst add = (key: string, value: string) => {\n\t\tconst name = key.toLowerCase();\n\t\tif (!STRIP_HEADERS.has(name)) result[name] = value;\n\t};\n\tconst headers = init?.headers;\n\tif (headers === undefined) {\n\t\tif (request) {\n\t\t\tfor (const [key, value] of request.headers) add(key, value);\n\t\t}\n\t} else if (headers instanceof Headers) {\n\t\tfor (const [key, value] of headers) add(key, value);\n\t} else if (Array.isArray(headers)) {\n\t\tfor (const [key, value] of headers) add(key, value);\n\t} else {\n\t\tfor (const [key, value] of Object.entries(headers)) {\n\t\t\tif (value !== undefined) add(key, String(value));\n\t\t}\n\t}\n\treturn result;\n}\n"]}