@letrquan/book 0.2.0 → 0.4.0-beta.0

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 (177) hide show
  1. package/CHANGELOG.md +3771 -40
  2. package/README.md +135 -1311
  3. package/dist/additional-roots-VAWRQ625.js +32 -0
  4. package/dist/{app-MOXXWEOY.js → app-J7BVPSUH.js} +9648 -5509
  5. package/dist/app-J7BVPSUH.js.map +1 -0
  6. package/dist/chunk-2AAOF5B5.js +28 -0
  7. package/dist/chunk-2AAOF5B5.js.map +1 -0
  8. package/dist/chunk-32WBCUAN.js +660 -0
  9. package/dist/chunk-32WBCUAN.js.map +1 -0
  10. package/dist/chunk-4DKHLZRI.js +120 -0
  11. package/dist/chunk-4DKHLZRI.js.map +1 -0
  12. package/dist/{chunk-CAGOB2N4.js → chunk-62EEAUID.js} +6 -4
  13. package/dist/chunk-62EEAUID.js.map +1 -0
  14. package/dist/chunk-AHC7OU75.js +1124 -0
  15. package/dist/chunk-AHC7OU75.js.map +1 -0
  16. package/dist/{chunk-E75IPST5.js → chunk-AJZWLKLQ.js} +73 -70
  17. package/dist/chunk-AJZWLKLQ.js.map +1 -0
  18. package/dist/chunk-B5FC3MYZ.js +395 -0
  19. package/dist/chunk-B5FC3MYZ.js.map +1 -0
  20. package/dist/{chunk-KWHQ2XXL.js → chunk-DNFQTSMJ.js} +55 -13
  21. package/dist/chunk-DNFQTSMJ.js.map +1 -0
  22. package/dist/{chunk-J7MIZ5CA.js → chunk-E5WLQSKA.js} +719 -89
  23. package/dist/chunk-E5WLQSKA.js.map +1 -0
  24. package/dist/chunk-ECLC4CIJ.js +1181 -0
  25. package/dist/chunk-ECLC4CIJ.js.map +1 -0
  26. package/dist/chunk-EEX6QNXV.js +127 -0
  27. package/dist/chunk-EEX6QNXV.js.map +1 -0
  28. package/dist/chunk-EFXVSQ5J.js +931 -0
  29. package/dist/chunk-EFXVSQ5J.js.map +1 -0
  30. package/dist/{chunk-V4BOYR52.js → chunk-ENLB7NYV.js} +1304 -326
  31. package/dist/chunk-ENLB7NYV.js.map +1 -0
  32. package/dist/chunk-FPR7SKIE.js +6926 -0
  33. package/dist/chunk-FPR7SKIE.js.map +1 -0
  34. package/dist/{chunk-DGMQEI6Y.js → chunk-HFB74CLM.js} +30 -9
  35. package/dist/chunk-HFB74CLM.js.map +1 -0
  36. package/dist/{chunk-5RDRAO4B.js → chunk-HOZXKOE3.js} +37 -1
  37. package/dist/chunk-HOZXKOE3.js.map +1 -0
  38. package/dist/chunk-IDYWN37T.js +36 -0
  39. package/dist/chunk-IDYWN37T.js.map +1 -0
  40. package/dist/chunk-J4F2QLM2.js +273 -0
  41. package/dist/chunk-J4F2QLM2.js.map +1 -0
  42. package/dist/chunk-JGF5HLSI.js +1065 -0
  43. package/dist/chunk-JGF5HLSI.js.map +1 -0
  44. package/dist/{chunk-XIFFTATU.js → chunk-KDNWMKXV.js} +7166 -4237
  45. package/dist/chunk-KDNWMKXV.js.map +1 -0
  46. package/dist/chunk-KXAYUIKP.js +90 -0
  47. package/dist/chunk-KXAYUIKP.js.map +1 -0
  48. package/dist/{chunk-3U5IBM24.js → chunk-MEOB2DZA.js} +12 -2
  49. package/dist/chunk-MEOB2DZA.js.map +1 -0
  50. package/dist/{chunk-WKBO5R4O.js → chunk-MV5PQNIA.js} +2 -2
  51. package/dist/chunk-MV5PQNIA.js.map +1 -0
  52. package/dist/chunk-N2G4Q7ME.js +2672 -0
  53. package/dist/chunk-N2G4Q7ME.js.map +1 -0
  54. package/dist/chunk-OJBMDM5H.js +299 -0
  55. package/dist/chunk-OJBMDM5H.js.map +1 -0
  56. package/dist/{chunk-URLQAXTR.js → chunk-PTDC7ZUP.js} +2 -2
  57. package/dist/chunk-PTDC7ZUP.js.map +1 -0
  58. package/dist/{chunk-574C6MQR.js → chunk-REASC2QG.js} +173 -64
  59. package/dist/chunk-REASC2QG.js.map +1 -0
  60. package/dist/{chunk-7N4J557C.js → chunk-RT5SATF7.js} +1 -1
  61. package/dist/chunk-RT5SATF7.js.map +1 -0
  62. package/dist/chunk-SBTU5BEC.js +114 -0
  63. package/dist/chunk-SBTU5BEC.js.map +1 -0
  64. package/dist/{chunk-5KLVH3PY.js → chunk-SWPDKLNM.js} +5 -3
  65. package/dist/chunk-SWPDKLNM.js.map +1 -0
  66. package/dist/chunk-UCFJ43OS.js +520 -0
  67. package/dist/chunk-UCFJ43OS.js.map +1 -0
  68. package/dist/{chunk-VZQXX3WW.js → chunk-W7SLYS3M.js} +80 -16
  69. package/dist/chunk-W7SLYS3M.js.map +1 -0
  70. package/dist/{chunk-ANSJPYGI.js → chunk-WV3O5PGX.js} +3 -3
  71. package/dist/chunk-WV3O5PGX.js.map +1 -0
  72. package/dist/chunk-XHAO3K7I.js +18 -0
  73. package/dist/chunk-XHAO3K7I.js.map +1 -0
  74. package/dist/chunk-XJRJVXJR.js +1050 -0
  75. package/dist/chunk-XJRJVXJR.js.map +1 -0
  76. package/dist/chunk-YIL6HTJX.js +663 -0
  77. package/dist/chunk-YIL6HTJX.js.map +1 -0
  78. package/dist/{chunk-24AXE6SP.js → chunk-ZPSZZUWF.js} +1 -1
  79. package/dist/chunk-ZPSZZUWF.js.map +1 -0
  80. package/dist/{command-approvals-6Y57VCWZ.js → command-approvals-ZRY66LCW.js} +7 -5
  81. package/dist/context-C3RAP6K5.js +41 -0
  82. package/dist/{hook-approvals-CTUFKEJT.js → hook-approvals-DPQD5H65.js} +6 -5
  83. package/dist/index.js +947 -203
  84. package/dist/index.js.map +1 -1
  85. package/dist/{interactive-assets-CCAAWVKH.js → interactive-assets-73AQO5WP.js} +18 -9
  86. package/dist/interactive-assets-73AQO5WP.js.map +1 -0
  87. package/dist/job-runner.js +166 -111
  88. package/dist/job-runner.js.map +1 -1
  89. package/dist/job-supervisor.d.ts +2 -0
  90. package/dist/job-supervisor.js +121 -0
  91. package/dist/job-supervisor.js.map +1 -0
  92. package/dist/loader-DQNPDKOX.js +27 -0
  93. package/dist/{mcp-approvals-WNPH65BK.js → mcp-approvals-27LJYADQ.js} +6 -5
  94. package/dist/{mcp-config-YIJIES2C.js → mcp-config-GXSODNCM.js} +4 -3
  95. package/dist/memory-store-I6Z54VUN.js +51 -0
  96. package/dist/{model-window-store-YFVNXCIP.js → model-window-store-6VKXXTPJ.js} +9 -4
  97. package/dist/openai-compatible-IPUNBJ53.js +16 -0
  98. package/dist/permission-approvals-Q5IMFEZI.js +15 -0
  99. package/dist/permissions-7Y35X6HZ.js +62 -0
  100. package/dist/provider-approvals-5AGH3RVT.js +76 -0
  101. package/dist/sandbox-N2RY72GN.js +42 -0
  102. package/dist/{scrollback-LUKLIBVI.js → scrollback-GD76GNM4.js} +44 -26
  103. package/dist/scrollback-GD76GNM4.js.map +1 -0
  104. package/dist/sdk.d.ts +2739 -2069
  105. package/dist/sdk.js +77 -42
  106. package/dist/sdk.js.map +1 -1
  107. package/dist/settings-loader-KDBH45AQ.js +43 -0
  108. package/dist/settings-loader-KDBH45AQ.js.map +1 -0
  109. package/dist/{settings-removed-JWWPT4WA.js → settings-removed-3ATWJEF3.js} +2 -2
  110. package/dist/settings-removed-3ATWJEF3.js.map +1 -0
  111. package/dist/shell-selection-Z5WORZU4.js +31 -0
  112. package/dist/shell-selection-Z5WORZU4.js.map +1 -0
  113. package/package.json +25 -24
  114. package/dist/app-MOXXWEOY.js.map +0 -1
  115. package/dist/chunk-24AXE6SP.js.map +0 -1
  116. package/dist/chunk-3U5IBM24.js.map +0 -1
  117. package/dist/chunk-574C6MQR.js.map +0 -1
  118. package/dist/chunk-5GDZ22YP.js +0 -449
  119. package/dist/chunk-5GDZ22YP.js.map +0 -1
  120. package/dist/chunk-5KLVH3PY.js.map +0 -1
  121. package/dist/chunk-5RDRAO4B.js.map +0 -1
  122. package/dist/chunk-7N4J557C.js.map +0 -1
  123. package/dist/chunk-A7JLBK2Y.js +0 -3506
  124. package/dist/chunk-A7JLBK2Y.js.map +0 -1
  125. package/dist/chunk-ANSJPYGI.js.map +0 -1
  126. package/dist/chunk-BTZZUCLI.js +0 -264
  127. package/dist/chunk-BTZZUCLI.js.map +0 -1
  128. package/dist/chunk-C7D4NJ2D.js +0 -217
  129. package/dist/chunk-C7D4NJ2D.js.map +0 -1
  130. package/dist/chunk-CAGOB2N4.js.map +0 -1
  131. package/dist/chunk-DGMQEI6Y.js.map +0 -1
  132. package/dist/chunk-E75IPST5.js.map +0 -1
  133. package/dist/chunk-EYGD6X2T.js +0 -180
  134. package/dist/chunk-EYGD6X2T.js.map +0 -1
  135. package/dist/chunk-GTVB4HEM.js +0 -765
  136. package/dist/chunk-GTVB4HEM.js.map +0 -1
  137. package/dist/chunk-J7MIZ5CA.js.map +0 -1
  138. package/dist/chunk-KWHQ2XXL.js.map +0 -1
  139. package/dist/chunk-L6LK2QIQ.js +0 -657
  140. package/dist/chunk-L6LK2QIQ.js.map +0 -1
  141. package/dist/chunk-MAQXCUR4.js +0 -1007
  142. package/dist/chunk-MAQXCUR4.js.map +0 -1
  143. package/dist/chunk-S4XL7HOM.js +0 -402
  144. package/dist/chunk-S4XL7HOM.js.map +0 -1
  145. package/dist/chunk-URLQAXTR.js.map +0 -1
  146. package/dist/chunk-V4BOYR52.js.map +0 -1
  147. package/dist/chunk-VZQXX3WW.js.map +0 -1
  148. package/dist/chunk-WKBO5R4O.js.map +0 -1
  149. package/dist/chunk-XIFFTATU.js.map +0 -1
  150. package/dist/context-ZC5IFEAX.js +0 -25
  151. package/dist/interactive-assets-CCAAWVKH.js.map +0 -1
  152. package/dist/loader-4IDS2RIE.js +0 -15
  153. package/dist/openai-compatible-RHZUBHV6.js +0 -10
  154. package/dist/permission-approvals-76PVREFD.js +0 -14
  155. package/dist/permissions-CHCKTFQQ.js +0 -28
  156. package/dist/sandbox-CNVAQDA7.js +0 -23
  157. package/dist/scrollback-LUKLIBVI.js.map +0 -1
  158. package/dist/settings-loader-M2QP5TX2.js +0 -21
  159. package/dist/shell-selection-V6QAYXPX.js +0 -17
  160. package/patches/ink+6.8.0.patch +0 -13
  161. package/scripts/apply-ink-patch.mjs +0 -29
  162. package/scripts/ink-patch.mjs +0 -59
  163. package/scripts/verify-ink-patch.mjs +0 -11
  164. /package/dist/{command-approvals-6Y57VCWZ.js.map → additional-roots-VAWRQ625.js.map} +0 -0
  165. /package/dist/{context-ZC5IFEAX.js.map → command-approvals-ZRY66LCW.js.map} +0 -0
  166. /package/dist/{hook-approvals-CTUFKEJT.js.map → context-C3RAP6K5.js.map} +0 -0
  167. /package/dist/{loader-4IDS2RIE.js.map → hook-approvals-DPQD5H65.js.map} +0 -0
  168. /package/dist/{mcp-approvals-WNPH65BK.js.map → loader-DQNPDKOX.js.map} +0 -0
  169. /package/dist/{mcp-config-YIJIES2C.js.map → mcp-approvals-27LJYADQ.js.map} +0 -0
  170. /package/dist/{model-window-store-YFVNXCIP.js.map → mcp-config-GXSODNCM.js.map} +0 -0
  171. /package/dist/{openai-compatible-RHZUBHV6.js.map → memory-store-I6Z54VUN.js.map} +0 -0
  172. /package/dist/{permission-approvals-76PVREFD.js.map → model-window-store-6VKXXTPJ.js.map} +0 -0
  173. /package/dist/{permissions-CHCKTFQQ.js.map → openai-compatible-IPUNBJ53.js.map} +0 -0
  174. /package/dist/{sandbox-CNVAQDA7.js.map → permission-approvals-Q5IMFEZI.js.map} +0 -0
  175. /package/dist/{settings-loader-M2QP5TX2.js.map → permissions-7Y35X6HZ.js.map} +0 -0
  176. /package/dist/{settings-removed-JWWPT4WA.js.map → provider-approvals-5AGH3RVT.js.map} +0 -0
  177. /package/dist/{shell-selection-V6QAYXPX.js.map → sandbox-N2RY72GN.js.map} +0 -0
@@ -0,0 +1,1124 @@
1
+ import {
2
+ escapeInvisibleCharacters,
3
+ foldControlCharacters
4
+ } from "./chunk-2AAOF5B5.js";
5
+ import {
6
+ systemClock
7
+ } from "./chunk-RT5SATF7.js";
8
+ import {
9
+ createDebugLogger,
10
+ isDebugEnabled
11
+ } from "./chunk-OJBMDM5H.js";
12
+
13
+ // src/provider/reliability.ts
14
+ function classifyHttpStatus(status) {
15
+ if (status === 413) return { code: "context_overflow", retryable: false };
16
+ if (status === 429) return { code: "rate_limited", retryable: true };
17
+ if (status === 529) return { code: "overloaded", retryable: true };
18
+ if (status >= 500 && status < 600) return { code: "server_error", retryable: true };
19
+ if (status === 408) return { code: "timeout", retryable: true };
20
+ if (status === 401 || status === 403) return { code: "auth", retryable: false };
21
+ if (status === 402) return { code: "quota", retryable: false };
22
+ if (status === 400) return { code: "bad_request", retryable: false };
23
+ if (status === 404) return { code: "not_found", retryable: false };
24
+ if (status === 422) return { code: "unprocessable", retryable: false };
25
+ return { code: "unknown", retryable: false };
26
+ }
27
+ var CAPACITY_OUTAGE_404 = /no\s+(?:endpoints|accounts)\s+found/i;
28
+ var RESET_AFTER = /\(reset after\s*[^)]+\)/i;
29
+ function quotedUpstreamStatus(body) {
30
+ if (!body) return void 0;
31
+ const text = body.length > 65536 ? body.slice(0, 65536) : body;
32
+ const patterns = [
33
+ /\[[^\]\s]+\]\s*\[(4\d\d)\]:/,
34
+ /"error"\s*:\s*\{[^{}]*?"code"\s*:\s*"?(4\d\d)(?![\d.])/
35
+ ];
36
+ const capacityOutage = CAPACITY_OUTAGE_404.test(text) && RESET_AFTER.test(text);
37
+ if (capacityOutage) return void 0;
38
+ for (const pattern of patterns) {
39
+ const match = text.match(pattern);
40
+ if (match) return Number(match[1]);
41
+ }
42
+ return void 0;
43
+ }
44
+ function wrappedUpstreamStatus(status, body) {
45
+ return classifyHttpStatus(status).retryable ? quotedUpstreamStatus(body) : void 0;
46
+ }
47
+ var CONTEXT_OVERFLOW_ERROR_NAMES = /* @__PURE__ */ new Set([
48
+ "context_length_exceeded",
49
+ "request_too_large",
50
+ // llama.cpp's server.
51
+ "exceed_context_size_error"
52
+ ]);
53
+ var MODEL_UNAVAILABLE_ERROR_NAMES = /* @__PURE__ */ new Set([
54
+ "model_not_found",
55
+ "model_not_available",
56
+ "model_unavailable"
57
+ ]);
58
+ var MODEL_GONE_PHRASES = [
59
+ "no longer available",
60
+ "no longer supported",
61
+ "not found",
62
+ "does not exist",
63
+ "is not supported",
64
+ "not available",
65
+ "has been retired",
66
+ "has been removed",
67
+ "has been deprecated"
68
+ ];
69
+ var CHOOSE_ANOTHER_MODEL = /(?:select|choose)\s+(?:another|a\s+different)\s+model\b(?!\s+provider\b)/i;
70
+ var MODEL_GONE_PROXIMITY_CHARS = 60;
71
+ var CREDENTIAL_WORD = /\b(?:api\s+key|key|token|credential)s?\b/i;
72
+ function splitSentences(text) {
73
+ return text.split(/[.!?:;]+(?:\s+|$)|[\r\n]+/);
74
+ }
75
+ var INSUFFICIENT_QUOTA_ERROR_NAMES = /* @__PURE__ */ new Set(["insufficient_quota"]);
76
+ function hasInsufficientQuotaName(body) {
77
+ if (!body) return false;
78
+ const { names, raw } = errorBodyParts(body);
79
+ if (names.some((name) => INSUFFICIENT_QUOTA_ERROR_NAMES.has(name.toLowerCase()))) {
80
+ return true;
81
+ }
82
+ return raw !== void 0 && hasInsufficientQuotaName(raw);
83
+ }
84
+ var SPENT_LIMIT_VERBS = /\b(?:reached|exceeded|exhausted|hit|used\s+up|spent|r(?:un|an)\s+out|depleted)\b/i;
85
+ var SPENT_USAGE_LIMIT_PHRASES = /\b(?:usage\s+limits?|plan\s+limits?|quotas?|subscription\s+limits?|limits?\s+for\s+your\s+plan)\b/i;
86
+ var LONG_WINDOW_PHRASES = /\b(?:weekly|daily|monthly|\d+(?:\.\d+)?(?:-|\s+)hours?|per\s+(?:day|week|month))\b/i;
87
+ var SHORT_WINDOW_PHRASES = /\b(?:per\s+min(?:ute)?|per\s+sec(?:ond)?|rpm|tpm|rps)\b/i;
88
+ function statesSpentUsageLimit(body) {
89
+ if (!body) return false;
90
+ const { message, raw } = errorBodyParts(body);
91
+ const saysIt = (text) => {
92
+ if (!text || SHORT_WINDOW_PHRASES.test(text)) return false;
93
+ for (const sentence of splitSentences(text)) {
94
+ if (SPENT_LIMIT_VERBS.test(sentence) && SPENT_USAGE_LIMIT_PHRASES.test(sentence) && LONG_WINDOW_PHRASES.test(sentence)) {
95
+ return true;
96
+ }
97
+ }
98
+ return false;
99
+ };
100
+ return saysIt(message) || raw !== void 0 && statesSpentUsageLimit(raw);
101
+ }
102
+ var ABSOLUTE_RESET_PATTERN = /\b(?:resets?)\s+(?:at|on)\s+(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:?\d{2}))\b/i;
103
+ function parseAbsoluteReset(body) {
104
+ if (!body) return void 0;
105
+ const { message, raw } = errorBodyParts(body);
106
+ const match = message.match(ABSOLUTE_RESET_PATTERN);
107
+ if (match) {
108
+ const timestamp = Date.parse(match[1]);
109
+ if (Number.isFinite(timestamp)) {
110
+ return { epochMs: timestamp, iso: match[1] };
111
+ }
112
+ }
113
+ if (raw !== void 0) {
114
+ return parseAbsoluteReset(raw);
115
+ }
116
+ return void 0;
117
+ }
118
+ function statesModelUnavailable(body, effectiveStatus) {
119
+ const { message, names, raw } = errorBodyParts(body);
120
+ if (names.some((name) => MODEL_UNAVAILABLE_ERROR_NAMES.has(name.toLowerCase()))) return true;
121
+ if (effectiveStatus === 404 && names.some((name) => name.toLowerCase() === "not_found_error") && message.trim().toLowerCase().startsWith("model:")) {
122
+ return true;
123
+ }
124
+ const saysIt = (text) => {
125
+ for (const sentence of splitSentences(text)) {
126
+ if (CREDENTIAL_WORD.test(sentence)) continue;
127
+ if (CHOOSE_ANOTHER_MODEL.test(sentence)) return true;
128
+ const lower = sentence.toLowerCase();
129
+ const modelWords = [...lower.matchAll(/\bmodels?\b/g)].map((m) => m.index);
130
+ if (modelWords.length === 0) continue;
131
+ for (const phrase of MODEL_GONE_PHRASES) {
132
+ let gone = lower.indexOf(phrase);
133
+ while (gone >= 0) {
134
+ if (modelWords.some((at) => at < gone && gone - at <= MODEL_GONE_PROXIMITY_CHARS)) {
135
+ return true;
136
+ }
137
+ gone = lower.indexOf(phrase, gone + 1);
138
+ }
139
+ }
140
+ }
141
+ return false;
142
+ };
143
+ return saysIt(message) || raw !== void 0 && statesModelUnavailable(raw, effectiveStatus);
144
+ }
145
+ function errorBodyParts(body) {
146
+ let parsedJson;
147
+ try {
148
+ parsedJson = JSON.parse(body);
149
+ } catch {
150
+ const reachedCap = new TextEncoder().encode(body).byteLength >= ERROR_BODY_MAX_BYTES - 3;
151
+ if (!reachedCap || !/^\s*(?:\{|\[\s*\{)/.test(body)) {
152
+ return { message: body, names: [], parsed: false };
153
+ }
154
+ const readString = (key) => {
155
+ const match = body.match(new RegExp(`"${key}"\\s*:\\s*"((?:[^"\\\\]|\\\\.)*)"`));
156
+ if (!match) return "";
157
+ try {
158
+ return JSON.parse(`"${match[1]}"`);
159
+ } catch {
160
+ return "";
161
+ }
162
+ };
163
+ const message2 = readString("message") || readString("error") || readString("detail");
164
+ const names = [body.match(/"code"\s*:\s*"([^"]*)"/), body.match(/"type"\s*:\s*"([^"]*)"/)].filter((name) => name !== null).map((name) => name[1]);
165
+ return { message: message2, names, parsed: true };
166
+ }
167
+ const root = Array.isArray(parsedJson) ? parsedJson[0] : parsedJson;
168
+ if (typeof root !== "object" || root === null) {
169
+ return { message: body, names: [], parsed: false };
170
+ }
171
+ const error = root.error;
172
+ if (typeof error === "string") return { message: error, names: [], parsed: true };
173
+ const source = typeof error === "object" && error !== null ? error : root;
174
+ const fallback = root.detail;
175
+ const message = typeof source.message === "string" ? source.message : typeof fallback === "string" ? fallback : "";
176
+ const metadata = source.metadata;
177
+ const raw = metadata?.raw;
178
+ return {
179
+ message,
180
+ names: [source.code, source.type].filter((name) => typeof name === "string"),
181
+ raw: typeof raw === "string" ? raw : typeof raw === "object" && raw !== null ? JSON.stringify(raw) : void 0,
182
+ parsed: true
183
+ };
184
+ }
185
+ function statesContextOverflow(body) {
186
+ const { message, names, raw } = errorBodyParts(body);
187
+ return names.some((name) => CONTEXT_OVERFLOW_ERROR_NAMES.has(name.toLowerCase())) || isContextOverflowError(message) || raw !== void 0 && statesContextOverflow(raw);
188
+ }
189
+ function classifyApiError(status, body, quotedStatus) {
190
+ const effective = quotedStatus !== void 0 ? classifyHttpStatus(status).retryable ? quotedStatus : status : wrappedUpstreamStatus(status, body) ?? status;
191
+ if (effective === 429) {
192
+ if (hasInsufficientQuotaName(body)) return "quota";
193
+ if (statesSpentUsageLimit(body)) return "usage_limit";
194
+ }
195
+ const code = classifyHttpStatus(effective).code;
196
+ if ((effective === 403 || effective === 404) && statesModelUnavailable(body, effective)) {
197
+ return "model_unavailable";
198
+ }
199
+ return code === "bad_request" && statesContextOverflow(body) ? "context_overflow" : code;
200
+ }
201
+ function classifyProviderError(error) {
202
+ if (error instanceof ProviderRedirectError) return "redirect";
203
+ if (error instanceof DOMException && error.name === "TimeoutError") return "timeout";
204
+ const message = (error instanceof Error ? error.message : String(error)).toLowerCase();
205
+ if (message.includes("timed out") || message.includes("timeout")) return "timeout";
206
+ if (isContextOverflowError(message)) return "context_overflow";
207
+ return "network";
208
+ }
209
+ function safeModelUnavailableDetail(body) {
210
+ const { raw } = errorBodyParts(body);
211
+ const main = safeErrorDetail(body);
212
+ if (raw) {
213
+ const rawDetail = safeErrorDetail(raw);
214
+ if (rawDetail && rawDetail !== main) {
215
+ return main ? `${main}: ${rawDetail}` : rawDetail;
216
+ }
217
+ }
218
+ return main || "The provider did not describe why";
219
+ }
220
+ function requestOrigin(request) {
221
+ let host;
222
+ try {
223
+ host = request?.url ? new URL(request.url).host : void 0;
224
+ } catch {
225
+ host = void 0;
226
+ }
227
+ return [host && `from ${host}`, request?.model && `for model ${request.model}`].filter(Boolean).join(" ");
228
+ }
229
+ function isUninformativeDetail(text) {
230
+ if (/^(?:\d{3}\s+)?page not found\.?$/i.test(text)) return true;
231
+ if (isHtmlDetail(text)) return true;
232
+ if (!/^[[{]/.test(text)) return false;
233
+ try {
234
+ const parsed = JSON.parse(text);
235
+ if (Array.isArray(parsed)) return parsed.length === 0;
236
+ return typeof parsed === "object" && parsed !== null && Object.keys(parsed).length === 0;
237
+ } catch {
238
+ return false;
239
+ }
240
+ }
241
+ function isHtmlDetail(text) {
242
+ return /^<(?:!doctype\s+html|html|head|body)\b/i.test(text);
243
+ }
244
+ var MAX_SHOWN_TITLE_CHARS = 100;
245
+ function describeHtmlPage(body) {
246
+ const title = body.match(/<title[^>]*>([\s\S]*?)<\/title>/i)?.[1];
247
+ const shown = title ? foldControlCharacters(title).replace(/\s+/g, " ").trim() : "";
248
+ if (!shown) return "an HTML page";
249
+ const clipped = shown.length > MAX_SHOWN_TITLE_CHARS ? `${shown.slice(0, MAX_SHOWN_TITLE_CHARS)}\u2026` : shown;
250
+ return `an HTML page titled "${clipped}"`;
251
+ }
252
+ function detailSentence(detail, request, body = detail) {
253
+ const text = detail.trim();
254
+ const origin = requestOrigin(request);
255
+ if (!text) {
256
+ const absent = request?.bodyReceived === false ? "no response body received" : "empty response body";
257
+ return { sentence: `${origin ? `${origin} ` : ""}(${absent}).`, rewritten: false };
258
+ }
259
+ const uninformative = isUninformativeDetail(text);
260
+ const shown = isHtmlDetail(text) ? describeHtmlPage(body) : text;
261
+ const sentence = /[.!?]$/.test(shown) ? shown : `${shown}.`;
262
+ const hinted = uninformative && origin !== "";
263
+ return {
264
+ sentence: hinted ? `${sentence} The reply came ${origin}.` : sentence,
265
+ rewritten: hinted || shown !== text
266
+ };
267
+ }
268
+ function resetStatement(said, resetIso, clause) {
269
+ return said.includes(resetIso) ? "" : clause;
270
+ }
271
+ function formatApiError(status, body, quotedStatus, request) {
272
+ const code = classifyApiError(status, body, quotedStatus);
273
+ const detail = safeErrorDetail(body);
274
+ const { sentence: said, rewritten } = detailSentence(detail, request, body);
275
+ const base = `API Error: ${status}`;
276
+ switch (code) {
277
+ case "context_overflow":
278
+ return `${base} ${detail.trim() ? said : "Input exceeds the model context window."} Reduce the conversation or tool output and try again.`;
279
+ case "rate_limited": {
280
+ if (isOversizedForRateLimit(detail)) {
281
+ return `${base} ${said} The request is larger than the rate limit allows at once, so it has to shrink; waiting will not help.`;
282
+ }
283
+ const reset = parseAbsoluteReset(body);
284
+ if (reset !== void 0 && reset.epochMs > Date.now()) {
285
+ const resetClause = resetStatement(said, reset.iso, ` It resets at ${reset.iso}.`);
286
+ return `${base} ${said}${resetClause} Wait for the reset, then try again.`;
287
+ }
288
+ return `${base} ${said} This may be a temporary capacity issue. Try again in a moment.`;
289
+ }
290
+ case "overloaded":
291
+ return `${base} Repeated 529 Overloaded errors. The API is at capacity \u2014 this is usually temporary. Try again in a moment.`;
292
+ case "server_error":
293
+ return `${base} ${said} This is a server-side issue, usually temporary \u2014 try again in a moment.`;
294
+ case "timeout":
295
+ return "Request timed out. Check your network connection and try again.";
296
+ case "auth":
297
+ return `${base} ${said} Check BOOK_API_KEY, or provider.<id>.apiKey in settings.`;
298
+ case "model_unavailable": {
299
+ const modelDetail = detailSentence(safeModelUnavailableDetail(body)).sentence;
300
+ return `${base} ${modelDetail} This model is not available on this provider or route \u2014 choose another with --model or /model.`;
301
+ }
302
+ case "usage_limit": {
303
+ const reset = parseAbsoluteReset(body);
304
+ const resetClause = reset !== void 0 ? resetStatement(said, reset.iso, `; it resets at ${reset.iso}`) : "";
305
+ return `${base} ${said} The usage limit for this plan is spent${resetClause}. Wait for the reset, or choose another model with --model or /model.`;
306
+ }
307
+ case "quota":
308
+ return `${base} ${said} Check your usage/credits.`;
309
+ default:
310
+ if (detail.trim() && !rewritten) return `${base} ${detail}`;
311
+ return code === "not_found" ? `${base} ${said} Check the base URL and model id.` : `${base} ${said}`;
312
+ }
313
+ }
314
+ function isContextOverflowError(error) {
315
+ const normalized = (error instanceof Error ? error.message : String(error)).toLowerCase();
316
+ return /\b(?:api\s+error|http|(?:error|status)(?:\s+code)?)\s*:?\s*413\b/.test(normalized) || normalized.includes("request entity too large") || normalized.includes("payload too large") || normalized.includes("request too large") || normalized.includes("context_length_exceeded") || normalized.includes("maximum context length") || normalized.includes("context window") || normalized.includes("prompt too long") || /prompt\s+is\s+too\s+long/.test(normalized) || /input\s+(?:is\s+)?too\s+long/.test(normalized) || normalized.includes("too many tokens") || // Gemini names the count: `The input token count (1196266) exceeds the maximum …`.
317
+ /input\s+(?:token\s+count\s+)?(?:\(\d+\)\s+)?exceeds?.*(?:context|limit|maximum)/.test(
318
+ normalized
319
+ ) || /input\s+exceeds?.*(?:context\s+window|context\s+length|token\s+limit)/.test(normalized) || /maximum\s+context|context\s+(?:length|window|size).*(?:exceed|overflow|too\s+(?:long|large)|maximum)/.test(
320
+ normalized
321
+ ) || /too\s+many\s+(?:input\s+)?tokens|request\s+too\s+large.*token/.test(normalized);
322
+ }
323
+ function isOversizedForRateLimit(text) {
324
+ if (!/\brequest too large\b/i.test(text)) return false;
325
+ const limit = text.match(/\blimit:?\s*(\d+)/i);
326
+ const requested = text.match(/\brequested:?\s*(\d+)/i);
327
+ return !limit || !requested || Number(requested[1]) > Number(limit[1]);
328
+ }
329
+ function statedRateLimit(text) {
330
+ if (!isOversizedForRateLimit(text)) return void 0;
331
+ const limit = text.match(/\blimit:?\s*(\d+)/i);
332
+ return limit ? Number(limit[1]) : void 0;
333
+ }
334
+ var ERROR_ENVELOPE_MAX_CHARS = 2e3;
335
+ function isZeroUsage(usage) {
336
+ return usage != null && usage.promptTokens === 0 && usage.completionTokens === 0 && (usage.cacheReadInputTokens ?? 0) === 0 && (usage.cacheCreationInputTokens ?? 0) === 0;
337
+ }
338
+ function isErrorEnvelopeShape(text, usage) {
339
+ if (isZeroUsage(usage)) return /^\s*\[Error\]/i.test(text);
340
+ const trimmed = text.trim();
341
+ return trimmed.length <= ERROR_ENVELOPE_MAX_CHARS && /^\[Error\]/i.test(trimmed) && !/[\r\n]/.test(trimmed);
342
+ }
343
+ var ROUTER_FAILED_RESPONSE = /^\s*\[Error\]\s*The model failed to generate a response\.?\s*$/i;
344
+ function isUpstreamErrorEnvelope(text, usage) {
345
+ if (!isErrorEnvelopeShape(text, usage)) return false;
346
+ return isZeroUsage(usage) || /\brequest id\b/i.test(text) || /help\.openai\.com/i.test(text) || /error occurred while processing/i.test(text) || ROUTER_FAILED_RESPONSE.test(text);
347
+ }
348
+ var ERROR_BODY_READ_TIMEOUT_MS = 5e3;
349
+ var ERROR_BODY_MAX_BYTES = 65536;
350
+ async function readErrorBody(response, signal) {
351
+ if (response instanceof BufferedErrorResponse) {
352
+ return { text: response.bufferedText, received: response.bodyReceived };
353
+ }
354
+ if (signal?.aborted) return { text: "", received: false };
355
+ if (!response.body) return { text: "", received: true };
356
+ let reader;
357
+ try {
358
+ reader = response.body.getReader();
359
+ } catch {
360
+ return { text: "", received: false };
361
+ }
362
+ const decoder = new TextDecoder();
363
+ let text = "";
364
+ let received = 0;
365
+ let interrupted = false;
366
+ const stop = () => {
367
+ interrupted = true;
368
+ reader.cancel().catch(() => {
369
+ });
370
+ };
371
+ const timer = setTimeout(stop, ERROR_BODY_READ_TIMEOUT_MS);
372
+ signal?.addEventListener("abort", stop, { once: true });
373
+ try {
374
+ while (received < ERROR_BODY_MAX_BYTES) {
375
+ const chunk = await reader.read();
376
+ if (chunk.done) break;
377
+ const bytes = chunk.value.subarray(0, ERROR_BODY_MAX_BYTES - received);
378
+ received += bytes.byteLength;
379
+ text += decoder.decode(bytes, { stream: true });
380
+ }
381
+ } catch {
382
+ interrupted = true;
383
+ } finally {
384
+ clearTimeout(timer);
385
+ signal?.removeEventListener("abort", stop);
386
+ reader.cancel().catch(() => {
387
+ });
388
+ try {
389
+ reader.releaseLock();
390
+ } catch {
391
+ }
392
+ }
393
+ return { text: text + decoder.decode(), received: received > 0 || !interrupted };
394
+ }
395
+ var BufferedErrorResponse = class extends Response {
396
+ bufferedText;
397
+ bodyReceived;
398
+ constructor(body, init, bodyReceived) {
399
+ super(body, init);
400
+ this.bufferedText = body;
401
+ this.bodyReceived = bodyReceived;
402
+ }
403
+ };
404
+ var ProviderRedirectError = class extends Error {
405
+ constructor(status, target) {
406
+ const answered = status === 0 ? "answered with a redirect" : `answered HTTP ${status}`;
407
+ super(
408
+ target === void 0 ? `Provider endpoint ${answered}${status === 0 ? " that has" : " with"} no redirect target Book could read, and Book does not follow provider redirects. Check the provider's base URL.` : `Provider endpoint ${answered}${status === 0 ? "" : " with a redirect"} to ${target}, and Book does not follow provider redirects. If that host is the endpoint you meant, set the provider's base URL to that address without the endpoint path Book adds (https://gw.example/v1/chat/completions becomes https://gw.example/v1).`
409
+ );
410
+ this.status = status;
411
+ this.target = target;
412
+ this.name = "ProviderRedirectError";
413
+ }
414
+ status;
415
+ target;
416
+ };
417
+ function sanitizeRedirectTarget(location, requestUrl) {
418
+ if (!location) return void 0;
419
+ try {
420
+ const target = new URL(location, requestUrl);
421
+ if (!target.host) return void 0;
422
+ return `${target.protocol}//${target.host}${target.pathname}`;
423
+ } catch {
424
+ return void 0;
425
+ }
426
+ }
427
+ function isRedirectResponse(response) {
428
+ return response.type === "opaqueredirect" || response.status >= 300 && response.status <= 399;
429
+ }
430
+ async function cancelBody(response) {
431
+ try {
432
+ await response.body?.cancel();
433
+ } catch {
434
+ }
435
+ }
436
+ async function refuseRedirect(response, requestUrl) {
437
+ await cancelBody(response);
438
+ return new ProviderRedirectError(
439
+ response.status,
440
+ sanitizeRedirectTarget(response.headers.get("location"), requestUrl)
441
+ );
442
+ }
443
+ async function fetchWithRetry(url, init, retry, signal, onRetry, logger, clock = systemClock) {
444
+ const startMs = clock.monotonicNowMs();
445
+ const maxAttempts = retry.watchdog ? Number.MAX_SAFE_INTEGER : retry.maxAttempts;
446
+ let lastError = null;
447
+ for (let attempt = 0; attempt <= maxAttempts; attempt++) {
448
+ if (attempt > 0 && signal?.aborted) throw abortError(signal);
449
+ const fetchSignal = requestSignal(signal, retry.requestTimeoutMs);
450
+ let response;
451
+ try {
452
+ response = await fetch(url, { ...init, redirect: "manual", signal: fetchSignal });
453
+ } catch (error) {
454
+ if (signal?.aborted) throw error;
455
+ lastError = error instanceof Error ? error.message : String(error);
456
+ if (attempt >= maxAttempts || budgetExhausted(clock, startMs, retry.totalBudgetMs)) break;
457
+ const delay2 = boundedDelay(clock, backoffMs(attempt, retry), startMs, retry.totalBudgetMs);
458
+ logger?.warn("retry network error", {
459
+ attempt: attempt + 1,
460
+ delayMs: delay2,
461
+ error: lastError
462
+ });
463
+ onRetry?.(attempt + 1, maxAttempts === Number.MAX_SAFE_INTEGER ? -1 : maxAttempts, delay2);
464
+ await sleep(delay2, signal);
465
+ continue;
466
+ }
467
+ if (isRedirectResponse(response)) throw await refuseRedirect(response, url);
468
+ const classification = classifyHttpStatus(response.status);
469
+ if (!classification.retryable) return response;
470
+ lastError = `API error ${response.status}`;
471
+ const errorBody = await readErrorBody(response, signal);
472
+ const bodyText = errorBody.text;
473
+ const buffered = (init2) => new BufferedErrorResponse(bodyText, init2, errorBody.received);
474
+ const quoted = quotedUpstreamStatus(bodyText);
475
+ if (quoted !== void 0 && !classifyHttpStatus(quoted).retryable) {
476
+ logger?.warn("upstream error quoted in retryable status; not retrying", {
477
+ status: response.status,
478
+ upstreamStatus: quoted
479
+ });
480
+ return buffered({
481
+ status: response.status,
482
+ statusText: response.statusText,
483
+ headers: response.headers
484
+ });
485
+ }
486
+ const errorCode = classifyApiError(response.status, bodyText, quoted);
487
+ if ((errorCode === "quota" || errorCode === "usage_limit") && !retry.watchdog) {
488
+ logger?.warn("usage limit reached; not retrying", {
489
+ status: response.status
490
+ });
491
+ return buffered({
492
+ status: response.status,
493
+ statusText: response.statusText,
494
+ headers: response.headers
495
+ });
496
+ }
497
+ if ((quoted ?? response.status) === 429 && isOversizedForRateLimit(formatApiError(response.status, bodyText, quoted))) {
498
+ logger?.warn("rate limit on a request too large to ever fit; not retrying", {
499
+ status: response.status
500
+ });
501
+ return buffered({
502
+ status: response.status,
503
+ statusText: response.statusText,
504
+ headers: response.headers
505
+ });
506
+ }
507
+ const stated = statedWait(response.headers.get("retry-after"), bodyText);
508
+ if ((quoted ?? response.status) === 429 && !retry.watchdog) {
509
+ if (stated !== void 0) {
510
+ const attemptsWaitMs = (maxAttempts - attempt) * retry.maxDelayMs;
511
+ const remainingBudgetMs = retry.totalBudgetMs > 0 ? Math.max(0, retry.totalBudgetMs - (clock.monotonicNowMs() - startMs)) : void 0;
512
+ const remainingWaitMs = remainingBudgetMs !== void 0 ? Math.min(remainingBudgetMs, attemptsWaitMs) : attemptsWaitMs;
513
+ const stopsCall = stated.waitMs > remainingWaitMs && (stated.source === "reset-time" || stated.waitMs >= FAR_STATED_WAIT_MS);
514
+ if (stopsCall) {
515
+ logger?.warn("stated wait beyond retry budget; not retrying", {
516
+ status: response.status,
517
+ source: stated.source
518
+ });
519
+ const init2 = {
520
+ status: response.status,
521
+ statusText: response.statusText,
522
+ headers: response.headers
523
+ };
524
+ const wholeCallMs = maxAttempts * retry.maxDelayMs;
525
+ const farWait = stated.waitMs >= FAR_STATED_WAIT_MS && stated.waitMs > (retry.totalBudgetMs > 0 ? Math.min(retry.totalBudgetMs, wholeCallMs) : wholeCallMs);
526
+ return farWait ? new WaitStoppedResponse(bodyText, init2, stated, errorBody.received) : buffered(init2);
527
+ }
528
+ }
529
+ }
530
+ const watchdogRetry = retry.watchdog && (response.status === 429 || response.status === 529);
531
+ const effectiveMax = watchdogRetry ? Number.MAX_SAFE_INTEGER : maxAttempts;
532
+ if (attempt >= effectiveMax || budgetExhausted(clock, startMs, retry.totalBudgetMs)) {
533
+ return buffered({
534
+ status: response.status,
535
+ statusText: response.statusText,
536
+ headers: response.headers
537
+ });
538
+ }
539
+ const delay = boundedDelay(
540
+ clock,
541
+ backoffMs(attempt, retry, stated),
542
+ startMs,
543
+ retry.totalBudgetMs
544
+ );
545
+ logger?.warn("retry http status", {
546
+ attempt: attempt + 1,
547
+ status: response.status,
548
+ delayMs: delay
549
+ });
550
+ onRetry?.(attempt + 1, effectiveMax === Number.MAX_SAFE_INTEGER ? -1 : effectiveMax, delay);
551
+ try {
552
+ await sleep(delay, signal);
553
+ } catch {
554
+ return buffered({
555
+ status: response.status,
556
+ statusText: response.statusText,
557
+ headers: response.headers
558
+ });
559
+ }
560
+ await cancelBody(response);
561
+ }
562
+ throw new Error(lastError ?? "request failed after retries");
563
+ }
564
+ async function readStreamChunk(reader, signal, stallTimeoutMs) {
565
+ if (signal?.aborted) return { tag: "abort" };
566
+ let timeout;
567
+ let onAbort;
568
+ try {
569
+ return await Promise.race([
570
+ reader.read().then((result) => ({
571
+ tag: "read",
572
+ done: result.done,
573
+ value: result.value
574
+ })),
575
+ ...stallTimeoutMs > 0 ? [
576
+ new Promise((resolve) => {
577
+ timeout = setTimeout(() => resolve({ tag: "stall" }), stallTimeoutMs);
578
+ })
579
+ ] : [],
580
+ ...signal ? [
581
+ new Promise((resolve) => {
582
+ onAbort = () => resolve({ tag: "abort" });
583
+ signal.addEventListener("abort", onAbort, { once: true });
584
+ })
585
+ ] : []
586
+ ]);
587
+ } finally {
588
+ if (timeout) clearTimeout(timeout);
589
+ if (signal && onAbort) signal.removeEventListener("abort", onAbort);
590
+ }
591
+ }
592
+ function requestSignal(signal, timeoutMs) {
593
+ if (timeoutMs <= 0) return signal;
594
+ const timeout = AbortSignal.timeout(timeoutMs);
595
+ return signal ? AbortSignal.any([signal, timeout]) : timeout;
596
+ }
597
+ function abortError(signal) {
598
+ return signal.reason instanceof Error ? signal.reason : new Error("Aborted");
599
+ }
600
+ function safeErrorDetail(body) {
601
+ const { message, parsed } = errorBodyParts(body);
602
+ if (parsed && message) return message.slice(0, 2e3);
603
+ return body.slice(0, 200);
604
+ }
605
+ function sleep(ms, signal) {
606
+ return new Promise((resolve, reject) => {
607
+ if (signal?.aborted) {
608
+ reject(abortError(signal));
609
+ return;
610
+ }
611
+ const timer = setTimeout(() => {
612
+ signal?.removeEventListener("abort", onAbort);
613
+ resolve();
614
+ }, ms);
615
+ const onAbort = () => {
616
+ clearTimeout(timer);
617
+ reject(signal ? abortError(signal) : new Error("Aborted"));
618
+ };
619
+ signal?.addEventListener("abort", onAbort, { once: true });
620
+ });
621
+ }
622
+ var FAR_STATED_WAIT_MS = 15 * 6e4;
623
+ function statedWait(retryAfter, body) {
624
+ const header = parseRetryAfter(retryAfter);
625
+ const cooldown = header === void 0 ? parseCooldownReset(body) : void 0;
626
+ const relative = header !== void 0 ? { waitMs: header, source: "retry-after" } : cooldown !== void 0 ? { waitMs: cooldown, source: "cooldown" } : void 0;
627
+ const reset = parseAbsoluteReset(body);
628
+ const resetWaitMs = reset !== void 0 ? reset.epochMs - Date.now() : 0;
629
+ if (reset !== void 0 && resetWaitMs > 0 && (relative === void 0 || resetWaitMs > relative.waitMs)) {
630
+ return { waitMs: resetWaitMs, source: "reset-time", resetIso: reset.iso };
631
+ }
632
+ return relative;
633
+ }
634
+ var WaitStoppedResponse = class extends BufferedErrorResponse {
635
+ statedWait;
636
+ constructor(body, init, wait, bodyReceived) {
637
+ super(body, init, bodyReceived);
638
+ this.statedWait = wait;
639
+ }
640
+ };
641
+ function statedWaitStop(response) {
642
+ return response instanceof WaitStoppedResponse ? response.statedWait : void 0;
643
+ }
644
+ function describeWait(waitMs) {
645
+ const count = (value, unit) => `${value} ${unit}${value === 1 ? "" : "s"}`;
646
+ const seconds = Math.max(1, Math.round(waitMs / 1e3));
647
+ if (seconds < 90) return count(seconds, "second");
648
+ const minutes = Math.round(seconds / 60);
649
+ if (minutes < 90) return count(minutes, "minute");
650
+ const hours = Math.round(minutes / 60);
651
+ if (hours < 48) return count(hours, "hour");
652
+ return count(Math.round(hours / 24), "day");
653
+ }
654
+ function formatStatedWaitError(status, body, stop, request) {
655
+ const said = detailSentence(safeErrorDetail(body), request, body).sentence;
656
+ const when = stop.resetIso !== void 0 ? `The limit resets${resetStatement(said, stop.resetIso, ` at ${stop.resetIso},`)} in about ${describeWait(stop.waitMs)}` : `The provider asks for a wait of about ${describeWait(stop.waitMs)}`;
657
+ return `API Error: ${status} ${said} ${when}, longer than this request will wait. Wait for the reset, or choose another model with --model or /model.`;
658
+ }
659
+ function statedWaitErrorEvent(response, errorText, request) {
660
+ const waitStop = statedWaitStop(response);
661
+ return {
662
+ type: "error",
663
+ error: waitStop ? formatStatedWaitError(response.status, errorText, waitStop, request) : formatApiError(response.status, errorText, void 0, request),
664
+ errorCode: waitStop ? "rate_limit_wait" : classifyApiError(response.status, errorText),
665
+ upstreamStatus: wrappedUpstreamStatus(response.status, errorText)
666
+ };
667
+ }
668
+ function backoffMs(attempt, retry, stated) {
669
+ if (stated !== void 0) return Math.min(stated.waitMs, retry.maxDelayMs);
670
+ const exponential = Math.min(retry.baseDelayMs * 2 ** attempt, retry.maxDelayMs);
671
+ return Math.round(exponential * (0.5 + Math.random()));
672
+ }
673
+ function parseRetryAfter(value) {
674
+ if (!value) return void 0;
675
+ const seconds = Number(value);
676
+ if (Number.isFinite(seconds) && seconds > 0) return seconds * 1e3;
677
+ const timestamp = Date.parse(value);
678
+ if (!Number.isFinite(timestamp)) return void 0;
679
+ return Math.max(0, timestamp - Date.now());
680
+ }
681
+ var COOLDOWN_UNIT_MS = {
682
+ ms: 1,
683
+ s: 1e3,
684
+ sec: 1e3,
685
+ secs: 1e3,
686
+ second: 1e3,
687
+ seconds: 1e3,
688
+ m: 6e4,
689
+ min: 6e4,
690
+ mins: 6e4,
691
+ minute: 6e4,
692
+ minutes: 6e4,
693
+ h: 36e5,
694
+ hr: 36e5,
695
+ hrs: 36e5,
696
+ hour: 36e5,
697
+ hours: 36e5
698
+ };
699
+ var COOLDOWN_UNIT = /(\d+)\s*(ms|sec(?:ond)?s?|min(?:ute)?s?|h(?:ours?|rs?)?|s|m)(?![a-z])/gi;
700
+ function parseCooldownReset(body) {
701
+ if (!body) return void 0;
702
+ const { message, raw } = errorBodyParts(body);
703
+ const own = cooldownIn(message);
704
+ if (own !== void 0) return own;
705
+ return raw !== void 0 ? parseCooldownReset(raw) : void 0;
706
+ }
707
+ function cooldownIn(text) {
708
+ const stated = text.match(/\(reset after\s*([^)]+)\)/i);
709
+ if (!stated) return void 0;
710
+ let totalMs = 0;
711
+ let statedUnits = 0;
712
+ for (const unit of stated[1].matchAll(COOLDOWN_UNIT)) {
713
+ totalMs += Number(unit[1]) * COOLDOWN_UNIT_MS[unit[2].toLowerCase()];
714
+ statedUnits++;
715
+ }
716
+ return statedUnits > 0 && totalMs > 0 ? totalMs : void 0;
717
+ }
718
+ function budgetExhausted(clock, startMs, budgetMs) {
719
+ return budgetMs > 0 && clock.monotonicNowMs() - startMs >= budgetMs;
720
+ }
721
+ function boundedDelay(clock, delayMs, startMs, budgetMs) {
722
+ if (budgetMs <= 0) return delayMs;
723
+ return Math.min(delayMs, Math.max(0, budgetMs - (clock.monotonicNowMs() - startMs)));
724
+ }
725
+
726
+ // src/provider/openai-compatible.ts
727
+ var log = createDebugLogger("provider");
728
+ function parseToolArguments(raw) {
729
+ if (!raw.trim()) return { arguments: {} };
730
+ try {
731
+ const parsed = JSON.parse(raw);
732
+ return {
733
+ arguments: parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {}
734
+ };
735
+ } catch (error) {
736
+ return {
737
+ arguments: {},
738
+ unparsedArguments: { raw, error: error instanceof Error ? error.message : String(error) }
739
+ };
740
+ }
741
+ }
742
+ function parseReasoningDelta(delta) {
743
+ for (const key of ["reasoning_content", "reasoning", "thinking", "analysis"]) {
744
+ const value = delta[key];
745
+ if (typeof value === "string" && value) return value;
746
+ }
747
+ const details = delta.reasoning_details;
748
+ if (!Array.isArray(details)) return void 0;
749
+ const text = details.map((detail) => {
750
+ if (typeof detail === "string") return detail;
751
+ if (!detail || typeof detail !== "object") return "";
752
+ const item = detail;
753
+ for (const key of ["text", "reasoning", "content"]) {
754
+ if (typeof item[key] === "string") return item[key];
755
+ }
756
+ return "";
757
+ }).filter(Boolean).join("");
758
+ return text || void 0;
759
+ }
760
+ function isSystemPromptZones(content) {
761
+ return !!content && typeof content === "object" && "cachedPrefix" in content && "dynamicSuffix" in content;
762
+ }
763
+ function flattenMessages(messages) {
764
+ return messages.map(({ providerMetadata: _providerMetadata, reasoningContent, ...msg }) => ({
765
+ ...msg,
766
+ content: isSystemPromptZones(msg.content) ? [msg.content.cachedPrefix, msg.content.dynamicSuffix].filter(Boolean).join("\n\n") : Array.isArray(msg.content) ? msg.content.map(
767
+ (part) => part.type === "text" ? part : {
768
+ type: "image_url",
769
+ image_url: { url: `data:${part.mediaType};base64,${part.data}` }
770
+ }
771
+ ) : reasoningContent ? ["<reasoning_context>", reasoningContent, "</reasoning_context>", msg.content ?? ""].filter(Boolean).join("\n") : msg.content
772
+ }));
773
+ }
774
+ function parseCompatibleUsage(raw) {
775
+ if (!raw || typeof raw !== "object") return null;
776
+ const usage = raw;
777
+ const details = usage.prompt_tokens_details && typeof usage.prompt_tokens_details === "object" ? usage.prompt_tokens_details : {};
778
+ const tokens = (value) => typeof value === "number" && Number.isFinite(value) && value > 0 ? value : 0;
779
+ const firstCount = (...values) => {
780
+ for (const value of values) {
781
+ const count = tokens(value);
782
+ if (count > 0) return count;
783
+ }
784
+ return 0;
785
+ };
786
+ const prompt = tokens(usage.prompt_tokens);
787
+ const base = {
788
+ promptTokens: prompt,
789
+ completionTokens: tokens(usage.completion_tokens),
790
+ // A provider that omits `total_tokens` would otherwise read as zero context pressure.
791
+ totalTokens: tokens(usage.total_tokens) || prompt + tokens(usage.completion_tokens)
792
+ };
793
+ const topRead = tokens(usage.cache_read_input_tokens);
794
+ const topWrite = tokens(usage.cache_creation_input_tokens);
795
+ const normalisedRead = firstCount(
796
+ details.cached_tokens,
797
+ usage.prompt_cache_hit_tokens,
798
+ usage.cached_tokens
799
+ );
800
+ const normalisedWrite = firstCount(
801
+ details.cache_creation_tokens,
802
+ details.cache_write_tokens,
803
+ details.cache_creation_input_tokens
804
+ );
805
+ const read = normalisedRead || topRead;
806
+ const write = normalisedWrite || topWrite;
807
+ if (read === 0 && write === 0) return base;
808
+ const reportsOpenAiCacheField = [
809
+ details.cached_tokens,
810
+ details.cache_creation_tokens,
811
+ details.cache_write_tokens,
812
+ details.cache_creation_input_tokens,
813
+ usage.prompt_cache_hit_tokens,
814
+ usage.cached_tokens
815
+ ].some((value) => typeof value === "number");
816
+ const anthropicPassThrough = !reportsOpenAiCacheField && topRead + topWrite > 0;
817
+ if (!anthropicPassThrough && read + write <= prompt) {
818
+ return {
819
+ ...base,
820
+ promptTokens: prompt - read - write,
821
+ cacheReadInputTokens: read,
822
+ cacheCreationInputTokens: write
823
+ };
824
+ }
825
+ return {
826
+ ...base,
827
+ cacheReadInputTokens: read,
828
+ cacheCreationInputTokens: write,
829
+ contextTokens: prompt + read + write
830
+ };
831
+ }
832
+ function convertTools(tools) {
833
+ return tools.map((tool) => ({
834
+ type: "function",
835
+ function: {
836
+ name: tool.name,
837
+ description: tool.description,
838
+ parameters: tool.inputSchema ?? tool.parameters
839
+ }
840
+ }));
841
+ }
842
+ async function* chatCompletionStream(config, messages, tools, options) {
843
+ const retry = config.retry;
844
+ const signal = options?.signal;
845
+ const url = `${config.baseUrl}/chat/completions`;
846
+ const body = {
847
+ model: config.model,
848
+ messages: flattenMessages(messages),
849
+ stream: true,
850
+ // Request token usage in the final SSE chunk so we can track cost.
851
+ stream_options: { include_usage: true }
852
+ };
853
+ if (options?.maxOutputTokens || config.maxTokensExplicit || config.modelInfo?.maxOutputTokens) {
854
+ body.max_tokens = options?.maxOutputTokens ?? config.maxTokens;
855
+ }
856
+ if (config.effortExplicit && config.effort) body.reasoning_effort = config.effort;
857
+ const reasoningEnabled = body.reasoning_effort !== void 0 || typeof config.modelInfo?.effort === "object";
858
+ if (tools.length > 0) {
859
+ body.tools = convertTools(tools);
860
+ }
861
+ log.debug("chatCompletionStream request", {
862
+ model: config.model,
863
+ messageCount: messages.length,
864
+ toolCount: tools.length,
865
+ maxTokens: config.maxTokens
866
+ });
867
+ let response;
868
+ try {
869
+ response = await fetchWithRetry(
870
+ url,
871
+ {
872
+ method: "POST",
873
+ headers: {
874
+ "Content-Type": "application/json",
875
+ Authorization: `Bearer ${config.apiKey}`
876
+ },
877
+ body: JSON.stringify(body)
878
+ },
879
+ retry,
880
+ signal,
881
+ options?.onRetry,
882
+ log
883
+ );
884
+ } catch (e) {
885
+ if (signal?.aborted) return;
886
+ log.warn("fetchWithRetry failed", e instanceof Error ? e.message : String(e));
887
+ yield {
888
+ type: "error",
889
+ error: e instanceof Error ? e.message : String(e),
890
+ errorCode: classifyProviderError(e)
891
+ };
892
+ return;
893
+ }
894
+ if (signal?.aborted) {
895
+ await response.body?.cancel();
896
+ return;
897
+ }
898
+ log.debug("response received", { status: response.status, ok: response.ok });
899
+ if (!response.ok) {
900
+ const errorBody = await readErrorBody(response, signal);
901
+ yield statedWaitErrorEvent(response, errorBody.text, {
902
+ url,
903
+ model: config.model,
904
+ bodyReceived: errorBody.received
905
+ });
906
+ return;
907
+ }
908
+ const reader = response.body?.getReader();
909
+ if (!reader) {
910
+ yield { type: "error", error: "No response body", errorCode: "protocol_error" };
911
+ return;
912
+ }
913
+ const decoder = new TextDecoder();
914
+ let buffer = "";
915
+ const toolCallParts = [];
916
+ let currentUsage = null;
917
+ let responseModel;
918
+ let responseId;
919
+ const finishReasons = /* @__PURE__ */ new Set();
920
+ const emitToolCalls = function* () {
921
+ for (const part of [...toolCallParts].sort((a, b) => a.index - b.index)) {
922
+ if (!part.id && !part.name) continue;
923
+ const { arguments: arguments_, unparsedArguments } = parseToolArguments(part.arguments);
924
+ const emptyOnWire = !part.arguments.trim();
925
+ if (emptyOnWire) {
926
+ log.warn("tool call arrived with empty arguments", {
927
+ index: part.index,
928
+ id: part.id,
929
+ name: escapeInvisibleCharacters(part.name),
930
+ fragments: part.fragments
931
+ });
932
+ }
933
+ if (unparsedArguments) {
934
+ log.warn("tool call arguments are not valid JSON", {
935
+ index: part.index,
936
+ id: part.id,
937
+ name: escapeInvisibleCharacters(part.name),
938
+ fragments: part.fragments,
939
+ length: part.arguments.length,
940
+ head: escapeInvisibleCharacters(part.arguments.slice(0, 120))
941
+ });
942
+ }
943
+ yield {
944
+ type: "tool_call",
945
+ toolCall: {
946
+ id: part.id || `tool-${part.index}`,
947
+ name: part.name,
948
+ arguments: arguments_,
949
+ ...unparsedArguments ? { unparsedArguments } : {},
950
+ ...emptyOnWire ? { emptyOnWire: true } : {}
951
+ }
952
+ };
953
+ }
954
+ };
955
+ const emitDone = function* (terminal) {
956
+ yield* emitToolCalls();
957
+ log.info("stream done", {
958
+ terminal,
959
+ promptTokens: currentUsage?.promptTokens ?? 0,
960
+ completionTokens: currentUsage?.completionTokens ?? 0,
961
+ totalTokens: currentUsage?.totalTokens ?? 0,
962
+ cacheReadInputTokens: currentUsage?.cacheReadInputTokens ?? 0,
963
+ cacheCreationInputTokens: currentUsage?.cacheCreationInputTokens ?? 0
964
+ });
965
+ yield {
966
+ type: "done",
967
+ usage: currentUsage ?? void 0,
968
+ ...responseModel ? { responseModel } : {},
969
+ ...responseId ? { responseId } : {},
970
+ ...finishReasons.size > 0 ? { finishReasons: [...finishReasons] } : {}
971
+ };
972
+ };
973
+ const processSseLine = function* (line) {
974
+ const trimmed = line.trim();
975
+ if (!trimmed || !trimmed.startsWith("data:")) return false;
976
+ const data = trimmed.slice(5).replace(/^ /, "");
977
+ if (data === "[DONE]") {
978
+ yield* emitDone("[DONE]");
979
+ return true;
980
+ }
981
+ try {
982
+ const parsed = JSON.parse(data);
983
+ if (typeof parsed.model === "string") responseModel = parsed.model;
984
+ if (typeof parsed.id === "string") responseId = parsed.id;
985
+ const usage = parseCompatibleUsage(parsed.usage);
986
+ if (usage) currentUsage = usage;
987
+ const choice = parsed.choices?.[0];
988
+ if (!choice) return false;
989
+ const finishReason = choice.finish_reason;
990
+ if (typeof finishReason === "string" && finishReason) finishReasons.add(finishReason);
991
+ const delta = choice.delta;
992
+ if (!delta) return false;
993
+ const reasoning = parseReasoningDelta(delta);
994
+ if (reasoning) {
995
+ stallTimeoutMs = retry.thinkingStallTimeoutMs ? Math.max(startingStallTimeoutMs, retry.thinkingStallTimeoutMs) : startingStallTimeoutMs;
996
+ log.debug("stream reasoning", { len: reasoning.length });
997
+ yield { type: "reasoning", reasoning };
998
+ }
999
+ if (delta.content) {
1000
+ stallTimeoutMs = startingStallTimeoutMs;
1001
+ log.debug("stream text", { len: delta.content.length });
1002
+ yield { type: "text", content: delta.content };
1003
+ }
1004
+ if (delta.tool_calls) {
1005
+ stallTimeoutMs = startingStallTimeoutMs;
1006
+ for (const tc of delta.tool_calls) {
1007
+ const explicitIndex = Number.isInteger(tc.index) ? tc.index : void 0;
1008
+ let index = explicitIndex;
1009
+ if (index === void 0 && tc.id) {
1010
+ index = toolCallParts.find((part2) => part2.id === tc.id)?.index;
1011
+ }
1012
+ if (index === void 0) {
1013
+ index = toolCallParts.length;
1014
+ }
1015
+ let part = toolCallParts.find((p) => p.index === index);
1016
+ if (!part) {
1017
+ part = { index, id: "", name: "", arguments: "", fragments: 0 };
1018
+ toolCallParts.push(part);
1019
+ }
1020
+ if (tc.id) part.id = tc.id;
1021
+ if (tc.function?.name) part.name = tc.function.name;
1022
+ if (tc.function?.arguments) {
1023
+ if (part.fragments === 0 && isDebugEnabled()) {
1024
+ log.debug("stream tool_call delta", {
1025
+ index,
1026
+ id: tc.id,
1027
+ name: escapeInvisibleCharacters(tc.function.name ?? ""),
1028
+ argumentsLength: tc.function.arguments.length,
1029
+ argumentsHead: escapeInvisibleCharacters(tc.function.arguments.slice(0, 120))
1030
+ });
1031
+ }
1032
+ part.arguments += tc.function.arguments;
1033
+ part.fragments++;
1034
+ }
1035
+ }
1036
+ }
1037
+ } catch {
1038
+ }
1039
+ return false;
1040
+ };
1041
+ const startingStallTimeoutMs = reasoningEnabled && retry.thinkingStallTimeoutMs ? Math.max(retry.streamStallTimeoutMs, retry.thinkingStallTimeoutMs) : retry.streamStallTimeoutMs;
1042
+ const firstDeltaApplies = !reasoningEnabled && config.modelInfo?.effort !== false;
1043
+ let stallTimeoutMs = firstDeltaApplies ? Math.max(startingStallTimeoutMs, retry.firstDeltaStallTimeoutMs ?? 0) : startingStallTimeoutMs;
1044
+ try {
1045
+ while (true) {
1046
+ const result = await readStreamChunk(reader, signal, stallTimeoutMs);
1047
+ if (result.tag === "abort") {
1048
+ try {
1049
+ await reader.cancel();
1050
+ } catch {
1051
+ }
1052
+ return;
1053
+ }
1054
+ if (result.tag === "stall") {
1055
+ log.warn("stream stalled", { timeoutMs: stallTimeoutMs });
1056
+ options?.onStreamStall?.(stallTimeoutMs);
1057
+ try {
1058
+ await reader.cancel();
1059
+ } catch {
1060
+ }
1061
+ yield {
1062
+ type: "error",
1063
+ error: "Stream stalled: no data received for " + stallTimeoutMs + "ms",
1064
+ errorCode: "stream_stall"
1065
+ };
1066
+ return;
1067
+ }
1068
+ if (result.done) break;
1069
+ const value = result.value;
1070
+ if (!value) continue;
1071
+ buffer += decoder.decode(value, { stream: true });
1072
+ const lines = buffer.split("\n");
1073
+ buffer = lines.pop() || "";
1074
+ for (const line of lines) {
1075
+ if (yield* processSseLine(line)) return;
1076
+ }
1077
+ }
1078
+ buffer += decoder.decode();
1079
+ for (const line of buffer.split("\n")) {
1080
+ if (yield* processSseLine(line)) return;
1081
+ }
1082
+ if (finishReasons.size > 0) {
1083
+ yield* emitDone("finish_reason");
1084
+ return;
1085
+ }
1086
+ } catch (e) {
1087
+ if (signal?.aborted) return;
1088
+ yield {
1089
+ type: "error",
1090
+ error: e instanceof Error ? e.message : String(e),
1091
+ errorCode: classifyProviderError(e)
1092
+ };
1093
+ return;
1094
+ } finally {
1095
+ try {
1096
+ reader.releaseLock();
1097
+ } catch {
1098
+ }
1099
+ }
1100
+ yield {
1101
+ type: "error",
1102
+ error: "Provider stream ended before its terminal event.",
1103
+ errorCode: "transport_interrupted"
1104
+ };
1105
+ }
1106
+
1107
+ export {
1108
+ classifyProviderError,
1109
+ isContextOverflowError,
1110
+ isOversizedForRateLimit,
1111
+ statedRateLimit,
1112
+ isErrorEnvelopeShape,
1113
+ isUpstreamErrorEnvelope,
1114
+ readErrorBody,
1115
+ isRedirectResponse,
1116
+ refuseRedirect,
1117
+ fetchWithRetry,
1118
+ readStreamChunk,
1119
+ statedWaitErrorEvent,
1120
+ parseCompatibleUsage,
1121
+ convertTools,
1122
+ chatCompletionStream
1123
+ };
1124
+ //# sourceMappingURL=chunk-AHC7OU75.js.map