@steipete/summarize 0.11.1 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (361) hide show
  1. package/CHANGELOG.md +38 -1
  2. package/README.md +63 -17
  3. package/dist/cli.js +1 -1
  4. package/dist/esm/cache-keys.js +75 -0
  5. package/dist/esm/cache-keys.js.map +1 -0
  6. package/dist/esm/cache-slides-cleanup.js +47 -0
  7. package/dist/esm/cache-slides-cleanup.js.map +1 -0
  8. package/dist/esm/cache.js +14 -91
  9. package/dist/esm/cache.js.map +1 -1
  10. package/dist/esm/config/env.js +49 -0
  11. package/dist/esm/config/env.js.map +1 -0
  12. package/dist/esm/config/model.js +193 -0
  13. package/dist/esm/config/model.js.map +1 -0
  14. package/dist/esm/config/parse-helpers.js +50 -0
  15. package/dist/esm/config/parse-helpers.js.map +1 -0
  16. package/dist/esm/config/read.js +83 -0
  17. package/dist/esm/config/read.js.map +1 -0
  18. package/dist/esm/config/sections.js +438 -0
  19. package/dist/esm/config/sections.js.map +1 -0
  20. package/dist/esm/config/types.js +2 -0
  21. package/dist/esm/config/types.js.map +1 -0
  22. package/dist/esm/config.js +24 -807
  23. package/dist/esm/config.js.map +1 -1
  24. package/dist/esm/content/asset.js +2 -2
  25. package/dist/esm/content/asset.js.map +1 -1
  26. package/dist/esm/daemon/agent-model.js +235 -0
  27. package/dist/esm/daemon/agent-model.js.map +1 -0
  28. package/dist/esm/daemon/agent-request.js +87 -0
  29. package/dist/esm/daemon/agent-request.js.map +1 -0
  30. package/dist/esm/daemon/agent.js +42 -243
  31. package/dist/esm/daemon/agent.js.map +1 -1
  32. package/dist/esm/daemon/chat.js +69 -8
  33. package/dist/esm/daemon/chat.js.map +1 -1
  34. package/dist/esm/daemon/cli.js +21 -4
  35. package/dist/esm/daemon/cli.js.map +1 -1
  36. package/dist/esm/daemon/config.js +65 -9
  37. package/dist/esm/daemon/config.js.map +1 -1
  38. package/dist/esm/daemon/env-snapshot.js +4 -0
  39. package/dist/esm/daemon/env-snapshot.js.map +1 -1
  40. package/dist/esm/daemon/flow-context.js +8 -1
  41. package/dist/esm/daemon/flow-context.js.map +1 -1
  42. package/dist/esm/daemon/models.js +16 -0
  43. package/dist/esm/daemon/models.js.map +1 -1
  44. package/dist/esm/daemon/process-registry.js.map +1 -1
  45. package/dist/esm/daemon/server-admin-routes.js +134 -0
  46. package/dist/esm/daemon/server-admin-routes.js.map +1 -0
  47. package/dist/esm/daemon/server-agent-route.js +104 -0
  48. package/dist/esm/daemon/server-agent-route.js.map +1 -0
  49. package/dist/esm/daemon/server-http.js +89 -0
  50. package/dist/esm/daemon/server-http.js.map +1 -0
  51. package/dist/esm/daemon/server-session-routes.js +209 -0
  52. package/dist/esm/daemon/server-session-routes.js.map +1 -0
  53. package/dist/esm/daemon/server-session.js +118 -0
  54. package/dist/esm/daemon/server-session.js.map +1 -0
  55. package/dist/esm/daemon/server-sse.js +28 -0
  56. package/dist/esm/daemon/server-sse.js.map +1 -0
  57. package/dist/esm/daemon/server-summarize-execution.js +357 -0
  58. package/dist/esm/daemon/server-summarize-execution.js.map +1 -0
  59. package/dist/esm/daemon/server-summarize-request.js +119 -0
  60. package/dist/esm/daemon/server-summarize-request.js.map +1 -0
  61. package/dist/esm/daemon/server.js +72 -1121
  62. package/dist/esm/daemon/server.js.map +1 -1
  63. package/dist/esm/daemon/summarize-progress.js +1 -1
  64. package/dist/esm/daemon/summarize-progress.js.map +1 -1
  65. package/dist/esm/daemon/summarize.js.map +1 -1
  66. package/dist/esm/llm/cli-exec.js +75 -0
  67. package/dist/esm/llm/cli-exec.js.map +1 -0
  68. package/dist/esm/llm/cli-provider-output.js +191 -0
  69. package/dist/esm/llm/cli-provider-output.js.map +1 -0
  70. package/dist/esm/llm/cli.js +3 -212
  71. package/dist/esm/llm/cli.js.map +1 -1
  72. package/dist/esm/llm/generate-text-document.js +109 -0
  73. package/dist/esm/llm/generate-text-document.js.map +1 -0
  74. package/dist/esm/llm/generate-text-shared.js +102 -0
  75. package/dist/esm/llm/generate-text-shared.js.map +1 -0
  76. package/dist/esm/llm/generate-text-stream.js +258 -0
  77. package/dist/esm/llm/generate-text-stream.js.map +1 -0
  78. package/dist/esm/llm/generate-text.js +145 -480
  79. package/dist/esm/llm/generate-text.js.map +1 -1
  80. package/dist/esm/llm/model-id.js +21 -20
  81. package/dist/esm/llm/model-id.js.map +1 -1
  82. package/dist/esm/llm/provider-capabilities.js +2 -0
  83. package/dist/esm/llm/provider-capabilities.js.map +1 -0
  84. package/dist/esm/llm/provider-profile.js +142 -0
  85. package/dist/esm/llm/provider-profile.js.map +1 -0
  86. package/dist/esm/llm/providers/google.js +42 -5
  87. package/dist/esm/llm/providers/google.js.map +1 -1
  88. package/dist/esm/llm/providers/models.js +13 -0
  89. package/dist/esm/llm/providers/models.js.map +1 -1
  90. package/dist/esm/llm/providers/openai.js.map +1 -1
  91. package/dist/esm/llm/transcript-to-markdown.js.map +1 -1
  92. package/dist/esm/model-auto-cli.js +89 -0
  93. package/dist/esm/model-auto-cli.js.map +1 -0
  94. package/dist/esm/model-auto-rules.js +86 -0
  95. package/dist/esm/model-auto-rules.js.map +1 -0
  96. package/dist/esm/model-auto.js +10 -245
  97. package/dist/esm/model-auto.js.map +1 -1
  98. package/dist/esm/model-spec.js +23 -17
  99. package/dist/esm/model-spec.js.map +1 -1
  100. package/dist/esm/refresh-free.js +1 -1
  101. package/dist/esm/refresh-free.js.map +1 -1
  102. package/dist/esm/run/attachments.js +1 -1
  103. package/dist/esm/run/attachments.js.map +1 -1
  104. package/dist/esm/run/bird/exec.js +23 -0
  105. package/dist/esm/run/bird/exec.js.map +1 -0
  106. package/dist/esm/run/bird/media.js +171 -0
  107. package/dist/esm/run/bird/media.js.map +1 -0
  108. package/dist/esm/run/bird/parse.js +82 -0
  109. package/dist/esm/run/bird/parse.js.map +1 -0
  110. package/dist/esm/run/bird/types.js +2 -0
  111. package/dist/esm/run/bird/types.js.map +1 -0
  112. package/dist/esm/run/bird.js +86 -144
  113. package/dist/esm/run/bird.js.map +1 -1
  114. package/dist/esm/run/cache-state.js.map +1 -1
  115. package/dist/esm/run/constants.js +2 -1
  116. package/dist/esm/run/constants.js.map +1 -1
  117. package/dist/esm/run/env.js +3 -0
  118. package/dist/esm/run/env.js.map +1 -1
  119. package/dist/esm/run/finish-line-labels.js +76 -0
  120. package/dist/esm/run/finish-line-labels.js.map +1 -0
  121. package/dist/esm/run/finish-line-lengths.js +96 -0
  122. package/dist/esm/run/finish-line-lengths.js.map +1 -0
  123. package/dist/esm/run/finish-line.js +3 -169
  124. package/dist/esm/run/finish-line.js.map +1 -1
  125. package/dist/esm/run/flows/asset/extract.js.map +1 -1
  126. package/dist/esm/run/flows/asset/input.js +1 -1
  127. package/dist/esm/run/flows/asset/input.js.map +1 -1
  128. package/dist/esm/run/flows/asset/media.js +19 -10
  129. package/dist/esm/run/flows/asset/media.js.map +1 -1
  130. package/dist/esm/run/flows/asset/output.js.map +1 -1
  131. package/dist/esm/run/flows/asset/preprocess.js.map +1 -1
  132. package/dist/esm/run/flows/asset/summary-attempts.js +109 -0
  133. package/dist/esm/run/flows/asset/summary-attempts.js.map +1 -0
  134. package/dist/esm/run/flows/asset/summary.js +19 -107
  135. package/dist/esm/run/flows/asset/summary.js.map +1 -1
  136. package/dist/esm/run/flows/url/extract.js +7 -4
  137. package/dist/esm/run/flows/url/extract.js.map +1 -1
  138. package/dist/esm/run/flows/url/flow-progress.js +119 -0
  139. package/dist/esm/run/flows/url/flow-progress.js.map +1 -0
  140. package/dist/esm/run/flows/url/flow.js +22 -93
  141. package/dist/esm/run/flows/url/flow.js.map +1 -1
  142. package/dist/esm/run/flows/url/markdown.js +21 -3
  143. package/dist/esm/run/flows/url/markdown.js.map +1 -1
  144. package/dist/esm/run/flows/url/progress-status.js +56 -0
  145. package/dist/esm/run/flows/url/progress-status.js.map +1 -0
  146. package/dist/esm/run/flows/url/slides-output-render.js +78 -0
  147. package/dist/esm/run/flows/url/slides-output-render.js.map +1 -0
  148. package/dist/esm/run/flows/url/slides-output-state.js +86 -0
  149. package/dist/esm/run/flows/url/slides-output-state.js.map +1 -0
  150. package/dist/esm/run/flows/url/slides-output-stream.js +271 -0
  151. package/dist/esm/run/flows/url/slides-output-stream.js.map +1 -0
  152. package/dist/esm/run/flows/url/slides-output.js +29 -422
  153. package/dist/esm/run/flows/url/slides-output.js.map +1 -1
  154. package/dist/esm/run/flows/url/slides-text-markdown.js +431 -0
  155. package/dist/esm/run/flows/url/slides-text-markdown.js.map +1 -0
  156. package/dist/esm/run/flows/url/slides-text-transcript.js +199 -0
  157. package/dist/esm/run/flows/url/slides-text-transcript.js.map +1 -0
  158. package/dist/esm/run/flows/url/slides-text-types.js +2 -0
  159. package/dist/esm/run/flows/url/slides-text-types.js.map +1 -0
  160. package/dist/esm/run/flows/url/slides-text.js +2 -627
  161. package/dist/esm/run/flows/url/slides-text.js.map +1 -1
  162. package/dist/esm/run/flows/url/summary-finish.js +34 -0
  163. package/dist/esm/run/flows/url/summary-finish.js.map +1 -0
  164. package/dist/esm/run/flows/url/summary-json.js +32 -0
  165. package/dist/esm/run/flows/url/summary-json.js.map +1 -0
  166. package/dist/esm/run/flows/url/summary-prompt.js +147 -0
  167. package/dist/esm/run/flows/url/summary-prompt.js.map +1 -0
  168. package/dist/esm/run/flows/url/summary-resolution.js +320 -0
  169. package/dist/esm/run/flows/url/summary-resolution.js.map +1 -0
  170. package/dist/esm/run/flows/url/summary-timestamps.js +136 -0
  171. package/dist/esm/run/flows/url/summary-timestamps.js.map +1 -0
  172. package/dist/esm/run/flows/url/summary.js +49 -543
  173. package/dist/esm/run/flows/url/summary.js.map +1 -1
  174. package/dist/esm/run/help.js +9 -3
  175. package/dist/esm/run/help.js.map +1 -1
  176. package/dist/esm/run/markdown-transforms.js +89 -0
  177. package/dist/esm/run/markdown-transforms.js.map +1 -0
  178. package/dist/esm/run/markdown.js +1 -96
  179. package/dist/esm/run/markdown.js.map +1 -1
  180. package/dist/esm/run/run-env.js +28 -7
  181. package/dist/esm/run/run-env.js.map +1 -1
  182. package/dist/esm/run/run-settings-parse.js +73 -0
  183. package/dist/esm/run/run-settings-parse.js.map +1 -0
  184. package/dist/esm/run/run-settings.js +1 -72
  185. package/dist/esm/run/run-settings.js.map +1 -1
  186. package/dist/esm/run/runner-contexts.js +116 -0
  187. package/dist/esm/run/runner-contexts.js.map +1 -0
  188. package/dist/esm/run/runner-execution.js +62 -0
  189. package/dist/esm/run/runner-execution.js.map +1 -0
  190. package/dist/esm/run/runner-flags.js +97 -0
  191. package/dist/esm/run/runner-flags.js.map +1 -0
  192. package/dist/esm/run/runner-setup.js +109 -0
  193. package/dist/esm/run/runner-setup.js.map +1 -0
  194. package/dist/esm/run/runner-slides.js +38 -0
  195. package/dist/esm/run/runner-slides.js.map +1 -0
  196. package/dist/esm/run/runner.js +99 -390
  197. package/dist/esm/run/runner.js.map +1 -1
  198. package/dist/esm/run/slides-render.js +5 -2
  199. package/dist/esm/run/slides-render.js.map +1 -1
  200. package/dist/esm/run/stdin-temp-file.js +1 -1
  201. package/dist/esm/run/stdin-temp-file.js.map +1 -1
  202. package/dist/esm/run/streaming.js +1 -0
  203. package/dist/esm/run/streaming.js.map +1 -1
  204. package/dist/esm/run/summary-engine.js +26 -10
  205. package/dist/esm/run/summary-engine.js.map +1 -1
  206. package/dist/esm/run/summary-llm.js +2 -1
  207. package/dist/esm/run/summary-llm.js.map +1 -1
  208. package/dist/esm/run/terminal.js +4 -1
  209. package/dist/esm/run/terminal.js.map +1 -1
  210. package/dist/esm/run/transcriber-cli.js +1 -1
  211. package/dist/esm/run/transcriber-cli.js.map +1 -1
  212. package/dist/esm/slides/download.js +242 -0
  213. package/dist/esm/slides/download.js.map +1 -0
  214. package/dist/esm/slides/extract-finalize.js +98 -0
  215. package/dist/esm/slides/extract-finalize.js.map +1 -0
  216. package/dist/esm/slides/extract.js +64 -1621
  217. package/dist/esm/slides/extract.js.map +1 -1
  218. package/dist/esm/slides/frame-extraction.js +372 -0
  219. package/dist/esm/slides/frame-extraction.js.map +1 -0
  220. package/dist/esm/slides/ingest.js +167 -0
  221. package/dist/esm/slides/ingest.js.map +1 -0
  222. package/dist/esm/slides/ocr.js +91 -0
  223. package/dist/esm/slides/ocr.js.map +1 -0
  224. package/dist/esm/slides/process.js +218 -0
  225. package/dist/esm/slides/process.js.map +1 -0
  226. package/dist/esm/slides/scene-detection.js +387 -0
  227. package/dist/esm/slides/scene-detection.js.map +1 -0
  228. package/dist/esm/slides/source-id.js +42 -0
  229. package/dist/esm/slides/source-id.js.map +1 -0
  230. package/dist/esm/tty/progress/fetch-html.js.map +1 -1
  231. package/dist/esm/tty/progress/transcript.js +21 -8
  232. package/dist/esm/tty/progress/transcript.js.map +1 -1
  233. package/dist/esm/tty/spinner.js +8 -2
  234. package/dist/esm/tty/spinner.js.map +1 -1
  235. package/dist/esm/tty/website-progress.js +5 -3
  236. package/dist/esm/tty/website-progress.js.map +1 -1
  237. package/dist/esm/version.js +1 -1
  238. package/dist/types/cache-keys.d.ts +44 -0
  239. package/dist/types/cache-slides-cleanup.d.ts +1 -0
  240. package/dist/types/cache.d.ts +1 -9
  241. package/dist/types/config/env.d.ts +6 -0
  242. package/dist/types/config/model.d.ts +3 -0
  243. package/dist/types/config/parse-helpers.d.ts +7 -0
  244. package/dist/types/config/read.d.ts +2 -0
  245. package/dist/types/config/sections.d.ts +33 -0
  246. package/dist/types/config/types.d.ts +230 -0
  247. package/dist/types/config.d.ts +3 -209
  248. package/dist/types/costs.d.ts +1 -1
  249. package/dist/types/daemon/agent-model.d.ts +40 -0
  250. package/dist/types/daemon/agent-request.d.ts +14 -0
  251. package/dist/types/daemon/chat.d.ts +3 -1
  252. package/dist/types/daemon/config.d.ts +13 -2
  253. package/dist/types/daemon/env-snapshot.d.ts +1 -1
  254. package/dist/types/daemon/flow-context.d.ts +1 -1
  255. package/dist/types/daemon/models.d.ts +1 -0
  256. package/dist/types/daemon/server-admin-routes.d.ts +22 -0
  257. package/dist/types/daemon/server-agent-route.d.ts +9 -0
  258. package/dist/types/daemon/server-http.d.ts +10 -0
  259. package/dist/types/daemon/server-session-routes.d.ts +11 -0
  260. package/dist/types/daemon/server-session.d.ts +52 -0
  261. package/dist/types/daemon/server-sse.d.ts +12 -0
  262. package/dist/types/daemon/server-summarize-execution.d.ts +70 -0
  263. package/dist/types/daemon/server-summarize-request.d.ts +36 -0
  264. package/dist/types/daemon/server.d.ts +3 -4
  265. package/dist/types/daemon/summarize.d.ts +1 -1
  266. package/dist/types/llm/cli-exec.d.ts +13 -0
  267. package/dist/types/llm/cli-provider-output.d.ts +16 -0
  268. package/dist/types/llm/generate-text-document.d.ts +34 -0
  269. package/dist/types/llm/generate-text-shared.d.ts +25 -0
  270. package/dist/types/llm/generate-text-stream.d.ts +26 -0
  271. package/dist/types/llm/generate-text.d.ts +6 -26
  272. package/dist/types/llm/html-to-markdown.d.ts +1 -1
  273. package/dist/types/llm/model-id.d.ts +1 -1
  274. package/dist/types/llm/provider-capabilities.d.ts +2 -0
  275. package/dist/types/llm/provider-profile.d.ts +31 -0
  276. package/dist/types/llm/providers/google.d.ts +6 -0
  277. package/dist/types/llm/providers/models.d.ts +5 -0
  278. package/dist/types/llm/transcript-to-markdown.d.ts +1 -1
  279. package/dist/types/model-auto-cli.d.ts +15 -0
  280. package/dist/types/model-auto-rules.d.ts +7 -0
  281. package/dist/types/model-auto.d.ts +5 -7
  282. package/dist/types/model-spec.d.ts +2 -2
  283. package/dist/types/run/attachments.d.ts +2 -2
  284. package/dist/types/run/bird/exec.d.ts +1 -0
  285. package/dist/types/run/bird/media.d.ts +3 -0
  286. package/dist/types/run/bird/parse.d.ts +3 -0
  287. package/dist/types/run/bird/types.d.ts +18 -0
  288. package/dist/types/run/bird.d.ts +12 -17
  289. package/dist/types/run/cache-state.d.ts +1 -1
  290. package/dist/types/run/constants.d.ts +2 -1
  291. package/dist/types/run/env.d.ts +1 -0
  292. package/dist/types/run/finish-line-labels.d.ts +29 -0
  293. package/dist/types/run/finish-line-lengths.d.ts +23 -0
  294. package/dist/types/run/finish-line.d.ts +2 -52
  295. package/dist/types/run/flows/asset/extract.d.ts +1 -1
  296. package/dist/types/run/flows/asset/input.d.ts +1 -1
  297. package/dist/types/run/flows/asset/preprocess.d.ts +1 -1
  298. package/dist/types/run/flows/asset/summary-attempts.d.ts +24 -0
  299. package/dist/types/run/flows/asset/summary.d.ts +6 -2
  300. package/dist/types/run/flows/url/flow-progress.d.ts +41 -0
  301. package/dist/types/run/flows/url/markdown.d.ts +2 -2
  302. package/dist/types/run/flows/url/progress-status.d.ts +16 -0
  303. package/dist/types/run/flows/url/slides-output-render.d.ts +43 -0
  304. package/dist/types/run/flows/url/slides-output-state.d.ts +21 -0
  305. package/dist/types/run/flows/url/slides-output-stream.d.ts +18 -0
  306. package/dist/types/run/flows/url/slides-output.d.ts +2 -17
  307. package/dist/types/run/flows/url/slides-text-markdown.d.ts +46 -0
  308. package/dist/types/run/flows/url/slides-text-transcript.d.ts +36 -0
  309. package/dist/types/run/flows/url/slides-text-types.d.ts +8 -0
  310. package/dist/types/run/flows/url/slides-text.d.ts +3 -87
  311. package/dist/types/run/flows/url/summary-finish.d.ts +16 -0
  312. package/dist/types/run/flows/url/summary-json.d.ts +51 -0
  313. package/dist/types/run/flows/url/summary-prompt.d.ts +22 -0
  314. package/dist/types/run/flows/url/summary-resolution.d.ts +31 -0
  315. package/dist/types/run/flows/url/summary-timestamps.d.ts +11 -0
  316. package/dist/types/run/flows/url/types.d.ts +4 -0
  317. package/dist/types/run/markdown-transforms.d.ts +3 -0
  318. package/dist/types/run/run-context.d.ts +4 -0
  319. package/dist/types/run/run-env.d.ts +4 -0
  320. package/dist/types/run/run-settings-parse.d.ts +5 -0
  321. package/dist/types/run/runner-contexts.d.ts +62 -0
  322. package/dist/types/run/runner-execution.d.ts +57 -0
  323. package/dist/types/run/runner-flags.d.ts +41 -0
  324. package/dist/types/run/runner-setup.d.ts +21 -0
  325. package/dist/types/run/runner-slides.d.ts +8 -0
  326. package/dist/types/run/streaming.d.ts +1 -1
  327. package/dist/types/run/summary-engine.d.ts +8 -4
  328. package/dist/types/run/summary-llm.d.ts +4 -3
  329. package/dist/types/run/terminal.d.ts +2 -0
  330. package/dist/types/run/types.d.ts +2 -2
  331. package/dist/types/slides/download.d.ts +29 -0
  332. package/dist/types/slides/extract-finalize.d.ts +57 -0
  333. package/dist/types/slides/extract.d.ts +1 -7
  334. package/dist/types/slides/frame-extraction.d.ts +38 -0
  335. package/dist/types/slides/ingest.d.ts +47 -0
  336. package/dist/types/slides/ocr.d.ts +5 -0
  337. package/dist/types/slides/process.d.ts +22 -0
  338. package/dist/types/slides/scene-detection.d.ts +75 -0
  339. package/dist/types/slides/source-id.d.ts +2 -0
  340. package/dist/types/version.d.ts +1 -1
  341. package/docs/_config.yml +1 -0
  342. package/docs/agent.md +3 -2
  343. package/docs/assets/site.css +134 -2
  344. package/docs/cache.md +2 -1
  345. package/docs/chrome-extension.md +12 -4
  346. package/docs/cli.md +2 -2
  347. package/docs/config.md +11 -4
  348. package/docs/index.html +5 -0
  349. package/docs/llm.md +5 -2
  350. package/docs/manual-tests.md +3 -0
  351. package/docs/media.md +3 -1
  352. package/docs/model-auto.md +2 -2
  353. package/docs/model-provider-resolution.md +57 -0
  354. package/docs/site/index.html +5 -0
  355. package/docs/slides-rendering-flow.md +46 -0
  356. package/docs/slides.md +5 -5
  357. package/docs/smoketest.md +1 -1
  358. package/docs/transcript-provider-flow.md +66 -0
  359. package/docs/website.md +1 -0
  360. package/docs/youtube.md +4 -2
  361. package/package.json +11 -11
@@ -1,174 +1,26 @@
1
- import { isTwitterStatusUrl, isYouTubeUrl } from "@steipete/summarize-core/content/url";
2
- import { countTokens } from "gpt-tokenizer";
3
1
  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";
2
+ import { buildExtractFinishLabel, writeFinishLine } from "../../finish-line.js";
13
3
  import { writeVerbose } from "../../logging.js";
14
4
  import { prepareMarkdownForTerminal } from "../../markdown.js";
15
- import { runModelAttempts } from "../../model-attempts.js";
16
- import { buildOpenRouterNoAllowedProvidersMessage } from "../../openrouter.js";
17
5
  import { isRichTty, markdownRenderWidth, supportsColor } from "../../terminal.js";
18
- import { coerceSummaryWithSlides, interleaveSlidesIntoTranscript, normalizeSummarySlideHeadings, } from "./slides-text.js";
19
- const MAX_SLIDE_TRANSCRIPT_CHARS_BY_PRESET = {
20
- short: 2500,
21
- medium: 5000,
22
- long: 9000,
23
- xl: 15000,
24
- xxl: 24000,
25
- };
26
- const SLIDE_TRANSCRIPT_DEFAULT_EDGE_SECONDS = 30;
27
- const SLIDE_TRANSCRIPT_LEEWAY_SECONDS = 10;
28
- function parseTimestampSeconds(value) {
29
- const parts = value.split(":").map((item) => Number(item));
30
- if (parts.some((item) => !Number.isFinite(item)))
31
- return null;
32
- if (parts.length === 2) {
33
- const [minutes, seconds] = parts;
34
- return minutes * 60 + seconds;
35
- }
36
- if (parts.length === 3) {
37
- const [hours, minutes, seconds] = parts;
38
- return hours * 3600 + minutes * 60 + seconds;
39
- }
40
- return null;
41
- }
42
- function parseTranscriptTimedText(input) {
43
- if (!input)
44
- return [];
45
- const segments = [];
46
- for (const line of input.split("\n")) {
47
- const trimmed = line.trim();
48
- if (!trimmed.startsWith("["))
49
- continue;
50
- const match = trimmed.match(/^\[(\d{1,2}:\d{2}(?::\d{2})?)\]\s*(.*)$/);
51
- if (!match)
52
- continue;
53
- const seconds = parseTimestampSeconds(match[1]);
54
- if (seconds == null)
55
- continue;
56
- const text = (match[2] ?? "").trim();
57
- if (!text)
58
- continue;
59
- segments.push({ startSeconds: seconds, text });
60
- }
61
- segments.sort((a, b) => a.startSeconds - b.startSeconds);
62
- return segments;
63
- }
64
- function formatTimestamp(seconds) {
65
- const clamped = Math.max(0, Math.floor(seconds));
66
- const hours = Math.floor(clamped / 3600);
67
- const minutes = Math.floor((clamped % 3600) / 60);
68
- const secs = clamped % 60;
69
- const mm = String(minutes).padStart(2, "0");
70
- const ss = String(secs).padStart(2, "0");
71
- if (hours <= 0)
72
- return `${minutes}:${ss}`;
73
- const hh = String(hours).padStart(2, "0");
74
- return `${hh}:${mm}:${ss}`;
75
- }
76
- function truncateTranscript(value, limit) {
77
- if (value.length <= limit)
78
- return value;
79
- const truncated = value.slice(0, limit).trimEnd();
80
- const clean = truncated.replace(/\s+\S*$/, "").trim();
81
- const result = clean.length > 0 ? clean : truncated.trim();
82
- return result.length > 0 ? `${result}…` : "";
83
- }
84
- function buildSlidesPromptText({ slides, transcriptTimedText, preset, }) {
85
- if (!slides || slides.slides.length === 0)
86
- return null;
87
- const segments = parseTranscriptTimedText(transcriptTimedText);
88
- const slidesWithTimestamps = slides.slides
89
- .filter((slide) => Number.isFinite(slide.timestamp))
90
- .map((slide) => ({ index: slide.index, timestamp: Math.max(0, Math.floor(slide.timestamp)) }))
91
- .sort((a, b) => a.timestamp - b.timestamp);
92
- if (slidesWithTimestamps.length === 0)
93
- return null;
94
- const totalBudget = Number(MAX_SLIDE_TRANSCRIPT_CHARS_BY_PRESET[preset]);
95
- const perSlideBudget = Math.max(120, Math.floor(totalBudget / Math.max(1, slidesWithTimestamps.length)));
96
- let remaining = totalBudget;
97
- const blocks = [];
98
- for (let i = 0; i < slidesWithTimestamps.length; i += 1) {
99
- const slide = slidesWithTimestamps[i];
100
- if (!slide)
101
- continue;
102
- const prev = slidesWithTimestamps[i - 1];
103
- const next = slidesWithTimestamps[i + 1];
104
- const startBase = prev ? Math.floor((prev.timestamp + slide.timestamp) / 2) : slide.timestamp;
105
- const endBase = next ? Math.ceil((slide.timestamp + next.timestamp) / 2) : slide.timestamp;
106
- const start = Math.max(0, (prev ? startBase : slide.timestamp - SLIDE_TRANSCRIPT_DEFAULT_EDGE_SECONDS) -
107
- SLIDE_TRANSCRIPT_LEEWAY_SECONDS);
108
- const end = (next ? endBase : slide.timestamp + SLIDE_TRANSCRIPT_DEFAULT_EDGE_SECONDS) +
109
- SLIDE_TRANSCRIPT_LEEWAY_SECONDS;
110
- const excerptParts = [];
111
- for (const segment of segments) {
112
- if (segment.startSeconds < start)
113
- continue;
114
- if (segment.startSeconds > end)
115
- break;
116
- excerptParts.push(segment.text);
117
- }
118
- const excerptRaw = excerptParts.join(" ").trim().replace(/\s+/g, " ");
119
- const excerptBudget = remaining > 0 ? Math.min(perSlideBudget, remaining) : 0;
120
- const excerpt = excerptRaw && excerptBudget > 0 ? truncateTranscript(excerptRaw, excerptBudget) : "";
121
- const label = `[slide:${slide.index}] [${formatTimestamp(start)}–${formatTimestamp(end)}]`;
122
- const block = excerpt ? `${label}\n${excerpt}` : label;
123
- blocks.push(block);
124
- remaining = Math.max(0, remaining - block.length);
125
- }
126
- return blocks.length > 0 ? blocks.join("\n\n") : null;
127
- }
6
+ import { coerceSummaryWithSlides, interleaveSlidesIntoTranscript, } from "./slides-text.js";
7
+ import { buildFinishExtras, pickModelForFinishLine, } from "./summary-finish.js";
8
+ import { buildUrlJsonEnv, buildUrlJsonInput } from "./summary-json.js";
9
+ import { buildUrlPrompt as buildSummaryPrompt, } from "./summary-prompt.js";
10
+ import { resolveUrlSummaryExecution } from "./summary-resolution.js";
11
+ import { buildSummaryTimestampLimitInstruction } from "./summary-timestamps.js";
128
12
  export function buildUrlPrompt({ extracted, outputLanguage, lengthArg, promptOverride, lengthInstruction, languageInstruction, slides, }) {
129
- const isYouTube = extracted.siteName === "YouTube";
130
- const preset = lengthArg.kind === "preset" ? lengthArg.preset : "medium";
131
- const slidesText = buildSlidesPromptText({
132
- slides,
133
- transcriptTimedText: extracted.transcriptTimedText,
134
- preset,
135
- });
136
- return buildLinkSummaryPrompt({
137
- url: extracted.url,
138
- title: extracted.title,
139
- siteName: extracted.siteName,
140
- description: extracted.description,
141
- content: extracted.content,
142
- truncated: extracted.truncated,
143
- hasTranscript: isYouTube ||
144
- (extracted.transcriptSource !== null && extracted.transcriptSource !== "unavailable"),
145
- hasTranscriptTimestamps: Boolean(extracted.transcriptTimedText),
146
- slides: slidesText ? { count: slides?.slides.length ?? 0, text: slidesText } : null,
147
- summaryLength: lengthArg.kind === "preset" ? lengthArg.preset : { maxCharacters: lengthArg.maxCharacters },
13
+ return buildSummaryPrompt({
14
+ extracted,
148
15
  outputLanguage,
149
- shares: [],
150
- promptOverride: promptOverride ?? null,
151
- lengthInstruction: lengthInstruction ?? null,
152
- languageInstruction: languageInstruction ?? null,
16
+ lengthArg,
17
+ promptOverride,
18
+ lengthInstruction,
19
+ languageInstruction,
20
+ slides,
21
+ buildSummaryTimestampLimitInstruction,
153
22
  });
154
23
  }
155
- function shouldBypassShortContentSummary({ extracted, lengthArg, forceSummary, maxOutputTokensArg, json, }) {
156
- if (forceSummary)
157
- return false;
158
- if (!extracted.content || extracted.content.length === 0)
159
- return false;
160
- const targetCharacters = resolveTargetCharacters(lengthArg, SUMMARY_LENGTH_TARGET_CHARACTERS);
161
- if (!Number.isFinite(targetCharacters) || targetCharacters <= 0)
162
- return false;
163
- if (extracted.content.length > targetCharacters)
164
- return false;
165
- if (!json && typeof maxOutputTokensArg === "number") {
166
- const tokenCount = countTokens(extracted.content);
167
- if (tokenCount > maxOutputTokensArg)
168
- return false;
169
- }
170
- return true;
171
- }
172
24
  async function outputSummaryFromExtractedContent({ ctx, url, extracted, extractionUi, prompt, effectiveMarkdownMode, transcriptionCostLabel, slides, footerLabel, verboseMessage, }) {
173
25
  const { io, flags, model, hooks } = ctx;
174
26
  hooks.clearProgressForStdout();
@@ -177,30 +29,14 @@ async function outputSummaryFromExtractedContent({ ctx, url, extracted, extracti
177
29
  const finishReport = flags.shouldComputeReport ? await hooks.buildReport() : null;
178
30
  const payload = {
179
31
  input: {
180
- kind: "url",
181
- url,
182
- timeoutMs: flags.timeoutMs,
183
- youtube: flags.youtubeMode,
184
- firecrawl: flags.firecrawlMode,
185
- format: flags.format,
186
- markdown: effectiveMarkdownMode,
187
- timestamps: flags.transcriptTimestamps,
188
- length: flags.lengthArg.kind === "preset"
189
- ? { kind: "preset", preset: flags.lengthArg.preset }
190
- : { kind: "chars", maxCharacters: flags.lengthArg.maxCharacters },
191
- maxOutputTokens: flags.maxOutputTokensArg,
192
- model: model.requestedModelLabel,
193
- language: formatOutputLanguageForJson(flags.outputLanguage),
194
- },
195
- env: {
196
- hasXaiKey: Boolean(model.apiStatus.xaiApiKey),
197
- hasOpenAIKey: Boolean(model.apiStatus.apiKey),
198
- hasOpenRouterKey: Boolean(model.apiStatus.openrouterApiKey),
199
- hasApifyToken: Boolean(model.apiStatus.apifyToken),
200
- hasFirecrawlKey: model.apiStatus.firecrawlConfigured,
201
- hasGoogleKey: model.apiStatus.googleConfigured,
202
- hasAnthropicKey: model.apiStatus.anthropicConfigured,
32
+ ...buildUrlJsonInput({
33
+ flags,
34
+ url,
35
+ effectiveMarkdownMode,
36
+ modelLabel: model.requestedModelLabel,
37
+ }),
203
38
  },
39
+ env: buildUrlJsonEnv(model.apiStatus),
204
40
  extracted,
205
41
  slides,
206
42
  prompt,
@@ -243,37 +79,6 @@ async function outputSummaryFromExtractedContent({ ctx, url, extracted, extracti
243
79
  writeVerbose(io.stderr, flags.verbose, verboseMessage, flags.verboseColor, io.envForRun);
244
80
  }
245
81
  }
246
- const buildFinishExtras = ({ extracted, metricsDetailed, transcriptionCostLabel, }) => {
247
- const parts = [
248
- ...(buildLengthPartsForFinishLine(extracted, metricsDetailed) ?? []),
249
- ...(transcriptionCostLabel ? [transcriptionCostLabel] : []),
250
- ];
251
- return parts.length > 0 ? parts : null;
252
- };
253
- const pickModelForFinishLine = (llmCalls, fallback) => {
254
- const findLastModel = (purpose) => {
255
- for (let i = llmCalls.length - 1; i >= 0; i -= 1) {
256
- const call = llmCalls[i];
257
- if (call && call.purpose === purpose)
258
- return call.model;
259
- }
260
- return null;
261
- };
262
- return (findLastModel("summary") ??
263
- findLastModel("markdown") ??
264
- (llmCalls.length > 0 ? (llmCalls[llmCalls.length - 1]?.model ?? null) : null) ??
265
- fallback);
266
- };
267
- const buildModelMetaFromAttempt = (attempt) => {
268
- if (attempt.transport === "cli") {
269
- return { provider: "cli", canonical: attempt.userModelId };
270
- }
271
- const parsed = parseGatewayStyleModelId(attempt.llmModelId ?? attempt.userModelId);
272
- const canonical = attempt.userModelId.toLowerCase().startsWith("openrouter/")
273
- ? attempt.userModelId
274
- : parsed.canonical;
275
- return { provider: parsed.provider, canonical };
276
- };
277
82
  export async function outputExtractedUrl({ ctx, url, extracted, extractionUi, prompt, effectiveMarkdownMode, transcriptionCostLabel, slides, slidesOutput, }) {
278
83
  const { io, flags, model, hooks } = ctx;
279
84
  hooks.clearProgressForStdout();
@@ -288,30 +93,14 @@ export async function outputExtractedUrl({ ctx, url, extracted, extractionUi, pr
288
93
  const finishReport = flags.shouldComputeReport ? await hooks.buildReport() : null;
289
94
  const payload = {
290
95
  input: {
291
- kind: "url",
292
- url,
293
- timeoutMs: flags.timeoutMs,
294
- youtube: flags.youtubeMode,
295
- firecrawl: flags.firecrawlMode,
296
- format: flags.format,
297
- markdown: effectiveMarkdownMode,
298
- timestamps: flags.transcriptTimestamps,
299
- length: flags.lengthArg.kind === "preset"
300
- ? { kind: "preset", preset: flags.lengthArg.preset }
301
- : { kind: "chars", maxCharacters: flags.lengthArg.maxCharacters },
302
- maxOutputTokens: flags.maxOutputTokensArg,
303
- model: model.requestedModelLabel,
304
- language: formatOutputLanguageForJson(flags.outputLanguage),
305
- },
306
- env: {
307
- hasXaiKey: Boolean(model.apiStatus.xaiApiKey),
308
- hasOpenAIKey: Boolean(model.apiStatus.apiKey),
309
- hasOpenRouterKey: Boolean(model.apiStatus.openrouterApiKey),
310
- hasApifyToken: Boolean(model.apiStatus.apifyToken),
311
- hasFirecrawlKey: model.apiStatus.firecrawlConfigured,
312
- hasGoogleKey: model.apiStatus.googleConfigured,
313
- hasAnthropicKey: model.apiStatus.anthropicConfigured,
96
+ ...buildUrlJsonInput({
97
+ flags,
98
+ url,
99
+ effectiveMarkdownMode,
100
+ modelLabel: model.requestedModelLabel,
101
+ }),
314
102
  },
103
+ env: buildUrlJsonEnv(model.apiStatus),
315
104
  extracted,
316
105
  slides,
317
106
  prompt,
@@ -435,251 +224,16 @@ export async function outputExtractedUrl({ ctx, url, extracted, extractionUi, pr
435
224
  }
436
225
  export async function summarizeExtractedUrl({ ctx, url, extracted, extractionUi, prompt, effectiveMarkdownMode, transcriptionCostLabel, onModelChosen, slides, slidesOutput, }) {
437
226
  const { io, flags, model, cache: cacheState, hooks } = ctx;
438
- const lastSuccessfulCliProvider = model.isFallbackModel
439
- ? await readLastSuccessfulCliProvider(io.envForRun)
440
- : null;
441
- const promptPayload = { system: SUMMARY_SYSTEM_PROMPT, userText: prompt };
442
- const promptTokens = countTokens(promptPayload.userText);
443
- const kindForAuto = extracted.siteName === "YouTube" ? "youtube" : "website";
444
- const attempts = await (async () => {
445
- if (model.isFallbackModel) {
446
- const catalog = await model.getLiteLlmCatalog();
447
- const list = buildAutoModelAttempts({
448
- kind: kindForAuto,
449
- promptTokens,
450
- desiredOutputTokens: model.desiredOutputTokens,
451
- requiresVideoUnderstanding: false,
452
- env: model.envForAuto,
453
- config: model.configForModelSelection,
454
- catalog,
455
- openrouterProvidersFromEnv: null,
456
- cliAvailability: model.cliAvailability,
457
- isImplicitAutoSelection: model.isImplicitAutoSelection,
458
- allowAutoCliFallback: model.allowAutoCliFallback,
459
- lastSuccessfulCliProvider,
460
- });
461
- if (flags.verbose) {
462
- for (const attempt of list.slice(0, 8)) {
463
- writeVerbose(io.stderr, flags.verbose, `auto candidate ${attempt.debug}`, flags.verboseColor, io.envForRun);
464
- }
465
- }
466
- return list.map((attempt) => {
467
- if (attempt.transport !== "cli")
468
- return model.summaryEngine.applyZaiOverrides(attempt);
469
- const parsed = parseCliUserModelId(attempt.userModelId);
470
- return { ...attempt, cliProvider: parsed.provider, cliModel: parsed.model };
471
- });
472
- }
473
- /* v8 ignore next */
474
- if (!model.fixedModelSpec) {
475
- throw new Error("Internal error: missing fixed model spec");
476
- }
477
- if (model.fixedModelSpec.transport === "cli") {
478
- return [
479
- {
480
- transport: "cli",
481
- userModelId: model.fixedModelSpec.userModelId,
482
- llmModelId: null,
483
- cliProvider: model.fixedModelSpec.cliProvider,
484
- cliModel: model.fixedModelSpec.cliModel,
485
- openrouterProviders: null,
486
- forceOpenRouter: false,
487
- requiredEnv: model.fixedModelSpec.requiredEnv,
488
- },
489
- ];
490
- }
491
- const openaiOverrides = model.fixedModelSpec.requiredEnv === "Z_AI_API_KEY"
492
- ? {
493
- openaiApiKeyOverride: model.apiStatus.zaiApiKey,
494
- openaiBaseUrlOverride: model.apiStatus.zaiBaseUrl,
495
- forceChatCompletions: true,
496
- }
497
- : {};
498
- return [
499
- {
500
- transport: model.fixedModelSpec.transport === "openrouter" ? "openrouter" : "native",
501
- userModelId: model.fixedModelSpec.userModelId,
502
- llmModelId: model.fixedModelSpec.llmModelId,
503
- openrouterProviders: model.fixedModelSpec.openrouterProviders,
504
- forceOpenRouter: model.fixedModelSpec.forceOpenRouter,
505
- requiredEnv: model.fixedModelSpec.requiredEnv,
506
- ...openaiOverrides,
507
- },
508
- ];
509
- })();
510
- const cacheStore = cacheState.mode === "default" && !flags.summaryCacheBypass ? cacheState.store : null;
511
- const contentHash = cacheStore ? hashString(normalizeContentForHash(extracted.content)) : null;
512
- const promptHash = cacheStore ? buildPromptHash(prompt) : null;
513
- const lengthKey = buildLengthKey(flags.lengthArg);
514
- const languageKey = buildLanguageKey(flags.outputLanguage);
515
- const autoSelectionCacheModel = model.isFallbackModel
516
- ? `selection:${model.requestedModelInput.toLowerCase()}`
517
- : null;
518
- let summaryResult = null;
519
- let usedAttempt = null;
520
- let summaryFromCache = false;
521
- let cacheChecked = false;
522
- const isTweet = extracted.siteName?.toLowerCase() === "x" || isTwitterStatusUrl(extracted.url);
523
- const isYouTube = extracted.siteName === "YouTube" || isYouTubeUrl(url);
524
- const hasMedia = Boolean(extracted.video) ||
525
- (extracted.transcriptSource != null && extracted.transcriptSource !== "unavailable") ||
526
- (typeof extracted.mediaDurationSeconds === "number" && extracted.mediaDurationSeconds > 0) ||
527
- extracted.isVideoOnly === true;
528
- const autoBypass = ctx.model.isFallbackModel && !ctx.model.isNamedModelSelection;
529
- const canBypassShortContent = (autoBypass || isTweet) &&
530
- !flags.slides &&
531
- !hasMedia &&
532
- flags.streamMode !== "on" &&
533
- !isYouTube &&
534
- shouldBypassShortContentSummary({
535
- extracted,
536
- lengthArg: flags.lengthArg,
537
- forceSummary: flags.forceSummary,
538
- maxOutputTokensArg: flags.maxOutputTokensArg,
539
- json: flags.json,
540
- });
541
- if (canBypassShortContent) {
542
- await outputSummaryFromExtractedContent({
543
- ctx,
544
- url,
545
- extracted,
546
- extractionUi,
547
- prompt,
548
- effectiveMarkdownMode,
549
- transcriptionCostLabel,
550
- slides,
551
- footerLabel: "short content",
552
- verboseMessage: "short content: skipping summary",
553
- });
554
- return;
555
- }
556
- if (cacheStore && contentHash && promptHash) {
557
- cacheChecked = true;
558
- if (autoSelectionCacheModel) {
559
- const key = buildSummaryCacheKey({
560
- contentHash,
561
- promptHash,
562
- model: autoSelectionCacheModel,
563
- lengthKey,
564
- languageKey,
565
- });
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
- }
619
- }
620
- }
621
- if (cacheChecked && !summaryFromCache) {
622
- writeVerbose(io.stderr, flags.verbose, "cache miss summary", flags.verboseColor, io.envForRun);
623
- }
624
- ctx.hooks.onSummaryCached?.(summaryFromCache);
625
- let lastError = null;
626
- let missingRequiredEnvs = new Set();
627
- let sawOpenRouterNoAllowedProviders = false;
628
- if (!summaryResult || !usedAttempt) {
629
- const attemptOutcome = await runModelAttempts({
630
- attempts,
631
- isFallbackModel: model.isFallbackModel,
632
- isNamedModelSelection: model.isNamedModelSelection,
633
- envHasKeyFor: model.summaryEngine.envHasKeyFor,
634
- formatMissingModelError: model.summaryEngine.formatMissingModelError,
635
- onAutoSkip: (attempt) => {
636
- writeVerbose(io.stderr, flags.verbose, `auto skip ${attempt.userModelId}: missing ${attempt.requiredEnv}`, flags.verboseColor, io.envForRun);
637
- },
638
- onAutoFailure: (attempt, error) => {
639
- writeVerbose(io.stderr, flags.verbose, `auto failed ${attempt.userModelId}: ${error instanceof Error ? error.message : String(error)}`, flags.verboseColor, io.envForRun);
640
- },
641
- onFixedModelError: (_attempt, error) => {
642
- throw error;
643
- },
644
- runAttempt: (attempt) => model.summaryEngine.runSummaryAttempt({
645
- attempt,
646
- prompt: promptPayload,
647
- allowStreaming: flags.streamingEnabled,
648
- onModelChosen: onModelChosen ?? null,
649
- streamHandler: slidesOutput?.streamHandler ?? null,
650
- }),
651
- });
652
- summaryResult = attemptOutcome.result;
653
- usedAttempt = attemptOutcome.usedAttempt;
654
- lastError = attemptOutcome.lastError;
655
- missingRequiredEnvs = attemptOutcome.missingRequiredEnvs;
656
- sawOpenRouterNoAllowedProviders = attemptOutcome.sawOpenRouterNoAllowedProviders;
657
- }
658
- if (!summaryResult || !usedAttempt) {
659
- // Auto mode: surface raw extracted content when no model can run.
660
- const withFreeTip = (message) => {
661
- if (!model.isNamedModelSelection || !model.wantsFreeNamedModel)
662
- return message;
663
- return (`${message}\n` +
664
- `Tip: run "summarize refresh-free" to refresh the free model candidates (writes ~/.summarize/config.json).`);
665
- };
666
- if (model.isNamedModelSelection) {
667
- if (lastError === null && missingRequiredEnvs.size > 0) {
668
- throw new Error(withFreeTip(`Missing ${Array.from(missingRequiredEnvs).sort().join(", ")} for --model ${model.requestedModelInput}.`));
669
- }
670
- if (lastError instanceof Error) {
671
- if (sawOpenRouterNoAllowedProviders) {
672
- const message = await buildOpenRouterNoAllowedProvidersMessage({
673
- attempts,
674
- fetchImpl: io.fetch,
675
- timeoutMs: flags.timeoutMs,
676
- });
677
- throw new Error(withFreeTip(message), { cause: lastError });
678
- }
679
- throw new Error(withFreeTip(lastError.message), { cause: lastError });
680
- }
681
- throw new Error(withFreeTip(`No model available for --model ${model.requestedModelInput}`));
682
- }
227
+ const resolution = await resolveUrlSummaryExecution({
228
+ ctx,
229
+ url,
230
+ extracted,
231
+ prompt,
232
+ onModelChosen,
233
+ slides,
234
+ slidesOutput,
235
+ });
236
+ if (resolution.kind === "use-extracted") {
683
237
  await outputSummaryFromExtractedContent({
684
238
  ctx,
685
239
  url,
@@ -689,72 +243,24 @@ export async function summarizeExtractedUrl({ ctx, url, extracted, extractionUi,
689
243
  effectiveMarkdownMode,
690
244
  transcriptionCostLabel,
691
245
  slides,
692
- footerLabel: "no model",
693
- verboseMessage: lastError instanceof Error ? `auto failed all models: ${lastError.message}` : null,
246
+ footerLabel: resolution.footerLabel,
247
+ verboseMessage: resolution.verboseMessage,
694
248
  });
695
249
  return;
696
250
  }
697
- if (!summaryFromCache && cacheStore && contentHash && promptHash) {
698
- const perModelKey = buildSummaryCacheKey({
699
- contentHash,
700
- promptHash,
701
- model: usedAttempt.userModelId,
702
- lengthKey,
703
- languageKey,
704
- });
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
- });
727
- }
728
- const { summary, summaryAlreadyPrinted, modelMeta, maxOutputTokensForCall } = summaryResult;
729
- const normalizedSummary = slides && slides.slides.length > 0 ? normalizeSummarySlideHeadings(summary) : summary;
251
+ const { normalizedSummary, summaryAlreadyPrinted, summaryFromCache, usedAttempt, modelMeta, maxOutputTokensForCall, } = resolution;
730
252
  if (flags.json) {
731
253
  const finishReport = flags.shouldComputeReport ? await hooks.buildReport() : null;
732
254
  const payload = {
733
255
  input: {
734
- kind: "url",
735
- url,
736
- timeoutMs: flags.timeoutMs,
737
- youtube: flags.youtubeMode,
738
- firecrawl: flags.firecrawlMode,
739
- format: flags.format,
740
- markdown: effectiveMarkdownMode,
741
- timestamps: flags.transcriptTimestamps,
742
- length: flags.lengthArg.kind === "preset"
743
- ? { kind: "preset", preset: flags.lengthArg.preset }
744
- : { kind: "chars", maxCharacters: flags.lengthArg.maxCharacters },
745
- maxOutputTokens: flags.maxOutputTokensArg,
746
- model: model.requestedModelLabel,
747
- language: formatOutputLanguageForJson(flags.outputLanguage),
748
- },
749
- env: {
750
- hasXaiKey: Boolean(model.apiStatus.xaiApiKey),
751
- hasOpenAIKey: Boolean(model.apiStatus.apiKey),
752
- hasOpenRouterKey: Boolean(model.apiStatus.openrouterApiKey),
753
- hasApifyToken: Boolean(model.apiStatus.apifyToken),
754
- hasFirecrawlKey: model.apiStatus.firecrawlConfigured,
755
- hasGoogleKey: model.apiStatus.googleConfigured,
756
- hasAnthropicKey: model.apiStatus.anthropicConfigured,
256
+ ...buildUrlJsonInput({
257
+ flags,
258
+ url,
259
+ effectiveMarkdownMode,
260
+ modelLabel: model.requestedModelLabel,
261
+ }),
757
262
  },
263
+ env: buildUrlJsonEnv(model.apiStatus),
758
264
  extracted,
759
265
  slides,
760
266
  prompt,