@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.
- package/CHANGELOG.md +1024 -0
- package/README.md +6 -3
- package/dist/{additional-roots-H7UNAEHS.js → additional-roots-VAWRQ625.js} +8 -7
- package/dist/{app-GE4OHJOC.js → app-J7BVPSUH.js} +5397 -2953
- package/dist/app-J7BVPSUH.js.map +1 -0
- package/dist/chunk-32WBCUAN.js +660 -0
- package/dist/chunk-32WBCUAN.js.map +1 -0
- package/dist/{chunk-525CAZPY.js → chunk-4DKHLZRI.js} +6 -6
- package/dist/{chunk-4AS7LKKQ.js → chunk-62EEAUID.js} +2 -2
- package/dist/{chunk-AQEQX4FF.js → chunk-AHC7OU75.js} +385 -55
- package/dist/chunk-AHC7OU75.js.map +1 -0
- package/dist/{chunk-EFJ2NH6E.js → chunk-AJZWLKLQ.js} +63 -14
- package/dist/chunk-AJZWLKLQ.js.map +1 -0
- package/dist/chunk-B5FC3MYZ.js +395 -0
- package/dist/chunk-B5FC3MYZ.js.map +1 -0
- package/dist/{chunk-67UZEWWF.js → chunk-DNFQTSMJ.js} +53 -11
- package/dist/chunk-DNFQTSMJ.js.map +1 -0
- package/dist/{chunk-TLX3H2CL.js → chunk-E5WLQSKA.js} +231 -40
- package/dist/chunk-E5WLQSKA.js.map +1 -0
- package/dist/chunk-ECLC4CIJ.js +1181 -0
- package/dist/chunk-ECLC4CIJ.js.map +1 -0
- package/dist/chunk-EEX6QNXV.js +127 -0
- package/dist/chunk-EEX6QNXV.js.map +1 -0
- package/dist/chunk-EFXVSQ5J.js +931 -0
- package/dist/chunk-EFXVSQ5J.js.map +1 -0
- package/dist/{chunk-RFJ6Q6LE.js → chunk-ENLB7NYV.js} +577 -164
- package/dist/chunk-ENLB7NYV.js.map +1 -0
- package/dist/{chunk-IME7R2UI.js → chunk-FPR7SKIE.js} +1268 -883
- package/dist/chunk-FPR7SKIE.js.map +1 -0
- package/dist/{chunk-VPNLCMVR.js → chunk-HFB74CLM.js} +13 -5
- package/dist/chunk-HFB74CLM.js.map +1 -0
- package/dist/{chunk-5RDRAO4B.js → chunk-HOZXKOE3.js} +37 -1
- package/dist/chunk-HOZXKOE3.js.map +1 -0
- package/dist/chunk-IDYWN37T.js +36 -0
- package/dist/chunk-IDYWN37T.js.map +1 -0
- package/dist/{chunk-YC6W4EJH.js → chunk-JGF5HLSI.js} +193 -75
- package/dist/chunk-JGF5HLSI.js.map +1 -0
- package/dist/{chunk-5YHTKWJN.js → chunk-KDNWMKXV.js} +2447 -907
- package/dist/chunk-KDNWMKXV.js.map +1 -0
- package/dist/{chunk-Y2VHN7TC.js → chunk-MEOB2DZA.js} +11 -1
- package/dist/chunk-MEOB2DZA.js.map +1 -0
- package/dist/{chunk-KBQS35OV.js → chunk-MV5PQNIA.js} +2 -2
- package/dist/chunk-N2G4Q7ME.js +2672 -0
- package/dist/chunk-N2G4Q7ME.js.map +1 -0
- package/dist/{chunk-62QL5QUP.js → chunk-OJBMDM5H.js} +17 -1
- package/dist/chunk-OJBMDM5H.js.map +1 -0
- package/dist/{chunk-56SIEMDX.js → chunk-PTDC7ZUP.js} +2 -2
- package/dist/{chunk-X4NJT436.js → chunk-REASC2QG.js} +67 -28
- package/dist/chunk-REASC2QG.js.map +1 -0
- package/dist/chunk-SBTU5BEC.js +114 -0
- package/dist/chunk-SBTU5BEC.js.map +1 -0
- package/dist/{chunk-HQI7NYGX.js → chunk-SWPDKLNM.js} +2 -2
- package/dist/{chunk-W3XFHEF4.js → chunk-UCFJ43OS.js} +140 -28
- package/dist/chunk-UCFJ43OS.js.map +1 -0
- package/dist/{chunk-QYBD22H5.js → chunk-W7SLYS3M.js} +80 -16
- package/dist/chunk-W7SLYS3M.js.map +1 -0
- package/dist/{chunk-SOW4WRQP.js → chunk-WV3O5PGX.js} +3 -3
- package/dist/{chunk-JTPOI3KU.js → chunk-XJRJVXJR.js} +262 -38
- package/dist/chunk-XJRJVXJR.js.map +1 -0
- package/dist/{chunk-224FJZLH.js → chunk-YIL6HTJX.js} +228 -5
- package/dist/chunk-YIL6HTJX.js.map +1 -0
- package/dist/{command-approvals-VBIU6BLM.js → command-approvals-ZRY66LCW.js} +5 -4
- package/dist/context-C3RAP6K5.js +41 -0
- package/dist/{hook-approvals-ZVT4V7IM.js → hook-approvals-DPQD5H65.js} +5 -4
- package/dist/index.js +657 -754
- package/dist/index.js.map +1 -1
- package/dist/{interactive-assets-T4I4SEHU.js → interactive-assets-73AQO5WP.js} +11 -6
- package/dist/{interactive-assets-T4I4SEHU.js.map → interactive-assets-73AQO5WP.js.map} +1 -1
- package/dist/job-runner.js +10 -3
- package/dist/job-runner.js.map +1 -1
- package/dist/job-supervisor.js +13 -4
- package/dist/job-supervisor.js.map +1 -1
- package/dist/loader-DQNPDKOX.js +27 -0
- package/dist/{mcp-approvals-HJ2VDYMC.js → mcp-approvals-27LJYADQ.js} +5 -4
- package/dist/{mcp-config-RNB5LEVE.js → mcp-config-GXSODNCM.js} +3 -2
- package/dist/memory-store-I6Z54VUN.js +51 -0
- package/dist/{model-window-store-MTSQ5JDJ.js → model-window-store-6VKXXTPJ.js} +4 -3
- package/dist/{openai-compatible-2OB54YST.js → openai-compatible-IPUNBJ53.js} +5 -3
- package/dist/{permission-approvals-YLV6UC5W.js → permission-approvals-Q5IMFEZI.js} +5 -4
- package/dist/{permissions-DHOKF3W2.js → permissions-7Y35X6HZ.js} +26 -9
- package/dist/provider-approvals-5AGH3RVT.js +76 -0
- package/dist/{sandbox-WRYTZH3D.js → sandbox-N2RY72GN.js} +6 -5
- package/dist/{scrollback-35ME2GFX.js → scrollback-GD76GNM4.js} +36 -28
- package/dist/scrollback-GD76GNM4.js.map +1 -0
- package/dist/sdk.d.ts +876 -64
- package/dist/sdk.js +57 -37
- package/dist/sdk.js.map +1 -1
- package/dist/settings-loader-KDBH45AQ.js +43 -0
- package/dist/{settings-removed-JWWPT4WA.js → settings-removed-3ATWJEF3.js} +2 -2
- package/dist/shell-selection-Z5WORZU4.js +31 -0
- package/dist/shell-selection-Z5WORZU4.js.map +1 -0
- package/package.json +5 -5
- package/dist/app-GE4OHJOC.js.map +0 -1
- package/dist/chunk-224FJZLH.js.map +0 -1
- package/dist/chunk-5RDRAO4B.js.map +0 -1
- package/dist/chunk-5YHTKWJN.js.map +0 -1
- package/dist/chunk-62QL5QUP.js.map +0 -1
- package/dist/chunk-67UZEWWF.js.map +0 -1
- package/dist/chunk-6OQQOMZR.js +0 -785
- package/dist/chunk-6OQQOMZR.js.map +0 -1
- package/dist/chunk-AQEQX4FF.js.map +0 -1
- package/dist/chunk-C7ADUNPR.js +0 -273
- package/dist/chunk-C7ADUNPR.js.map +0 -1
- package/dist/chunk-EFJ2NH6E.js.map +0 -1
- package/dist/chunk-IME7R2UI.js.map +0 -1
- package/dist/chunk-J62VZYOB.js +0 -1339
- package/dist/chunk-J62VZYOB.js.map +0 -1
- package/dist/chunk-JTPOI3KU.js.map +0 -1
- package/dist/chunk-QYBD22H5.js.map +0 -1
- package/dist/chunk-RFJ6Q6LE.js.map +0 -1
- package/dist/chunk-S4JMID4G.js +0 -259
- package/dist/chunk-S4JMID4G.js.map +0 -1
- package/dist/chunk-TLX3H2CL.js.map +0 -1
- package/dist/chunk-VPNLCMVR.js.map +0 -1
- package/dist/chunk-W3XFHEF4.js.map +0 -1
- package/dist/chunk-X4NJT436.js.map +0 -1
- package/dist/chunk-X66I2NQO.js +0 -137
- package/dist/chunk-X66I2NQO.js.map +0 -1
- package/dist/chunk-Y2VHN7TC.js.map +0 -1
- package/dist/chunk-YC6W4EJH.js.map +0 -1
- package/dist/context-CBOBDY5Q.js +0 -38
- package/dist/loader-S77OVTHH.js +0 -18
- package/dist/memory-store-DZUT5HUH.js +0 -75
- package/dist/scrollback-35ME2GFX.js.map +0 -1
- package/dist/settings-loader-JUNPGAAM.js +0 -35
- package/dist/shell-selection-CDHGJ4GC.js +0 -17
- /package/dist/{additional-roots-H7UNAEHS.js.map → additional-roots-VAWRQ625.js.map} +0 -0
- /package/dist/{chunk-525CAZPY.js.map → chunk-4DKHLZRI.js.map} +0 -0
- /package/dist/{chunk-4AS7LKKQ.js.map → chunk-62EEAUID.js.map} +0 -0
- /package/dist/{chunk-KBQS35OV.js.map → chunk-MV5PQNIA.js.map} +0 -0
- /package/dist/{chunk-56SIEMDX.js.map → chunk-PTDC7ZUP.js.map} +0 -0
- /package/dist/{chunk-HQI7NYGX.js.map → chunk-SWPDKLNM.js.map} +0 -0
- /package/dist/{chunk-SOW4WRQP.js.map → chunk-WV3O5PGX.js.map} +0 -0
- /package/dist/{command-approvals-VBIU6BLM.js.map → command-approvals-ZRY66LCW.js.map} +0 -0
- /package/dist/{context-CBOBDY5Q.js.map → context-C3RAP6K5.js.map} +0 -0
- /package/dist/{hook-approvals-ZVT4V7IM.js.map → hook-approvals-DPQD5H65.js.map} +0 -0
- /package/dist/{loader-S77OVTHH.js.map → loader-DQNPDKOX.js.map} +0 -0
- /package/dist/{mcp-approvals-HJ2VDYMC.js.map → mcp-approvals-27LJYADQ.js.map} +0 -0
- /package/dist/{mcp-config-RNB5LEVE.js.map → mcp-config-GXSODNCM.js.map} +0 -0
- /package/dist/{memory-store-DZUT5HUH.js.map → memory-store-I6Z54VUN.js.map} +0 -0
- /package/dist/{model-window-store-MTSQ5JDJ.js.map → model-window-store-6VKXXTPJ.js.map} +0 -0
- /package/dist/{openai-compatible-2OB54YST.js.map → openai-compatible-IPUNBJ53.js.map} +0 -0
- /package/dist/{permission-approvals-YLV6UC5W.js.map → permission-approvals-Q5IMFEZI.js.map} +0 -0
- /package/dist/{permissions-DHOKF3W2.js.map → permissions-7Y35X6HZ.js.map} +0 -0
- /package/dist/{sandbox-WRYTZH3D.js.map → provider-approvals-5AGH3RVT.js.map} +0 -0
- /package/dist/{settings-loader-JUNPGAAM.js.map → sandbox-N2RY72GN.js.map} +0 -0
- /package/dist/{settings-removed-JWWPT4WA.js.map → settings-loader-KDBH45AQ.js.map} +0 -0
- /package/dist/{shell-selection-CDHGJ4GC.js.map → settings-removed-3ATWJEF3.js.map} +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/provider/reliability.ts","../src/provider/openai-compatible.ts"],"sourcesContent":["import type { RetryConfig } from '../types/runtime.js';\n\ninterface RetryLogger {\n warn(message: string, data?: Record<string, unknown>): void;\n}\n\nimport { systemClock, type Clock, type MonotonicMs } from '../clock.js';\nimport { foldControlCharacters } from '../control-characters.js';\n\nexport type ProviderErrorCode =\n | 'network'\n | 'timeout'\n | 'context_overflow'\n | 'rate_limited'\n | 'overloaded'\n | 'server_error'\n | 'auth'\n | 'bad_request'\n | 'not_found'\n | 'unprocessable'\n | 'quota'\n /**\n * A 429 that states a spent usage or plan limit (#409): the run ends\n * `quota_exhausted`, not `credentials_rejected`. A 402 and OpenAI's\n * `insufficient_quota` stay `quota`.\n */\n | 'usage_limit'\n /**\n * A 429 whose stated wait (Retry-After, `(reset after …)`, `resets at <ISO>`)\n * is longer than the retry layer can wait. Not produced by `classifyApiError`:\n * the retry layer's early stop carries it out (`statedWaitStop`).\n */\n | 'rate_limit_wait'\n | 'model_unavailable'\n | 'redirect'\n | 'unknown';\n\nexport function classifyHttpStatus(status: number): {\n code: ProviderErrorCode;\n retryable: boolean;\n} {\n if (status === 413) return { code: 'context_overflow', retryable: false };\n if (status === 429) return { code: 'rate_limited', retryable: true };\n if (status === 529) return { code: 'overloaded', retryable: true };\n if (status >= 500 && status < 600) return { code: 'server_error', retryable: true };\n if (status === 408) return { code: 'timeout', retryable: true };\n if (status === 401 || status === 403) return { code: 'auth', retryable: false };\n if (status === 402) return { code: 'quota', retryable: false };\n if (status === 400) return { code: 'bad_request', retryable: false };\n if (status === 404) return { code: 'not_found', retryable: false };\n if (status === 422) return { code: 'unprocessable', retryable: false };\n return { code: 'unknown', retryable: false };\n}\n\n/**\n * The two 404 wordings a router hands back when its route has nothing left to\n * serve a model with (#383). Recognized only when the body also states a\n * `(reset after …)` cooldown, so a permanent \"No endpoints found for\n * unknown-model\" — a missing model, not a capacity moment — stays a terminal\n * 404.\n */\nconst CAPACITY_OUTAGE_404 = /no\\s+(?:endpoints|accounts)\\s+found/i;\n\n/** The cooldown statement the capacity outage arrives with. */\nconst RESET_AFTER = /\\(reset after\\s*[^)]+\\)/i;\n\n/**\n * The upstream HTTP status a router quoted inside its own error body, if any.\n *\n * 9router wraps an upstream 4xx as a 503 plus a cooldown, so the wrapper's status\n * says \"transient\" while the body says the request itself is invalid. Only a 4xx\n * is ever taken from the body: a quoted 5xx says nothing the wrapper did not.\n *\n * Only two shapes count as a quote: 9router's own `[<route>] [400]:` prefix, and\n * a JSON `\"error\"` object whose `code` is a 4xx. A status mentioned in prose\n * (`upstream sent HTTP 403`, `chunk [404]`) or a longer number that merely starts\n * like one (`\"code\": 4001`) is not: misreading an outage body ends the run after\n * a single request, or parks it as a rejected credential.\n *\n * A quoted 404 whose body says no endpoints or no accounts were found — and\n * states a cooldown — is a capacity outage (#383): the router turns an upstream\n * `[CommandCode error: No endpoints found for <model>.]` into a 503 with a\n * `(reset after …)` cooldown, so it is transient and the quote must not end the\n * retries. The read ends there: such a body can also carry a JSON error object\n * with some other code, and reading that quote would park the run as if the\n * credential had been refused. A cooldown-less \"no endpoints found\" is kept as\n * the 404 it is.\n */\nexport function quotedUpstreamStatus(body: string): number | undefined {\n if (!body) return undefined;\n const text = body.length > 65536 ? body.slice(0, 65536) : body;\n const patterns = [\n /\\[[^\\]\\s]+\\]\\s*\\[(4\\d\\d)\\]:/,\n /\"error\"\\s*:\\s*\\{[^{}]*?\"code\"\\s*:\\s*\"?(4\\d\\d)(?![\\d.])/,\n ];\n const capacityOutage = CAPACITY_OUTAGE_404.test(text) && RESET_AFTER.test(text);\n if (capacityOutage) return undefined;\n for (const pattern of patterns) {\n const match = text.match(pattern);\n if (match) return Number(match[1]);\n }\n return undefined;\n}\n\n/**\n * The upstream 4xx quoted inside a retryable response, when that quote is what\n * classifies it: 9router answers 503 and names the real status in the body. A\n * non-retryable status is its own answer, and its body is never re-read.\n */\nexport function wrappedUpstreamStatus(status: number, body: string): number | undefined {\n return classifyHttpStatus(status).retryable ? quotedUpstreamStatus(body) : undefined;\n}\n\n/** `error.code` or `error.type` values that name a context overflow outright. */\nconst CONTEXT_OVERFLOW_ERROR_NAMES: ReadonlySet<string> = new Set([\n 'context_length_exceeded',\n 'request_too_large',\n // llama.cpp's server.\n 'exceed_context_size_error',\n]);\n\n/**\n * `error.code` or `error.type` values that name the model itself as gone (#387):\n * OpenAI's `model_not_found`, and the spellings routers invent for the same\n * verdict. Names, not statuses — a `model_not_found` code on a wrapped 403 is\n * still about the model, not about the request or the key.\n */\nconst MODEL_UNAVAILABLE_ERROR_NAMES: ReadonlySet<string> = new Set([\n 'model_not_found',\n 'model_not_available',\n 'model_unavailable',\n]);\n\n/**\n * The wordings a provider uses to say the model itself is gone or unusable\n * (#387). An explicit \"please pick another\" instruction needs no corroboration;\n * every other phrase is only believed when it shares a sentence with a model\n * word, so a 403 that says an endpoint is \"not available today\" is not read as\n * a retirement of the model named three sentences earlier.\n */\nconst MODEL_GONE_PHRASES = [\n 'no longer available',\n 'no longer supported',\n 'not found',\n 'does not exist',\n 'is not supported',\n 'not available',\n 'has been retired',\n 'has been removed',\n 'has been deprecated',\n];\n\n/** \"Please pick another model\" — the wordings that name their own remedy. */\nconst CHOOSE_ANOTHER_MODEL =\n /(?:select|choose)\\s+(?:another|a\\s+different)\\s+model\\b(?!\\s+provider\\b)/i;\n\n/** How far a gone-phrase may sit from a model word and still be about it. */\nconst MODEL_GONE_PROXIMITY_CHARS = 60;\n\n/** Credential words: a sentence that names a credential is never about a model. */\nconst CREDENTIAL_WORD = /\\b(?:api\\s+key|key|token|credential)s?\\b/i;\n\n/** Sentence punctuation followed by whitespace or end of string, or newlines. */\nfunction splitSentences(text: string): string[] {\n return text.split(/[.!?:;]+(?:\\s+|$)|[\\r\\n]+/);\n}\n\n/** `error.code` or `error.type` values that name a spent quota outright (#400). */\nconst INSUFFICIENT_QUOTA_ERROR_NAMES: ReadonlySet<string> = new Set(['insufficient_quota']);\n\n/** True when an error body's code or type names `insufficient_quota` (#400). */\nfunction hasInsufficientQuotaName(body: string): boolean {\n if (!body) return false;\n const { names, raw } = errorBodyParts(body);\n if (names.some((name) => INSUFFICIENT_QUOTA_ERROR_NAMES.has(name.toLowerCase()))) {\n return true;\n }\n return raw !== undefined && hasInsufficientQuotaName(raw);\n}\n\n/**\n * Verbs that state a limit was reached, exceeded, exhausted, or spent (#400).\n * Pairs with usage/quota phrases and long windows.\n */\nconst SPENT_LIMIT_VERBS =\n /\\b(?:reached|exceeded|exhausted|hit|used\\s+up|spent|r(?:un|an)\\s+out|depleted)\\b/i;\n\n/** Limit phrases for spent quota or plan usage limits (#400). */\nconst SPENT_USAGE_LIMIT_PHRASES =\n /\\b(?:usage\\s+limits?|plan\\s+limits?|quotas?|subscription\\s+limits?|limits?\\s+for\\s+your\\s+plan)\\b/i;\n\n/** Long window phrases: weekly, daily, monthly, N-hour, per day/week/month (#400). */\nconst LONG_WINDOW_PHRASES =\n /\\b(?:weekly|daily|monthly|\\d+(?:\\.\\d+)?(?:-|\\s+)hours?|per\\s+(?:day|week|month))\\b/i;\n\n/**\n * Short window phrases that designate rate limits rather than plan quotas (#400).\n * A message containing any of these stays rate_limited.\n */\nconst SHORT_WINDOW_PHRASES = /\\b(?:per\\s+min(?:ute)?|per\\s+sec(?:ond)?|rpm|tpm|rps)\\b/i;\n\n/**\n * True when an error body says a usage or plan limit is reached, exceeded, or\n * spent over a long window (weekly, daily, monthly, N-hour) (#400).\n *\n * Read through `errorBodyParts` (the message, and OpenRouter's forwarded `raw`).\n * A short window phrase (per minute, RPM, TPM) anywhere in the message or raw\n * text keeps it a rate limit.\n */\nfunction statesSpentUsageLimit(body: string): boolean {\n if (!body) return false;\n const { message, raw } = errorBodyParts(body);\n const saysIt = (text: string): boolean => {\n if (!text || SHORT_WINDOW_PHRASES.test(text)) return false;\n for (const sentence of splitSentences(text)) {\n if (\n SPENT_LIMIT_VERBS.test(sentence) &&\n SPENT_USAGE_LIMIT_PHRASES.test(sentence) &&\n LONG_WINDOW_PHRASES.test(sentence)\n ) {\n return true;\n }\n }\n return false;\n };\n return saysIt(message) || (raw !== undefined && statesSpentUsageLimit(raw));\n}\n\n/**\n * An absolute reset time stated in an error body (#400):\n * `resets at <ISO>`, `reset at <ISO>`, `resets on <ISO>`, `reset on <ISO>`.\n * The ISO timestamp must have a date, `T`, time, and `Z` or `±hh:mm` offset.\n *\n * Read only from the message parts (`errorBodyParts`: the message, then\n * OpenRouter's forwarded `raw`), never from arbitrary text elsewhere in the\n * body, which may echo the request.\n */\nconst ABSOLUTE_RESET_PATTERN =\n /\\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;\n\nexport type AbsoluteReset = {\n epochMs: number;\n iso: string;\n};\n\nexport function parseAbsoluteReset(body?: string | null): AbsoluteReset | undefined {\n if (!body) return undefined;\n const { message, raw } = errorBodyParts(body);\n const match = message.match(ABSOLUTE_RESET_PATTERN);\n if (match) {\n const timestamp = Date.parse(match[1]);\n if (Number.isFinite(timestamp)) {\n return { epochMs: timestamp, iso: match[1] };\n }\n }\n if (raw !== undefined) {\n return parseAbsoluteReset(raw);\n }\n return undefined;\n}\n\n/**\n * True when an error body says the model is gone or unusable, read from the\n * same message parts every other body reading uses (`errorBodyParts`: the\n * message, the `code` / `type` names, and OpenRouter's forwarded `raw`) —\n * never from arbitrary text elsewhere in the body, which may echo the request\n * or a model list.\n *\n * A match is either an explicit \"select / choose another model\" instruction, a\n * gone-phrase within `MODEL_GONE_PROXIMITY_CHARS` after a model word in the same\n * sentence, Anthropic's 404 `not_found_error` naming `model:`, or a `code` /\n * `type` name that says so outright. A body that says only \"Invalid API key\n * provided\", \"Forbidden\" or \"Not Found\" never matches, so a genuine credential\n * problem keeps its own classification.\n */\nfunction statesModelUnavailable(body: string, effectiveStatus?: number): boolean {\n const { message, names, raw } = errorBodyParts(body);\n if (names.some((name) => MODEL_UNAVAILABLE_ERROR_NAMES.has(name.toLowerCase()))) return true;\n if (\n effectiveStatus === 404 &&\n names.some((name) => name.toLowerCase() === 'not_found_error') &&\n message.trim().toLowerCase().startsWith('model:')\n ) {\n return true;\n }\n const saysIt = (text: string): boolean => {\n // Sentence punctuation followed by whitespace or end of string, or newlines.\n // Dotted model ids (gpt-4.1) and route colons stay within one sentence.\n for (const sentence of splitSentences(text)) {\n if (CREDENTIAL_WORD.test(sentence)) continue;\n if (CHOOSE_ANOTHER_MODEL.test(sentence)) return true;\n const lower = sentence.toLowerCase();\n const modelWords = [...lower.matchAll(/\\bmodels?\\b/g)].map((m) => m.index);\n if (modelWords.length === 0) continue;\n for (const phrase of MODEL_GONE_PHRASES) {\n let gone = lower.indexOf(phrase);\n while (gone >= 0) {\n if (modelWords.some((at) => at < gone && gone - at <= MODEL_GONE_PROXIMITY_CHARS)) {\n return true;\n }\n gone = lower.indexOf(phrase, gone + 1);\n }\n }\n }\n return false;\n };\n return saysIt(message) || (raw !== undefined && statesModelUnavailable(raw, effectiveStatus));\n}\n\n/**\n * The message and the `code` / `type` names of a provider's error body. The\n * message is `error.message` (or `error` itself when it is a string), else a\n * top-level `message` or `detail`; a body that is not JSON is its own message.\n * A body that reached the read cap and starts like JSON but does not parse is\n * read for its first `\"message\"`, `\"error\"` or `\"detail\"` string and its first\n * `\"code\"` / `\"type\"` names alone, never for the rest, which may echo the\n * request. `raw` is the upstream's own error body that OpenRouter forwards in\n * `error.metadata.raw`, as a string or as an object. `parsed` says the body\n * read as a JSON object, so a reader can tell a body with no message of its own\n * from a body that is not JSON at all.\n */\nfunction errorBodyParts(body: string): {\n message: string;\n names: string[];\n raw?: string;\n parsed: boolean;\n} {\n let parsedJson: unknown;\n try {\n parsedJson = JSON.parse(body);\n } catch {\n // Only a body that reached the read cap can be JSON the cap cut; a short malformed one\n // (a Python-repr `{'error': …}`) is plain text, read whole. A router's plain text can open\n // with a bracket of its own (9router's `[antigravity/x] [400]: …`), so only a `{` or an\n // array of objects counts as JSON.\n // The `- 3` allows for a multi-byte character the cap cut and the decoder replaced.\n const reachedCap = new TextEncoder().encode(body).byteLength >= ERROR_BODY_MAX_BYTES - 3;\n if (!reachedCap || !/^\\s*(?:\\{|\\[\\s*\\{)/.test(body)) {\n return { message: body, names: [], parsed: false };\n }\n // Cut JSON: the first string of `\"message\"`, else of `\"error\"`, else of `\"detail\"`, and\n // the first `\"code\"` / `\"type\"` names — never the rest, which may echo the request.\n const readString = (key: string): string => {\n const match = body.match(new RegExp(`\"${key}\"\\\\s*:\\\\s*\"((?:[^\"\\\\\\\\]|\\\\\\\\.)*)\"`));\n if (!match) return '';\n try {\n return JSON.parse(`\"${match[1]}\"`) as string;\n } catch {\n return '';\n }\n };\n const message = readString('message') || readString('error') || readString('detail');\n const names = [body.match(/\"code\"\\s*:\\s*\"([^\"]*)\"/), body.match(/\"type\"\\s*:\\s*\"([^\"]*)\"/)]\n .filter((name): name is RegExpMatchArray => name !== null)\n .map((name) => name[1]);\n return { message, names, parsed: true };\n }\n const root: unknown = Array.isArray(parsedJson) ? parsedJson[0] : parsedJson;\n if (typeof root !== 'object' || root === null) {\n return { message: body, names: [], parsed: false };\n }\n const error: unknown = (root as { error?: unknown }).error;\n if (typeof error === 'string') return { message: error, names: [], parsed: true };\n const source = (typeof error === 'object' && error !== null ? error : root) as Record<\n string,\n unknown\n >;\n const fallback = (root as { detail?: unknown }).detail;\n const message =\n typeof source.message === 'string'\n ? source.message\n : typeof fallback === 'string'\n ? fallback\n : '';\n const metadata = source.metadata as { raw?: unknown } | null | undefined;\n const raw = metadata?.raw;\n return {\n message,\n names: [source.code, source.type].filter((name): name is string => typeof name === 'string'),\n raw:\n typeof raw === 'string'\n ? raw\n : typeof raw === 'object' && raw !== null\n ? JSON.stringify(raw)\n : undefined,\n parsed: true,\n };\n}\n\n/**\n * True when an error body states that the input is too large: its `error.code`\n * or `error.type` names an overflow, or its message says so\n * (`isContextOverflowError`). Only the message is read for wording, never the\n * rest of the body: a field path such as `contents[413]` or an id such as\n * `req-413-x` elsewhere in a 400 says nothing about the context window, and a\n * stated overflow permanently lowers the model's learned window. OpenRouter's\n * own message is generic (`Provider returned error`), so the upstream body it\n * forwards is read the same way; each level is shorter than the last.\n */\nfunction statesContextOverflow(body: string): boolean {\n const { message, names, raw } = errorBodyParts(body);\n return (\n names.some((name) => CONTEXT_OVERFLOW_ERROR_NAMES.has(name.toLowerCase())) ||\n isContextOverflowError(message) ||\n (raw !== undefined && statesContextOverflow(raw))\n );\n}\n\n/**\n * Classify a provider error response.\n *\n * The effective status is the upstream one a router quoted, else the response's\n * own: a 503 that quotes `[403]` is the 403. On an effective 403 or 404 the body\n * is read for a statement that the model itself is gone (#387) — a retirement\n * notice, a `model_not_found` code — which is `model_unavailable`, not `auth`:\n * nothing is wrong with the credential, and an operator must pick another\n * model. A 401 is always `auth` and its body is never read for this; a 403 or\n * 404 without such wording keeps the plain classification. The #383 capacity\n * outage stays out of this path: its quote is suppressed by\n * `quotedUpstreamStatus`, so the 503 classifies as a retryable server error.\n */\nexport function classifyApiError(\n status: number,\n body: string,\n quotedStatus?: number,\n): ProviderErrorCode {\n const effective =\n quotedStatus !== undefined\n ? classifyHttpStatus(status).retryable\n ? quotedStatus\n : status\n : (wrappedUpstreamStatus(status, body) ?? status);\n if (effective === 429) {\n // The split `formatApiError` has always made: `insufficient_quota` is about credits, a\n // long-window plan limit is about a reset.\n if (hasInsufficientQuotaName(body)) return 'quota';\n if (statesSpentUsageLimit(body)) return 'usage_limit';\n }\n const code = classifyHttpStatus(effective).code;\n if ((effective === 403 || effective === 404) && statesModelUnavailable(body, effective)) {\n return 'model_unavailable';\n }\n return code === 'bad_request' && statesContextOverflow(body) ? 'context_overflow' : code;\n}\n\nexport function classifyProviderError(error: unknown): ProviderErrorCode {\n if (error instanceof ProviderRedirectError) return 'redirect';\n if (error instanceof DOMException && error.name === 'TimeoutError') return 'timeout';\n const message = (error instanceof Error ? error.message : String(error)).toLowerCase();\n if (message.includes('timed out') || message.includes('timeout')) return 'timeout';\n if (isContextOverflowError(message)) return 'context_overflow';\n return 'network';\n}\n\n/**\n * The message detail for model_unavailable errors. When OpenRouter forwarded\n * an upstream error body in `error.metadata.raw`, its sanitized message is\n * included so the retirement wording reaches the user.\n */\nfunction safeModelUnavailableDetail(body: string): string {\n const { raw } = errorBodyParts(body);\n const main = safeErrorDetail(body);\n if (raw) {\n const rawDetail = safeErrorDetail(raw);\n if (rawDetail && rawDetail !== main) {\n return main ? `${main}: ${rawDetail}` : rawDetail;\n }\n }\n return main || 'The provider did not describe why';\n}\n\n/** Where a failed request went, for an error whose body cannot say. */\nexport interface ApiErrorRequest {\n /** The request URL. Only its host is shown, never a path or query string. */\n url?: string;\n model?: string;\n /**\n * False when the error body never arrived (the read timed out, the stream broke or was aborted\n * before a byte came), as `readErrorBody` reports it. An absent body then reads \"no response\n * body received\" instead of \"(empty response body)\", which says the provider sent none.\n */\n bodyReceived?: boolean;\n}\n\nfunction requestOrigin(request: ApiErrorRequest | undefined): string {\n let host: string | undefined;\n try {\n host = request?.url ? new URL(request.url).host : undefined;\n } catch {\n host = undefined;\n }\n return [host && `from ${host}`, request?.model && `for model ${request.model}`]\n .filter(Boolean)\n .join(' ');\n}\n\n/**\n * True for a body that tells the user nothing about what went wrong, so that where the request\n * went is the only useful thing to add: Go's `404 page not found`, an empty JSON object or\n * array, and an HTML page (a proxy's or gateway's own error page). Kept narrow on purpose: a\n * body with any message of its own is never one of these.\n */\nfunction isUninformativeDetail(text: string): boolean {\n if (/^(?:\\d{3}\\s+)?page not found\\.?$/i.test(text)) return true;\n if (isHtmlDetail(text)) return true;\n if (!/^[[{]/.test(text)) return false;\n try {\n const parsed: unknown = JSON.parse(text);\n if (Array.isArray(parsed)) return parsed.length === 0;\n return typeof parsed === 'object' && parsed !== null && Object.keys(parsed).length === 0;\n } catch {\n return false;\n }\n}\n\nfunction isHtmlDetail(text: string): boolean {\n return /^<(?:!doctype\\s+html|html|head|body)\\b/i.test(text);\n}\n\n/** How long a page title may be before it is cut: it is shown inside an error sentence. */\nconst MAX_SHOWN_TITLE_CHARS = 100;\n\n/**\n * An HTML error page as one phrase: `an HTML page`, plus its `<title>` when it has one. The page\n * is never shown as markup (the first 200 characters of one end mid-tag), and its title is\n * folded to one line since it came from the network. `body` is the whole body, not the 200\n * characters `detail` keeps, because the title often comes after a long `<head>` preamble.\n */\nfunction describeHtmlPage(body: string): string {\n const title = body.match(/<title[^>]*>([\\s\\S]*?)<\\/title>/i)?.[1];\n const shown = title ? foldControlCharacters(title).replace(/\\s+/g, ' ').trim() : '';\n if (!shown) return 'an HTML page';\n const clipped =\n shown.length > MAX_SHOWN_TITLE_CHARS ? `${shown.slice(0, MAX_SHOWN_TITLE_CHARS)}…` : shown;\n return `an HTML page titled \"${clipped}\"`;\n}\n\ninterface DetailSentence {\n sentence: string;\n /**\n * True when the sentence is not just the body's own text: an HTML page summarised, or the\n * origin added to a body that says nothing. The one decision both callers act on.\n */\n rewritten: boolean;\n}\n\n/**\n * The provider's message as one sentence for Book's advice to follow (#424). A\n * message that already ends in `.`, `!`, or `?` gets no second period. An empty\n * body, one that never arrived, or one that says nothing useful names where the\n * request went, since a bare status cannot tell a wrong base URL from a wrong\n * model id.\n */\nfunction detailSentence(\n detail: string,\n request?: ApiErrorRequest,\n body: string = detail,\n): DetailSentence {\n const text = detail.trim();\n const origin = requestOrigin(request);\n if (!text) {\n const absent =\n request?.bodyReceived === false ? 'no response body received' : 'empty response body';\n return { sentence: `${origin ? `${origin} ` : ''}(${absent}).`, rewritten: false };\n }\n const uninformative = isUninformativeDetail(text);\n const shown = isHtmlDetail(text) ? describeHtmlPage(body) : text;\n const sentence = /[.!?]$/.test(shown) ? shown : `${shown}.`;\n const hinted = uninformative && origin !== '';\n return {\n sentence: hinted ? `${sentence} The reply came ${origin}.` : sentence,\n rewritten: hinted || shown !== text,\n };\n}\n\n/**\n * The reset sentence for Book's advice (#513), or `''` when the quoted provider text\n * (`said`) already states that reset. `reset.iso` is the provider's own text as written, so\n * it is compared literally, never normalised. A `said` cut short, or a reset read from\n * OpenRouter's `metadata.raw`, does not contain it and keeps Book's clause.\n */\nfunction resetStatement(said: string, resetIso: string, clause: string): string {\n return said.includes(resetIso) ? '' : clause;\n}\n\nexport function formatApiError(\n status: number,\n body: string,\n quotedStatus?: number,\n request?: ApiErrorRequest,\n): string {\n const code = classifyApiError(status, body, quotedStatus);\n const detail = safeErrorDetail(body);\n const { sentence: said, rewritten } = detailSentence(detail, request, body);\n const base = `API Error: ${status}`;\n switch (code) {\n case 'context_overflow':\n return `${base} ${detail.trim() ? said : 'Input exceeds the model context window.'} Reduce the conversation or tool output and try again.`;\n case 'rate_limited': {\n if (isOversizedForRateLimit(detail)) {\n return `${base} ${said} The request is larger than the rate limit allows at once, so it has to shrink; waiting will not help.`;\n }\n const reset = parseAbsoluteReset(body);\n if (reset !== undefined && reset.epochMs > Date.now()) {\n const resetClause = resetStatement(said, reset.iso, ` It resets at ${reset.iso}.`);\n return `${base} ${said}${resetClause} Wait for the reset, then try again.`;\n }\n return `${base} ${said} This may be a temporary capacity issue. Try again in a moment.`;\n }\n case 'overloaded':\n return `${base} Repeated 529 Overloaded errors. The API is at capacity — this is usually temporary. Try again in a moment.`;\n case 'server_error':\n return `${base} ${said} This is a server-side issue, usually temporary — try again in a moment.`;\n case 'timeout':\n return 'Request timed out. Check your network connection and try again.';\n case 'auth':\n return `${base} ${said} Check BOOK_API_KEY, or provider.<id>.apiKey in settings.`;\n case 'model_unavailable': {\n // Not a credential problem (#387): the key is fine, the model is not\n // served here any more. Name the remedy, never the API key.\n const modelDetail = detailSentence(safeModelUnavailableDetail(body)).sentence;\n return `${base} ${modelDetail} This model is not available on this provider or route — choose another with --model or /model.`;\n }\n case 'usage_limit': {\n const reset = parseAbsoluteReset(body);\n const resetClause =\n reset !== undefined ? resetStatement(said, reset.iso, `; it resets at ${reset.iso}`) : '';\n return `${base} ${said} The usage limit for this plan is spent${resetClause}. Wait for the reset, or choose another model with --model or /model.`;\n }\n case 'quota':\n return `${base} ${said} Check your usage/credits.`;\n default:\n if (detail.trim() && !rewritten) return `${base} ${detail}`;\n return code === 'not_found'\n ? `${base} ${said} Check the base URL and model id.`\n : `${base} ${said}`;\n }\n}\n\n/**\n * Detect provider/router responses that mean the input, rather than transport, is\n * too large. A 413 counts only where it is named as a status (`API Error: 413`,\n * `HTTP 413`, `status 413`, `Error code: 413`), never as a bare number: a field\n * path such as `contents[413]` or an id such as `req-413-x` is not one.\n */\nexport function isContextOverflowError(error: unknown): boolean {\n const normalized = (error instanceof Error ? error.message : String(error)).toLowerCase();\n return (\n /\\b(?:api\\s+error|http|(?:error|status)(?:\\s+code)?)\\s*:?\\s*413\\b/.test(normalized) ||\n normalized.includes('request entity too large') ||\n normalized.includes('payload too large') ||\n normalized.includes('request too large') ||\n normalized.includes('context_length_exceeded') ||\n normalized.includes('maximum context length') ||\n normalized.includes('context window') ||\n normalized.includes('prompt too long') ||\n /prompt\\s+is\\s+too\\s+long/.test(normalized) ||\n /input\\s+(?:is\\s+)?too\\s+long/.test(normalized) ||\n normalized.includes('too many tokens') ||\n // Gemini names the count: `The input token count (1196266) exceeds the maximum …`.\n /input\\s+(?:token\\s+count\\s+)?(?:\\(\\d+\\)\\s+)?exceeds?.*(?:context|limit|maximum)/.test(\n normalized,\n ) ||\n /input\\s+exceeds?.*(?:context\\s+window|context\\s+length|token\\s+limit)/.test(normalized) ||\n /maximum\\s+context|context\\s+(?:length|window|size).*(?:exceed|overflow|too\\s+(?:long|large)|maximum)/.test(\n normalized,\n ) ||\n /too\\s+many\\s+(?:input\\s+)?tokens|request\\s+too\\s+large.*token/.test(normalized)\n );\n}\n\n/**\n * True when a rate-limit error says the request itself is larger than the limit allows at\n * once, which no amount of waiting fixes: OpenAI's `Request too large for <model> … on tokens\n * per min (TPM): Limit 30000, Requested 45000.` A transient per-minute limit reads `Rate limit\n * reached …` and is not this, and neither is a bare \"too many tokens\". When the text gives\n * both numbers, the request must exceed the limit.\n */\nexport function isOversizedForRateLimit(text: string): boolean {\n if (!/\\brequest too large\\b/i.test(text)) return false;\n const limit = text.match(/\\blimit:?\\s*(\\d+)/i);\n const requested = text.match(/\\brequested:?\\s*(\\d+)/i);\n return !limit || !requested || Number(requested[1]) > Number(limit[1]);\n}\n\n/** The per-request limit an oversized rate-limit error states (`Limit 30000`), if any. */\nexport function statedRateLimit(text: string): number | undefined {\n if (!isOversizedForRateLimit(text)) return undefined;\n const limit = text.match(/\\blimit:?\\s*(\\d+)/i);\n return limit ? Number(limit[1]) : undefined;\n}\n\n/**\n * The longest text still read as an error envelope rather than an answer when a\n * model may have written it. OpenAI's server-error sentence is about 260\n * characters, and a JSON-rendered upstream error rarely passes 1,000.\n */\nconst ERROR_ENVELOPE_MAX_CHARS = 2_000;\n\ntype EnvelopeUsage = {\n promptTokens: number;\n completionTokens: number;\n cacheReadInputTokens?: number;\n cacheCreationInputTokens?: number;\n} | null;\n\n/**\n * A usage block that reports zero tokens both ways: what a router reports for\n * text it wrote itself. A provider that sends no usage block at all gives\n * `undefined`, which is not this.\n */\nfunction isZeroUsage(usage?: EnvelopeUsage): boolean {\n // A full cache hit leaves `promptTokens` at 0 on its own; the cache counts are usage too.\n return (\n usage != null &&\n usage.promptTokens === 0 &&\n usage.completionTokens === 0 &&\n (usage.cacheReadInputTokens ?? 0) === 0 &&\n (usage.cacheCreationInputTokens ?? 0) === 0\n );\n}\n\n/**\n * True when `text` has the shape a router gives an upstream error it renders as\n * the answer. 9router's Responses translator writes an upstream `error` or\n * `response.failed` event as `[Error] <message>` (or the error object as JSON\n * when it has no message) and ends the stream there. When a model may have\n * written the text, the envelope must be the whole answer: one `[Error] …` line.\n * An answer that quotes such a line and goes on to explain it is an answer. With\n * a usage block that reports zero tokens both ways any answer that opens with\n * `[Error]` is read as the router's, however many lines it runs to.\n */\nexport function isErrorEnvelopeShape(text: string, usage?: EnvelopeUsage): boolean {\n if (isZeroUsage(usage)) return /^\\s*\\[Error\\]/i.test(text);\n const trimmed = text.trim();\n return (\n trimmed.length <= ERROR_ENVELOPE_MAX_CHARS &&\n /^\\[Error\\]/i.test(trimmed) &&\n !/[\\r\\n]/.test(trimmed)\n );\n}\n\n/** 9router's whole sentence, so a model's own answer that merely says the words stays an answer. */\nconst ROUTER_FAILED_RESPONSE = /^\\s*\\[Error\\]\\s*The model failed to generate a response\\.?\\s*$/i;\n\n/**\n * A router that answers 200 with the upstream's error as the assistant text has\n * not answered. The envelope shape (`isErrorEnvelopeShape`) is required; then\n * either a usage block reporting zero tokens both ways or the upstream's own\n * failure wording confirms it. The supported shapes are the table in\n * `reliability.test.ts`:\n * - OpenAI's `[Error] An error occurred while processing your request. …\n * help.openai.com … Please include the request ID <id> in your message.`,\n * whatever the usage says;\n * - any other one-line server error that names its request ID;\n * - 9router's `[Error] The model failed to generate a response.`, whatever the\n * usage says: it writes an upstream `response.failed` that way after the\n * upstream already streamed reasoning, so the turn's usage is not zero (#404);\n * - with a usage block that reports zero tokens both ways, any answer that\n * opens with `[Error]`, one line or many.\n * A model's own one-line `[Error] …` answer with billed usage and none of this\n * wording stays an answer.\n */\nexport function isUpstreamErrorEnvelope(text: string, usage?: EnvelopeUsage): boolean {\n if (!isErrorEnvelopeShape(text, usage)) return false;\n return (\n isZeroUsage(usage) ||\n /\\brequest id\\b/i.test(text) ||\n /help\\.openai\\.com/i.test(text) ||\n /error occurred while processing/i.test(text) ||\n ROUTER_FAILED_RESPONSE.test(text)\n );\n}\n\n/**\n * How long a retryable response's error body may take to arrive. The body only\n * informs the retry decision (a quoted upstream 4xx stops the retries), so a\n * router that sends its headers and then stalls must not hold every attempt for\n * the full `requestTimeoutMs`: ten attempts at 600 s each by default. After this\n * long the decision is made on what arrived.\n */\nexport const ERROR_BODY_READ_TIMEOUT_MS = 5_000;\n\n/** The most of an error body that is ever read; the rest is cancelled unread. */\nconst ERROR_BODY_MAX_BYTES = 65_536;\n\n/** What `readErrorBody` got from an error response. */\nexport interface ErrorBody {\n text: string;\n /**\n * False when no body arrived at all: the signal was already aborted, the body was unreadable,\n * or the read timed out, failed or was aborted before its first byte. An empty body that\n * completed is `received: true`; so is a partial one.\n */\n received: boolean;\n}\n\n/**\n * Read at most ERROR_BODY_MAX_BYTES of an error body, for at most\n * ERROR_BODY_READ_TIMEOUT_MS. What arrived before the cap, the deadline, an\n * abort or a stream error is kept, and the rest of the body is cancelled.\n *\n * Every error body goes through here, retryable or not: a non-retryable one is\n * what the classification and the text the user is shown are both read from, so\n * an unbounded read there is a socket held for as long as the provider sent.\n *\n * `received` is what lets the text shown tell a provider that sent an empty body\n * from a read that got nothing. A response `fetchWithRetry` rebuilt from a body it\n * had already read hands that text and answer back as they are\n * (`BufferedErrorResponse`), before any signal check: the body is in memory, so a\n * signal that aborted since cannot make it unread.\n */\nexport async function readErrorBody(response: Response, signal?: AbortSignal): Promise<ErrorBody> {\n if (response instanceof BufferedErrorResponse) {\n return { text: response.bufferedText, received: response.bodyReceived };\n }\n if (signal?.aborted) return { text: '', received: false };\n if (!response.body) return { text: '', received: true };\n let reader: ReadableStreamDefaultReader<Uint8Array>;\n try {\n reader = response.body.getReader();\n } catch {\n // A body something else already locked has nothing left to read here.\n return { text: '', received: false };\n }\n const decoder = new TextDecoder();\n let text = '';\n let received = 0;\n // Set by the deadline, the abort and a stream error: the ways the body ends that are not the\n // provider finishing it.\n let interrupted = false;\n // Cancelling resolves the pending read as done, which ends the loop below.\n const stop = (): void => {\n interrupted = true;\n reader.cancel().catch(() => {});\n };\n const timer = setTimeout(stop, ERROR_BODY_READ_TIMEOUT_MS);\n signal?.addEventListener('abort', stop, { once: true });\n try {\n while (received < ERROR_BODY_MAX_BYTES) {\n const chunk = await reader.read();\n if (chunk.done) break;\n const bytes = chunk.value.subarray(0, ERROR_BODY_MAX_BYTES - received);\n received += bytes.byteLength;\n text += decoder.decode(bytes, { stream: true });\n }\n } catch {\n // A body that fails part-way still leaves what arrived before it.\n interrupted = true;\n } finally {\n clearTimeout(timer);\n signal?.removeEventListener('abort', stop);\n // Cancelling a body that already ended is a no-op; one stopped at the cap is released here.\n reader.cancel().catch(() => {});\n try {\n reader.releaseLock();\n } catch {\n // Nothing is pending once the loop has ended.\n }\n }\n return { text: text + decoder.decode(), received: received > 0 || !interrupted };\n}\n\n/**\n * A response `fetchWithRetry` rebuilt from an error body it had already read, so that the\n * adapter's own read of it (`readErrorBody`) gets that text and whether it ever arrived\n * (`bodyReceived`) as they were: reading the copy's stream would make a body that timed out\n * look like one that completed empty, and an aborted signal would hide the text altogether.\n */\nclass BufferedErrorResponse extends Response {\n readonly bufferedText: string;\n readonly bodyReceived: boolean;\n\n constructor(body: string, init: ResponseInit, bodyReceived: boolean) {\n super(body, init);\n this.bufferedText = body;\n this.bodyReceived = bodyReceived;\n }\n}\n\n/**\n * A provider endpoint answered with an HTTP redirect (#445). Book refuses every one: a followed\n * redirect can carry the API key (`x-api-key`) and the whole conversation to a host the user\n * never configured. Never retried.\n */\nexport class ProviderRedirectError extends Error {\n constructor(\n readonly status: number,\n /** Scheme, host and path of the redirect target; never userinfo, query or fragment. */\n readonly target: string | undefined,\n ) {\n // An `opaqueredirect` response reports status 0, which is not an HTTP status to quote.\n const answered = status === 0 ? 'answered with a redirect' : `answered HTTP ${status}`;\n super(\n target === undefined\n ? `Provider endpoint ${answered}${status === 0 ? ' that has' : ' with'} no redirect target Book could read, and Book does not follow provider redirects. ` +\n \"Check the provider's base URL.\"\n : `Provider endpoint ${answered}${status === 0 ? '' : ' with a redirect'} to ${target}, and Book does not follow provider redirects. ` +\n \"If that host is the endpoint you meant, set the provider's base URL to that address without the endpoint path Book adds \" +\n '(https://gw.example/v1/chat/completions becomes https://gw.example/v1).',\n );\n this.name = 'ProviderRedirectError';\n }\n}\n\n/**\n * The redirect target reduced to scheme, host (with port) and path, resolved against the request\n * URL since `Location` may be relative. Userinfo, query string and fragment are dropped: they\n * can carry secrets and this text reaches logs and the screen. Undefined when `Location` is\n * missing or does not parse.\n */\nexport function sanitizeRedirectTarget(\n location: string | null | undefined,\n requestUrl: string,\n): string | undefined {\n if (!location) return undefined;\n try {\n const target = new URL(location, requestUrl);\n if (!target.host) return undefined;\n return `${target.protocol}//${target.host}${target.pathname}`;\n } catch {\n return undefined;\n }\n}\n\n/** True for a response that is, or hides, an HTTP redirect. */\nexport function isRedirectResponse(response: Response): boolean {\n return response.type === 'opaqueredirect' || (response.status >= 300 && response.status <= 399);\n}\n\n/** Cancel a response body that will not be read, ignoring a body something else has locked. */\nasync function cancelBody(response: Response): Promise<void> {\n try {\n await response.body?.cancel();\n } catch {\n // A custom fetch implementation may expose an already-locked body.\n }\n}\n\n/** Cancel a redirect's body and build the error for it. */\nexport async function refuseRedirect(\n response: Response,\n requestUrl: string,\n): Promise<ProviderRedirectError> {\n await cancelBody(response);\n return new ProviderRedirectError(\n response.status,\n sanitizeRedirectTarget(response.headers.get('location'), requestUrl),\n );\n}\n\nexport async function fetchWithRetry(\n url: string,\n init: RequestInit,\n retry: RetryConfig,\n signal?: AbortSignal,\n onRetry?: (attempt: number, max: number, delayMs: number) => void,\n logger?: RetryLogger,\n // The retry budget is a duration, so it reads the monotonic clock. On the\n // wall clock an NTP step or a resumed VM could hand a week-long run either\n // failure mode: a backwards correction makes the budget never expire and the\n // call retries forever, a forwards one exhausts it on the first attempt.\n clock: Clock = systemClock,\n): Promise<Response> {\n const startMs = clock.monotonicNowMs();\n const maxAttempts = retry.watchdog ? Number.MAX_SAFE_INTEGER : retry.maxAttempts;\n let lastError: string | null = null;\n\n for (let attempt = 0; attempt <= maxAttempts; attempt++) {\n if (attempt > 0 && signal?.aborted) throw abortError(signal);\n const fetchSignal = requestSignal(signal, retry.requestTimeoutMs);\n let response: Response;\n try {\n // `redirect` comes after the spread so no caller can turn following back on (#445).\n response = await fetch(url, { ...init, redirect: 'manual', signal: fetchSignal });\n } catch (error) {\n if (signal?.aborted) throw error;\n lastError = error instanceof Error ? error.message : String(error);\n if (attempt >= maxAttempts || budgetExhausted(clock, startMs, retry.totalBudgetMs)) break;\n const delay = boundedDelay(clock, backoffMs(attempt, retry), startMs, retry.totalBudgetMs);\n logger?.warn('retry network error', {\n attempt: attempt + 1,\n delayMs: delay,\n error: lastError,\n });\n onRetry?.(attempt + 1, maxAttempts === Number.MAX_SAFE_INTEGER ? -1 : maxAttempts, delay);\n await sleep(delay, signal);\n continue;\n }\n\n // Outside the catch above, so a redirect is never retried, watchdog or not.\n if (isRedirectResponse(response)) throw await refuseRedirect(response, url);\n\n const classification = classifyHttpStatus(response.status);\n if (!classification.retryable) return response;\n lastError = `API error ${response.status}`;\n\n const errorBody = await readErrorBody(response, signal);\n const bodyText = errorBody.text;\n // Every response returned below is a copy of this read body. `bodyReceived` rides along so\n // the adapter's own read of the copy can still tell an empty body from one that never came.\n const buffered = (init: ResponseInit): BufferedErrorResponse =>\n new BufferedErrorResponse(bodyText, init, errorBody.received);\n const quoted = quotedUpstreamStatus(bodyText);\n if (quoted !== undefined && !classifyHttpStatus(quoted).retryable) {\n logger?.warn('upstream error quoted in retryable status; not retrying', {\n status: response.status,\n upstreamStatus: quoted,\n });\n return buffered({\n status: response.status,\n statusText: response.statusText,\n headers: response.headers,\n });\n }\n\n const errorCode = classifyApiError(response.status, bodyText, quoted);\n if ((errorCode === 'quota' || errorCode === 'usage_limit') && !retry.watchdog) {\n logger?.warn('usage limit reached; not retrying', {\n status: response.status,\n });\n return buffered({\n status: response.status,\n statusText: response.statusText,\n headers: response.headers,\n });\n }\n\n // A 429 that states the request itself is too large (OpenAI's per-minute cap on one request\n // that can never fit under it) is refused the same way however long the retries wait: the\n // loop's overflow recovery is what lets it through, so it gets the response now. Tested on\n // the same formatted text the loop reads, so a statement buried in `metadata.raw` — which\n // that text does not carry — leaves the retries running. A router may wrap that 429 in\n // another status (`503 [route] [429]: …`), so the quoted one counts.\n if (\n (quoted ?? response.status) === 429 &&\n isOversizedForRateLimit(formatApiError(response.status, bodyText, quoted))\n ) {\n logger?.warn('rate limit on a request too large to ever fit; not retrying', {\n status: response.status,\n });\n return buffered({\n status: response.status,\n statusText: response.statusText,\n headers: response.headers,\n });\n }\n\n // Read once: the early stop below and the backoff delay both use it.\n const stated = statedWait(response.headers.get('retry-after'), bodyText);\n\n // For a retryable 429 (plain or quoted) that states how long to wait, by whichever source\n // (Retry-After, a router's `(reset after …)`, an absolute reset): if that is longer than what\n // this call can still wait, return at once rather than clamping it to `maxDelayMs` and\n // burning every attempt on a request the provider has said will be refused (#409). A\n // Retry-After or cooldown only stops the call when it is far (`FAR_STATED_WAIT_MS`); a\n // shorter one is honoured, capped at `maxDelayMs`, and retried. An absolute reset keeps\n // its earlier rule: any wait beyond the remaining one stops the call.\n if ((quoted ?? response.status) === 429 && !retry.watchdog) {\n if (stated !== undefined) {\n const attemptsWaitMs = (maxAttempts - attempt) * retry.maxDelayMs;\n const remainingBudgetMs =\n retry.totalBudgetMs > 0\n ? Math.max(0, retry.totalBudgetMs - (clock.monotonicNowMs() - startMs))\n : undefined;\n const remainingWaitMs =\n remainingBudgetMs !== undefined\n ? Math.min(remainingBudgetMs, attemptsWaitMs)\n : attemptsWaitMs;\n const stopsCall =\n stated.waitMs > remainingWaitMs &&\n (stated.source === 'reset-time' || stated.waitMs >= FAR_STATED_WAIT_MS);\n if (stopsCall) {\n logger?.warn('stated wait beyond retry budget; not retrying', {\n status: response.status,\n source: stated.source,\n });\n const init = {\n status: response.status,\n statusText: response.statusText,\n headers: response.headers,\n };\n // Only a far wait that no fresh call could sit out either is carried to the loop as a\n // park. A shorter wait, or one that merely met the end of the attempts, is an ordinary\n // exhausted rate limit, which the loop may send again.\n const wholeCallMs = maxAttempts * retry.maxDelayMs;\n const farWait =\n stated.waitMs >= FAR_STATED_WAIT_MS &&\n stated.waitMs >\n (retry.totalBudgetMs > 0 ? Math.min(retry.totalBudgetMs, wholeCallMs) : wholeCallMs);\n return farWait\n ? new WaitStoppedResponse(bodyText, init, stated, errorBody.received)\n : buffered(init);\n }\n }\n }\n\n const watchdogRetry = retry.watchdog && (response.status === 429 || response.status === 529);\n const effectiveMax = watchdogRetry ? Number.MAX_SAFE_INTEGER : maxAttempts;\n if (attempt >= effectiveMax || budgetExhausted(clock, startMs, retry.totalBudgetMs)) {\n return buffered({\n status: response.status,\n statusText: response.statusText,\n headers: response.headers,\n });\n }\n\n const delay = boundedDelay(\n clock,\n backoffMs(attempt, retry, stated),\n startMs,\n retry.totalBudgetMs,\n );\n logger?.warn('retry http status', {\n attempt: attempt + 1,\n status: response.status,\n delayMs: delay,\n });\n onRetry?.(attempt + 1, effectiveMax === Number.MAX_SAFE_INTEGER ? -1 : effectiveMax, delay);\n try {\n await sleep(delay, signal);\n } catch {\n return buffered({\n status: response.status,\n statusText: response.statusText,\n headers: response.headers,\n });\n }\n await cancelBody(response);\n }\n\n throw new Error(lastError ?? 'request failed after retries');\n}\n\nexport async function readStreamChunk(\n reader: ReadableStreamDefaultReader<Uint8Array>,\n signal: AbortSignal | undefined,\n stallTimeoutMs: number,\n): Promise<\n | { tag: 'read'; done: boolean; value: Uint8Array | undefined }\n | { tag: 'stall' }\n | { tag: 'abort' }\n> {\n if (signal?.aborted) return { tag: 'abort' };\n let timeout: ReturnType<typeof setTimeout> | undefined;\n let onAbort: (() => void) | undefined;\n try {\n return await Promise.race([\n reader.read().then((result) => ({\n tag: 'read' as const,\n done: result.done,\n value: result.value,\n })),\n ...(stallTimeoutMs > 0\n ? [\n new Promise<{ tag: 'stall' }>((resolve) => {\n timeout = setTimeout(() => resolve({ tag: 'stall' }), stallTimeoutMs);\n }),\n ]\n : []),\n ...(signal\n ? [\n new Promise<{ tag: 'abort' }>((resolve) => {\n onAbort = () => resolve({ tag: 'abort' });\n signal.addEventListener('abort', onAbort, { once: true });\n }),\n ]\n : []),\n ]);\n } finally {\n if (timeout) clearTimeout(timeout);\n if (signal && onAbort) signal.removeEventListener('abort', onAbort);\n }\n}\n\nfunction requestSignal(\n signal: AbortSignal | undefined,\n timeoutMs: number,\n): AbortSignal | undefined {\n if (timeoutMs <= 0) return signal;\n const timeout = AbortSignal.timeout(timeoutMs);\n return signal ? AbortSignal.any([signal, timeout]) : timeout;\n}\n\nfunction abortError(signal: AbortSignal): Error {\n return signal.reason instanceof Error ? signal.reason : new Error('Aborted');\n}\n\n/**\n * The message `errorBodyParts` reads, so the text shown and the text classified are the\n * same. A body with no message of its own (a nested `error.error.message`) or that is not\n * JSON shows its first characters instead.\n */\nfunction safeErrorDetail(body: string): string {\n const { message, parsed } = errorBodyParts(body);\n if (parsed && message) return message.slice(0, 2000);\n return body.slice(0, 200);\n}\n\nfunction sleep(ms: number, signal?: AbortSignal): Promise<void> {\n return new Promise((resolve, reject) => {\n if (signal?.aborted) {\n reject(abortError(signal));\n return;\n }\n const timer = setTimeout(() => {\n signal?.removeEventListener('abort', onAbort);\n resolve();\n }, ms);\n const onAbort = () => {\n clearTimeout(timer);\n reject(signal ? abortError(signal) : new Error('Aborted'));\n };\n signal?.addEventListener('abort', onAbort, { once: true });\n });\n}\n\n/** Where a provider's stated wait came from. */\nexport type StatedWaitSource = 'retry-after' | 'cooldown' | 'reset-time';\n\n/** How long a provider says to wait before the next request (#409). */\nexport interface StatedWait {\n waitMs: number;\n source: StatedWaitSource;\n /** The absolute reset time, for `reset-time` only. */\n resetIso?: string;\n}\n\n/**\n * A stated wait is only parked (`rate_limit_wait`, ending the run `quota_exhausted`) when it is\n * at least this long and also beyond what the whole call could wait. The floor is fixed, not\n * derived from the retry settings: a tight `maxAttempts` or `maxDelayMs` (the compaction\n * reducer uses two attempts) must not turn a stated wait of seconds into a parked run.\n */\nexport const FAR_STATED_WAIT_MS = 15 * 60_000;\n\n/**\n * The wait a provider states: the longer of its relative wait and its absolute reset time, so a\n * short Retry-After never masks a far reset time in the body. The relative wait is the\n * Retry-After header, else a router's `(reset after …)` cooldown (the header is the\n * authoritative of the two, #383); the absolute one is a `resets at <ISO>` that is still ahead.\n * Computed once per response and shared by the early stop and the backoff delay, so a wait that\n * is honoured and a wait that is refused are measured the same way.\n */\nfunction statedWait(retryAfter?: string | null, body?: string): StatedWait | undefined {\n const header = parseRetryAfter(retryAfter);\n const cooldown = header === undefined ? parseCooldownReset(body) : undefined;\n const relative: StatedWait | undefined =\n header !== undefined\n ? { waitMs: header, source: 'retry-after' }\n : cooldown !== undefined\n ? { waitMs: cooldown, source: 'cooldown' }\n : undefined;\n const reset = parseAbsoluteReset(body);\n // Wall clock on purpose, exactly like parseRetryAfter: the server sent an absolute\n // timestamp, which is a point on the wall clock by definition. Subtracting a monotonic\n // reading from it would be meaningless.\n const resetWaitMs = reset !== undefined ? reset.epochMs - Date.now() : 0;\n if (\n reset !== undefined &&\n resetWaitMs > 0 &&\n (relative === undefined || resetWaitMs > relative.waitMs)\n ) {\n return { waitMs: resetWaitMs, source: 'reset-time', resetIso: reset.iso };\n }\n return relative;\n}\n\n/**\n * The response `fetchWithRetry` returns when it stopped because the provider's stated wait is\n * longer than any call could sit out. It is the provider's own response, body and all; the\n * subclass only carries the wait to the provider adapter, which reports it under the\n * `rate_limit_wait` error code so the loop parks instead of re-issuing the request.\n */\nclass WaitStoppedResponse extends BufferedErrorResponse {\n readonly statedWait: StatedWait;\n\n constructor(body: string, init: ResponseInit, wait: StatedWait, bodyReceived: boolean) {\n super(body, init, bodyReceived);\n this.statedWait = wait;\n }\n}\n\n/** The stated wait that stopped the retries on `response`, if one did (#409). */\nexport function statedWaitStop(response: Response): StatedWait | undefined {\n return response instanceof WaitStoppedResponse ? response.statedWait : undefined;\n}\n\n/** `7200000` as `2 hours`: the coarsest unit that keeps the figure readable. */\nfunction describeWait(waitMs: number): string {\n const count = (value: number, unit: string): string =>\n `${value} ${unit}${value === 1 ? '' : 's'}`;\n const seconds = Math.max(1, Math.round(waitMs / 1_000));\n if (seconds < 90) return count(seconds, 'second');\n const minutes = Math.round(seconds / 60);\n if (minutes < 90) return count(minutes, 'minute');\n const hours = Math.round(minutes / 60);\n if (hours < 48) return count(hours, 'hour');\n return count(Math.round(hours / 24), 'day');\n}\n\n/**\n * The error text for a response `fetchWithRetry` stopped on (`statedWaitStop`): what the\n * provider said, how long it asked for, and the two remedies.\n */\nexport function formatStatedWaitError(\n status: number,\n body: string,\n stop: StatedWait,\n request?: ApiErrorRequest,\n): string {\n const said = detailSentence(safeErrorDetail(body), request, body).sentence;\n const when =\n stop.resetIso !== undefined\n ? `The limit resets${resetStatement(said, stop.resetIso, ` at ${stop.resetIso},`)} in about ${describeWait(stop.waitMs)}`\n : `The provider asks for a wait of about ${describeWait(stop.waitMs)}`;\n return `API Error: ${status} ${said} ${when}, longer than this request will wait. Wait for the reset, or choose another model with --model or /model.`;\n}\n\n/**\n * The `error` stream event for a non-OK response, shared by every provider adapter. A stated\n * wait the retries stopped on (`statedWaitStop`) is worded with the wait and coded\n * `rate_limit_wait`, so the loop parks instead of re-issuing the request (#409); any other\n * response is formatted and classified from its body.\n */\nexport function statedWaitErrorEvent(\n response: Response,\n errorText: string,\n request?: ApiErrorRequest,\n): {\n type: 'error';\n error: string;\n errorCode: ProviderErrorCode;\n upstreamStatus: number | undefined;\n} {\n const waitStop = statedWaitStop(response);\n return {\n type: 'error',\n error: waitStop\n ? formatStatedWaitError(response.status, errorText, waitStop, request)\n : formatApiError(response.status, errorText, undefined, request),\n errorCode: waitStop ? 'rate_limit_wait' : classifyApiError(response.status, errorText),\n upstreamStatus: wrappedUpstreamStatus(response.status, errorText),\n };\n}\n\nfunction backoffMs(attempt: number, retry: RetryConfig, stated?: StatedWait): number {\n if (stated !== undefined) return Math.min(stated.waitMs, retry.maxDelayMs);\n const exponential = Math.min(retry.baseDelayMs * 2 ** attempt, retry.maxDelayMs);\n return Math.round(exponential * (0.5 + Math.random()));\n}\n\nfunction parseRetryAfter(value?: string | null): number | undefined {\n if (!value) return undefined;\n const seconds = Number(value);\n if (Number.isFinite(seconds) && seconds > 0) return seconds * 1000;\n const timestamp = Date.parse(value);\n if (!Number.isFinite(timestamp)) return undefined;\n // Wall clock on purpose, and the one place in this file that should be: the\n // server sent an HTTP date, which is a point on the wall clock by definition.\n // Subtracting a monotonic reading from it would be meaningless.\n return Math.max(0, timestamp - Date.now());\n}\n\n/**\n * How many milliseconds each unit word in a stated cooldown is worth. Keyed by\n * every spelling the matcher accepts.\n */\nconst COOLDOWN_UNIT_MS: Record<string, number> = {\n ms: 1,\n s: 1_000,\n sec: 1_000,\n secs: 1_000,\n second: 1_000,\n seconds: 1_000,\n m: 60_000,\n min: 60_000,\n mins: 60_000,\n minute: 60_000,\n minutes: 60_000,\n h: 3_600_000,\n hr: 3_600_000,\n hrs: 3_600_000,\n hour: 3_600_000,\n hours: 3_600_000,\n};\n\n// Longest unit spellings first, and no unit may be followed by another letter:\n// the boundary is what keeps `500ms` from being read as 500 minutes — the `m`\n// would otherwise swallow the `ms` — while still reading compound forms such as\n// `1m26s`, where a digit follows the unit.\nconst COOLDOWN_UNIT = /(\\d+)\\s*(ms|sec(?:ond)?s?|min(?:ute)?s?|h(?:ours?|rs?)?|s|m)(?![a-z])/gi;\n\n/**\n * The cooldown a router states in its error body (`(reset after 5s)`), read when\n * there is no Retry-After header to wait by (#383). Forms seen in the wild:\n * `5s`, `42s`, `1m26s`, `1 min 26 s`, and millisecond durations such as `500ms`\n * and `1s 200ms`. Units are ms, s/sec/seconds, m/min/minutes and h/hours, so `m`\n * is never read when it belongs to `ms`. Returns milliseconds, or undefined\n * when the body states no duration.\n */\nfunction parseCooldownReset(body?: string | null): number | undefined {\n if (!body) return undefined;\n // Read like `parseAbsoluteReset`: the message parts only, never the rest of a JSON body,\n // which may echo the request. A router's plain-text body is its own message.\n const { message, raw } = errorBodyParts(body);\n const own = cooldownIn(message);\n if (own !== undefined) return own;\n return raw !== undefined ? parseCooldownReset(raw) : undefined;\n}\n\nfunction cooldownIn(text: string): number | undefined {\n const stated = text.match(/\\(reset after\\s*([^)]+)\\)/i);\n if (!stated) return undefined;\n let totalMs = 0;\n let statedUnits = 0;\n for (const unit of stated[1].matchAll(COOLDOWN_UNIT)) {\n totalMs += Number(unit[1]) * COOLDOWN_UNIT_MS[unit[2].toLowerCase()];\n statedUnits++;\n }\n return statedUnits > 0 && totalMs > 0 ? totalMs : undefined;\n}\n\nfunction budgetExhausted(clock: Clock, startMs: MonotonicMs, budgetMs: number): boolean {\n return budgetMs > 0 && clock.monotonicNowMs() - startMs >= budgetMs;\n}\n\nfunction boundedDelay(\n clock: Clock,\n delayMs: number,\n startMs: MonotonicMs,\n budgetMs: number,\n): number {\n if (budgetMs <= 0) return delayMs;\n return Math.min(delayMs, Math.max(0, budgetMs - (clock.monotonicNowMs() - startMs)));\n}\n","import type { AgentConfig } from '../types/runtime.js';\nimport type {\n ProviderMessage,\n ProviderStreamEvent,\n SystemPromptZones,\n} from '../types/providers.js';\nimport type { ToolDefinition } from '../types/tools.js';\nimport type { Usage } from '../types/messages.js';\nimport { createDebugLogger, isDebugEnabled } from '../debug-log.js';\nimport { escapeInvisibleCharacters } from '../control-characters.js';\nimport {\n classifyProviderError,\n fetchWithRetry,\n readErrorBody,\n readStreamChunk,\n statedWaitErrorEvent,\n} from './reliability.js';\n\nconst log = createDebugLogger('provider');\n\nfunction parseToolArguments(raw: string): {\n arguments: Record<string, unknown>;\n unparsedArguments?: { raw: string; error: string };\n} {\n if (!raw.trim()) return { arguments: {} };\n try {\n const parsed = JSON.parse(raw);\n return {\n arguments: parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {},\n };\n } catch (error) {\n return {\n arguments: {},\n unparsedArguments: { raw, error: error instanceof Error ? error.message : String(error) },\n };\n }\n}\n\nfunction parseReasoningDelta(delta: Record<string, unknown>): string | undefined {\n for (const key of ['reasoning_content', 'reasoning', 'thinking', 'analysis']) {\n const value = delta[key];\n if (typeof value === 'string' && value) return value;\n }\n const details = delta.reasoning_details;\n if (!Array.isArray(details)) return undefined;\n const text = details\n .map((detail) => {\n if (typeof detail === 'string') return detail;\n if (!detail || typeof detail !== 'object') return '';\n const item = detail as Record<string, unknown>;\n for (const key of ['text', 'reasoning', 'content']) {\n if (typeof item[key] === 'string') return item[key] as string;\n }\n return '';\n })\n .filter(Boolean)\n .join('');\n return text || undefined;\n}\n\nfunction isSystemPromptZones(content: ProviderMessage['content']): content is SystemPromptZones {\n return (\n !!content &&\n typeof content === 'object' &&\n 'cachedPrefix' in content &&\n 'dynamicSuffix' in content\n );\n}\n\nfunction flattenMessages(messages: ProviderMessage[]): Array<{\n role: string;\n content:\n | string\n | null\n | Array<{ type: 'text'; text: string } | { type: 'image_url'; image_url: { url: string } }>;\n tool_calls?: ProviderMessage['tool_calls'];\n tool_call_id?: string;\n}> {\n return messages.map(({ providerMetadata: _providerMetadata, reasoningContent, ...msg }) => ({\n ...msg,\n content: isSystemPromptZones(msg.content)\n ? [msg.content.cachedPrefix, msg.content.dynamicSuffix].filter(Boolean).join('\\n\\n')\n : Array.isArray(msg.content)\n ? msg.content.map((part) =>\n part.type === 'text'\n ? part\n : {\n type: 'image_url' as const,\n image_url: { url: `data:${part.mediaType};base64,${part.data}` },\n },\n )\n : reasoningContent\n ? ['<reasoning_context>', reasoningContent, '</reasoning_context>', msg.content ?? '']\n .filter(Boolean)\n .join('\\n')\n : msg.content,\n }));\n}\n\n/**\n * Map an OpenAI-compatible `usage` object onto Book's `Usage`, prompt-cache tokens included.\n *\n * Book's `promptTokens` is the uncached input, as on the Anthropic path, with cache reads and\n * writes counted separately. Where the cache counts sit depends on who reported them:\n *\n * - `prompt_tokens_details.cached_tokens` / `cache_creation_tokens` / `cache_write_tokens`\n * (OpenAI, xAI, 9router, OpenRouter) and DeepSeek's `prompt_cache_hit_tokens`, plus Moonshot's\n * top-level `cached_tokens` and DashScope's `prompt_tokens_details.cache_creation_input_tokens`,\n * are part of `prompt_tokens`, so they are subtracted from it. `total_tokens` already covers them,\n * so `contextTokens` stays unset and compaction pressure is `total_tokens`, as before.\n * - Anthropic's own top-level names (`cache_read_input_tokens`, `cache_creation_input_tokens`)\n * from a proxy that sends no `prompt_tokens_details` are Anthropic's numbers passed through,\n * where the input count excludes the cache: they are added on top, and `contextTokens` carries\n * the whole prompt. A proxy that reports an OpenAI-style cache field beside\n * them, even at zero (LiteLLM's `prompt_tokens_details.cached_tokens`), has normalised\n * `prompt_tokens` to include them.\n *\n * Counts that cannot fit inside `prompt_tokens` are treated as on top of it whatever their\n * source. A usage with no cache tokens maps exactly as before, with no cache fields.\n */\nexport function parseCompatibleUsage(raw: unknown): Usage | null {\n if (!raw || typeof raw !== 'object') return null;\n const usage = raw as Record<string, unknown>;\n const details =\n usage.prompt_tokens_details && typeof usage.prompt_tokens_details === 'object'\n ? (usage.prompt_tokens_details as Record<string, unknown>)\n : {};\n const tokens = (value: unknown): number =>\n typeof value === 'number' && Number.isFinite(value) && value > 0 ? value : 0;\n const firstCount = (...values: unknown[]): number => {\n for (const value of values) {\n const count = tokens(value);\n if (count > 0) return count;\n }\n return 0;\n };\n const prompt = tokens(usage.prompt_tokens);\n const base: Usage = {\n promptTokens: prompt,\n completionTokens: tokens(usage.completion_tokens),\n // A provider that omits `total_tokens` would otherwise read as zero context pressure.\n totalTokens: tokens(usage.total_tokens) || prompt + tokens(usage.completion_tokens),\n };\n const topRead = tokens(usage.cache_read_input_tokens);\n const topWrite = tokens(usage.cache_creation_input_tokens);\n // Inside `prompt_tokens` by the OpenAI convention: OpenAI, xAI, 9router, OpenRouter, DeepSeek,\n // Moonshot (top-level `cached_tokens`) and DashScope (`cache_creation_input_tokens` in details).\n const normalisedRead = firstCount(\n details.cached_tokens,\n usage.prompt_cache_hit_tokens,\n usage.cached_tokens,\n );\n const normalisedWrite = firstCount(\n details.cache_creation_tokens,\n details.cache_write_tokens,\n details.cache_creation_input_tokens,\n );\n const read = normalisedRead || topRead;\n const write = normalisedWrite || topWrite;\n if (read === 0 && write === 0) return base;\n // Anthropic's top-level names with no OpenAI-style cache field beside them (not even a zero)\n // are Anthropic's numbers passed through, where the input count excludes the cache. A field\n // beside them, even `cached_tokens: 0` on LiteLLM's cold turn, says `prompt_tokens` was\n // normalised to include them.\n const reportsOpenAiCacheField = [\n details.cached_tokens,\n details.cache_creation_tokens,\n details.cache_write_tokens,\n details.cache_creation_input_tokens,\n usage.prompt_cache_hit_tokens,\n usage.cached_tokens,\n ].some((value) => typeof value === 'number');\n const anthropicPassThrough = !reportsOpenAiCacheField && topRead + topWrite > 0;\n if (!anthropicPassThrough && read + write <= prompt) {\n return {\n ...base,\n promptTokens: prompt - read - write,\n cacheReadInputTokens: read,\n cacheCreationInputTokens: write,\n };\n }\n return {\n ...base,\n cacheReadInputTokens: read,\n cacheCreationInputTokens: write,\n contextTokens: prompt + read + write,\n };\n}\n\nexport function convertTools(tools: ToolDefinition[]): Array<{\n type: 'function';\n function: { name: string; description: string; parameters: Record<string, unknown> };\n}> {\n return tools.map((tool) => ({\n type: 'function',\n function: {\n name: tool.name,\n description: tool.description,\n parameters: tool.inputSchema ?? tool.parameters,\n },\n }));\n}\n\nexport async function* chatCompletionStream(\n config: AgentConfig,\n messages: ProviderMessage[],\n tools: ToolDefinition[],\n options?: {\n signal?: AbortSignal;\n onRetry?: (attempt: number, max: number, delayMs: number) => void;\n onStreamStall?: (countdownMs: number) => void;\n onStreamResume?: () => void;\n maxOutputTokens?: number;\n },\n): AsyncGenerator<ProviderStreamEvent> {\n const retry = config.retry;\n const signal = options?.signal;\n const url = `${config.baseUrl}/chat/completions`;\n\n const body: Record<string, unknown> = {\n model: config.model,\n messages: flattenMessages(messages),\n stream: true,\n // Request token usage in the final SSE chunk so we can track cost.\n stream_options: { include_usage: true },\n };\n if (options?.maxOutputTokens || config.maxTokensExplicit || config.modelInfo?.maxOutputTokens) {\n body.max_tokens = options?.maxOutputTokens ?? config.maxTokens;\n }\n if (config.effortExplicit && config.effort) body.reasoning_effort = config.effort;\n\n // Whether this request expects the model to reason before answering: either we\n // asked it to, or the model's catalog entry declares an effort range. An entry\n // of `false` means the model does not reason at all; an absent entry is unknown,\n // and stays on the chat regime rather than guessing.\n const reasoningEnabled =\n body.reasoning_effort !== undefined || typeof config.modelInfo?.effort === 'object';\n\n if (tools.length > 0) {\n body.tools = convertTools(tools);\n }\n\n log.debug('chatCompletionStream request', {\n model: config.model,\n messageCount: messages.length,\n toolCount: tools.length,\n maxTokens: config.maxTokens,\n });\n\n let response: Response;\n try {\n response = await fetchWithRetry(\n url,\n {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${config.apiKey}`,\n },\n body: JSON.stringify(body),\n },\n retry,\n signal,\n options?.onRetry,\n log,\n );\n } catch (e) {\n if (signal?.aborted) return;\n log.warn('fetchWithRetry failed', e instanceof Error ? e.message : String(e));\n yield {\n type: 'error',\n error: e instanceof Error ? e.message : String(e),\n errorCode: classifyProviderError(e),\n };\n return;\n }\n\n if (signal?.aborted) {\n await response.body?.cancel();\n return;\n }\n\n log.debug('response received', { status: response.status, ok: response.ok });\n\n if (!response.ok) {\n const errorBody = await readErrorBody(response, signal);\n yield statedWaitErrorEvent(response, errorBody.text, {\n url,\n model: config.model,\n bodyReceived: errorBody.received,\n });\n return;\n }\n\n const reader = response.body?.getReader();\n if (!reader) {\n yield { type: 'error', error: 'No response body', errorCode: 'protocol_error' };\n return;\n }\n\n const decoder = new TextDecoder();\n let buffer = '';\n const toolCallParts: Array<{\n index: number;\n id: string;\n name: string;\n arguments: string;\n /** Deltas that carried an arguments fragment: a wire format that sends one, vs many. */\n fragments: number;\n }> = [];\n let currentUsage: Usage | null = null;\n let responseModel: string | undefined;\n let responseId: string | undefined;\n const finishReasons = new Set<string>();\n\n const emitToolCalls = function* (): Generator<ProviderStreamEvent> {\n for (const part of [...toolCallParts].sort((a, b) => a.index - b.index)) {\n if (!part.id && !part.name) continue;\n const { arguments: arguments_, unparsedArguments } = parseToolArguments(part.arguments);\n // Nothing at all after the name and id: kept apart from a literal `{}` so the registry can\n // blame the route, not the model, for a required argument that is missing (#403).\n const emptyOnWire = !part.arguments.trim();\n if (emptyOnWire) {\n log.warn('tool call arrived with empty arguments', {\n index: part.index,\n id: part.id,\n name: escapeInvisibleCharacters(part.name),\n fragments: part.fragments,\n });\n }\n if (unparsedArguments) {\n // The head, not the whole text: enough to see whether the call arrived with its\n // first fragment missing (#260), which is a route's wire format, not the model's JSON.\n log.warn('tool call arguments are not valid JSON', {\n index: part.index,\n id: part.id,\n name: escapeInvisibleCharacters(part.name),\n fragments: part.fragments,\n length: part.arguments.length,\n head: escapeInvisibleCharacters(part.arguments.slice(0, 120)),\n });\n }\n yield {\n type: 'tool_call',\n toolCall: {\n id: part.id || `tool-${part.index}`,\n name: part.name,\n arguments: arguments_,\n ...(unparsedArguments ? { unparsedArguments } : {}),\n ...(emptyOnWire ? { emptyOnWire: true as const } : {}),\n },\n };\n }\n };\n\n const emitDone = function* (\n terminal: '[DONE]' | 'finish_reason',\n ): Generator<ProviderStreamEvent> {\n yield* emitToolCalls();\n log.info('stream done', {\n terminal,\n promptTokens: currentUsage?.promptTokens ?? 0,\n completionTokens: currentUsage?.completionTokens ?? 0,\n totalTokens: currentUsage?.totalTokens ?? 0,\n cacheReadInputTokens: currentUsage?.cacheReadInputTokens ?? 0,\n cacheCreationInputTokens: currentUsage?.cacheCreationInputTokens ?? 0,\n });\n yield {\n type: 'done',\n usage: currentUsage ?? undefined,\n ...(responseModel ? { responseModel } : {}),\n ...(responseId ? { responseId } : {}),\n ...(finishReasons.size > 0 ? { finishReasons: [...finishReasons] } : {}),\n };\n };\n\n const processSseLine = function* (line: string): Generator<ProviderStreamEvent, boolean> {\n const trimmed = line.trim();\n if (!trimmed || !trimmed.startsWith('data:')) return false;\n\n const data = trimmed.slice(5).replace(/^ /, '');\n if (data === '[DONE]') {\n yield* emitDone('[DONE]');\n return true;\n }\n\n try {\n const parsed = JSON.parse(data);\n if (typeof parsed.model === 'string') responseModel = parsed.model;\n if (typeof parsed.id === 'string') responseId = parsed.id;\n // OpenAI sends usage on the final chunk when stream_options.include_usage is set.\n const usage = parseCompatibleUsage(parsed.usage);\n if (usage) currentUsage = usage;\n const choice = parsed.choices?.[0];\n if (!choice) return false;\n const finishReason: unknown = choice.finish_reason;\n if (typeof finishReason === 'string' && finishReason) finishReasons.add(finishReason);\n\n const delta = choice.delta as\n | (Record<string, unknown> & {\n content?: string;\n tool_calls?: Array<{\n index?: number;\n id?: string;\n function?: { name?: string; arguments?: string };\n }>;\n })\n | undefined;\n if (!delta) return false;\n\n const reasoning = parseReasoningDelta(delta);\n if (reasoning) {\n // The model is reasoning, whatever the request could not say (#379), so\n // this phase waits out the thinking ceiling. Only this phase does.\n stallTimeoutMs = retry.thinkingStallTimeoutMs\n ? Math.max(startingStallTimeoutMs, retry.thinkingStallTimeoutMs)\n : startingStallTimeoutMs;\n log.debug('stream reasoning', { len: reasoning.length });\n yield { type: 'reasoning', reasoning };\n }\n\n if (delta.content) {\n // The model has started answering, so the chat ceiling bounds this\n // phase again — reasoning can resume and promote the ceiling back.\n stallTimeoutMs = startingStallTimeoutMs;\n log.debug('stream text', { len: delta.content.length });\n yield { type: 'text', content: delta.content };\n }\n\n if (delta.tool_calls) {\n // A tool call is the answer phase too, and the one where a stall costs\n // most: the turn is waiting on a function call that is not coming.\n stallTimeoutMs = startingStallTimeoutMs;\n for (const tc of delta.tool_calls) {\n const explicitIndex = Number.isInteger(tc.index) ? tc.index : undefined;\n let index = explicitIndex;\n\n if (index === undefined && tc.id) {\n index = toolCallParts.find((part) => part.id === tc.id)?.index;\n }\n if (index === undefined) {\n index = toolCallParts.length;\n }\n\n let part = toolCallParts.find((p) => p.index === index);\n if (!part) {\n part = { index, id: '', name: '', arguments: '', fragments: 0 };\n toolCallParts.push(part);\n }\n\n if (tc.id) part.id = tc.id;\n if (tc.function?.name) part.name = tc.function.name;\n if (tc.function?.arguments) {\n // The head of each call's first fragment, not of every one: a call that arrives\n // missing its opening `{\"filePath\": ` is visible here (#260) and nowhere else.\n // Escaped, because the text is the model's and a debug log on stderr may be a\n // terminal; behind the flag, so a large streamed Write does no string work and\n // writes no lines when debugging is off.\n if (part.fragments === 0 && isDebugEnabled()) {\n log.debug('stream tool_call delta', {\n index,\n id: tc.id,\n name: escapeInvisibleCharacters(tc.function.name ?? ''),\n argumentsLength: tc.function.arguments.length,\n argumentsHead: escapeInvisibleCharacters(tc.function.arguments.slice(0, 120)),\n });\n }\n part.arguments += tc.function.arguments;\n part.fragments++;\n }\n }\n }\n } catch {\n // Skip unparseable lines.\n }\n\n return false;\n };\n\n // Stream stall detection: if no data arrives for the ceiling in force, call\n // the onStreamStall callback and yield a visible error instead of leaving the\n // TUI stuck in a pending read forever.\n //\n // The ceiling is chosen per stream and follows its phase, not just per\n // request. It starts at what the request can already prove: the thinking\n // ceiling when we asked for reasoning or the model's catalog entry declares an\n // effort range, the chat-tuned `streamStallTimeoutMs` otherwise. What a\n // request cannot prove is whether the model reasons, because plenty of routes\n // serve a reasoning model Book has no catalog entry for, and send it no\n // `reasoning_effort` — there is no evidence before the stream opens, so the\n // first thinking pause lands on the chat ceiling and a healthy request is\n // cancelled mid-thought and reported as a stalled stream (#379), the single\n // most common way a run \"just stops\". A reasoning delta is that evidence\n // arriving late, so it promotes that phase to the thinking ceiling.\n //\n // For a model nothing settles the question for — no `reasoning_effort` on the\n // request, no catalog `effort` entry, not an `effort: false` one — the stream\n // opens on the first-delta ceiling instead (#379 tail): a router that buffers\n // the whole thinking block sends a role-only chunk and then stays silent for\n // as long as the model thinks, up to about 80 s, which the chat ceiling reads\n // as a fault. The first meaningful delta — non-empty content, a tool call, or\n // reasoning — spends that grace and hands the stream to the per-phase logic\n // below; a role-only chunk, an empty content string and a usage-only chunk do\n // not.\n //\n // A content or tool-call delta ends the thinking phase, and the ceiling goes\n // back to where it started. Keeping the promotion for the rest of the stream\n // would apply fifteen minutes to the answer phase, where a dead socket is the\n // commonest fault by far and is worth noticing in twenty seconds; reasoning\n // that resumes mid-answer promotes the ceiling again. A stream that never\n // reasons keeps the chat ceiling throughout, and a request with an explicit\n // effort or a catalogued one is unchanged.\n const startingStallTimeoutMs =\n reasoningEnabled && retry.thinkingStallTimeoutMs\n ? Math.max(retry.streamStallTimeoutMs, retry.thinkingStallTimeoutMs)\n : retry.streamStallTimeoutMs;\n const firstDeltaApplies = !reasoningEnabled && config.modelInfo?.effort !== false;\n let stallTimeoutMs = firstDeltaApplies\n ? Math.max(startingStallTimeoutMs, retry.firstDeltaStallTimeoutMs ?? 0)\n : startingStallTimeoutMs;\n\n try {\n while (true) {\n const result = await readStreamChunk(reader, signal, stallTimeoutMs);\n\n if (result.tag === 'abort') {\n try {\n await reader.cancel();\n } catch {\n // ignore cancellation failures\n }\n return;\n }\n\n if (result.tag === 'stall') {\n log.warn('stream stalled', { timeoutMs: stallTimeoutMs });\n options?.onStreamStall?.(stallTimeoutMs);\n try {\n await reader.cancel();\n } catch {\n // ignore cancellation failures\n }\n yield {\n type: 'error',\n error: 'Stream stalled: no data received for ' + stallTimeoutMs + 'ms',\n errorCode: 'stream_stall',\n };\n return;\n }\n\n if (result.done) break;\n\n const value = result.value;\n if (!value) continue;\n\n buffer += decoder.decode(value, { stream: true });\n const lines = buffer.split('\\n');\n buffer = lines.pop() || '';\n\n for (const line of lines) {\n if (yield* processSseLine(line)) return;\n }\n }\n\n buffer += decoder.decode();\n for (const line of buffer.split('\\n')) {\n if (yield* processSseLine(line)) return;\n }\n\n // Some OpenAI-compatible gateways omit [DONE] and close after a finish_reason chunk.\n if (finishReasons.size > 0) {\n yield* emitDone('finish_reason');\n return;\n }\n } catch (e) {\n if (signal?.aborted) return;\n // Unexpected stream error — surface it instead of silently completing,\n // otherwise the TUI can show an empty completed assistant message.\n yield {\n type: 'error',\n error: e instanceof Error ? e.message : String(e),\n errorCode: classifyProviderError(e),\n };\n return;\n } finally {\n try {\n reader.releaseLock();\n } catch {\n // ignore: the reader may already have been cancelled/released\n }\n }\n\n yield {\n type: 'error',\n error: 'Provider stream ended before its terminal event.',\n errorCode: 'transport_interrupted',\n };\n}\n"],"mappings":"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","names":["message","delay","init","part"]}
|
|
@@ -5,10 +5,22 @@ import {
|
|
|
5
5
|
applyShellInjections,
|
|
6
6
|
assertProjectCommandApproved,
|
|
7
7
|
extractShellInjections
|
|
8
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-SWPDKLNM.js";
|
|
9
9
|
import {
|
|
10
10
|
buildChildEnv
|
|
11
11
|
} from "./chunk-XHAO3K7I.js";
|
|
12
|
+
import {
|
|
13
|
+
formatSkippedNotice,
|
|
14
|
+
projectContainmentRoot,
|
|
15
|
+
refusalFor,
|
|
16
|
+
tryRealPath
|
|
17
|
+
} from "./chunk-SBTU5BEC.js";
|
|
18
|
+
import {
|
|
19
|
+
isOutside
|
|
20
|
+
} from "./chunk-YIL6HTJX.js";
|
|
21
|
+
import {
|
|
22
|
+
reachesUncPath
|
|
23
|
+
} from "./chunk-EEX6QNXV.js";
|
|
12
24
|
import {
|
|
13
25
|
resolveBookHome
|
|
14
26
|
} from "./chunk-ZPSZZUWF.js";
|
|
@@ -156,9 +168,14 @@ function escapeRegex(s) {
|
|
|
156
168
|
}
|
|
157
169
|
|
|
158
170
|
// src/commands/loader.ts
|
|
159
|
-
function loadCommandFile(filePath, source) {
|
|
160
|
-
if (!existsSync(
|
|
161
|
-
|
|
171
|
+
function loadCommandFile(filePath, source, readFrom = filePath) {
|
|
172
|
+
if (!existsSync(readFrom) || extname(filePath) !== ".md") return null;
|
|
173
|
+
let raw;
|
|
174
|
+
try {
|
|
175
|
+
raw = readFileSync(readFrom, "utf-8");
|
|
176
|
+
} catch {
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
162
179
|
const { body, frontmatter } = parseFrontmatter(raw);
|
|
163
180
|
const name = filePath.split(/[/\\]/).pop().replace(/\.md$/, "");
|
|
164
181
|
return {
|
|
@@ -172,11 +189,18 @@ function loadCommandFile(filePath, source) {
|
|
|
172
189
|
source
|
|
173
190
|
};
|
|
174
191
|
}
|
|
175
|
-
function
|
|
192
|
+
function inspectCommands(workspace) {
|
|
176
193
|
const userDir = join(resolveBookHome(), "commands");
|
|
177
194
|
const projectDir = join(workspace, ".book", "commands");
|
|
195
|
+
const userReal = tryRealPath(userDir);
|
|
196
|
+
const projectReal = tryRealPath(projectDir);
|
|
197
|
+
const projectIsUserDir = userReal !== void 0 && projectReal !== void 0 && !isOutside(userReal, projectReal) && !isOutside(projectReal, userReal);
|
|
198
|
+
const checked = !projectIsUserDir;
|
|
199
|
+
const rootReal = checked ? tryRealPath(projectContainmentRoot(workspace)) : void 0;
|
|
178
200
|
const byName = /* @__PURE__ */ new Map();
|
|
201
|
+
const skipped = [];
|
|
179
202
|
for (const dir of [userDir, projectDir]) {
|
|
203
|
+
if (dir === projectDir && reachesUncPath(dir)) continue;
|
|
180
204
|
if (!existsSync(dir)) continue;
|
|
181
205
|
let entries;
|
|
182
206
|
try {
|
|
@@ -187,21 +211,44 @@ function discoverCommands(workspace) {
|
|
|
187
211
|
for (const entry of entries) {
|
|
188
212
|
if (!entry.endsWith(".md")) continue;
|
|
189
213
|
const fullPath = join(dir, entry);
|
|
190
|
-
let
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
214
|
+
let readFrom = fullPath;
|
|
215
|
+
if (dir === projectDir && checked) {
|
|
216
|
+
const real = tryRealPath(fullPath);
|
|
217
|
+
if (rootReal === void 0 || real === void 0) continue;
|
|
218
|
+
try {
|
|
219
|
+
if (!statSync(real).isFile()) continue;
|
|
220
|
+
} catch {
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
const reason = refusalFor(rootReal, real);
|
|
224
|
+
if (reason) {
|
|
225
|
+
skipped.push({ path: fullPath, target: real, root: rootReal, reason });
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
readFrom = real;
|
|
229
|
+
} else {
|
|
230
|
+
try {
|
|
231
|
+
if (!statSync(fullPath).isFile()) continue;
|
|
232
|
+
} catch {
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
195
235
|
}
|
|
196
|
-
if (!stat.isFile()) continue;
|
|
197
236
|
const source = dir === userDir ? "user" : "project";
|
|
198
|
-
const cmd = loadCommandFile(fullPath, source);
|
|
237
|
+
const cmd = loadCommandFile(fullPath, source, readFrom);
|
|
199
238
|
if (cmd) {
|
|
200
239
|
byName.set(cmd.name, cmd);
|
|
201
240
|
}
|
|
202
241
|
}
|
|
203
242
|
}
|
|
204
|
-
return Array.from(byName.values());
|
|
243
|
+
return { commands: Array.from(byName.values()), skipped };
|
|
244
|
+
}
|
|
245
|
+
function discoverCommands(workspace) {
|
|
246
|
+
return inspectCommands(workspace).commands;
|
|
247
|
+
}
|
|
248
|
+
function collectSkippedCommandNotices(workspace) {
|
|
249
|
+
return inspectCommands(workspace).skipped.map(
|
|
250
|
+
(entry) => formatSkippedNotice("project command", workspace, entry)
|
|
251
|
+
);
|
|
205
252
|
}
|
|
206
253
|
function generateCommandListing(commands, budgetChars = 1024) {
|
|
207
254
|
const visible = commands.filter((c) => c.source !== void 0);
|
|
@@ -237,7 +284,9 @@ export {
|
|
|
237
284
|
parseSlashInput,
|
|
238
285
|
resolveCommandBody,
|
|
239
286
|
commandEnforcementContext,
|
|
287
|
+
inspectCommands,
|
|
240
288
|
discoverCommands,
|
|
289
|
+
collectSkippedCommandNotices,
|
|
241
290
|
generateCommandListing
|
|
242
291
|
};
|
|
243
|
-
//# sourceMappingURL=chunk-
|
|
292
|
+
//# sourceMappingURL=chunk-AJZWLKLQ.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/commands/loader.ts","../src/commands/resolve.ts"],"sourcesContent":["import { readFileSync, existsSync, readdirSync, statSync } from 'fs';\nimport { join, extname } from 'path';\nimport { parseFrontmatter } from '../frontmatter.js';\nimport type { SlashCommand } from '../types/commands.js';\nimport { resolveBookHome } from '../book-home.js';\nimport {\n formatSkippedNotice,\n projectContainmentRoot,\n refusalFor,\n tryRealPath,\n type SkippedProjectFile,\n} from '../repo-containment.js';\nimport { isOutside } from '../tools/path-utils.js';\nimport { reachesUncPath } from '../unc-path.js';\n\n/**\n * Load a single .md command file. Returns null if the file doesn't exist or can't be read. The command is named\n * after `filePath`; `readFrom` is the real path that was checked, when it is not `filePath`.\n */\nfunction loadCommandFile(\n filePath: string,\n source: 'user' | 'project',\n readFrom: string = filePath,\n): SlashCommand | null {\n if (!existsSync(readFrom) || extname(filePath) !== '.md') return null;\n let raw: string;\n try {\n raw = readFileSync(readFrom, 'utf-8');\n } catch {\n // An unreadable file is skipped, not thrown: discovery also runs for the start-up notices.\n return null;\n }\n const { body, frontmatter } = parseFrontmatter(raw);\n const name = filePath.split(/[/\\\\]/).pop()!.replace(/\\.md$/, '');\n return {\n name,\n description: (frontmatter.description as string) || name,\n argumentHint: frontmatter['argument-hint'] as string | undefined,\n arguments: Array.isArray(frontmatter.arguments)\n ? (frontmatter.arguments as string[])\n : typeof frontmatter.arguments === 'string'\n ? (frontmatter.arguments as string).split(/[,\\s]+/).filter(Boolean)\n : undefined,\n allowedTools: Array.isArray(frontmatter['allowed-tools'])\n ? (frontmatter['allowed-tools'] as string[])\n : undefined,\n model: frontmatter.model as string | undefined,\n body,\n source,\n };\n}\n\nexport interface CommandInspection {\n commands: SlashCommand[];\n /** Project command files that were found but refused (#543). */\n skipped: SkippedProjectFile[];\n}\n\n/**\n * Discovery plus the project command files it refused (#543). A repository can commit a command\n * file as a link to a file outside it, and the target would be sent to the provider when its\n * `/name` is run, so each entry of the project directory loads only when its real path is a\n * Markdown file inside the real path of the repository root (the workspace root with no\n * repository). That covers a linked `.book` or `.book/commands` directory, and the file is read\n * from the real path that was checked. User commands are the user's own and are never checked,\n * and neither are the entries of the project directory when it is the user directory (the\n * workspace is the home directory).\n *\n * Search order (project wins on name collision):\n * 1. ~/.book/commands/*.md (user)\n * 2. .book/commands/*.md (project)\n */\nexport function inspectCommands(workspace: string): CommandInspection {\n const userDir = join(resolveBookHome(), 'commands');\n const projectDir = join(workspace, '.book', 'commands');\n const userReal = tryRealPath(userDir);\n const projectReal = tryRealPath(projectDir);\n const projectIsUserDir =\n userReal !== undefined &&\n projectReal !== undefined &&\n !isOutside(userReal, projectReal) &&\n !isOutside(projectReal, userReal);\n // Fails closed: a project directory whose real path cannot be had is checked (and so skipped).\n const checked = !projectIsUserDir;\n const rootReal = checked ? tryRealPath(projectContainmentRoot(workspace)) : undefined;\n\n const byName = new Map<string, SlashCommand>();\n const skipped: SkippedProjectFile[] = [];\n\n // User commands first (lowest priority).\n for (const dir of [userDir, projectDir]) {\n // A `.book` committed as a link to a network share is not listed: listing it would reach the\n // host (#567).\n if (dir === projectDir && reachesUncPath(dir)) continue;\n if (!existsSync(dir)) continue;\n let entries: string[];\n try {\n entries = readdirSync(dir);\n } catch {\n continue;\n }\n for (const entry of entries) {\n if (!entry.endsWith('.md')) continue;\n const fullPath = join(dir, entry);\n // Only a file inside the repository is read, by its real path; a name that links elsewhere\n // is reported, not followed.\n let readFrom = fullPath;\n if (dir === projectDir && checked) {\n const real = tryRealPath(fullPath);\n if (rootReal === undefined || real === undefined) continue;\n try {\n if (!statSync(real).isFile()) continue;\n } catch {\n continue;\n }\n const reason = refusalFor(rootReal, real);\n if (reason) {\n skipped.push({ path: fullPath, target: real, root: rootReal, reason });\n continue;\n }\n readFrom = real;\n } else {\n try {\n if (!statSync(fullPath).isFile()) continue;\n } catch {\n continue;\n }\n }\n const source = dir === userDir ? 'user' : 'project';\n const cmd = loadCommandFile(fullPath, source, readFrom);\n if (cmd) {\n byName.set(cmd.name, cmd); // project overrides user via set\n }\n }\n }\n\n return { commands: Array.from(byName.values()), skipped };\n}\n\n/**\n * Discover all slash commands from the conventional directories. Project commands with the same\n * name as a user command override the user one. See `inspectCommands` for what is refused.\n */\nexport function discoverCommands(workspace: string): SlashCommand[] {\n return inspectCommands(workspace).commands;\n}\n\n/** One start-up warning per project command file that was not loaded. */\nexport function collectSkippedCommandNotices(workspace: string): string[] {\n return inspectCommands(workspace).skipped.map((entry) =>\n formatSkippedNotice('project command', workspace, entry),\n );\n}\n\n// Re-export resolveCommandBody from resolve.ts for backward compatibility.\nexport { resolveCommandBody } from './resolve.js';\n\n/**\n * Generate a compact text listing of available commands for injection into the\n * system prompt. Commands with disableModelInvocation are excluded.\n *\n * Pattern: mirror generateSkillListing from src/skills.ts.\n */\nexport function generateCommandListing(\n commands: SlashCommand[],\n budgetChars: number = 1024,\n): string {\n const visible = commands.filter((c) => c.source !== undefined); // all commands are visible\n if (visible.length === 0) return '';\n\n const lines: string[] = [];\n lines.push('## Available slash commands');\n // No command bodies follow, and nothing downstream resolves a bare `/name`\n // string: the host expands an invoked command before the turn reaches the\n // model. Promising \"execute its instructions below\" taught a mechanism that\n // does not exist.\n lines.push(\n 'The user can invoke these by typing /name. The host expands an invoked command into the conversation before it reaches you. If a message merely mentions a command name, treat it as a reference — do not attempt to execute the command yourself.',\n );\n\n let remaining = budgetChars - lines.join('\\n').length;\n let shown = 0;\n\n for (const cmd of visible) {\n const hint = cmd.argumentHint ? ` ${cmd.argumentHint}` : '';\n const entry = `- **/${cmd.name}${hint}**: ${cmd.description}`;\n if (entry.length > remaining) {\n // Try bare name\n const bare = `- **/${cmd.name}**`;\n if (bare.length <= remaining) {\n lines.push(bare);\n shown++;\n remaining -= bare.length + 1;\n }\n break;\n }\n lines.push(entry);\n shown++;\n remaining -= entry.length + 1;\n }\n\n if (shown < visible.length) lines.push(`- …and ${visible.length - shown} more not shown`);\n\n return lines.join('\\n');\n}\n","import { exec } from 'child_process';\nimport { buildChildEnv } from '../child-env.js';\nimport { assertProjectCommandApproved, type ProjectCommandStore } from '../command-approvals.js';\nimport type { CommandContext, SlashCommand } from '../types/commands.js';\nimport {\n applyShellInjections,\n extractShellInjections,\n type ShellInjection,\n} from './shell-injection.js';\n\nexport interface ParsedSlashInput {\n name: string;\n rawArguments: string;\n}\n\n/** Parse slash input once; command identity is always an exact token match. */\nexport function parseSlashInput(value: string): ParsedSlashInput | null {\n if (!value.startsWith('/')) return null;\n const body = value.slice(1);\n const separator = body.search(/\\s/);\n return separator === -1\n ? { name: body, rawArguments: '' }\n : { name: body.slice(0, separator), rawArguments: body.slice(separator + 1).trim() };\n}\n\n/**\n * Parse raw argument string with shell-style quoting.\n *\n * Examples:\n * \"hello world\" → [\"hello\", \"world\"]\n * '\"hello world\" arg' → [\"hello world\", \"arg\"]\n * '' → []\n */\nexport function parseArgs(raw: string): string[] {\n const trimmed = raw.trim();\n if (!trimmed) return [];\n\n const argv: string[] = [];\n let current = '';\n let inDouble = false;\n let inSingle = false;\n\n for (let i = 0; i < trimmed.length; i++) {\n const ch = trimmed[i];\n\n if (inDouble) {\n if (ch === '\"') {\n inDouble = false;\n } else {\n current += ch;\n }\n } else if (inSingle) {\n if (ch === \"'\") {\n inSingle = false;\n } else {\n current += ch;\n }\n } else {\n if (ch === '\"') {\n inDouble = true;\n } else if (ch === \"'\") {\n inSingle = true;\n } else if (ch === ' ' || ch === '\\t') {\n if (current.length > 0) {\n argv.push(current);\n current = '';\n }\n } else {\n current += ch;\n }\n }\n }\n\n if (current.length > 0) {\n argv.push(current);\n }\n\n return argv;\n}\n\n/**\n * Resolve all variables in a command body.\n *\n * Supported variables:\n * $ARGUMENTS / $* — all arguments joined by spaces\n * $1 through $9 — positional arguments (1-indexed)\n * $name — named argument from `arguments` frontmatter\n * ${BOOK_SESSION_ID} — current session ID\n * ${BOOK_WORKSPACE} — workspace root path\n * ${BOOK_MODEL} — current model name\n * ${BOOK_DATE} — current date (YYYY-MM-DD)\n */\nexport function resolveVariables(\n body: string,\n argv: string[],\n namedArgs: string[],\n context?: { sessionId?: string; workspace?: string; model?: string },\n): string {\n let resolved = body;\n\n // $ARGUMENTS / $* — all arguments joined\n const allArgs = argv.join(' ');\n resolved = resolved.replace(/\\$ARGUMENTS|\\$\\*/g, allArgs);\n\n // $1..$9 — positional arguments (1-indexed)\n for (let i = 1; i <= 9; i++) {\n const val = i <= argv.length ? argv[i - 1] : '';\n resolved = resolved.replace(new RegExp(`\\\\$${i}`, 'g'), val);\n }\n\n // $name — named arguments from `arguments` frontmatter\n for (let i = 0; i < namedArgs.length; i++) {\n const name = namedArgs[i];\n const val = i < argv.length ? argv[i] : '';\n resolved = resolved.replace(new RegExp(`\\\\$${escapeRegex(name)}`, 'g'), val);\n }\n\n // ${ENV_VAR} style\n if (context?.sessionId) {\n resolved = resolved.replace(/\\$\\{BOOK_SESSION_ID\\}/g, context.sessionId);\n }\n if (context?.workspace) {\n resolved = resolved.replace(/\\$\\{BOOK_WORKSPACE\\}/g, context.workspace);\n }\n if (context?.model) {\n resolved = resolved.replace(/\\$\\{BOOK_MODEL\\}/g, context.model);\n }\n resolved = resolved.replace(/\\$\\{BOOK_DATE\\}/g, new Date().toISOString().split('T')[0]);\n\n return resolved;\n}\n\n/**\n * Resolve inline shell commands and fenced shell blocks in a command body.\n *\n * Spans come from a single scan of the raw body (`shell-injection.ts`), which\n * is also what the approval fingerprint digests, and output is substituted\n * back without rescanning: a command that prints an injection marker cannot\n * get it executed.\n *\n * Each shell command runs with a 5-second timeout. Output replaces the\n * placeholder inline. Errors are collected.\n */\nexport async function resolveShellInjection(\n body: string,\n workspace: string,\n signal?: AbortSignal,\n): Promise<{ resolved: string; errors: string[] }> {\n const errors: string[] = [];\n const injections = extractShellInjections(body);\n const outputs: string[] = [];\n // Sequential: a body may depend on one command having run before the next.\n for (const injection of injections) {\n outputs.push(await executeInjection(injection, workspace, errors, signal));\n }\n return { resolved: applyShellInjections(body, injections, outputs), errors };\n}\n\n/**\n * Everything a command body may need substituted into it.\n */\nexport interface CommandResolutionContext {\n sessionId?: string;\n workspace?: string;\n model?: string;\n /**\n * Recorded decisions about repository-declared shell substitution\n * (`settings.commands.projectCommands`).\n *\n * Omitting it fails closed: a project command that substitutes shell is\n * refused rather than run on an unrecorded decision, so a host that has not\n * been wired for the gate cannot quietly reopen it.\n */\n projectCommands?: ProjectCommandStore;\n}\n\n/**\n * Resolve a command body by substituting arguments, named args, env vars,\n * and shell injections.\n *\n * Returns the fully resolved body and any shell errors encountered. Throws\n * `ProjectCommandApprovalError` when a repository-declared command would run\n * shell the user has not approved.\n */\nexport async function resolveCommandBody(\n command: SlashCommand,\n args: string,\n context?: CommandResolutionContext,\n signal?: AbortSignal,\n): Promise<{ resolved: string; shellErrors: string[] }> {\n // Before anything runs: a checked-in body can execute shell outside the\n // permission system, so the decision is checked ahead of the first spawn.\n assertProjectCommandApproved(command, {\n commands: { projectCommands: context?.projectCommands },\n });\n\n const argv = parseArgs(args);\n const namedArgs = command.arguments ?? [];\n\n // Step 1: Resolve shell injections first (raw body, before var substitution).\n // This allows shell commands to produce text that may contain $variables,\n // and is why the approved fingerprint digests the raw body: no argument can\n // reach the shell.\n const { resolved: afterShell, errors } = await resolveShellInjection(\n command.body,\n context?.workspace ?? process.cwd(),\n signal,\n );\n\n // Step 2: Resolve variables ($ARGUMENTS, $1..$9, $name, ${ENV}).\n const resolved = resolveVariables(afterShell, argv, namedArgs, context);\n\n return { resolved, shellErrors: errors };\n}\n\n/**\n * Frontmatter enforcement carried into the agent loop for an invoked custom\n * command, or undefined when the command restricts nothing.\n *\n * Shared so every host (TUI, print/headless) enforces `allowed-tools` and\n * `model` identically.\n */\nexport function commandEnforcementContext(\n command: SlashCommand,\n resolvedBody: string,\n): CommandContext | undefined {\n return command.allowedTools || command.model\n ? {\n command,\n resolvedBody,\n modelOverride: command.model,\n allowedTools: command.allowedTools,\n }\n : undefined;\n}\n\nfunction executeInjection(\n injection: ShellInjection,\n workspace: string,\n errors: string[],\n signal?: AbortSignal,\n): Promise<string> {\n return new Promise((resolve) => {\n exec(\n injection.command,\n {\n cwd: workspace,\n encoding: 'utf8',\n // A `!command` in a slash command is a command the user wrote, not one Book chose, so it\n // must not inherit the NODE_ENV Book defaulted for its own renderer.\n env: buildChildEnv(),\n timeout: 5_000,\n maxBuffer: 1024 * 1024,\n signal,\n windowsHide: true,\n },\n (error, stdout) => {\n if (!error) {\n resolve(stdout.trim());\n return;\n }\n const message = error.message;\n errors.push(\n injection.kind === 'block'\n ? `Shell block failed: ${message}`\n : `Shell command '${injection.command}' failed: ${message}`,\n );\n resolve(`[shell error: ${message}]`);\n },\n );\n });\n}\n\nfunction escapeRegex(s: string): string {\n return s.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n}\n"],"mappings":"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","names":[]}
|