@steipete/summarize 0.10.0 → 0.11.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (374) hide show
  1. package/CHANGELOG.md +80 -28
  2. package/README.md +115 -30
  3. package/dist/cli.js +1 -1
  4. package/dist/esm/cache.js +67 -65
  5. package/dist/esm/cache.js.map +1 -1
  6. package/dist/esm/cli-main.js +27 -27
  7. package/dist/esm/cli-main.js.map +1 -1
  8. package/dist/esm/cli.js +2 -2
  9. package/dist/esm/cli.js.map +1 -1
  10. package/dist/esm/config.js +310 -166
  11. package/dist/esm/config.js.map +1 -1
  12. package/dist/esm/content/asset.js +53 -50
  13. package/dist/esm/content/asset.js.map +1 -1
  14. package/dist/esm/content/index.js +1 -1
  15. package/dist/esm/content/index.js.map +1 -1
  16. package/dist/esm/costs.js +1 -1
  17. package/dist/esm/costs.js.map +1 -1
  18. package/dist/esm/daemon/agent.js +165 -164
  19. package/dist/esm/daemon/agent.js.map +1 -1
  20. package/dist/esm/daemon/auto-mode.js +3 -3
  21. package/dist/esm/daemon/auto-mode.js.map +1 -1
  22. package/dist/esm/daemon/chat.js +16 -14
  23. package/dist/esm/daemon/chat.js.map +1 -1
  24. package/dist/esm/daemon/cli-entrypoint.js +72 -0
  25. package/dist/esm/daemon/cli-entrypoint.js.map +1 -0
  26. package/dist/esm/daemon/cli.js +63 -87
  27. package/dist/esm/daemon/cli.js.map +1 -1
  28. package/dist/esm/daemon/config.js +15 -15
  29. package/dist/esm/daemon/config.js.map +1 -1
  30. package/dist/esm/daemon/constants.js +6 -6
  31. package/dist/esm/daemon/constants.js.map +1 -1
  32. package/dist/esm/daemon/env-merge.js.map +1 -1
  33. package/dist/esm/daemon/env-snapshot.js +36 -31
  34. package/dist/esm/daemon/env-snapshot.js.map +1 -1
  35. package/dist/esm/daemon/flow-context.js +59 -28
  36. package/dist/esm/daemon/flow-context.js.map +1 -1
  37. package/dist/esm/daemon/launchd.js +100 -55
  38. package/dist/esm/daemon/launchd.js.map +1 -1
  39. package/dist/esm/daemon/meta.js +5 -5
  40. package/dist/esm/daemon/meta.js.map +1 -1
  41. package/dist/esm/daemon/models.js +54 -31
  42. package/dist/esm/daemon/models.js.map +1 -1
  43. package/dist/esm/daemon/process-registry.js +15 -15
  44. package/dist/esm/daemon/process-registry.js.map +1 -1
  45. package/dist/esm/daemon/schtasks.js +42 -42
  46. package/dist/esm/daemon/schtasks.js.map +1 -1
  47. package/dist/esm/daemon/server.js +248 -244
  48. package/dist/esm/daemon/server.js.map +1 -1
  49. package/dist/esm/daemon/summarize-progress.js +11 -11
  50. package/dist/esm/daemon/summarize-progress.js.map +1 -1
  51. package/dist/esm/daemon/summarize.js +29 -29
  52. package/dist/esm/daemon/summarize.js.map +1 -1
  53. package/dist/esm/daemon/systemd.js +47 -47
  54. package/dist/esm/daemon/systemd.js.map +1 -1
  55. package/dist/esm/firecrawl.js +12 -12
  56. package/dist/esm/firecrawl.js.map +1 -1
  57. package/dist/esm/flags.js +32 -32
  58. package/dist/esm/flags.js.map +1 -1
  59. package/dist/esm/index.js +3 -3
  60. package/dist/esm/index.js.map +1 -1
  61. package/dist/esm/language.js +1 -1
  62. package/dist/esm/language.js.map +1 -1
  63. package/dist/esm/llm/cli.js +128 -64
  64. package/dist/esm/llm/cli.js.map +1 -1
  65. package/dist/esm/llm/errors.js +1 -1
  66. package/dist/esm/llm/errors.js.map +1 -1
  67. package/dist/esm/llm/generate-text.js +107 -98
  68. package/dist/esm/llm/generate-text.js.map +1 -1
  69. package/dist/esm/llm/google-models.js +17 -17
  70. package/dist/esm/llm/google-models.js.map +1 -1
  71. package/dist/esm/llm/html-to-markdown.js +3 -3
  72. package/dist/esm/llm/html-to-markdown.js.map +1 -1
  73. package/dist/esm/llm/model-id.js +38 -16
  74. package/dist/esm/llm/model-id.js.map +1 -1
  75. package/dist/esm/llm/prompt.js +5 -5
  76. package/dist/esm/llm/prompt.js.map +1 -1
  77. package/dist/esm/llm/providers/anthropic.js +33 -33
  78. package/dist/esm/llm/providers/anthropic.js.map +1 -1
  79. package/dist/esm/llm/providers/google.js +19 -19
  80. package/dist/esm/llm/providers/google.js.map +1 -1
  81. package/dist/esm/llm/providers/models.js +30 -30
  82. package/dist/esm/llm/providers/models.js.map +1 -1
  83. package/dist/esm/llm/providers/openai.js +35 -34
  84. package/dist/esm/llm/providers/openai.js.map +1 -1
  85. package/dist/esm/llm/providers/shared.js +8 -8
  86. package/dist/esm/llm/providers/shared.js.map +1 -1
  87. package/dist/esm/llm/transcript-to-markdown.js +9 -5
  88. package/dist/esm/llm/transcript-to-markdown.js.map +1 -1
  89. package/dist/esm/llm/usage.js +18 -18
  90. package/dist/esm/llm/usage.js.map +1 -1
  91. package/dist/esm/logging/daemon.js +21 -21
  92. package/dist/esm/logging/daemon.js.map +1 -1
  93. package/dist/esm/logging/ring-file.js +5 -5
  94. package/dist/esm/logging/ring-file.js.map +1 -1
  95. package/dist/esm/markitdown.js +21 -19
  96. package/dist/esm/markitdown.js.map +1 -1
  97. package/dist/esm/media-cache.js +39 -39
  98. package/dist/esm/media-cache.js.map +1 -1
  99. package/dist/esm/model-auto.js +175 -106
  100. package/dist/esm/model-auto.js.map +1 -1
  101. package/dist/esm/model-spec.js +52 -42
  102. package/dist/esm/model-spec.js.map +1 -1
  103. package/dist/esm/pricing/litellm.js +4 -4
  104. package/dist/esm/pricing/litellm.js.map +1 -1
  105. package/dist/esm/processes.js +1 -1
  106. package/dist/esm/processes.js.map +1 -1
  107. package/dist/esm/prompts/index.js +1 -1
  108. package/dist/esm/prompts/index.js.map +1 -1
  109. package/dist/esm/refresh-free.js +81 -81
  110. package/dist/esm/refresh-free.js.map +1 -1
  111. package/dist/esm/run/attachments.js +47 -44
  112. package/dist/esm/run/attachments.js.map +1 -1
  113. package/dist/esm/run/bird.js +26 -26
  114. package/dist/esm/run/bird.js.map +1 -1
  115. package/dist/esm/run/cache-state.js +7 -7
  116. package/dist/esm/run/cache-state.js.map +1 -1
  117. package/dist/esm/run/cli-fallback-state.js +45 -0
  118. package/dist/esm/run/cli-fallback-state.js.map +1 -0
  119. package/dist/esm/run/cli-preflight.js +24 -24
  120. package/dist/esm/run/cli-preflight.js.map +1 -1
  121. package/dist/esm/run/constants.js +12 -12
  122. package/dist/esm/run/constants.js.map +1 -1
  123. package/dist/esm/run/cookies/twitter.js +47 -47
  124. package/dist/esm/run/cookies/twitter.js.map +1 -1
  125. package/dist/esm/run/env.js +21 -15
  126. package/dist/esm/run/env.js.map +1 -1
  127. package/dist/esm/run/fetch-with-timeout.js +4 -4
  128. package/dist/esm/run/fetch-with-timeout.js.map +1 -1
  129. package/dist/esm/run/finish-line.js +68 -68
  130. package/dist/esm/run/finish-line.js.map +1 -1
  131. package/dist/esm/run/flows/asset/extract.js +15 -15
  132. package/dist/esm/run/flows/asset/extract.js.map +1 -1
  133. package/dist/esm/run/flows/asset/input.js +47 -66
  134. package/dist/esm/run/flows/asset/input.js.map +1 -1
  135. package/dist/esm/run/flows/asset/media-policy.js +1 -1
  136. package/dist/esm/run/flows/asset/media-policy.js.map +1 -1
  137. package/dist/esm/run/flows/asset/media.js +49 -40
  138. package/dist/esm/run/flows/asset/media.js.map +1 -1
  139. package/dist/esm/run/flows/asset/output.js +12 -12
  140. package/dist/esm/run/flows/asset/output.js.map +1 -1
  141. package/dist/esm/run/flows/asset/preprocess.js +79 -44
  142. package/dist/esm/run/flows/asset/preprocess.js.map +1 -1
  143. package/dist/esm/run/flows/asset/summary.js +173 -106
  144. package/dist/esm/run/flows/asset/summary.js.map +1 -1
  145. package/dist/esm/run/flows/url/extract.js +26 -26
  146. package/dist/esm/run/flows/url/extract.js.map +1 -1
  147. package/dist/esm/run/flows/url/flow.js +104 -98
  148. package/dist/esm/run/flows/url/flow.js.map +1 -1
  149. package/dist/esm/run/flows/url/markdown.js +57 -57
  150. package/dist/esm/run/flows/url/markdown.js.map +1 -1
  151. package/dist/esm/run/flows/url/slides-output.js +61 -59
  152. package/dist/esm/run/flows/url/slides-output.js.map +1 -1
  153. package/dist/esm/run/flows/url/slides-text.js +85 -85
  154. package/dist/esm/run/flows/url/slides-text.js.map +1 -1
  155. package/dist/esm/run/flows/url/summary.js +174 -107
  156. package/dist/esm/run/flows/url/summary.js.map +1 -1
  157. package/dist/esm/run/format.js +10 -10
  158. package/dist/esm/run/format.js.map +1 -1
  159. package/dist/esm/run/help.js +141 -135
  160. package/dist/esm/run/help.js.map +1 -1
  161. package/dist/esm/run/logging.js +10 -10
  162. package/dist/esm/run/logging.js.map +1 -1
  163. package/dist/esm/run/markdown.js +12 -12
  164. package/dist/esm/run/markdown.js.map +1 -1
  165. package/dist/esm/run/media-cache-state.js +5 -5
  166. package/dist/esm/run/media-cache-state.js.map +1 -1
  167. package/dist/esm/run/model-attempts.js.map +1 -1
  168. package/dist/esm/run/openrouter.js +11 -11
  169. package/dist/esm/run/openrouter.js.map +1 -1
  170. package/dist/esm/run/progress.js +1 -1
  171. package/dist/esm/run/progress.js.map +1 -1
  172. package/dist/esm/run/run-config.js +16 -16
  173. package/dist/esm/run/run-config.js.map +1 -1
  174. package/dist/esm/run/run-context.js +2 -2
  175. package/dist/esm/run/run-context.js.map +1 -1
  176. package/dist/esm/run/run-env.js +55 -54
  177. package/dist/esm/run/run-env.js.map +1 -1
  178. package/dist/esm/run/run-input.js +3 -3
  179. package/dist/esm/run/run-input.js.map +1 -1
  180. package/dist/esm/run/run-metrics.js +16 -16
  181. package/dist/esm/run/run-metrics.js.map +1 -1
  182. package/dist/esm/run/run-models.js +28 -23
  183. package/dist/esm/run/run-models.js.map +1 -1
  184. package/dist/esm/run/run-output.js +3 -3
  185. package/dist/esm/run/run-output.js.map +1 -1
  186. package/dist/esm/run/run-settings.js +83 -34
  187. package/dist/esm/run/run-settings.js.map +1 -1
  188. package/dist/esm/run/run-stream.js +4 -4
  189. package/dist/esm/run/run-stream.js.map +1 -1
  190. package/dist/esm/run/runner.js +166 -127
  191. package/dist/esm/run/runner.js.map +1 -1
  192. package/dist/esm/run/slides-cli.js +43 -42
  193. package/dist/esm/run/slides-cli.js.map +1 -1
  194. package/dist/esm/run/slides-render.js +36 -36
  195. package/dist/esm/run/slides-render.js.map +1 -1
  196. package/dist/esm/run/stdin-temp-file.js +77 -0
  197. package/dist/esm/run/stdin-temp-file.js.map +1 -0
  198. package/dist/esm/run/stream-output.js +7 -7
  199. package/dist/esm/run/stream-output.js.map +1 -1
  200. package/dist/esm/run/streaming.js +16 -16
  201. package/dist/esm/run/streaming.js.map +1 -1
  202. package/dist/esm/run/summary-engine.js +57 -51
  203. package/dist/esm/run/summary-engine.js.map +1 -1
  204. package/dist/esm/run/summary-llm.js +3 -3
  205. package/dist/esm/run/summary-llm.js.map +1 -1
  206. package/dist/esm/run/terminal.js +4 -4
  207. package/dist/esm/run/terminal.js.map +1 -1
  208. package/dist/esm/run/tips.js +2 -2
  209. package/dist/esm/run/tips.js.map +1 -1
  210. package/dist/esm/run/transcriber-cli.js +49 -49
  211. package/dist/esm/run/transcriber-cli.js.map +1 -1
  212. package/dist/esm/run.js +1 -1
  213. package/dist/esm/run.js.map +1 -1
  214. package/dist/esm/shared/contracts.js +1 -1
  215. package/dist/esm/shared/contracts.js.map +1 -1
  216. package/dist/esm/shared/sse-events.js +16 -16
  217. package/dist/esm/shared/sse-events.js.map +1 -1
  218. package/dist/esm/shared/streaming-merge.js +3 -3
  219. package/dist/esm/shared/streaming-merge.js.map +1 -1
  220. package/dist/esm/slides/extract.js +258 -249
  221. package/dist/esm/slides/extract.js.map +1 -1
  222. package/dist/esm/slides/index.js +3 -3
  223. package/dist/esm/slides/index.js.map +1 -1
  224. package/dist/esm/slides/settings.js +14 -14
  225. package/dist/esm/slides/settings.js.map +1 -1
  226. package/dist/esm/slides/store.js +9 -9
  227. package/dist/esm/slides/store.js.map +1 -1
  228. package/dist/esm/tty/format.js +13 -13
  229. package/dist/esm/tty/format.js.map +1 -1
  230. package/dist/esm/tty/osc-progress.js +1 -1
  231. package/dist/esm/tty/osc-progress.js.map +1 -1
  232. package/dist/esm/tty/progress/fetch-html.js +14 -14
  233. package/dist/esm/tty/progress/fetch-html.js.map +1 -1
  234. package/dist/esm/tty/progress/transcript.js +70 -62
  235. package/dist/esm/tty/progress/transcript.js.map +1 -1
  236. package/dist/esm/tty/spinner.js +20 -9
  237. package/dist/esm/tty/spinner.js.map +1 -1
  238. package/dist/esm/tty/theme.js +92 -92
  239. package/dist/esm/tty/theme.js.map +1 -1
  240. package/dist/esm/tty/website-progress.js +32 -32
  241. package/dist/esm/tty/website-progress.js.map +1 -1
  242. package/dist/esm/version.js +29 -29
  243. package/dist/esm/version.js.map +1 -1
  244. package/dist/types/cache.d.ts +6 -6
  245. package/dist/types/config.d.ts +49 -7
  246. package/dist/types/content/asset.d.ts +8 -6
  247. package/dist/types/content/index.d.ts +1 -1
  248. package/dist/types/costs.d.ts +3 -3
  249. package/dist/types/daemon/agent.d.ts +1 -1
  250. package/dist/types/daemon/auto-mode.d.ts +3 -3
  251. package/dist/types/daemon/chat.d.ts +2 -2
  252. package/dist/types/daemon/cli-entrypoint.d.ts +2 -0
  253. package/dist/types/daemon/config.d.ts +2 -2
  254. package/dist/types/daemon/env-merge.d.ts +1 -1
  255. package/dist/types/daemon/env-snapshot.d.ts +1 -1
  256. package/dist/types/daemon/flow-context.d.ts +7 -7
  257. package/dist/types/daemon/launchd.d.ts +8 -0
  258. package/dist/types/daemon/models.d.ts +6 -2
  259. package/dist/types/daemon/process-registry.d.ts +5 -5
  260. package/dist/types/daemon/server.d.ts +2 -2
  261. package/dist/types/daemon/summarize-progress.d.ts +1 -1
  262. package/dist/types/daemon/summarize.d.ts +7 -7
  263. package/dist/types/firecrawl.d.ts +1 -1
  264. package/dist/types/flags.d.ts +11 -11
  265. package/dist/types/index.d.ts +4 -4
  266. package/dist/types/language.d.ts +1 -1
  267. package/dist/types/llm/attachments.d.ts +1 -1
  268. package/dist/types/llm/cli.d.ts +3 -3
  269. package/dist/types/llm/generate-text.d.ts +7 -7
  270. package/dist/types/llm/html-to-markdown.d.ts +3 -3
  271. package/dist/types/llm/model-id.d.ts +1 -1
  272. package/dist/types/llm/prompt.d.ts +2 -2
  273. package/dist/types/llm/providers/anthropic.d.ts +3 -3
  274. package/dist/types/llm/providers/google.d.ts +3 -3
  275. package/dist/types/llm/providers/models.d.ts +2 -2
  276. package/dist/types/llm/providers/openai.d.ts +4 -4
  277. package/dist/types/llm/providers/shared.d.ts +2 -2
  278. package/dist/types/llm/transcript-to-markdown.d.ts +4 -2
  279. package/dist/types/llm/usage.d.ts +1 -1
  280. package/dist/types/logging/daemon.d.ts +4 -4
  281. package/dist/types/markitdown.d.ts +1 -1
  282. package/dist/types/media-cache.d.ts +2 -2
  283. package/dist/types/model-auto.d.ts +14 -4
  284. package/dist/types/model-spec.d.ts +10 -10
  285. package/dist/types/pricing/litellm.d.ts +1 -1
  286. package/dist/types/processes.d.ts +1 -1
  287. package/dist/types/prompts/index.d.ts +1 -1
  288. package/dist/types/run/attachments.d.ts +7 -7
  289. package/dist/types/run/bird.d.ts +2 -2
  290. package/dist/types/run/cache-state.d.ts +2 -2
  291. package/dist/types/run/cli-fallback-state.d.ts +6 -0
  292. package/dist/types/run/constants.d.ts +1 -1
  293. package/dist/types/run/cookies/twitter.d.ts +1 -1
  294. package/dist/types/run/env.d.ts +1 -1
  295. package/dist/types/run/finish-line.d.ts +5 -5
  296. package/dist/types/run/flows/asset/extract.d.ts +4 -4
  297. package/dist/types/run/flows/asset/input.d.ts +9 -3
  298. package/dist/types/run/flows/asset/media.d.ts +1 -1
  299. package/dist/types/run/flows/asset/output.d.ts +5 -5
  300. package/dist/types/run/flows/asset/preprocess.d.ts +23 -17
  301. package/dist/types/run/flows/asset/summary.d.ts +19 -17
  302. package/dist/types/run/flows/url/extract.d.ts +1 -1
  303. package/dist/types/run/flows/url/flow.d.ts +1 -1
  304. package/dist/types/run/flows/url/markdown.d.ts +6 -6
  305. package/dist/types/run/flows/url/slides-output.d.ts +7 -7
  306. package/dist/types/run/flows/url/slides-text.d.ts +9 -9
  307. package/dist/types/run/flows/url/summary.d.ts +11 -11
  308. package/dist/types/run/flows/url/types.d.ts +26 -22
  309. package/dist/types/run/format.d.ts +3 -3
  310. package/dist/types/run/help.d.ts +1 -1
  311. package/dist/types/run/media-cache-state.d.ts +2 -2
  312. package/dist/types/run/model-attempts.d.ts +1 -1
  313. package/dist/types/run/run-config.d.ts +4 -4
  314. package/dist/types/run/run-context.d.ts +3 -1
  315. package/dist/types/run/run-env.d.ts +3 -1
  316. package/dist/types/run/run-input.d.ts +2 -2
  317. package/dist/types/run/run-metrics.d.ts +3 -3
  318. package/dist/types/run/run-models.d.ts +3 -2
  319. package/dist/types/run/run-output.d.ts +1 -1
  320. package/dist/types/run/run-settings.d.ts +15 -6
  321. package/dist/types/run/run-stream.d.ts +2 -2
  322. package/dist/types/run/runner.d.ts +3 -2
  323. package/dist/types/run/slides-render.d.ts +4 -4
  324. package/dist/types/run/stdin-temp-file.d.ts +9 -0
  325. package/dist/types/run/stream-output.d.ts +1 -1
  326. package/dist/types/run/streaming.d.ts +4 -4
  327. package/dist/types/run/summary-engine.d.ts +11 -11
  328. package/dist/types/run/summary-llm.d.ts +5 -5
  329. package/dist/types/run/types.d.ts +4 -4
  330. package/dist/types/run.d.ts +1 -1
  331. package/dist/types/shared/contracts.d.ts +2 -2
  332. package/dist/types/shared/sse-events.d.ts +9 -9
  333. package/dist/types/slides/extract.d.ts +5 -4
  334. package/dist/types/slides/index.d.ts +5 -5
  335. package/dist/types/slides/store.d.ts +2 -2
  336. package/dist/types/slides/types.d.ts +2 -2
  337. package/dist/types/tty/osc-progress.d.ts +5 -5
  338. package/dist/types/tty/progress/fetch-html.d.ts +3 -3
  339. package/dist/types/tty/progress/transcript.d.ts +3 -3
  340. package/dist/types/tty/spinner.d.ts +2 -2
  341. package/dist/types/tty/theme.d.ts +2 -2
  342. package/dist/types/tty/website-progress.d.ts +3 -3
  343. package/dist/types/version.d.ts +1 -1
  344. package/docs/agent.md +38 -4
  345. package/docs/assets/site.js +46 -46
  346. package/docs/chrome-extension.md +11 -5
  347. package/docs/cli.md +59 -13
  348. package/docs/config.md +59 -10
  349. package/docs/extract-only.md +2 -0
  350. package/docs/index.html +33 -14
  351. package/docs/llm.md +7 -4
  352. package/docs/media.md +5 -4
  353. package/docs/model-auto.md +3 -2
  354. package/docs/nvidia-onnx-transcription.md +3 -3
  355. package/docs/openai.md +1 -1
  356. package/docs/releasing.md +3 -0
  357. package/docs/site/404.html +4 -1
  358. package/docs/site/assets/site.js +46 -46
  359. package/docs/site/docs/chrome-extension.html +18 -6
  360. package/docs/site/docs/config.html +29 -8
  361. package/docs/site/docs/extract-only.html +16 -4
  362. package/docs/site/docs/firecrawl.html +12 -3
  363. package/docs/site/docs/index.html +35 -6
  364. package/docs/site/docs/llm.html +19 -5
  365. package/docs/site/docs/openai.html +18 -5
  366. package/docs/site/docs/website.html +29 -9
  367. package/docs/site/docs/youtube.html +12 -3
  368. package/docs/site/index.html +33 -14
  369. package/docs/slides.md +13 -5
  370. package/docs/smoketest.md +29 -20
  371. package/docs/timestamps.md +21 -0
  372. package/docs/website.md +2 -1
  373. package/docs/youtube.md +4 -0
  374. package/package.json +36 -35
@@ -1,20 +1,21 @@
1
- import { isTwitterStatusUrl, isYouTubeUrl } from '@steipete/summarize-core/content/url';
2
- import { countTokens } from 'gpt-tokenizer';
3
- import { render as renderMarkdownAnsi } from 'markdansi';
4
- import { buildLanguageKey, buildLengthKey, buildPromptHash, buildSummaryCacheKey, hashString, normalizeContentForHash, } from '../../../cache.js';
5
- import { formatOutputLanguageForJson } from '../../../language.js';
6
- import { parseGatewayStyleModelId } from '../../../llm/model-id.js';
7
- import { buildAutoModelAttempts } from '../../../model-auto.js';
8
- import { buildLinkSummaryPrompt, SUMMARY_LENGTH_TARGET_CHARACTERS, SUMMARY_SYSTEM_PROMPT, } from '../../../prompts/index.js';
9
- import { parseCliUserModelId } from '../../env.js';
10
- import { buildExtractFinishLabel, buildLengthPartsForFinishLine, writeFinishLine, } from '../../finish-line.js';
11
- import { resolveTargetCharacters } from '../../format.js';
12
- import { writeVerbose } from '../../logging.js';
13
- import { prepareMarkdownForTerminal } from '../../markdown.js';
14
- import { runModelAttempts } from '../../model-attempts.js';
15
- import { buildOpenRouterNoAllowedProvidersMessage } from '../../openrouter.js';
16
- import { isRichTty, markdownRenderWidth, supportsColor } from '../../terminal.js';
17
- import { coerceSummaryWithSlides, interleaveSlidesIntoTranscript, normalizeSummarySlideHeadings, } from './slides-text.js';
1
+ import { isTwitterStatusUrl, isYouTubeUrl } from "@steipete/summarize-core/content/url";
2
+ import { countTokens } from "gpt-tokenizer";
3
+ import { render as renderMarkdownAnsi } from "markdansi";
4
+ import { buildLanguageKey, buildLengthKey, buildPromptHash, buildSummaryCacheKey, hashString, normalizeContentForHash, } from "../../../cache.js";
5
+ import { formatOutputLanguageForJson } from "../../../language.js";
6
+ import { parseGatewayStyleModelId } from "../../../llm/model-id.js";
7
+ import { buildAutoModelAttempts } from "../../../model-auto.js";
8
+ import { buildLinkSummaryPrompt, SUMMARY_LENGTH_TARGET_CHARACTERS, SUMMARY_SYSTEM_PROMPT, } from "../../../prompts/index.js";
9
+ import { readLastSuccessfulCliProvider, writeLastSuccessfulCliProvider, } from "../../cli-fallback-state.js";
10
+ import { parseCliUserModelId } from "../../env.js";
11
+ import { buildExtractFinishLabel, buildLengthPartsForFinishLine, writeFinishLine, } from "../../finish-line.js";
12
+ import { resolveTargetCharacters } from "../../format.js";
13
+ import { writeVerbose } from "../../logging.js";
14
+ import { prepareMarkdownForTerminal } from "../../markdown.js";
15
+ import { runModelAttempts } from "../../model-attempts.js";
16
+ import { buildOpenRouterNoAllowedProvidersMessage } from "../../openrouter.js";
17
+ import { isRichTty, markdownRenderWidth, supportsColor } from "../../terminal.js";
18
+ import { coerceSummaryWithSlides, interleaveSlidesIntoTranscript, normalizeSummarySlideHeadings, } from "./slides-text.js";
18
19
  const MAX_SLIDE_TRANSCRIPT_CHARS_BY_PRESET = {
19
20
  short: 2500,
20
21
  medium: 5000,
@@ -25,7 +26,7 @@ const MAX_SLIDE_TRANSCRIPT_CHARS_BY_PRESET = {
25
26
  const SLIDE_TRANSCRIPT_DEFAULT_EDGE_SECONDS = 30;
26
27
  const SLIDE_TRANSCRIPT_LEEWAY_SECONDS = 10;
27
28
  function parseTimestampSeconds(value) {
28
- const parts = value.split(':').map((item) => Number(item));
29
+ const parts = value.split(":").map((item) => Number(item));
29
30
  if (parts.some((item) => !Number.isFinite(item)))
30
31
  return null;
31
32
  if (parts.length === 2) {
@@ -42,9 +43,9 @@ function parseTranscriptTimedText(input) {
42
43
  if (!input)
43
44
  return [];
44
45
  const segments = [];
45
- for (const line of input.split('\n')) {
46
+ for (const line of input.split("\n")) {
46
47
  const trimmed = line.trim();
47
- if (!trimmed.startsWith('['))
48
+ if (!trimmed.startsWith("["))
48
49
  continue;
49
50
  const match = trimmed.match(/^\[(\d{1,2}:\d{2}(?::\d{2})?)\]\s*(.*)$/);
50
51
  if (!match)
@@ -52,7 +53,7 @@ function parseTranscriptTimedText(input) {
52
53
  const seconds = parseTimestampSeconds(match[1]);
53
54
  if (seconds == null)
54
55
  continue;
55
- const text = (match[2] ?? '').trim();
56
+ const text = (match[2] ?? "").trim();
56
57
  if (!text)
57
58
  continue;
58
59
  segments.push({ startSeconds: seconds, text });
@@ -65,20 +66,20 @@ function formatTimestamp(seconds) {
65
66
  const hours = Math.floor(clamped / 3600);
66
67
  const minutes = Math.floor((clamped % 3600) / 60);
67
68
  const secs = clamped % 60;
68
- const mm = String(minutes).padStart(2, '0');
69
- const ss = String(secs).padStart(2, '0');
69
+ const mm = String(minutes).padStart(2, "0");
70
+ const ss = String(secs).padStart(2, "0");
70
71
  if (hours <= 0)
71
72
  return `${minutes}:${ss}`;
72
- const hh = String(hours).padStart(2, '0');
73
+ const hh = String(hours).padStart(2, "0");
73
74
  return `${hh}:${mm}:${ss}`;
74
75
  }
75
76
  function truncateTranscript(value, limit) {
76
77
  if (value.length <= limit)
77
78
  return value;
78
79
  const truncated = value.slice(0, limit).trimEnd();
79
- const clean = truncated.replace(/\s+\S*$/, '').trim();
80
+ const clean = truncated.replace(/\s+\S*$/, "").trim();
80
81
  const result = clean.length > 0 ? clean : truncated.trim();
81
- return result.length > 0 ? `${result}…` : '';
82
+ return result.length > 0 ? `${result}…` : "";
82
83
  }
83
84
  function buildSlidesPromptText({ slides, transcriptTimedText, preset, }) {
84
85
  if (!slides || slides.slides.length === 0)
@@ -114,19 +115,19 @@ function buildSlidesPromptText({ slides, transcriptTimedText, preset, }) {
114
115
  break;
115
116
  excerptParts.push(segment.text);
116
117
  }
117
- const excerptRaw = excerptParts.join(' ').trim().replace(/\s+/g, ' ');
118
+ const excerptRaw = excerptParts.join(" ").trim().replace(/\s+/g, " ");
118
119
  const excerptBudget = remaining > 0 ? Math.min(perSlideBudget, remaining) : 0;
119
- const excerpt = excerptRaw && excerptBudget > 0 ? truncateTranscript(excerptRaw, excerptBudget) : '';
120
+ const excerpt = excerptRaw && excerptBudget > 0 ? truncateTranscript(excerptRaw, excerptBudget) : "";
120
121
  const label = `[slide:${slide.index}] [${formatTimestamp(start)}–${formatTimestamp(end)}]`;
121
122
  const block = excerpt ? `${label}\n${excerpt}` : label;
122
123
  blocks.push(block);
123
124
  remaining = Math.max(0, remaining - block.length);
124
125
  }
125
- return blocks.length > 0 ? blocks.join('\n\n') : null;
126
+ return blocks.length > 0 ? blocks.join("\n\n") : null;
126
127
  }
127
128
  export function buildUrlPrompt({ extracted, outputLanguage, lengthArg, promptOverride, lengthInstruction, languageInstruction, slides, }) {
128
- const isYouTube = extracted.siteName === 'YouTube';
129
- const preset = lengthArg.kind === 'preset' ? lengthArg.preset : 'medium';
129
+ const isYouTube = extracted.siteName === "YouTube";
130
+ const preset = lengthArg.kind === "preset" ? lengthArg.preset : "medium";
130
131
  const slidesText = buildSlidesPromptText({
131
132
  slides,
132
133
  transcriptTimedText: extracted.transcriptTimedText,
@@ -140,10 +141,10 @@ export function buildUrlPrompt({ extracted, outputLanguage, lengthArg, promptOve
140
141
  content: extracted.content,
141
142
  truncated: extracted.truncated,
142
143
  hasTranscript: isYouTube ||
143
- (extracted.transcriptSource !== null && extracted.transcriptSource !== 'unavailable'),
144
+ (extracted.transcriptSource !== null && extracted.transcriptSource !== "unavailable"),
144
145
  hasTranscriptTimestamps: Boolean(extracted.transcriptTimedText),
145
146
  slides: slidesText ? { count: slides?.slides.length ?? 0, text: slidesText } : null,
146
- summaryLength: lengthArg.kind === 'preset' ? lengthArg.preset : { maxCharacters: lengthArg.maxCharacters },
147
+ summaryLength: lengthArg.kind === "preset" ? lengthArg.preset : { maxCharacters: lengthArg.maxCharacters },
147
148
  outputLanguage,
148
149
  shares: [],
149
150
  promptOverride: promptOverride ?? null,
@@ -161,7 +162,7 @@ function shouldBypassShortContentSummary({ extracted, lengthArg, forceSummary, m
161
162
  return false;
162
163
  if (extracted.content.length > targetCharacters)
163
164
  return false;
164
- if (!json && typeof maxOutputTokensArg === 'number') {
165
+ if (!json && typeof maxOutputTokensArg === "number") {
165
166
  const tokenCount = countTokens(extracted.content);
166
167
  if (tokenCount > maxOutputTokensArg)
167
168
  return false;
@@ -176,7 +177,7 @@ async function outputSummaryFromExtractedContent({ ctx, url, extracted, extracti
176
177
  const finishReport = flags.shouldComputeReport ? await hooks.buildReport() : null;
177
178
  const payload = {
178
179
  input: {
179
- kind: 'url',
180
+ kind: "url",
180
181
  url,
181
182
  timeoutMs: flags.timeoutMs,
182
183
  youtube: flags.youtubeMode,
@@ -184,9 +185,9 @@ async function outputSummaryFromExtractedContent({ ctx, url, extracted, extracti
184
185
  format: flags.format,
185
186
  markdown: effectiveMarkdownMode,
186
187
  timestamps: flags.transcriptTimestamps,
187
- length: flags.lengthArg.kind === 'preset'
188
- ? { kind: 'preset', preset: flags.lengthArg.preset }
189
- : { kind: 'chars', maxCharacters: flags.lengthArg.maxCharacters },
188
+ length: flags.lengthArg.kind === "preset"
189
+ ? { kind: "preset", preset: flags.lengthArg.preset }
190
+ : { kind: "chars", maxCharacters: flags.lengthArg.maxCharacters },
190
191
  maxOutputTokens: flags.maxOutputTokensArg,
191
192
  model: model.requestedModelLabel,
192
193
  language: formatOutputLanguageForJson(flags.outputLanguage),
@@ -258,17 +259,17 @@ const pickModelForFinishLine = (llmCalls, fallback) => {
258
259
  }
259
260
  return null;
260
261
  };
261
- return (findLastModel('summary') ??
262
- findLastModel('markdown') ??
262
+ return (findLastModel("summary") ??
263
+ findLastModel("markdown") ??
263
264
  (llmCalls.length > 0 ? (llmCalls[llmCalls.length - 1]?.model ?? null) : null) ??
264
265
  fallback);
265
266
  };
266
267
  const buildModelMetaFromAttempt = (attempt) => {
267
- if (attempt.transport === 'cli') {
268
- return { provider: 'cli', canonical: attempt.userModelId };
268
+ if (attempt.transport === "cli") {
269
+ return { provider: "cli", canonical: attempt.userModelId };
269
270
  }
270
271
  const parsed = parseGatewayStyleModelId(attempt.llmModelId ?? attempt.userModelId);
271
- const canonical = attempt.userModelId.toLowerCase().startsWith('openrouter/')
272
+ const canonical = attempt.userModelId.toLowerCase().startsWith("openrouter/")
272
273
  ? attempt.userModelId
273
274
  : parsed.canonical;
274
275
  return { provider: parsed.provider, canonical };
@@ -280,14 +281,14 @@ export async function outputExtractedUrl({ ctx, url, extracted, extractionUi, pr
280
281
  extracted: { diagnostics: extracted.diagnostics },
281
282
  format: flags.format,
282
283
  markdownMode: effectiveMarkdownMode,
283
- hasMarkdownLlmCall: model.llmCalls.some((call) => call.purpose === 'markdown'),
284
+ hasMarkdownLlmCall: model.llmCalls.some((call) => call.purpose === "markdown"),
284
285
  });
285
286
  const finishModel = pickModelForFinishLine(model.llmCalls, null);
286
287
  if (flags.json) {
287
288
  const finishReport = flags.shouldComputeReport ? await hooks.buildReport() : null;
288
289
  const payload = {
289
290
  input: {
290
- kind: 'url',
291
+ kind: "url",
291
292
  url,
292
293
  timeoutMs: flags.timeoutMs,
293
294
  youtube: flags.youtubeMode,
@@ -295,9 +296,9 @@ export async function outputExtractedUrl({ ctx, url, extracted, extractionUi, pr
295
296
  format: flags.format,
296
297
  markdown: effectiveMarkdownMode,
297
298
  timestamps: flags.transcriptTimestamps,
298
- length: flags.lengthArg.kind === 'preset'
299
- ? { kind: 'preset', preset: flags.lengthArg.preset }
300
- : { kind: 'chars', maxCharacters: flags.lengthArg.maxCharacters },
299
+ length: flags.lengthArg.kind === "preset"
300
+ ? { kind: "preset", preset: flags.lengthArg.preset }
301
+ : { kind: "chars", maxCharacters: flags.lengthArg.maxCharacters },
301
302
  maxOutputTokens: flags.maxOutputTokensArg,
302
303
  model: model.requestedModelLabel,
303
304
  language: formatOutputLanguageForJson(flags.outputLanguage),
@@ -345,12 +346,12 @@ export async function outputExtractedUrl({ ctx, url, extracted, extractionUi, pr
345
346
  const extractCandidate = flags.transcriptTimestamps &&
346
347
  extracted.transcriptTimedText &&
347
348
  extracted.transcriptSource &&
348
- extracted.content.toLowerCase().startsWith('transcript:')
349
+ extracted.content.toLowerCase().startsWith("transcript:")
349
350
  ? `Transcript:\n${extracted.transcriptTimedText}`
350
351
  : extracted.content;
351
352
  const slideTags = slides?.slides && slides.slides.length > 0
352
- ? slides.slides.map((slide) => `[slide:${slide.index}]`).join('\n')
353
- : '';
353
+ ? slides.slides.map((slide) => `[slide:${slide.index}]`).join("\n")
354
+ : "";
354
355
  if (slidesOutput && slides?.slides && slides.slides.length > 0) {
355
356
  const transcriptText = extracted.transcriptTimedText
356
357
  ? `Transcript:\n${extracted.transcriptTimedText}`
@@ -390,7 +391,7 @@ export async function outputExtractedUrl({ ctx, url, extracted, extractionUi, pr
390
391
  }
391
392
  return;
392
393
  }
393
- const renderedExtract = flags.format === 'markdown' && !flags.plain && isRichTty(io.stdout)
394
+ const renderedExtract = flags.format === "markdown" && !flags.plain && isRichTty(io.stdout)
394
395
  ? renderMarkdownAnsi(prepareMarkdownForTerminal(extractCandidate), {
395
396
  width: markdownRenderWidth(io.stdout, io.env),
396
397
  wrap: true,
@@ -398,14 +399,14 @@ export async function outputExtractedUrl({ ctx, url, extracted, extractionUi, pr
398
399
  hyperlinks: true,
399
400
  })
400
401
  : extractCandidate;
401
- if (flags.format === 'markdown' && !flags.plain && isRichTty(io.stdout)) {
402
- io.stdout.write(`\n${renderedExtract.replace(/^\n+/, '')}`);
402
+ if (flags.format === "markdown" && !flags.plain && isRichTty(io.stdout)) {
403
+ io.stdout.write(`\n${renderedExtract.replace(/^\n+/, "")}`);
403
404
  }
404
405
  else {
405
406
  io.stdout.write(renderedExtract);
406
407
  }
407
- if (!renderedExtract.endsWith('\n')) {
408
- io.stdout.write('\n');
408
+ if (!renderedExtract.endsWith("\n")) {
409
+ io.stdout.write("\n");
409
410
  }
410
411
  hooks.restoreProgressAfterStdout?.();
411
412
  const slideFooter = slides ? [`slides ${slides.slides.length}`] : [];
@@ -434,9 +435,12 @@ export async function outputExtractedUrl({ ctx, url, extracted, extractionUi, pr
434
435
  }
435
436
  export async function summarizeExtractedUrl({ ctx, url, extracted, extractionUi, prompt, effectiveMarkdownMode, transcriptionCostLabel, onModelChosen, slides, slidesOutput, }) {
436
437
  const { io, flags, model, cache: cacheState, hooks } = ctx;
438
+ const lastSuccessfulCliProvider = model.isFallbackModel
439
+ ? await readLastSuccessfulCliProvider(io.envForRun)
440
+ : null;
437
441
  const promptPayload = { system: SUMMARY_SYSTEM_PROMPT, userText: prompt };
438
442
  const promptTokens = countTokens(promptPayload.userText);
439
- const kindForAuto = extracted.siteName === 'YouTube' ? 'youtube' : 'website';
443
+ const kindForAuto = extracted.siteName === "YouTube" ? "youtube" : "website";
440
444
  const attempts = await (async () => {
441
445
  if (model.isFallbackModel) {
442
446
  const catalog = await model.getLiteLlmCatalog();
@@ -450,6 +454,9 @@ export async function summarizeExtractedUrl({ ctx, url, extracted, extractionUi,
450
454
  catalog,
451
455
  openrouterProvidersFromEnv: null,
452
456
  cliAvailability: model.cliAvailability,
457
+ isImplicitAutoSelection: model.isImplicitAutoSelection,
458
+ allowAutoCliFallback: model.allowAutoCliFallback,
459
+ lastSuccessfulCliProvider,
453
460
  });
454
461
  if (flags.verbose) {
455
462
  for (const attempt of list.slice(0, 8)) {
@@ -457,7 +464,7 @@ export async function summarizeExtractedUrl({ ctx, url, extracted, extractionUi,
457
464
  }
458
465
  }
459
466
  return list.map((attempt) => {
460
- if (attempt.transport !== 'cli')
467
+ if (attempt.transport !== "cli")
461
468
  return model.summaryEngine.applyZaiOverrides(attempt);
462
469
  const parsed = parseCliUserModelId(attempt.userModelId);
463
470
  return { ...attempt, cliProvider: parsed.provider, cliModel: parsed.model };
@@ -465,12 +472,12 @@ export async function summarizeExtractedUrl({ ctx, url, extracted, extractionUi,
465
472
  }
466
473
  /* v8 ignore next */
467
474
  if (!model.fixedModelSpec) {
468
- throw new Error('Internal error: missing fixed model spec');
475
+ throw new Error("Internal error: missing fixed model spec");
469
476
  }
470
- if (model.fixedModelSpec.transport === 'cli') {
477
+ if (model.fixedModelSpec.transport === "cli") {
471
478
  return [
472
479
  {
473
- transport: 'cli',
480
+ transport: "cli",
474
481
  userModelId: model.fixedModelSpec.userModelId,
475
482
  llmModelId: null,
476
483
  cliProvider: model.fixedModelSpec.cliProvider,
@@ -481,7 +488,7 @@ export async function summarizeExtractedUrl({ ctx, url, extracted, extractionUi,
481
488
  },
482
489
  ];
483
490
  }
484
- const openaiOverrides = model.fixedModelSpec.requiredEnv === 'Z_AI_API_KEY'
491
+ const openaiOverrides = model.fixedModelSpec.requiredEnv === "Z_AI_API_KEY"
485
492
  ? {
486
493
  openaiApiKeyOverride: model.apiStatus.zaiApiKey,
487
494
  openaiBaseUrlOverride: model.apiStatus.zaiBaseUrl,
@@ -490,7 +497,7 @@ export async function summarizeExtractedUrl({ ctx, url, extracted, extractionUi,
490
497
  : {};
491
498
  return [
492
499
  {
493
- transport: model.fixedModelSpec.transport === 'openrouter' ? 'openrouter' : 'native',
500
+ transport: model.fixedModelSpec.transport === "openrouter" ? "openrouter" : "native",
494
501
  userModelId: model.fixedModelSpec.userModelId,
495
502
  llmModelId: model.fixedModelSpec.llmModelId,
496
503
  openrouterProviders: model.fixedModelSpec.openrouterProviders,
@@ -500,26 +507,29 @@ export async function summarizeExtractedUrl({ ctx, url, extracted, extractionUi,
500
507
  },
501
508
  ];
502
509
  })();
503
- const cacheStore = cacheState.mode === 'default' && !flags.summaryCacheBypass ? cacheState.store : null;
510
+ const cacheStore = cacheState.mode === "default" && !flags.summaryCacheBypass ? cacheState.store : null;
504
511
  const contentHash = cacheStore ? hashString(normalizeContentForHash(extracted.content)) : null;
505
512
  const promptHash = cacheStore ? buildPromptHash(prompt) : null;
506
513
  const lengthKey = buildLengthKey(flags.lengthArg);
507
514
  const languageKey = buildLanguageKey(flags.outputLanguage);
515
+ const autoSelectionCacheModel = model.isFallbackModel
516
+ ? `selection:${model.requestedModelInput.toLowerCase()}`
517
+ : null;
508
518
  let summaryResult = null;
509
519
  let usedAttempt = null;
510
520
  let summaryFromCache = false;
511
521
  let cacheChecked = false;
512
- const isTweet = extracted.siteName?.toLowerCase() === 'x' || isTwitterStatusUrl(extracted.url);
513
- const isYouTube = extracted.siteName === 'YouTube' || isYouTubeUrl(url);
522
+ const isTweet = extracted.siteName?.toLowerCase() === "x" || isTwitterStatusUrl(extracted.url);
523
+ const isYouTube = extracted.siteName === "YouTube" || isYouTubeUrl(url);
514
524
  const hasMedia = Boolean(extracted.video) ||
515
- (extracted.transcriptSource != null && extracted.transcriptSource !== 'unavailable') ||
516
- (typeof extracted.mediaDurationSeconds === 'number' && extracted.mediaDurationSeconds > 0) ||
525
+ (extracted.transcriptSource != null && extracted.transcriptSource !== "unavailable") ||
526
+ (typeof extracted.mediaDurationSeconds === "number" && extracted.mediaDurationSeconds > 0) ||
517
527
  extracted.isVideoOnly === true;
518
528
  const autoBypass = ctx.model.isFallbackModel && !ctx.model.isNamedModelSelection;
519
529
  const canBypassShortContent = (autoBypass || isTweet) &&
520
530
  !flags.slides &&
521
531
  !hasMedia &&
522
- flags.streamMode !== 'on' &&
532
+ flags.streamMode !== "on" &&
523
533
  !isYouTube &&
524
534
  shouldBypassShortContentSummary({
525
535
  extracted,
@@ -538,41 +548,78 @@ export async function summarizeExtractedUrl({ ctx, url, extracted, extractionUi,
538
548
  effectiveMarkdownMode,
539
549
  transcriptionCostLabel,
540
550
  slides,
541
- footerLabel: 'short content',
542
- verboseMessage: 'short content: skipping summary',
551
+ footerLabel: "short content",
552
+ verboseMessage: "short content: skipping summary",
543
553
  });
544
554
  return;
545
555
  }
546
556
  if (cacheStore && contentHash && promptHash) {
547
557
  cacheChecked = true;
548
- for (const attempt of attempts) {
549
- if (!model.summaryEngine.envHasKeyFor(attempt.requiredEnv))
550
- continue;
558
+ if (autoSelectionCacheModel) {
551
559
  const key = buildSummaryCacheKey({
552
560
  contentHash,
553
561
  promptHash,
554
- model: attempt.userModelId,
562
+ model: autoSelectionCacheModel,
555
563
  lengthKey,
556
564
  languageKey,
557
565
  });
558
- const cached = cacheStore.getText('summary', key);
559
- if (!cached)
560
- continue;
561
- writeVerbose(io.stderr, flags.verbose, 'cache hit summary', flags.verboseColor, io.envForRun);
562
- onModelChosen?.(attempt.userModelId);
563
- summaryResult = {
564
- summary: cached,
565
- summaryAlreadyPrinted: false,
566
- modelMeta: buildModelMetaFromAttempt(attempt),
567
- maxOutputTokensForCall: null,
568
- };
569
- usedAttempt = attempt;
570
- summaryFromCache = true;
571
- break;
566
+ const cached = cacheStore.getJson("summary", key);
567
+ const cachedSummary = cached && typeof cached.summary === "string" ? cached.summary.trim() : null;
568
+ const cachedModelId = cached && typeof cached.model === "string" ? cached.model.trim() : null;
569
+ if (cachedSummary) {
570
+ const cachedAttempt = cachedModelId
571
+ ? (attempts.find((attempt) => attempt.userModelId === cachedModelId) ?? null)
572
+ : null;
573
+ const fallbackAttempt = attempts.find((attempt) => model.summaryEngine.envHasKeyFor(attempt.requiredEnv)) ??
574
+ attempts[0] ??
575
+ null;
576
+ const matchedAttempt = cachedAttempt && model.summaryEngine.envHasKeyFor(cachedAttempt.requiredEnv)
577
+ ? cachedAttempt
578
+ : fallbackAttempt;
579
+ if (matchedAttempt) {
580
+ writeVerbose(io.stderr, flags.verbose, "cache hit summary (auto selection)", flags.verboseColor, io.envForRun);
581
+ onModelChosen?.(cachedModelId || matchedAttempt.userModelId);
582
+ summaryResult = {
583
+ summary: cachedSummary,
584
+ summaryAlreadyPrinted: false,
585
+ modelMeta: buildModelMetaFromAttempt(matchedAttempt),
586
+ maxOutputTokensForCall: null,
587
+ };
588
+ usedAttempt = matchedAttempt;
589
+ summaryFromCache = true;
590
+ }
591
+ }
592
+ }
593
+ if (!summaryFromCache) {
594
+ for (const attempt of attempts) {
595
+ if (!model.summaryEngine.envHasKeyFor(attempt.requiredEnv))
596
+ continue;
597
+ const key = buildSummaryCacheKey({
598
+ contentHash,
599
+ promptHash,
600
+ model: attempt.userModelId,
601
+ lengthKey,
602
+ languageKey,
603
+ });
604
+ const cached = cacheStore.getText("summary", key);
605
+ if (!cached)
606
+ continue;
607
+ writeVerbose(io.stderr, flags.verbose, "cache hit summary", flags.verboseColor, io.envForRun);
608
+ onModelChosen?.(attempt.userModelId);
609
+ summaryResult = {
610
+ summary: cached,
611
+ summaryAlreadyPrinted: false,
612
+ modelMeta: buildModelMetaFromAttempt(attempt),
613
+ maxOutputTokensForCall: null,
614
+ };
615
+ usedAttempt = attempt;
616
+ summaryFromCache = true;
617
+ break;
618
+ }
572
619
  }
573
620
  }
574
621
  if (cacheChecked && !summaryFromCache) {
575
- writeVerbose(io.stderr, flags.verbose, 'cache miss summary', flags.verboseColor, io.envForRun);
622
+ writeVerbose(io.stderr, flags.verbose, "cache miss summary", flags.verboseColor, io.envForRun);
576
623
  }
577
624
  ctx.hooks.onSummaryCached?.(summaryFromCache);
578
625
  let lastError = null;
@@ -618,7 +665,7 @@ export async function summarizeExtractedUrl({ ctx, url, extracted, extractionUi,
618
665
  };
619
666
  if (model.isNamedModelSelection) {
620
667
  if (lastError === null && missingRequiredEnvs.size > 0) {
621
- throw new Error(withFreeTip(`Missing ${Array.from(missingRequiredEnvs).sort().join(', ')} for --model ${model.requestedModelInput}.`));
668
+ throw new Error(withFreeTip(`Missing ${Array.from(missingRequiredEnvs).sort().join(", ")} for --model ${model.requestedModelInput}.`));
622
669
  }
623
670
  if (lastError instanceof Error) {
624
671
  if (sawOpenRouterNoAllowedProviders) {
@@ -642,21 +689,41 @@ export async function summarizeExtractedUrl({ ctx, url, extracted, extractionUi,
642
689
  effectiveMarkdownMode,
643
690
  transcriptionCostLabel,
644
691
  slides,
645
- footerLabel: 'no model',
692
+ footerLabel: "no model",
646
693
  verboseMessage: lastError instanceof Error ? `auto failed all models: ${lastError.message}` : null,
647
694
  });
648
695
  return;
649
696
  }
650
697
  if (!summaryFromCache && cacheStore && contentHash && promptHash) {
651
- const key = buildSummaryCacheKey({
698
+ const perModelKey = buildSummaryCacheKey({
652
699
  contentHash,
653
700
  promptHash,
654
701
  model: usedAttempt.userModelId,
655
702
  lengthKey,
656
703
  languageKey,
657
704
  });
658
- cacheStore.setText('summary', key, summaryResult.summary, cacheState.ttlMs);
659
- writeVerbose(io.stderr, flags.verbose, 'cache write summary', flags.verboseColor, io.envForRun);
705
+ cacheStore.setText("summary", perModelKey, summaryResult.summary, cacheState.ttlMs);
706
+ writeVerbose(io.stderr, flags.verbose, "cache write summary", flags.verboseColor, io.envForRun);
707
+ if (autoSelectionCacheModel) {
708
+ const selectionKey = buildSummaryCacheKey({
709
+ contentHash,
710
+ promptHash,
711
+ model: autoSelectionCacheModel,
712
+ lengthKey,
713
+ languageKey,
714
+ });
715
+ cacheStore.setJson("summary", selectionKey, { summary: summaryResult.summary, model: usedAttempt.userModelId }, cacheState.ttlMs);
716
+ writeVerbose(io.stderr, flags.verbose, "cache write summary (auto selection)", flags.verboseColor, io.envForRun);
717
+ }
718
+ }
719
+ if (!summaryFromCache &&
720
+ model.isFallbackModel &&
721
+ usedAttempt.transport === "cli" &&
722
+ usedAttempt.cliProvider) {
723
+ await writeLastSuccessfulCliProvider({
724
+ env: io.envForRun,
725
+ provider: usedAttempt.cliProvider,
726
+ });
660
727
  }
661
728
  const { summary, summaryAlreadyPrinted, modelMeta, maxOutputTokensForCall } = summaryResult;
662
729
  const normalizedSummary = slides && slides.slides.length > 0 ? normalizeSummarySlideHeadings(summary) : summary;
@@ -664,7 +731,7 @@ export async function summarizeExtractedUrl({ ctx, url, extracted, extractionUi,
664
731
  const finishReport = flags.shouldComputeReport ? await hooks.buildReport() : null;
665
732
  const payload = {
666
733
  input: {
667
- kind: 'url',
734
+ kind: "url",
668
735
  url,
669
736
  timeoutMs: flags.timeoutMs,
670
737
  youtube: flags.youtubeMode,
@@ -672,9 +739,9 @@ export async function summarizeExtractedUrl({ ctx, url, extracted, extractionUi,
672
739
  format: flags.format,
673
740
  markdown: effectiveMarkdownMode,
674
741
  timestamps: flags.transcriptTimestamps,
675
- length: flags.lengthArg.kind === 'preset'
676
- ? { kind: 'preset', preset: flags.lengthArg.preset }
677
- : { kind: 'chars', maxCharacters: flags.lengthArg.maxCharacters },
742
+ length: flags.lengthArg.kind === "preset"
743
+ ? { kind: "preset", preset: flags.lengthArg.preset }
744
+ : { kind: "chars", maxCharacters: flags.lengthArg.maxCharacters },
678
745
  maxOutputTokens: flags.maxOutputTokensArg,
679
746
  model: model.requestedModelLabel,
680
747
  language: formatOutputLanguageForJson(flags.outputLanguage),
@@ -695,7 +762,7 @@ export async function summarizeExtractedUrl({ ctx, url, extracted, extractionUi,
695
762
  provider: modelMeta.provider,
696
763
  model: usedAttempt.userModelId,
697
764
  maxCompletionTokens: maxOutputTokensForCall,
698
- strategy: 'single',
765
+ strategy: "single",
699
766
  },
700
767
  metrics: flags.metricsEnabled ? finishReport : null,
701
768
  summary: normalizedSummary,
@@ -707,7 +774,7 @@ export async function summarizeExtractedUrl({ ctx, url, extracted, extractionUi,
707
774
  stderr: io.stderr,
708
775
  env: io.envForRun,
709
776
  elapsedMs: Date.now() - flags.runStartedAtMs,
710
- elapsedLabel: summaryFromCache ? 'Cached' : null,
777
+ elapsedLabel: summaryFromCache ? "Cached" : null,
711
778
  label: extractionUi.finishSourceLabel,
712
779
  model: usedAttempt.userModelId,
713
780
  report: finishReport,
@@ -750,15 +817,15 @@ export async function summarizeExtractedUrl({ ctx, url, extracted, extractionUi,
750
817
  })
751
818
  : normalizedSummary;
752
819
  if (!flags.plain && isRichTty(io.stdout)) {
753
- io.stdout.write(`\n${rendered.replace(/^\n+/, '')}`);
820
+ io.stdout.write(`\n${rendered.replace(/^\n+/, "")}`);
754
821
  }
755
822
  else {
756
823
  if (isRichTty(io.stdout))
757
- io.stdout.write('\n');
758
- io.stdout.write(rendered.replace(/^\n+/, ''));
824
+ io.stdout.write("\n");
825
+ io.stdout.write(rendered.replace(/^\n+/, ""));
759
826
  }
760
- if (!rendered.endsWith('\n')) {
761
- io.stdout.write('\n');
827
+ if (!rendered.endsWith("\n")) {
828
+ io.stdout.write("\n");
762
829
  }
763
830
  hooks.restoreProgressAfterStdout?.();
764
831
  }
@@ -769,7 +836,7 @@ export async function summarizeExtractedUrl({ ctx, url, extracted, extractionUi,
769
836
  stderr: io.stderr,
770
837
  env: io.envForRun,
771
838
  elapsedMs: Date.now() - flags.runStartedAtMs,
772
- elapsedLabel: summaryFromCache ? 'Cached' : null,
839
+ elapsedLabel: summaryFromCache ? "Cached" : null,
773
840
  label: extractionUi.finishSourceLabel,
774
841
  model: modelMeta.canonical,
775
842
  report,