@letrquan/book 0.3.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 (148) hide show
  1. package/CHANGELOG.md +1024 -0
  2. package/README.md +6 -3
  3. package/dist/{additional-roots-H7UNAEHS.js → additional-roots-VAWRQ625.js} +8 -7
  4. package/dist/{app-GE4OHJOC.js → app-J7BVPSUH.js} +5397 -2953
  5. package/dist/app-J7BVPSUH.js.map +1 -0
  6. package/dist/chunk-32WBCUAN.js +660 -0
  7. package/dist/chunk-32WBCUAN.js.map +1 -0
  8. package/dist/{chunk-525CAZPY.js → chunk-4DKHLZRI.js} +6 -6
  9. package/dist/{chunk-4AS7LKKQ.js → chunk-62EEAUID.js} +2 -2
  10. package/dist/{chunk-AQEQX4FF.js → chunk-AHC7OU75.js} +385 -55
  11. package/dist/chunk-AHC7OU75.js.map +1 -0
  12. package/dist/{chunk-EFJ2NH6E.js → chunk-AJZWLKLQ.js} +63 -14
  13. package/dist/chunk-AJZWLKLQ.js.map +1 -0
  14. package/dist/chunk-B5FC3MYZ.js +395 -0
  15. package/dist/chunk-B5FC3MYZ.js.map +1 -0
  16. package/dist/{chunk-67UZEWWF.js → chunk-DNFQTSMJ.js} +53 -11
  17. package/dist/chunk-DNFQTSMJ.js.map +1 -0
  18. package/dist/{chunk-TLX3H2CL.js → chunk-E5WLQSKA.js} +231 -40
  19. package/dist/chunk-E5WLQSKA.js.map +1 -0
  20. package/dist/chunk-ECLC4CIJ.js +1181 -0
  21. package/dist/chunk-ECLC4CIJ.js.map +1 -0
  22. package/dist/chunk-EEX6QNXV.js +127 -0
  23. package/dist/chunk-EEX6QNXV.js.map +1 -0
  24. package/dist/chunk-EFXVSQ5J.js +931 -0
  25. package/dist/chunk-EFXVSQ5J.js.map +1 -0
  26. package/dist/{chunk-RFJ6Q6LE.js → chunk-ENLB7NYV.js} +577 -164
  27. package/dist/chunk-ENLB7NYV.js.map +1 -0
  28. package/dist/{chunk-IME7R2UI.js → chunk-FPR7SKIE.js} +1268 -883
  29. package/dist/chunk-FPR7SKIE.js.map +1 -0
  30. package/dist/{chunk-VPNLCMVR.js → chunk-HFB74CLM.js} +13 -5
  31. package/dist/chunk-HFB74CLM.js.map +1 -0
  32. package/dist/{chunk-5RDRAO4B.js → chunk-HOZXKOE3.js} +37 -1
  33. package/dist/chunk-HOZXKOE3.js.map +1 -0
  34. package/dist/chunk-IDYWN37T.js +36 -0
  35. package/dist/chunk-IDYWN37T.js.map +1 -0
  36. package/dist/{chunk-YC6W4EJH.js → chunk-JGF5HLSI.js} +193 -75
  37. package/dist/chunk-JGF5HLSI.js.map +1 -0
  38. package/dist/{chunk-5YHTKWJN.js → chunk-KDNWMKXV.js} +2447 -907
  39. package/dist/chunk-KDNWMKXV.js.map +1 -0
  40. package/dist/{chunk-Y2VHN7TC.js → chunk-MEOB2DZA.js} +11 -1
  41. package/dist/chunk-MEOB2DZA.js.map +1 -0
  42. package/dist/{chunk-KBQS35OV.js → chunk-MV5PQNIA.js} +2 -2
  43. package/dist/chunk-N2G4Q7ME.js +2672 -0
  44. package/dist/chunk-N2G4Q7ME.js.map +1 -0
  45. package/dist/{chunk-62QL5QUP.js → chunk-OJBMDM5H.js} +17 -1
  46. package/dist/chunk-OJBMDM5H.js.map +1 -0
  47. package/dist/{chunk-56SIEMDX.js → chunk-PTDC7ZUP.js} +2 -2
  48. package/dist/{chunk-X4NJT436.js → chunk-REASC2QG.js} +67 -28
  49. package/dist/chunk-REASC2QG.js.map +1 -0
  50. package/dist/chunk-SBTU5BEC.js +114 -0
  51. package/dist/chunk-SBTU5BEC.js.map +1 -0
  52. package/dist/{chunk-HQI7NYGX.js → chunk-SWPDKLNM.js} +2 -2
  53. package/dist/{chunk-W3XFHEF4.js → chunk-UCFJ43OS.js} +140 -28
  54. package/dist/chunk-UCFJ43OS.js.map +1 -0
  55. package/dist/{chunk-QYBD22H5.js → chunk-W7SLYS3M.js} +80 -16
  56. package/dist/chunk-W7SLYS3M.js.map +1 -0
  57. package/dist/{chunk-SOW4WRQP.js → chunk-WV3O5PGX.js} +3 -3
  58. package/dist/{chunk-JTPOI3KU.js → chunk-XJRJVXJR.js} +262 -38
  59. package/dist/chunk-XJRJVXJR.js.map +1 -0
  60. package/dist/{chunk-224FJZLH.js → chunk-YIL6HTJX.js} +228 -5
  61. package/dist/chunk-YIL6HTJX.js.map +1 -0
  62. package/dist/{command-approvals-VBIU6BLM.js → command-approvals-ZRY66LCW.js} +5 -4
  63. package/dist/context-C3RAP6K5.js +41 -0
  64. package/dist/{hook-approvals-ZVT4V7IM.js → hook-approvals-DPQD5H65.js} +5 -4
  65. package/dist/index.js +657 -754
  66. package/dist/index.js.map +1 -1
  67. package/dist/{interactive-assets-T4I4SEHU.js → interactive-assets-73AQO5WP.js} +11 -6
  68. package/dist/{interactive-assets-T4I4SEHU.js.map → interactive-assets-73AQO5WP.js.map} +1 -1
  69. package/dist/job-runner.js +10 -3
  70. package/dist/job-runner.js.map +1 -1
  71. package/dist/job-supervisor.js +13 -4
  72. package/dist/job-supervisor.js.map +1 -1
  73. package/dist/loader-DQNPDKOX.js +27 -0
  74. package/dist/{mcp-approvals-HJ2VDYMC.js → mcp-approvals-27LJYADQ.js} +5 -4
  75. package/dist/{mcp-config-RNB5LEVE.js → mcp-config-GXSODNCM.js} +3 -2
  76. package/dist/memory-store-I6Z54VUN.js +51 -0
  77. package/dist/{model-window-store-MTSQ5JDJ.js → model-window-store-6VKXXTPJ.js} +4 -3
  78. package/dist/{openai-compatible-2OB54YST.js → openai-compatible-IPUNBJ53.js} +5 -3
  79. package/dist/{permission-approvals-YLV6UC5W.js → permission-approvals-Q5IMFEZI.js} +5 -4
  80. package/dist/{permissions-DHOKF3W2.js → permissions-7Y35X6HZ.js} +26 -9
  81. package/dist/provider-approvals-5AGH3RVT.js +76 -0
  82. package/dist/{sandbox-WRYTZH3D.js → sandbox-N2RY72GN.js} +6 -5
  83. package/dist/{scrollback-35ME2GFX.js → scrollback-GD76GNM4.js} +36 -28
  84. package/dist/scrollback-GD76GNM4.js.map +1 -0
  85. package/dist/sdk.d.ts +876 -64
  86. package/dist/sdk.js +57 -37
  87. package/dist/sdk.js.map +1 -1
  88. package/dist/settings-loader-KDBH45AQ.js +43 -0
  89. package/dist/{settings-removed-JWWPT4WA.js → settings-removed-3ATWJEF3.js} +2 -2
  90. package/dist/shell-selection-Z5WORZU4.js +31 -0
  91. package/dist/shell-selection-Z5WORZU4.js.map +1 -0
  92. package/package.json +5 -5
  93. package/dist/app-GE4OHJOC.js.map +0 -1
  94. package/dist/chunk-224FJZLH.js.map +0 -1
  95. package/dist/chunk-5RDRAO4B.js.map +0 -1
  96. package/dist/chunk-5YHTKWJN.js.map +0 -1
  97. package/dist/chunk-62QL5QUP.js.map +0 -1
  98. package/dist/chunk-67UZEWWF.js.map +0 -1
  99. package/dist/chunk-6OQQOMZR.js +0 -785
  100. package/dist/chunk-6OQQOMZR.js.map +0 -1
  101. package/dist/chunk-AQEQX4FF.js.map +0 -1
  102. package/dist/chunk-C7ADUNPR.js +0 -273
  103. package/dist/chunk-C7ADUNPR.js.map +0 -1
  104. package/dist/chunk-EFJ2NH6E.js.map +0 -1
  105. package/dist/chunk-IME7R2UI.js.map +0 -1
  106. package/dist/chunk-J62VZYOB.js +0 -1339
  107. package/dist/chunk-J62VZYOB.js.map +0 -1
  108. package/dist/chunk-JTPOI3KU.js.map +0 -1
  109. package/dist/chunk-QYBD22H5.js.map +0 -1
  110. package/dist/chunk-RFJ6Q6LE.js.map +0 -1
  111. package/dist/chunk-S4JMID4G.js +0 -259
  112. package/dist/chunk-S4JMID4G.js.map +0 -1
  113. package/dist/chunk-TLX3H2CL.js.map +0 -1
  114. package/dist/chunk-VPNLCMVR.js.map +0 -1
  115. package/dist/chunk-W3XFHEF4.js.map +0 -1
  116. package/dist/chunk-X4NJT436.js.map +0 -1
  117. package/dist/chunk-X66I2NQO.js +0 -137
  118. package/dist/chunk-X66I2NQO.js.map +0 -1
  119. package/dist/chunk-Y2VHN7TC.js.map +0 -1
  120. package/dist/chunk-YC6W4EJH.js.map +0 -1
  121. package/dist/context-CBOBDY5Q.js +0 -38
  122. package/dist/loader-S77OVTHH.js +0 -18
  123. package/dist/memory-store-DZUT5HUH.js +0 -75
  124. package/dist/scrollback-35ME2GFX.js.map +0 -1
  125. package/dist/settings-loader-JUNPGAAM.js +0 -35
  126. package/dist/shell-selection-CDHGJ4GC.js +0 -17
  127. /package/dist/{additional-roots-H7UNAEHS.js.map → additional-roots-VAWRQ625.js.map} +0 -0
  128. /package/dist/{chunk-525CAZPY.js.map → chunk-4DKHLZRI.js.map} +0 -0
  129. /package/dist/{chunk-4AS7LKKQ.js.map → chunk-62EEAUID.js.map} +0 -0
  130. /package/dist/{chunk-KBQS35OV.js.map → chunk-MV5PQNIA.js.map} +0 -0
  131. /package/dist/{chunk-56SIEMDX.js.map → chunk-PTDC7ZUP.js.map} +0 -0
  132. /package/dist/{chunk-HQI7NYGX.js.map → chunk-SWPDKLNM.js.map} +0 -0
  133. /package/dist/{chunk-SOW4WRQP.js.map → chunk-WV3O5PGX.js.map} +0 -0
  134. /package/dist/{command-approvals-VBIU6BLM.js.map → command-approvals-ZRY66LCW.js.map} +0 -0
  135. /package/dist/{context-CBOBDY5Q.js.map → context-C3RAP6K5.js.map} +0 -0
  136. /package/dist/{hook-approvals-ZVT4V7IM.js.map → hook-approvals-DPQD5H65.js.map} +0 -0
  137. /package/dist/{loader-S77OVTHH.js.map → loader-DQNPDKOX.js.map} +0 -0
  138. /package/dist/{mcp-approvals-HJ2VDYMC.js.map → mcp-approvals-27LJYADQ.js.map} +0 -0
  139. /package/dist/{mcp-config-RNB5LEVE.js.map → mcp-config-GXSODNCM.js.map} +0 -0
  140. /package/dist/{memory-store-DZUT5HUH.js.map → memory-store-I6Z54VUN.js.map} +0 -0
  141. /package/dist/{model-window-store-MTSQ5JDJ.js.map → model-window-store-6VKXXTPJ.js.map} +0 -0
  142. /package/dist/{openai-compatible-2OB54YST.js.map → openai-compatible-IPUNBJ53.js.map} +0 -0
  143. /package/dist/{permission-approvals-YLV6UC5W.js.map → permission-approvals-Q5IMFEZI.js.map} +0 -0
  144. /package/dist/{permissions-DHOKF3W2.js.map → permissions-7Y35X6HZ.js.map} +0 -0
  145. /package/dist/{sandbox-WRYTZH3D.js.map → provider-approvals-5AGH3RVT.js.map} +0 -0
  146. /package/dist/{settings-loader-JUNPGAAM.js.map → sandbox-N2RY72GN.js.map} +0 -0
  147. /package/dist/{settings-removed-JWWPT4WA.js.map → settings-loader-KDBH45AQ.js.map} +0 -0
  148. /package/dist/{shell-selection-CDHGJ4GC.js.map → settings-removed-3ATWJEF3.js.map} +0 -0
@@ -1,5 +1,6 @@
1
1
  import {
2
- escapeInvisibleCharacters
2
+ escapeInvisibleCharacters,
3
+ foldControlCharacters
3
4
  } from "./chunk-2AAOF5B5.js";
4
5
  import {
5
6
  systemClock
@@ -7,7 +8,7 @@ import {
7
8
  import {
8
9
  createDebugLogger,
9
10
  isDebugEnabled
10
- } from "./chunk-62QL5QUP.js";
11
+ } from "./chunk-OJBMDM5H.js";
11
12
 
12
13
  // src/provider/reliability.ts
13
14
  function classifyHttpStatus(status) {
@@ -49,6 +50,98 @@ var CONTEXT_OVERFLOW_ERROR_NAMES = /* @__PURE__ */ new Set([
49
50
  // llama.cpp's server.
50
51
  "exceed_context_size_error"
51
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
+ }
52
145
  function errorBodyParts(body) {
53
146
  let parsedJson;
54
147
  try {
@@ -93,38 +186,129 @@ function statesContextOverflow(body) {
93
186
  const { message, names, raw } = errorBodyParts(body);
94
187
  return names.some((name) => CONTEXT_OVERFLOW_ERROR_NAMES.has(name.toLowerCase())) || isContextOverflowError(message) || raw !== void 0 && statesContextOverflow(raw);
95
188
  }
96
- function classifyApiError(status, body) {
97
- const code = classifyHttpStatus(wrappedUpstreamStatus(status, body) ?? status).code;
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
+ }
98
199
  return code === "bad_request" && statesContextOverflow(body) ? "context_overflow" : code;
99
200
  }
100
201
  function classifyProviderError(error) {
202
+ if (error instanceof ProviderRedirectError) return "redirect";
101
203
  if (error instanceof DOMException && error.name === "TimeoutError") return "timeout";
102
204
  const message = (error instanceof Error ? error.message : String(error)).toLowerCase();
103
205
  if (message.includes("timed out") || message.includes("timeout")) return "timeout";
104
206
  if (isContextOverflowError(message)) return "context_overflow";
105
207
  return "network";
106
208
  }
107
- function formatApiError(status, body) {
108
- const code = classifyApiError(status, body);
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);
109
273
  const detail = safeErrorDetail(body);
274
+ const { sentence: said, rewritten } = detailSentence(detail, request, body);
110
275
  const base = `API Error: ${status}`;
111
276
  switch (code) {
112
277
  case "context_overflow":
113
- return `${base} ${detail || "Input exceeds the model context window."} Reduce the conversation or tool output and try again.`;
114
- case "rate_limited":
115
- return isOversizedForRateLimit(detail) ? `${base} ${detail}. The request is larger than the rate limit allows at once, so it has to shrink; waiting will not help.` : `${base} ${detail}. This may be a temporary capacity issue. Try again in a moment.`;
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
+ }
116
290
  case "overloaded":
117
291
  return `${base} Repeated 529 Overloaded errors. The API is at capacity \u2014 this is usually temporary. Try again in a moment.`;
118
292
  case "server_error":
119
- return `${base} ${detail}. This is a server-side issue, usually temporary \u2014 try again in a moment.`;
293
+ return `${base} ${said} This is a server-side issue, usually temporary \u2014 try again in a moment.`;
120
294
  case "timeout":
121
295
  return "Request timed out. Check your network connection and try again.";
122
296
  case "auth":
123
- return `${base} ${detail}. Check BOOK_API_KEY, or provider.<id>.apiKey in settings.`;
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
+ }
124
307
  case "quota":
125
- return `${base} ${detail}. Check your usage/credits.`;
308
+ return `${base} ${said} Check your usage/credits.`;
126
309
  default:
127
- return `${base} ${detail}`;
310
+ if (detail.trim() && !rewritten) return `${base} ${detail}`;
311
+ return code === "not_found" ? `${base} ${said} Check the base URL and model id.` : `${base} ${said}`;
128
312
  }
129
313
  }
130
314
  function isContextOverflowError(error) {
@@ -156,25 +340,31 @@ function isErrorEnvelopeShape(text, usage) {
156
340
  const trimmed = text.trim();
157
341
  return trimmed.length <= ERROR_ENVELOPE_MAX_CHARS && /^\[Error\]/i.test(trimmed) && !/[\r\n]/.test(trimmed);
158
342
  }
343
+ var ROUTER_FAILED_RESPONSE = /^\s*\[Error\]\s*The model failed to generate a response\.?\s*$/i;
159
344
  function isUpstreamErrorEnvelope(text, usage) {
160
345
  if (!isErrorEnvelopeShape(text, usage)) return false;
161
- return isZeroUsage(usage) || /\brequest id\b/i.test(text) || /help\.openai\.com/i.test(text) || /error occurred while processing/i.test(text);
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);
162
347
  }
163
348
  var ERROR_BODY_READ_TIMEOUT_MS = 5e3;
164
349
  var ERROR_BODY_MAX_BYTES = 65536;
165
350
  async function readErrorBody(response, signal) {
166
- if (signal?.aborted || !response.body) return "";
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 };
167
356
  let reader;
168
357
  try {
169
358
  reader = response.body.getReader();
170
359
  } catch {
171
- return "";
360
+ return { text: "", received: false };
172
361
  }
173
362
  const decoder = new TextDecoder();
174
363
  let text = "";
175
364
  let received = 0;
176
- let finished = false;
365
+ let interrupted = false;
177
366
  const stop = () => {
367
+ interrupted = true;
178
368
  reader.cancel().catch(() => {
179
369
  });
180
370
  };
@@ -183,25 +373,72 @@ async function readErrorBody(response, signal) {
183
373
  try {
184
374
  while (received < ERROR_BODY_MAX_BYTES) {
185
375
  const chunk = await reader.read();
186
- if (chunk.done) {
187
- finished = true;
188
- break;
189
- }
376
+ if (chunk.done) break;
190
377
  const bytes = chunk.value.subarray(0, ERROR_BODY_MAX_BYTES - received);
191
378
  received += bytes.byteLength;
192
379
  text += decoder.decode(bytes, { stream: true });
193
380
  }
194
381
  } catch {
382
+ interrupted = true;
195
383
  } finally {
196
384
  clearTimeout(timer);
197
385
  signal?.removeEventListener("abort", stop);
198
- if (!finished) stop();
386
+ reader.cancel().catch(() => {
387
+ });
199
388
  try {
200
389
  reader.releaseLock();
201
390
  } catch {
202
391
  }
203
392
  }
204
- return text + decoder.decode();
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
+ );
205
442
  }
206
443
  async function fetchWithRetry(url, init, retry, signal, onRetry, logger, clock = systemClock) {
207
444
  const startMs = clock.monotonicNowMs();
@@ -212,7 +449,7 @@ async function fetchWithRetry(url, init, retry, signal, onRetry, logger, clock =
212
449
  const fetchSignal = requestSignal(signal, retry.requestTimeoutMs);
213
450
  let response;
214
451
  try {
215
- response = await fetch(url, { ...init, signal: fetchSignal });
452
+ response = await fetch(url, { ...init, redirect: "manual", signal: fetchSignal });
216
453
  } catch (error) {
217
454
  if (signal?.aborted) throw error;
218
455
  lastError = error instanceof Error ? error.message : String(error);
@@ -227,36 +464,73 @@ async function fetchWithRetry(url, init, retry, signal, onRetry, logger, clock =
227
464
  await sleep(delay2, signal);
228
465
  continue;
229
466
  }
467
+ if (isRedirectResponse(response)) throw await refuseRedirect(response, url);
230
468
  const classification = classifyHttpStatus(response.status);
231
469
  if (!classification.retryable) return response;
232
470
  lastError = `API error ${response.status}`;
233
- const bodyText = await readErrorBody(response, signal);
471
+ const errorBody = await readErrorBody(response, signal);
472
+ const bodyText = errorBody.text;
473
+ const buffered = (init2) => new BufferedErrorResponse(bodyText, init2, errorBody.received);
234
474
  const quoted = quotedUpstreamStatus(bodyText);
235
475
  if (quoted !== void 0 && !classifyHttpStatus(quoted).retryable) {
236
476
  logger?.warn("upstream error quoted in retryable status; not retrying", {
237
477
  status: response.status,
238
478
  upstreamStatus: quoted
239
479
  });
240
- return new Response(bodyText, {
480
+ return buffered({
241
481
  status: response.status,
242
482
  statusText: response.statusText,
243
483
  headers: response.headers
244
484
  });
245
485
  }
246
- if ((quoted ?? response.status) === 429 && isOversizedForRateLimit(formatApiError(response.status, bodyText))) {
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))) {
247
498
  logger?.warn("rate limit on a request too large to ever fit; not retrying", {
248
499
  status: response.status
249
500
  });
250
- return new Response(bodyText, {
501
+ return buffered({
251
502
  status: response.status,
252
503
  statusText: response.statusText,
253
504
  headers: response.headers
254
505
  });
255
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
+ }
256
530
  const watchdogRetry = retry.watchdog && (response.status === 429 || response.status === 529);
257
531
  const effectiveMax = watchdogRetry ? Number.MAX_SAFE_INTEGER : maxAttempts;
258
532
  if (attempt >= effectiveMax || budgetExhausted(clock, startMs, retry.totalBudgetMs)) {
259
- return new Response(bodyText, {
533
+ return buffered({
260
534
  status: response.status,
261
535
  statusText: response.statusText,
262
536
  headers: response.headers
@@ -264,7 +538,7 @@ async function fetchWithRetry(url, init, retry, signal, onRetry, logger, clock =
264
538
  }
265
539
  const delay = boundedDelay(
266
540
  clock,
267
- backoffMs(attempt, retry, response.headers.get("retry-after"), bodyText),
541
+ backoffMs(attempt, retry, stated),
268
542
  startMs,
269
543
  retry.totalBudgetMs
270
544
  );
@@ -277,16 +551,13 @@ async function fetchWithRetry(url, init, retry, signal, onRetry, logger, clock =
277
551
  try {
278
552
  await sleep(delay, signal);
279
553
  } catch {
280
- return new Response(bodyText, {
554
+ return buffered({
281
555
  status: response.status,
282
556
  statusText: response.statusText,
283
557
  headers: response.headers
284
558
  });
285
559
  }
286
- try {
287
- await response.body?.cancel();
288
- } catch {
289
- }
560
+ await cancelBody(response);
290
561
  }
291
562
  throw new Error(lastError ?? "request failed after retries");
292
563
  }
@@ -348,9 +619,54 @@ function sleep(ms, signal) {
348
619
  signal?.addEventListener("abort", onAbort, { once: true });
349
620
  });
350
621
  }
351
- function backoffMs(attempt, retry, retryAfter, body) {
352
- const retryAfterMs = parseRetryAfter(retryAfter) ?? parseCooldownReset(body);
353
- if (retryAfterMs !== void 0) return Math.min(retryAfterMs, retry.maxDelayMs);
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);
354
670
  const exponential = Math.min(retry.baseDelayMs * 2 ** attempt, retry.maxDelayMs);
355
671
  return Math.round(exponential * (0.5 + Math.random()));
356
672
  }
@@ -383,7 +699,13 @@ var COOLDOWN_UNIT_MS = {
383
699
  var COOLDOWN_UNIT = /(\d+)\s*(ms|sec(?:ond)?s?|min(?:ute)?s?|h(?:ours?|rs?)?|s|m)(?![a-z])/gi;
384
700
  function parseCooldownReset(body) {
385
701
  if (!body) return void 0;
386
- const stated = body.match(/\(reset after\s*([^)]+)\)/i);
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);
387
709
  if (!stated) return void 0;
388
710
  let totalMs = 0;
389
711
  let statedUnits = 0;
@@ -575,13 +897,12 @@ async function* chatCompletionStream(config, messages, tools, options) {
575
897
  }
576
898
  log.debug("response received", { status: response.status, ok: response.ok });
577
899
  if (!response.ok) {
578
- const errorText = await readErrorBody(response, signal);
579
- yield {
580
- type: "error",
581
- error: formatApiError(response.status, errorText),
582
- errorCode: classifyApiError(response.status, errorText),
583
- upstreamStatus: wrappedUpstreamStatus(response.status, errorText)
584
- };
900
+ const errorBody = await readErrorBody(response, signal);
901
+ yield statedWaitErrorEvent(response, errorBody.text, {
902
+ url,
903
+ model: config.model,
904
+ bodyReceived: errorBody.received
905
+ });
585
906
  return;
586
907
  }
587
908
  const reader = response.body?.getReader();
@@ -600,6 +921,15 @@ async function* chatCompletionStream(config, messages, tools, options) {
600
921
  for (const part of [...toolCallParts].sort((a, b) => a.index - b.index)) {
601
922
  if (!part.id && !part.name) continue;
602
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
+ }
603
933
  if (unparsedArguments) {
604
934
  log.warn("tool call arguments are not valid JSON", {
605
935
  index: part.index,
@@ -616,7 +946,8 @@ async function* chatCompletionStream(config, messages, tools, options) {
616
946
  id: part.id || `tool-${part.index}`,
617
947
  name: part.name,
618
948
  arguments: arguments_,
619
- ...unparsedArguments ? { unparsedArguments } : {}
949
+ ...unparsedArguments ? { unparsedArguments } : {},
950
+ ...emptyOnWire ? { emptyOnWire: true } : {}
620
951
  }
621
952
  };
622
953
  }
@@ -661,9 +992,7 @@ async function* chatCompletionStream(config, messages, tools, options) {
661
992
  if (!delta) return false;
662
993
  const reasoning = parseReasoningDelta(delta);
663
994
  if (reasoning) {
664
- if (retry.thinkingStallTimeoutMs) {
665
- stallTimeoutMs = Math.max(stallTimeoutMs, retry.thinkingStallTimeoutMs);
666
- }
995
+ stallTimeoutMs = retry.thinkingStallTimeoutMs ? Math.max(startingStallTimeoutMs, retry.thinkingStallTimeoutMs) : startingStallTimeoutMs;
667
996
  log.debug("stream reasoning", { len: reasoning.length });
668
997
  yield { type: "reasoning", reasoning };
669
998
  }
@@ -710,7 +1039,8 @@ async function* chatCompletionStream(config, messages, tools, options) {
710
1039
  return false;
711
1040
  };
712
1041
  const startingStallTimeoutMs = reasoningEnabled && retry.thinkingStallTimeoutMs ? Math.max(retry.streamStallTimeoutMs, retry.thinkingStallTimeoutMs) : retry.streamStallTimeoutMs;
713
- let stallTimeoutMs = startingStallTimeoutMs;
1042
+ const firstDeltaApplies = !reasoningEnabled && config.modelInfo?.effort !== false;
1043
+ let stallTimeoutMs = firstDeltaApplies ? Math.max(startingStallTimeoutMs, retry.firstDeltaStallTimeoutMs ?? 0) : startingStallTimeoutMs;
714
1044
  try {
715
1045
  while (true) {
716
1046
  const result = await readStreamChunk(reader, signal, stallTimeoutMs);
@@ -775,20 +1105,20 @@ async function* chatCompletionStream(config, messages, tools, options) {
775
1105
  }
776
1106
 
777
1107
  export {
778
- wrappedUpstreamStatus,
779
- classifyApiError,
780
1108
  classifyProviderError,
781
- formatApiError,
782
1109
  isContextOverflowError,
783
1110
  isOversizedForRateLimit,
784
1111
  statedRateLimit,
785
1112
  isErrorEnvelopeShape,
786
1113
  isUpstreamErrorEnvelope,
787
1114
  readErrorBody,
1115
+ isRedirectResponse,
1116
+ refuseRedirect,
788
1117
  fetchWithRetry,
789
1118
  readStreamChunk,
1119
+ statedWaitErrorEvent,
790
1120
  parseCompatibleUsage,
791
1121
  convertTools,
792
1122
  chatCompletionStream
793
1123
  };
794
- //# sourceMappingURL=chunk-AQEQX4FF.js.map
1124
+ //# sourceMappingURL=chunk-AHC7OU75.js.map