@steipete/summarize 0.4.0 → 0.5.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 (82) hide show
  1. package/CHANGELOG.md +73 -5
  2. package/README.md +116 -18
  3. package/dist/cli.cjs +8136 -4368
  4. package/dist/cli.cjs.map +4 -4
  5. package/dist/esm/cli-main.js +47 -2
  6. package/dist/esm/cli-main.js.map +1 -1
  7. package/dist/esm/config.js +368 -3
  8. package/dist/esm/config.js.map +1 -1
  9. package/dist/esm/content/link-preview/content/index.js +13 -0
  10. package/dist/esm/content/link-preview/content/index.js.map +1 -1
  11. package/dist/esm/content/link-preview/content/utils.js +3 -1
  12. package/dist/esm/content/link-preview/content/utils.js.map +1 -1
  13. package/dist/esm/content/link-preview/content/video.js +96 -0
  14. package/dist/esm/content/link-preview/content/video.js.map +1 -0
  15. package/dist/esm/content/link-preview/transcript/providers/youtube/captions.js +21 -21
  16. package/dist/esm/content/link-preview/transcript/providers/youtube/captions.js.map +1 -1
  17. package/dist/esm/costs.js.map +1 -1
  18. package/dist/esm/flags.js +23 -0
  19. package/dist/esm/flags.js.map +1 -1
  20. package/dist/esm/generate-free.js +616 -0
  21. package/dist/esm/generate-free.js.map +1 -0
  22. package/dist/esm/llm/cli.js +290 -0
  23. package/dist/esm/llm/cli.js.map +1 -0
  24. package/dist/esm/llm/generate-text.js +159 -105
  25. package/dist/esm/llm/generate-text.js.map +1 -1
  26. package/dist/esm/llm/html-to-markdown.js +4 -2
  27. package/dist/esm/llm/html-to-markdown.js.map +1 -1
  28. package/dist/esm/model-auto.js +353 -0
  29. package/dist/esm/model-auto.js.map +1 -0
  30. package/dist/esm/model-spec.js +82 -0
  31. package/dist/esm/model-spec.js.map +1 -0
  32. package/dist/esm/prompts/cli.js +18 -0
  33. package/dist/esm/prompts/cli.js.map +1 -0
  34. package/dist/esm/prompts/file.js +4 -4
  35. package/dist/esm/prompts/file.js.map +1 -1
  36. package/dist/esm/prompts/index.js +1 -0
  37. package/dist/esm/prompts/index.js.map +1 -1
  38. package/dist/esm/prompts/link-summary.js +3 -8
  39. package/dist/esm/prompts/link-summary.js.map +1 -1
  40. package/dist/esm/refresh-free.js +667 -0
  41. package/dist/esm/refresh-free.js.map +1 -0
  42. package/dist/esm/run.js +1384 -532
  43. package/dist/esm/run.js.map +1 -1
  44. package/dist/esm/version.js +1 -1
  45. package/dist/types/config.d.ts +58 -5
  46. package/dist/types/content/link-preview/content/types.d.ts +10 -0
  47. package/dist/types/content/link-preview/content/utils.d.ts +1 -1
  48. package/dist/types/content/link-preview/content/video.d.ts +5 -0
  49. package/dist/types/costs.d.ts +2 -1
  50. package/dist/types/flags.d.ts +3 -0
  51. package/dist/types/generate-free.d.ts +17 -0
  52. package/dist/types/llm/cli.d.ts +24 -0
  53. package/dist/types/llm/generate-text.d.ts +13 -4
  54. package/dist/types/llm/html-to-markdown.d.ts +9 -3
  55. package/dist/types/model-auto.d.ts +23 -0
  56. package/dist/types/model-spec.d.ts +33 -0
  57. package/dist/types/prompts/cli.d.ts +8 -0
  58. package/dist/types/prompts/index.d.ts +1 -0
  59. package/dist/types/refresh-free.d.ts +19 -0
  60. package/dist/types/version.d.ts +1 -1
  61. package/docs/README.md +3 -0
  62. package/docs/cli.md +95 -0
  63. package/docs/config.md +123 -1
  64. package/docs/llm.md +24 -4
  65. package/docs/manual-tests.md +40 -0
  66. package/docs/model-auto.md +92 -0
  67. package/docs/site/assets/site.js +20 -17
  68. package/docs/smoketest.md +58 -0
  69. package/docs/website.md +3 -1
  70. package/package.json +8 -4
  71. package/dist/esm/content/link-preview/transcript/providers/twitter.js +0 -12
  72. package/dist/esm/content/link-preview/transcript/providers/twitter.js.map +0 -1
  73. package/dist/esm/content/link-preview/transcript/providers/youtube/ytdlp.js +0 -114
  74. package/dist/esm/content/link-preview/transcript/providers/youtube/ytdlp.js.map +0 -1
  75. package/dist/esm/summarizeHome.js +0 -20
  76. package/dist/esm/summarizeHome.js.map +0 -1
  77. package/dist/esm/tty/live-markdown.js +0 -52
  78. package/dist/esm/tty/live-markdown.js.map +0 -1
  79. package/dist/types/content/link-preview/transcript/providers/twitter.d.ts +0 -3
  80. package/dist/types/content/link-preview/transcript/providers/youtube/ytdlp.d.ts +0 -3
  81. package/dist/types/summarizeHome.d.ts +0 -6
  82. package/dist/types/tty/live-markdown.d.ts +0 -10
@@ -14,7 +14,13 @@ export type LlmTokenUsage = {
14
14
  completionTokens: number | null;
15
15
  totalTokens: number | null;
16
16
  };
17
- export declare function generateTextWithModelId({ modelId, apiKeys, system, prompt, temperature, maxOutputTokens, timeoutMs, fetchImpl, openrouter, }: {
17
+ type RetryNotice = {
18
+ attempt: number;
19
+ maxRetries: number;
20
+ delayMs: number;
21
+ error: unknown;
22
+ };
23
+ export declare function generateTextWithModelId({ modelId, apiKeys, system, prompt, temperature, maxOutputTokens, timeoutMs, fetchImpl, forceOpenRouter, retries, onRetry, }: {
18
24
  modelId: string;
19
25
  apiKeys: LlmApiKeys;
20
26
  system?: string;
@@ -23,14 +29,16 @@ export declare function generateTextWithModelId({ modelId, apiKeys, system, prom
23
29
  maxOutputTokens?: number;
24
30
  timeoutMs: number;
25
31
  fetchImpl: typeof fetch;
26
- openrouter?: OpenRouterOptions;
32
+ forceOpenRouter?: boolean;
33
+ retries?: number;
34
+ onRetry?: (notice: RetryNotice) => void;
27
35
  }): Promise<{
28
36
  text: string;
29
37
  canonicalModelId: string;
30
38
  provider: 'xai' | 'openai' | 'google' | 'anthropic';
31
39
  usage: LlmTokenUsage | null;
32
40
  }>;
33
- export declare function streamTextWithModelId({ modelId, apiKeys, system, prompt, temperature, maxOutputTokens, timeoutMs, fetchImpl, openrouter, }: {
41
+ export declare function streamTextWithModelId({ modelId, apiKeys, system, prompt, temperature, maxOutputTokens, timeoutMs, fetchImpl, forceOpenRouter, }: {
34
42
  modelId: string;
35
43
  apiKeys: LlmApiKeys;
36
44
  system?: string;
@@ -39,7 +47,7 @@ export declare function streamTextWithModelId({ modelId, apiKeys, system, prompt
39
47
  maxOutputTokens?: number;
40
48
  timeoutMs: number;
41
49
  fetchImpl: typeof fetch;
42
- openrouter?: OpenRouterOptions;
50
+ forceOpenRouter?: boolean;
43
51
  }): Promise<{
44
52
  textStream: AsyncIterable<string>;
45
53
  canonicalModelId: string;
@@ -47,3 +55,4 @@ export declare function streamTextWithModelId({ modelId, apiKeys, system, prompt
47
55
  usage: Promise<LlmTokenUsage | null>;
48
56
  lastError: () => unknown;
49
57
  }>;
58
+ export {};
@@ -1,15 +1,21 @@
1
1
  import type { ConvertHtmlToMarkdown } from '../content/link-preview/deps.js';
2
2
  import type { LlmTokenUsage } from './generate-text.js';
3
- import { type OpenRouterOptions } from './generate-text.js';
4
- export declare function createHtmlToMarkdownConverter({ modelId, xaiApiKey, googleApiKey, openaiApiKey, anthropicApiKey, openrouterApiKey, openrouter, fetchImpl, onUsage, }: {
3
+ export declare function createHtmlToMarkdownConverter({ modelId, forceOpenRouter, xaiApiKey, googleApiKey, openaiApiKey, anthropicApiKey, openrouterApiKey, fetchImpl, retries, onRetry, onUsage, }: {
5
4
  modelId: string;
5
+ forceOpenRouter?: boolean;
6
6
  xaiApiKey: string | null;
7
7
  googleApiKey: string | null;
8
8
  openaiApiKey: string | null;
9
9
  fetchImpl: typeof fetch;
10
10
  anthropicApiKey: string | null;
11
11
  openrouterApiKey: string | null;
12
- openrouter?: OpenRouterOptions;
12
+ retries?: number;
13
+ onRetry?: (notice: {
14
+ attempt: number;
15
+ maxRetries: number;
16
+ delayMs: number;
17
+ error: unknown;
18
+ }) => void;
13
19
  onUsage?: (usage: {
14
20
  model: string;
15
21
  provider: 'xai' | 'openai' | 'google' | 'anthropic';
@@ -0,0 +1,23 @@
1
+ import type { AutoRuleKind, CliProvider, SummarizeConfig } from './config.js';
2
+ import type { LiteLlmCatalog } from './pricing/litellm.js';
3
+ export type AutoSelectionInput = {
4
+ kind: AutoRuleKind;
5
+ promptTokens: number | null;
6
+ desiredOutputTokens: number | null;
7
+ requiresVideoUnderstanding: boolean;
8
+ env: Record<string, string | undefined>;
9
+ config: SummarizeConfig | null;
10
+ catalog: LiteLlmCatalog | null;
11
+ openrouterProvidersFromEnv: string[] | null;
12
+ cliAvailability?: Partial<Record<CliProvider, boolean>>;
13
+ };
14
+ export type AutoModelAttempt = {
15
+ transport: 'native' | 'openrouter' | 'cli';
16
+ userModelId: string;
17
+ llmModelId: string | null;
18
+ openrouterProviders: string[] | null;
19
+ forceOpenRouter: boolean;
20
+ requiredEnv: 'XAI_API_KEY' | 'OPENAI_API_KEY' | 'GEMINI_API_KEY' | 'ANTHROPIC_API_KEY' | 'OPENROUTER_API_KEY' | 'CLI_CLAUDE' | 'CLI_CODEX' | 'CLI_GEMINI';
21
+ debug: string;
22
+ };
23
+ export declare function buildAutoModelAttempts(input: AutoSelectionInput): AutoModelAttempt[];
@@ -0,0 +1,33 @@
1
+ import type { CliProvider } from './config.js';
2
+ export type FixedModelSpec = {
3
+ transport: 'native';
4
+ userModelId: string;
5
+ llmModelId: string;
6
+ provider: 'xai' | 'openai' | 'google' | 'anthropic';
7
+ openrouterProviders: string[] | null;
8
+ forceOpenRouter: false;
9
+ requiredEnv: 'XAI_API_KEY' | 'OPENAI_API_KEY' | 'GEMINI_API_KEY' | 'ANTHROPIC_API_KEY';
10
+ } | {
11
+ transport: 'openrouter';
12
+ userModelId: string;
13
+ openrouterModelId: string;
14
+ llmModelId: string;
15
+ openrouterProviders: string[] | null;
16
+ forceOpenRouter: true;
17
+ requiredEnv: 'OPENROUTER_API_KEY';
18
+ } | {
19
+ transport: 'cli';
20
+ userModelId: string;
21
+ llmModelId: null;
22
+ openrouterProviders: null;
23
+ forceOpenRouter: false;
24
+ requiredEnv: 'CLI_CLAUDE' | 'CLI_CODEX' | 'CLI_GEMINI';
25
+ cliProvider: CliProvider;
26
+ cliModel: string | null;
27
+ };
28
+ export type RequestedModel = {
29
+ kind: 'auto';
30
+ } | ({
31
+ kind: 'fixed';
32
+ } & FixedModelSpec);
33
+ export declare function parseRequestedModelId(raw: string): RequestedModel;
@@ -0,0 +1,8 @@
1
+ import type { SummaryLengthTarget } from './link-summary.js';
2
+ export declare function buildPathSummaryPrompt({ kindLabel, filePath, filename, mediaType, summaryLength, }: {
3
+ kindLabel: 'file' | 'image';
4
+ filePath: string;
5
+ filename: string | null;
6
+ mediaType: string | null;
7
+ summaryLength: SummaryLengthTarget;
8
+ }): string;
@@ -1,3 +1,4 @@
1
1
  export type { SummaryLength } from '../shared/contracts.js';
2
+ export { buildPathSummaryPrompt } from './cli.js';
2
3
  export { buildFileSummaryPrompt, buildFileTextSummaryPrompt } from './file.js';
3
4
  export { buildLinkSummaryPrompt, estimateMaxCompletionTokensForCharacters, pickSummaryLengthForCharacters, type ShareContextEntry, SUMMARY_LENGTH_TO_TOKENS, type SummaryLengthTarget, } from './link-summary.js';
@@ -0,0 +1,19 @@
1
+ type GenerateFreeOptions = {
2
+ runs: number;
3
+ smart: number;
4
+ maxCandidates: number;
5
+ concurrency: number;
6
+ timeoutMs: number;
7
+ minParamB: number;
8
+ maxAgeDays: number;
9
+ setDefault: boolean;
10
+ };
11
+ export declare function refreshFree({ env, fetchImpl, stdout, stderr, verbose, options, }: {
12
+ env: Record<string, string | undefined>;
13
+ fetchImpl: typeof fetch;
14
+ stdout: NodeJS.WritableStream;
15
+ stderr: NodeJS.WritableStream;
16
+ verbose?: boolean;
17
+ options?: Partial<GenerateFreeOptions>;
18
+ }): Promise<void>;
19
+ export {};
@@ -1,2 +1,2 @@
1
- export declare const FALLBACK_VERSION = "0.4.0";
1
+ export declare const FALLBACK_VERSION = "0.5.0";
2
2
  export declare function resolvePackageVersion(importMetaUrl?: string): string;
package/docs/README.md CHANGED
@@ -4,6 +4,9 @@
4
4
  - `docs/youtube.md` — YouTube transcript extraction (youtubei / captionTracks / Apify)
5
5
  - `docs/firecrawl.md` — Firecrawl mode + API key
6
6
  - `docs/llm.md` — LLM summarization + model config (Gateway/OpenAI)
7
+ - `docs/cli.md` — CLI models (Claude/Codex/Gemini)
8
+ - `docs/model-auto.md` — automatic model selection (`--model auto`)
9
+ - `docs/manual-tests.md` — manual end-to-end test checklist
7
10
  - `docs/extract-only.md` — extract mode (no summary LLM call)
8
11
 
9
12
  ## Website
package/docs/cli.md ADDED
@@ -0,0 +1,95 @@
1
+ # CLI models
2
+
3
+ Summarize can use installed CLIs (Claude, Codex, Gemini) as local model backends.
4
+
5
+ ## Model ids
6
+
7
+ - `cli/claude/<model>` (e.g. `cli/claude/sonnet`)
8
+ - `cli/codex/<model>` (e.g. `cli/codex/gpt-5.2`)
9
+ - `cli/gemini/<model>` (e.g. `cli/gemini/gemini-3-flash-preview`)
10
+
11
+ Use `--cli [provider]` (case-insensitive) for the provider default, or `--model cli/<provider>/<model>` to pin a model.
12
+ If `--cli` is provided without a provider, auto selection is used with CLI enabled.
13
+
14
+ ## Auto mode
15
+
16
+ Auto mode does **not** use CLIs unless you set `cli.enabled` in config.
17
+
18
+ Why: CLI adds ~4s latency per attempt and higher variance.
19
+ Recommendation: enable only Gemini unless you have a reason to add others.
20
+
21
+ Gemini CLI performance: summarize sets `GEMINI_CLI_NO_RELAUNCH=true` for Gemini CLI runs to avoid a costly self-relaunch (can be overridden by setting it yourself).
22
+
23
+ When enabled, auto prepends CLI attempts in the order listed in `cli.enabled`
24
+ (recommended: `["gemini"]`).
25
+
26
+ Enable CLI attempts:
27
+
28
+ ```json
29
+ {
30
+ "cli": { "enabled": ["gemini"] }
31
+ }
32
+ ```
33
+
34
+ Disable CLI attempts:
35
+
36
+ ```json
37
+ {
38
+ "cli": { "enabled": [] }
39
+ }
40
+ ```
41
+
42
+ Note: when `cli.enabled` is set, it also acts as an allowlist for explicit `--cli` / `--model cli/...`.
43
+
44
+ ## CLI discovery
45
+
46
+ Binary lookup:
47
+
48
+ - `CLAUDE_PATH`, `CODEX_PATH`, `GEMINI_PATH` (optional overrides)
49
+ - Otherwise uses `PATH`
50
+
51
+ ## Attachments (images/files)
52
+
53
+ When a CLI attempt is used for an image or non-text file, Summarize switches to a
54
+ path-based prompt and enables the required tool flags:
55
+
56
+ - Claude: `--tools Read --dangerously-skip-permissions`
57
+ - Gemini: `--yolo` and `--include-directories <dir>`
58
+ - Codex: `codex exec --output-last-message ...` and `-i <image>` for images
59
+
60
+ ## Config
61
+
62
+ ```json
63
+ {
64
+ "cli": {
65
+ "enabled": ["claude", "gemini", "codex"],
66
+ "codex": { "model": "gpt-5.2" },
67
+ "gemini": { "model": "gemini-3-flash-preview", "extraArgs": ["--verbose"] },
68
+ "claude": {
69
+ "model": "sonnet",
70
+ "binary": "/usr/local/bin/claude",
71
+ "extraArgs": ["--verbose"]
72
+ }
73
+ }
74
+ }
75
+ ```
76
+
77
+ Notes:
78
+
79
+ - CLI output is treated as text only (no token accounting).
80
+ - If a CLI call fails, auto mode falls back to the next candidate.
81
+
82
+ ## Generate free preset (OpenRouter)
83
+
84
+ `summarize` ships with a built-in preset `free`, backed by OpenRouter `:free` models.
85
+ To regenerate the candidate list (and persist it in your config):
86
+
87
+ ```bash
88
+ summarize refresh-free
89
+ ```
90
+
91
+ Options:
92
+
93
+ - `--runs 2` (default): extra timing runs per selected model (total runs = 1 + runs)
94
+ - `--smart 3` (default): number of “smart-first” picks (rest filled by fastest)
95
+ - `--set-default`: also sets `"model": "free"` in `~/.summarize/config.json`
package/docs/config.md CHANGED
@@ -15,14 +15,136 @@ For `model`:
15
15
  1. CLI flag `--model`
16
16
  2. Env `SUMMARIZE_MODEL`
17
17
  3. Config file `model`
18
- 4. Built-in default (`google/gemini-3-flash-preview`)
18
+ 4. Built-in default (`auto`)
19
19
 
20
20
  ## Format
21
21
 
22
22
  `~/.summarize/config.json`:
23
23
 
24
+ ```json
25
+ {
26
+ "model": { "id": "google/gemini-3-flash-preview" }
27
+ }
28
+ ```
29
+
30
+ Shorthand (equivalent):
31
+
24
32
  ```json
25
33
  {
26
34
  "model": "google/gemini-3-flash-preview"
27
35
  }
28
36
  ```
37
+
38
+ `model` can also be auto:
39
+
40
+ ```json
41
+ {
42
+ "model": { "mode": "auto" }
43
+ }
44
+ ```
45
+
46
+ Shorthand (equivalent):
47
+
48
+ ```json
49
+ {
50
+ "model": "auto"
51
+ }
52
+ ```
53
+
54
+ ## Presets
55
+
56
+ Define presets you can select via `--model <preset>`:
57
+
58
+ ```json
59
+ {
60
+ "models": {
61
+ "fast": { "id": "openai/gpt-5-mini" },
62
+ "or-free": {
63
+ "rules": [
64
+ {
65
+ "candidates": [
66
+ "openrouter/google/gemini-2.0-flash-exp:free",
67
+ "openrouter/meta-llama/llama-3.3-70b-instruct:free"
68
+ ]
69
+ }
70
+ ]
71
+ }
72
+ }
73
+ }
74
+ ```
75
+
76
+ Notes:
77
+
78
+ - `auto` is reserved and can’t be defined as a preset.
79
+ - `free` is built-in (OpenRouter `:free` candidates). Override it by defining `models.free` in your config, or regenerate it via `summarize refresh-free`.
80
+
81
+ Use a preset as your default `model`:
82
+
83
+ ```json
84
+ {
85
+ "model": "fast"
86
+ }
87
+ ```
88
+
89
+ Notes:
90
+
91
+ - For presets, `"mode": "auto"` is optional when `"rules"` is present.
92
+
93
+ For auto selection with rules:
94
+
95
+ ```json
96
+ {
97
+ "model": {
98
+ "mode": "auto",
99
+ "rules": [
100
+ {
101
+ "when": ["video"],
102
+ "candidates": ["google/gemini-3-flash-preview"]
103
+ },
104
+ {
105
+ "when": ["website", "youtube"],
106
+ "bands": [
107
+ {
108
+ "token": { "max": 8000 },
109
+ "candidates": ["openai/gpt-5-mini"]
110
+ },
111
+ {
112
+ "candidates": ["xai/grok-4-fast-non-reasoning"]
113
+ }
114
+ ]
115
+ },
116
+ {
117
+ "candidates": ["openai/gpt-5-mini", "openrouter/openai/gpt-5-mini"]
118
+ }
119
+ ]
120
+ },
121
+ "media": { "videoMode": "auto" }
122
+ }
123
+ ```
124
+
125
+ Notes:
126
+
127
+ - Parsed leniently (JSON5), but **comments are not allowed**.
128
+ - Unknown keys are ignored.
129
+ - `model.rules` is optional. If omitted, built-in defaults apply.
130
+ - `model.rules[].when` (optional) must be an array (e.g. `["video","youtube"]`).
131
+ - `model.rules[]` must use either `candidates` or `bands`.
132
+
133
+ ## CLI config
134
+
135
+ ```json
136
+ {
137
+ "cli": {
138
+ "enabled": ["gemini"],
139
+ "codex": { "model": "gpt-5.2" },
140
+ "claude": { "binary": "/usr/local/bin/claude", "extraArgs": ["--verbose"] }
141
+ }
142
+ }
143
+ ```
144
+
145
+ Notes:
146
+
147
+ - `cli.enabled` is an allowlist (auto uses CLIs only when set; explicit `--cli` / `--model cli/...` must be included).
148
+ - Recommendation: keep `cli.enabled` to `["gemini"]` unless you have a reason to add others (extra latency/variance).
149
+ - `cli.<provider>.binary` overrides CLI binary discovery.
150
+ - `cli.<provider>.extraArgs` appends extra CLI args.
package/docs/llm.md CHANGED
@@ -1,37 +1,57 @@
1
1
  # LLM / summarization mode
2
2
 
3
- By default `summarize` will call an LLM using **direct provider API keys**.
3
+ By default `summarize` will call an LLM using **direct provider API keys**. When CLI tools are
4
+ installed, auto mode can use local CLI models when `cli.enabled` is set (see `docs/cli.md`).
4
5
 
5
6
  ## Defaults
6
7
 
7
- - Default model: `google/gemini-3-flash-preview`
8
+ - Default model: `auto`
8
9
  - Override with `SUMMARIZE_MODEL`, config file (`model`), or `--model`.
9
10
 
10
11
  ## Env
11
12
 
13
+ - `.env` (optional): when running the CLI, `summarize` also reads `.env` in the current working directory and merges it into the environment (real env vars win).
12
14
  - `XAI_API_KEY` (required for `xai/...` models)
13
15
  - `OPENAI_API_KEY` (required for `openai/...` models)
14
16
  - `OPENAI_BASE_URL` (optional; OpenAI-compatible API endpoint, e.g. OpenRouter)
15
- - `OPENROUTER_API_KEY` (optional; used when `OPENAI_BASE_URL` points to OpenRouter)
17
+ - `OPENROUTER_API_KEY` (optional; required for `openrouter/...` models; also used when `OPENAI_BASE_URL` points to OpenRouter)
16
18
  - `GEMINI_API_KEY` (required for `google/...` models; also accepts `GOOGLE_GENERATIVE_AI_API_KEY` / `GOOGLE_API_KEY`)
17
19
  - `ANTHROPIC_API_KEY` (required for `anthropic/...` models)
18
20
  - `SUMMARIZE_MODEL` (optional; overrides default model selection)
21
+ - `CLAUDE_PATH` / `CODEX_PATH` / `GEMINI_PATH` (optional; override CLI binary paths)
19
22
 
20
23
  ## Flags
21
24
 
22
25
  - `--model <model>`
23
26
  - Examples:
27
+ - `cli/codex/gpt-5.2`
28
+ - `cli/claude/sonnet`
29
+ - `cli/gemini/gemini-3-flash-preview`
24
30
  - `google/gemini-3-flash-preview`
25
- - `openai/gpt-5.2`
31
+ - `openai/gpt-5-mini`
26
32
  - `xai/grok-4-fast-non-reasoning`
27
33
  - `google/gemini-2.0-flash`
28
34
  - `anthropic/claude-sonnet-4-5`
35
+ - `openrouter/meta-llama/llama-3.3-70b-instruct:free` (force OpenRouter)
36
+ - `--cli [provider]`
37
+ - Examples: `--cli claude`, `--cli Gemini`, `--cli codex` (equivalent to `--model cli/<provider>`); `--cli` alone uses auto selection with CLI enabled.
38
+ - `--model auto`
39
+ - See `docs/model-auto.md`
40
+ - `--model <preset>`
41
+ - Uses a config-defined preset (see `docs/config.md` → “Presets”).
42
+ - `--video-mode auto|transcript|understand`
43
+ - Only relevant for video inputs / video-only pages.
29
44
  - `--length short|medium|long|xl|xxl|<chars>`
30
45
  - This is *soft guidance* to the model (no hard truncation).
31
46
  - Minimum numeric value: 50 chars.
47
+ - Default: `long`.
32
48
  - `--max-output-tokens <count>`
33
49
  - Hard cap for output tokens (optional).
50
+ - If omitted, no max token parameter is sent (provider default).
34
51
  - Minimum numeric value: 16.
52
+ - Recommendation: prefer `--length` unless you need a hard cap (some providers count “reasoning” into the cap).
53
+ - `--retries <count>`
54
+ - LLM retry attempts on timeout (default: 1).
35
55
  - `--json` (includes prompt + summary in one JSON object)
36
56
 
37
57
  ## Input limits
@@ -0,0 +1,40 @@
1
+ # Manual tests
2
+
3
+ Goal: sanity-check auto selection + presets end-to-end.
4
+
5
+ ## Setup
6
+
7
+ - `OPENAI_API_KEY=...` (optional)
8
+ - `GEMINI_API_KEY=...` (optional)
9
+ - `ANTHROPIC_API_KEY=...` (optional)
10
+ - `XAI_API_KEY=...` (optional)
11
+ - `OPENROUTER_API_KEY=...` (optional)
12
+
13
+ Tip: use `--verbose` to see model attempts + the chosen model.
14
+
15
+ ## Auto (default)
16
+
17
+ - Website summary (should pick a model, show it in spinner):
18
+ - `summarize --max-output-tokens 200 https://example.com`
19
+ - No-model-needed shortcut (should print extracted text; no footer “no model needed”):
20
+ - `summarize --max-output-tokens 99999 https://example.com`
21
+ - Missing-key skip (configure only one key; should skip other providers, still succeed):
22
+ - Set only `OPENAI_API_KEY`, then run a website summary; should not try Gemini/Anthropic/XAI.
23
+
24
+ ## Presets
25
+
26
+ - Define a preset in `~/.summarize/config.json` (see `docs/config.md` → “Presets”), then:
27
+ - `summarize --model <preset> --max-output-tokens 200 https://example.com`
28
+ - If the preset contains OpenRouter models, ensure `OPENROUTER_API_KEY` is set.
29
+
30
+ ## Images
31
+
32
+ - Local image (auto uses API models by default; enable CLI via `cli.enabled` to test CLIs):
33
+ - `summarize ./path/to/image.png --max-output-tokens 200`
34
+
35
+ ## Video
36
+
37
+ - YouTube:
38
+ - `summarize https://www.youtube.com/watch?v=dQw4w9WgXcQ --max-output-tokens 200`
39
+ - Local video understanding (requires Gemini video-capable model; otherwise expect an error or transcript-only behavior depending on input):
40
+ - `summarize ./path/to/video.mp4 --max-output-tokens 200`
@@ -0,0 +1,92 @@
1
+ # Auto model selection (`--model auto`)
2
+
3
+ `--model auto` picks a model based on input kind + token size, and retries with fallbacks when something fails.
4
+
5
+ This is also the built-in default when you don’t specify a model.
6
+
7
+ ## What it does
8
+
9
+ - Builds an ordered list of model “attempts” from `candidates[]` (native first, optional OpenRouter fallback).
10
+ - Skips attempts that don’t have the required API key configured.
11
+ - On any request error, tries the next attempt.
12
+ - If no model is usable, prints the extracted text (no LLM summary). Use `--extract` if you want the raw extracted content even when models are available.
13
+ - Auto prepends CLI attempts only when `cli.enabled` is set (see `docs/cli.md`).
14
+ - Order follows `cli.enabled`.
15
+
16
+ ## OpenRouter vs native
17
+
18
+ Model ids:
19
+
20
+ - Native: `<provider>/<model>` (e.g. `openai/gpt-5-mini`, `google/gemini-3-flash-preview`)
21
+ - Forced OpenRouter: `openrouter/<author>/<slug>` (e.g. `openrouter/meta-llama/llama-3.3-70b-instruct:free`)
22
+
23
+ Behavior:
24
+
25
+ - If you pass an `openrouter/...` model id, the request uses OpenRouter (and requires `OPENROUTER_API_KEY`).
26
+ - If you pass a native model id, the CLI prefers the native provider SDK when its key is available, and can fall back to OpenRouter when no native key exists (and `OPENROUTER_API_KEY` is set).
27
+
28
+ ## How selection works
29
+
30
+ - Uses the order you provide in `model.rules[].candidates[]` (or `bands[].candidates[]`).
31
+ - Filters out candidates that can’t fit the prompt (max input tokens, LiteLLM catalog).
32
+ - For a native candidate, auto mode may add an OpenRouter fallback attempt right after it (when `OPENROUTER_API_KEY` is set and video understanding isn’t required).
33
+
34
+ Notes:
35
+
36
+ - Auto mode is non-streaming (so a failed attempt won’t partially print output).
37
+ - Video understanding is only attempted when `--video-mode` is `auto` or `understand`, and a video-capable model is selected.
38
+
39
+ ## Config
40
+
41
+ Default config file: `~/.summarize/config.json`
42
+
43
+ This file is parsed leniently (JSON5), but **comments are not allowed**.
44
+
45
+ `model.rules` is optional; when omitted, built-in defaults apply.
46
+
47
+ `model.rules[].when` is optional, and when present must be an array (e.g. `["video","youtube"]`).
48
+
49
+ Rules can be either:
50
+
51
+ - `candidates: string[]`
52
+ - `bands: [{ token?: { min?: number; max?: number }, candidates: string[] }]`
53
+
54
+ Example:
55
+
56
+ ```json
57
+ {
58
+ "model": {
59
+ "mode": "auto",
60
+ "rules": [
61
+ {
62
+ "when": ["video"],
63
+ "candidates": ["google/gemini-3-flash-preview"]
64
+ },
65
+ {
66
+ "when": ["website", "youtube"],
67
+ "bands": [
68
+ {
69
+ "token": { "max": 8000 },
70
+ "candidates": ["openai/gpt-5-mini"]
71
+ },
72
+ {
73
+ "candidates": ["xai/grok-4-fast-non-reasoning"]
74
+ }
75
+ ]
76
+ },
77
+ {
78
+ "candidates": ["openai/gpt-5-mini", "openrouter/openai/gpt-5-mini"]
79
+ }
80
+ ]
81
+ },
82
+ "media": { "videoMode": "auto" }
83
+ }
84
+ ```
85
+
86
+ Minimal shorthand:
87
+
88
+ ```json
89
+ {
90
+ "model": "auto"
91
+ }
92
+ ```
@@ -31,24 +31,27 @@ const highlightNav = () => {
31
31
 
32
32
  const wireCopyButtons = () => {
33
33
  const buttons = document.querySelectorAll('[data-copy]')
34
+ const handleCopyClick = async (button) => {
35
+ const selector = button.getAttribute('data-copy')
36
+ const target = selector ? document.querySelector(selector) : null
37
+ const text = target?.textContent?.trim() ?? ''
38
+ if (!text) return
39
+ try {
40
+ await navigator.clipboard.writeText(text)
41
+ const prev = button.textContent ?? ''
42
+ button.textContent = 'Copied'
43
+ button.setAttribute('data-copied', '1')
44
+ window.setTimeout(() => {
45
+ button.textContent = prev
46
+ button.removeAttribute('data-copied')
47
+ }, 900)
48
+ } catch {
49
+ // ignore
50
+ }
51
+ }
34
52
  for (const button of buttons) {
35
- button.addEventListener('click', async () => {
36
- const selector = button.getAttribute('data-copy')
37
- const target = selector ? document.querySelector(selector) : null
38
- const text = target?.textContent?.trim() ?? ''
39
- if (!text) return
40
- try {
41
- await navigator.clipboard.writeText(text)
42
- const prev = button.textContent ?? ''
43
- button.textContent = 'Copied'
44
- button.setAttribute('data-copied', '1')
45
- window.setTimeout(() => {
46
- button.textContent = prev
47
- button.removeAttribute('data-copied')
48
- }, 900)
49
- } catch {
50
- // ignore
51
- }
53
+ button.addEventListener('click', () => {
54
+ void handleCopyClick(button)
52
55
  })
53
56
  }
54
57
  }