thinking-phrases 1.0.1 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +230 -142
- package/configs/hn-top.config.json +60 -27
- package/launchd/rss-update.error.log +3 -27
- package/launchd/rss-update.log +308 -0
- package/launchd/task-health.json +54 -0
- package/out/dwyl-quotes.json +1621 -0
- package/out/javascript-tips.json +107 -0
- package/out/league-loading-screen-tips.json +107 -0
- package/out/ruby-tips.json +115 -0
- package/out/settings-linux.json +87 -0
- package/out/settings-mac.json +87 -0
- package/out/settings-windows.json +87 -0
- package/out/typescript-tips.json +131 -0
- package/out/vscode-tips.json +87 -0
- package/out/wow-loading-screen-tips.json +116 -0
- package/package.json +19 -12
- package/scripts/build.ts +3 -3
- package/scripts/debug-hn-hydration.ts +33 -0
- package/scripts/run-rss-update.zsh +25 -3
- package/scripts/show-thinking-phrases-health.ts +74 -0
- package/scripts/trigger-thinking-phrases-scheduler.zsh +50 -0
- package/src/core/config.ts +65 -3
- package/src/core/githubModels.ts +200 -112
- package/src/core/interactive.ts +49 -67
- package/src/core/phraseCache.ts +242 -0
- package/src/core/phraseFormats.ts +243 -0
- package/src/core/presets.ts +1 -1
- package/src/core/runner.ts +246 -113
- package/src/core/scheduler.ts +1 -1
- package/src/core/taskHealth.ts +213 -0
- package/src/core/types.ts +32 -8
- package/src/core/utils.ts +27 -2
- package/src/sources/customJson.ts +28 -18
- package/src/sources/earthquakes.ts +4 -4
- package/src/sources/githubActivity.ts +120 -48
- package/src/sources/hackerNews.ts +19 -7
- package/src/sources/rss.ts +25 -11
- package/src/sources/stocks.ts +31 -10
- package/src/sources/weatherAlerts.ts +173 -7
- package/tsconfig.json +1 -1
- package/scripts/update-rss-settings.ts +0 -7
package/src/core/config.ts
CHANGED
|
@@ -28,35 +28,50 @@ export function resolveConfigPath(configPath?: string): string {
|
|
|
28
28
|
|
|
29
29
|
export const DEFAULT_CONFIG: Config = {
|
|
30
30
|
feeds: [],
|
|
31
|
-
|
|
31
|
+
rssFetchIntervalSeconds: 21600,
|
|
32
|
+
limit: 100,
|
|
32
33
|
mode: 'replace',
|
|
33
34
|
target: 'auto',
|
|
34
35
|
phraseFormatting: {
|
|
35
36
|
includeSource: true,
|
|
36
37
|
includeTime: true,
|
|
37
38
|
maxLength: 140,
|
|
39
|
+
templates: {
|
|
40
|
+
article: '%title% — %source% (%time%)',
|
|
41
|
+
hackerNews: '%title% — HN %score% (%time%)',
|
|
42
|
+
stock: '%symbol% %price% %change% %market%',
|
|
43
|
+
githubCommit: '%headline% — %repo% %delta% @%author% (%time%)',
|
|
44
|
+
githubFeed: '%action% — @%handle% (%time%)',
|
|
45
|
+
},
|
|
38
46
|
},
|
|
39
47
|
githubModels: {
|
|
40
48
|
enabled: false,
|
|
41
|
-
|
|
49
|
+
endpoint: 'https://models.github.ai/inference',
|
|
50
|
+
model: 'openai/gpt-4.1-mini',
|
|
42
51
|
tokenEnvVar: 'GITHUB_MODELS_TOKEN',
|
|
43
52
|
maxInputItems: 10,
|
|
44
|
-
|
|
53
|
+
maxInputTokens: 16000,
|
|
54
|
+
maxTokens: 500,
|
|
55
|
+
maxConcurrency: 1,
|
|
45
56
|
maxPhrasesPerArticle: 2,
|
|
46
57
|
temperature: 0.2,
|
|
47
58
|
fetchArticleContent: true,
|
|
48
59
|
maxArticleContentLength: 6000,
|
|
60
|
+
cacheTtlSeconds: 604800,
|
|
49
61
|
},
|
|
50
62
|
stockQuotes: {
|
|
51
63
|
enabled: false,
|
|
52
64
|
symbols: ['MSFT', 'NVDA', 'TSLA', 'AMZN', 'GOOGL', 'AMD'],
|
|
53
65
|
includeMarketState: true,
|
|
66
|
+
showClosed: false,
|
|
67
|
+
fetchIntervalSeconds: 60,
|
|
54
68
|
},
|
|
55
69
|
hackerNews: {
|
|
56
70
|
enabled: false,
|
|
57
71
|
feed: 'top',
|
|
58
72
|
maxItems: 10,
|
|
59
73
|
minScore: 50,
|
|
74
|
+
fetchIntervalSeconds: 300,
|
|
60
75
|
},
|
|
61
76
|
earthquakes: {
|
|
62
77
|
enabled: false,
|
|
@@ -66,6 +81,7 @@ export const DEFAULT_CONFIG: Config = {
|
|
|
66
81
|
limit: 10,
|
|
67
82
|
radiusKm: 500,
|
|
68
83
|
orderBy: 'time',
|
|
84
|
+
fetchIntervalSeconds: 1800,
|
|
69
85
|
},
|
|
70
86
|
weatherAlerts: {
|
|
71
87
|
enabled: false,
|
|
@@ -73,6 +89,7 @@ export const DEFAULT_CONFIG: Config = {
|
|
|
73
89
|
area: '',
|
|
74
90
|
minimumSeverity: 'moderate',
|
|
75
91
|
limit: 10,
|
|
92
|
+
fetchIntervalSeconds: 1800,
|
|
76
93
|
},
|
|
77
94
|
customJson: {
|
|
78
95
|
enabled: false,
|
|
@@ -86,6 +103,7 @@ export const DEFAULT_CONFIG: Config = {
|
|
|
86
103
|
dateField: 'publishedAt',
|
|
87
104
|
idField: 'id',
|
|
88
105
|
maxItems: 10,
|
|
106
|
+
fetchIntervalSeconds: 3600,
|
|
89
107
|
},
|
|
90
108
|
githubActivity: {
|
|
91
109
|
enabled: false,
|
|
@@ -98,6 +116,7 @@ export const DEFAULT_CONFIG: Config = {
|
|
|
98
116
|
maxItems: 10,
|
|
99
117
|
sinceHours: 24,
|
|
100
118
|
tokenEnvVar: 'GITHUB_TOKEN',
|
|
119
|
+
fetchIntervalSeconds: 300,
|
|
101
120
|
},
|
|
102
121
|
};
|
|
103
122
|
|
|
@@ -272,6 +291,12 @@ export function parseArgs(argv: string[]): CliOverrides {
|
|
|
272
291
|
index += 1;
|
|
273
292
|
}
|
|
274
293
|
break;
|
|
294
|
+
case '--models-max-input-tokens':
|
|
295
|
+
if (next) {
|
|
296
|
+
setModels({ maxInputTokens: Number(next) });
|
|
297
|
+
index += 1;
|
|
298
|
+
}
|
|
299
|
+
break;
|
|
275
300
|
case '--models-max-tokens':
|
|
276
301
|
if (next) {
|
|
277
302
|
setModels({ maxTokens: Number(next) });
|
|
@@ -290,6 +315,18 @@ export function parseArgs(argv: string[]): CliOverrides {
|
|
|
290
315
|
index += 1;
|
|
291
316
|
}
|
|
292
317
|
break;
|
|
318
|
+
case '--models-endpoint':
|
|
319
|
+
if (next) {
|
|
320
|
+
setModels({ endpoint: next });
|
|
321
|
+
index += 1;
|
|
322
|
+
}
|
|
323
|
+
break;
|
|
324
|
+
case '--models-max-concurrency':
|
|
325
|
+
if (next) {
|
|
326
|
+
setModels({ maxConcurrency: Number(next) });
|
|
327
|
+
index += 1;
|
|
328
|
+
}
|
|
329
|
+
break;
|
|
293
330
|
case '--fetch-article-content':
|
|
294
331
|
setModels({ fetchArticleContent: true });
|
|
295
332
|
break;
|
|
@@ -566,6 +603,7 @@ export function mergeConfig(base: Config, fileConfig: Partial<Config>, argConfig
|
|
|
566
603
|
verbose: argConfig.verbose ?? fileConfig.verbose ?? base.verbose,
|
|
567
604
|
debug: argConfig.debug ?? fileConfig.debug ?? base.debug,
|
|
568
605
|
feeds: argConfig.feeds ?? fileConfig.feeds ?? base.feeds,
|
|
606
|
+
rssFetchIntervalSeconds: argConfig.rssFetchIntervalSeconds ?? fileConfig.rssFetchIntervalSeconds ?? base.rssFetchIntervalSeconds,
|
|
569
607
|
phraseFormatting: {
|
|
570
608
|
...base.phraseFormatting,
|
|
571
609
|
...(fileConfig.phraseFormatting ?? {}),
|
|
@@ -601,6 +639,7 @@ export function mergeConfig(base: Config, fileConfig: Partial<Config>, argConfig
|
|
|
601
639
|
...(fileConfig.customJson ?? {}),
|
|
602
640
|
...(argConfig.customJson ?? {}),
|
|
603
641
|
},
|
|
642
|
+
customJsonSources: argConfig.customJsonSources ?? fileConfig.customJsonSources ?? base.customJsonSources,
|
|
604
643
|
githubActivity: {
|
|
605
644
|
...base.githubActivity,
|
|
606
645
|
...(fileConfig.githubActivity ?? {}),
|
|
@@ -617,6 +656,7 @@ export function validateConfig(config: Config): void {
|
|
|
617
656
|
&& !config.earthquakes.enabled
|
|
618
657
|
&& !config.weatherAlerts.enabled
|
|
619
658
|
&& !config.customJson.enabled
|
|
659
|
+
&& !(config.customJsonSources ?? []).some(s => s.enabled)
|
|
620
660
|
&& !config.githubActivity.enabled
|
|
621
661
|
) {
|
|
622
662
|
throw new Error('Configure at least one source before running dynamic phrases.');
|
|
@@ -647,6 +687,14 @@ export function validateConfig(config: Config): void {
|
|
|
647
687
|
throw new Error(`githubModels.temperature must be between 0 and 1. Received: ${config.githubModels.temperature}`);
|
|
648
688
|
}
|
|
649
689
|
|
|
690
|
+
if (!Number.isFinite(config.githubModels.maxConcurrency) || config.githubModels.maxConcurrency < 1) {
|
|
691
|
+
throw new Error(`githubModels.maxConcurrency must be at least 1. Received: ${config.githubModels.maxConcurrency}`);
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
if (config.githubModels.endpoint && !/^https?:\/\//u.test(config.githubModels.endpoint)) {
|
|
695
|
+
throw new Error(`githubModels.endpoint must be a valid HTTP(S) URL. Received: ${config.githubModels.endpoint}`);
|
|
696
|
+
}
|
|
697
|
+
|
|
650
698
|
const invalidFeed = config.feeds.find(feed => !feed.url.trim());
|
|
651
699
|
if (invalidFeed) {
|
|
652
700
|
throw new Error('Every feed entry must include a non-empty url.');
|
|
@@ -682,6 +730,20 @@ export function validateConfig(config: Config): void {
|
|
|
682
730
|
}
|
|
683
731
|
}
|
|
684
732
|
|
|
733
|
+
for (const [index, source] of (config.customJsonSources ?? []).entries()) {
|
|
734
|
+
if (!source.enabled) {
|
|
735
|
+
continue;
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
if (!source.url.trim()) {
|
|
739
|
+
throw new Error(`customJsonSources[${index}].url must be set when enabled.`);
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
if (!source.titleField.trim()) {
|
|
743
|
+
throw new Error(`customJsonSources[${index}].titleField must be set when enabled.`);
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
|
|
685
747
|
if (config.githubActivity.enabled) {
|
|
686
748
|
if (config.githubActivity.mode === 'repo-commits' && !config.githubActivity.repo?.trim()) {
|
|
687
749
|
throw new Error('githubActivity.repo must be set when githubActivity.mode is repo-commits.');
|
package/src/core/githubModels.ts
CHANGED
|
@@ -1,14 +1,114 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { AzureKeyCredential } from '@azure/core-auth';
|
|
1
|
+
import OpenAI from 'openai';
|
|
3
2
|
import { execFileSync } from 'node:child_process';
|
|
4
|
-
import type { ArticleItem, Config, GitHubModelsConfig
|
|
3
|
+
import type { ArticleItem, Config, GitHubModelsConfig } from './types.js';
|
|
5
4
|
import { decodeHtmlEntities, dedupePhrases, logDebug, singleLine } from './utils.js';
|
|
5
|
+
import { appendSourceSuffix } from './phraseFormats.js';
|
|
6
6
|
|
|
7
7
|
interface BuildModelArticlePhrasesOptions {
|
|
8
8
|
onProgress?: (message: string) => void;
|
|
9
|
+
/** Called with each batch of phrases as they're generated */
|
|
10
|
+
onPhrases?: (phrases: string[]) => void;
|
|
11
|
+
/** Source type for prompt selection (rss, hacker-news, github-activity, etc.) */
|
|
12
|
+
sourceType?: string;
|
|
9
13
|
}
|
|
10
14
|
|
|
11
|
-
const
|
|
15
|
+
const PROMPT_PREAMBLE = [
|
|
16
|
+
'IMPORTANT: Your ENTIRE response must be valid JSON and nothing else. No markdown, no explanation, no code fences.',
|
|
17
|
+
'Return exactly this shape: {"phrases":["phrase1","phrase2"]}.',
|
|
18
|
+
'Each phrase: factual, concrete, max maxLength chars. You may emit up to maxPhrasesPerArticle phrases per item.',
|
|
19
|
+
'NEVER include the source name, author, date, or time — those are appended automatically.',
|
|
20
|
+
'NEVER restate the title verbatim. The reader already saw the headline — give them the insight behind it.',
|
|
21
|
+
'Each phrase is displayed INDEPENDENTLY — never start with "It", "This", "The project", "The tool", or any pronoun that refers to something the reader hasn\'t seen.',
|
|
22
|
+
'Every phrase must be self-contained and make sense on its own without context from other phrases.',
|
|
23
|
+
].join(' ');
|
|
24
|
+
|
|
25
|
+
export const DEFAULT_SOURCE_PROMPTS: Record<string, string> = {
|
|
26
|
+
'rss': [
|
|
27
|
+
PROMPT_PREAMBLE,
|
|
28
|
+
'You are extracting insights from blog posts and news articles (RSS/Atom feeds).',
|
|
29
|
+
'You receive the article title AND the full article body text.',
|
|
30
|
+
'Your job: find the single most valuable, concrete takeaway buried in the article that the reader would NOT get from the title alone.',
|
|
31
|
+
'Prioritize: specific numbers, benchmarks, percentages, technical details, surprising findings, release dates, breaking changes, or "how it works" explanations.',
|
|
32
|
+
'BAD: "GitHub released a new feature for code review" (just restates the headline).',
|
|
33
|
+
'GOOD: "Copilot code review now uses multi-line comments that reduced cognitive load by 15% in A/B testing".',
|
|
34
|
+
'BAD: "The article discusses improvements to Docker performance".',
|
|
35
|
+
'GOOD: "Docker BuildKit v0.17 parallelizes dependency resolution, cutting cold builds from 4m to 90s on large monorepos".',
|
|
36
|
+
'If the article body has real data, use it. If it is too thin, extract the most specific claim from the title and sharpen it.',
|
|
37
|
+
].join(' '),
|
|
38
|
+
'hacker-news': [
|
|
39
|
+
PROMPT_PREAMBLE,
|
|
40
|
+
'You are extracting insights from Hacker News posts. You may receive:',
|
|
41
|
+
'(a) The HN title + the full linked article body (most common — link posts)',
|
|
42
|
+
'(b) The HN title + the self-post text (Ask HN, Show HN)',
|
|
43
|
+
'(c) The HN title + both the self-post text AND fetched article body',
|
|
44
|
+
'Your job: extract the ONE technical insight, surprising fact, or concrete detail that makes this post worth reading.',
|
|
45
|
+
'The reader has 3 seconds of glance time. Make it count with a real fact, not a summary.',
|
|
46
|
+
'BAD: "A database was built in a spreadsheet" (just restates the HN title).',
|
|
47
|
+
'GOOD: "The spreadsheet-database uses SQLite compiled to WASM, handling 10k rows with indexed queries under 50ms".',
|
|
48
|
+
'BAD: "The author discusses their experience with Rust".',
|
|
49
|
+
'GOOD: "Switching from Go to Rust cut their p99 latency from 12ms to 800μs by eliminating GC pauses".',
|
|
50
|
+
'For Show HN posts: what does it actually do and what makes it technically interesting?',
|
|
51
|
+
'For Ask HN posts: what is the most insightful or surprising answer/claim?',
|
|
52
|
+
].join(' '),
|
|
53
|
+
'github-activity': [
|
|
54
|
+
PROMPT_PREAMBLE,
|
|
55
|
+
'You are summarizing GitHub commits. You receive the commit message AND the full diff (added/removed lines).',
|
|
56
|
+
'Your job: explain the PURPOSE and IMPACT of the change in plain language. What is different for users or developers AFTER this commit?',
|
|
57
|
+
'Read the diff carefully — the commit message often undersells the change. The diff tells the real story.',
|
|
58
|
+
'BAD: "Fixed a null check in the settings handler" (says what, not why).',
|
|
59
|
+
'GOOD: "Settings panel no longer crashes when opening a workspace with a corrupted .vscode/settings.json".',
|
|
60
|
+
'BAD: "Refactored the entrypoint module".',
|
|
61
|
+
'GOOD: "DevTools now loads 40% faster after the entrypoint was split into lazy-loaded chunks".',
|
|
62
|
+
'For performance changes: include the before/after numbers if visible in the diff.',
|
|
63
|
+
'For bug fixes: describe the user-visible symptom that was fixed.',
|
|
64
|
+
'For new features: describe what users can now do that they couldn\'t before.',
|
|
65
|
+
'NEVER mention file paths, line counts, or SHA hashes — those appear in the metadata suffix.',
|
|
66
|
+
].join(' '),
|
|
67
|
+
'earthquakes': [
|
|
68
|
+
PROMPT_PREAMBLE,
|
|
69
|
+
'You are summarizing USGS earthquake data. You receive magnitude, location, significance score, alert level, and tsunami status.',
|
|
70
|
+
'Keep it concise and factual. The magnitude and location are already in the title — add context that helps the reader understand the severity.',
|
|
71
|
+
'If significance is high (>500) or an alert level is set, emphasize that.',
|
|
72
|
+
'If a tsunami bulletin was issued, lead with that.',
|
|
73
|
+
'BAD: "An earthquake happened near Ridgecrest" (obvious from the title).',
|
|
74
|
+
'GOOD: "Significance 680 with yellow alert — strongest quake in the region since the 2019 Ridgecrest sequence".',
|
|
75
|
+
'If the data is sparse (just magnitude + location with no alert), a clean one-liner with the depth or felt radius is fine.',
|
|
76
|
+
].join(' '),
|
|
77
|
+
'custom-json': [
|
|
78
|
+
PROMPT_PREAMBLE,
|
|
79
|
+
'You are summarizing items from a custom JSON API. The data structure varies.',
|
|
80
|
+
'Extract the most concrete, specific, and informative detail from each item.',
|
|
81
|
+
'Focus on facts the reader can learn in a glance: numbers, names, outcomes, technical details.',
|
|
82
|
+
'BAD: "An interesting article about cloud computing".',
|
|
83
|
+
'GOOD: "AWS Lambda now supports 10GB memory functions, enabling in-memory ML inference without containers".',
|
|
84
|
+
].join(' '),
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const DEFAULT_FALLBACK_PROMPT = [
|
|
88
|
+
PROMPT_PREAMBLE,
|
|
89
|
+
'Extract the single most valuable, concrete takeaway from each item.',
|
|
90
|
+
'Prioritize: specific numbers, technical details, surprising findings, or "what changed and why it matters".',
|
|
91
|
+
'The reader has 3 seconds. Give them a real insight, not a headline restatement.',
|
|
92
|
+
].join(' ');
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Resolve the prompt for a given source type.
|
|
96
|
+
* Priority: config per-source prompt > config systemPrompt > built-in per-source default > built-in fallback.
|
|
97
|
+
*/
|
|
98
|
+
export function resolvePrompt(config: GitHubModelsConfig, sourceType?: string): string {
|
|
99
|
+
if (sourceType && config.prompts?.[sourceType]) {
|
|
100
|
+
return config.prompts[sourceType];
|
|
101
|
+
}
|
|
102
|
+
if (config.systemPrompt) {
|
|
103
|
+
return config.systemPrompt;
|
|
104
|
+
}
|
|
105
|
+
if (sourceType && DEFAULT_SOURCE_PROMPTS[sourceType]) {
|
|
106
|
+
return DEFAULT_SOURCE_PROMPTS[sourceType];
|
|
107
|
+
}
|
|
108
|
+
return DEFAULT_FALLBACK_PROMPT;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const DEFAULT_ENDPOINT = 'https://models.github.ai/inference';
|
|
12
112
|
|
|
13
113
|
function getGitHubModelsToken(config: GitHubModelsConfig): string | undefined {
|
|
14
114
|
const envToken = process.env[config.tokenEnvVar] ?? process.env.GITHUB_TOKEN;
|
|
@@ -20,6 +120,13 @@ function getGitHubModelsToken(config: GitHubModelsConfig): string | undefined {
|
|
|
20
120
|
const token = execFileSync('gh', ['auth', 'token'], {
|
|
21
121
|
encoding: 'utf8',
|
|
22
122
|
stdio: ['ignore', 'pipe', 'ignore'],
|
|
123
|
+
env: {
|
|
124
|
+
...process.env,
|
|
125
|
+
GITHUB_TOKEN: '',
|
|
126
|
+
GH_TOKEN: '',
|
|
127
|
+
GITHUB_ENTERPRISE_TOKEN: '',
|
|
128
|
+
GH_ENTERPRISE_TOKEN: '',
|
|
129
|
+
},
|
|
23
130
|
}).trim();
|
|
24
131
|
|
|
25
132
|
return token || undefined;
|
|
@@ -28,7 +135,7 @@ function getGitHubModelsToken(config: GitHubModelsConfig): string | undefined {
|
|
|
28
135
|
}
|
|
29
136
|
}
|
|
30
137
|
|
|
31
|
-
function extractModelPhrases(input: string): string[] {
|
|
138
|
+
export function extractModelPhrases(input: string): string[] {
|
|
32
139
|
const candidate = (input.match(/```(?:json)?\s*([\s\S]*?)```/u)?.[1] ?? input).trim();
|
|
33
140
|
|
|
34
141
|
try {
|
|
@@ -65,7 +172,7 @@ function extractModelPhrases(input: string): string[] {
|
|
|
65
172
|
.split(/\r?\n/u)
|
|
66
173
|
.map(line => line.trim())
|
|
67
174
|
.filter(Boolean)
|
|
68
|
-
.map(line => line.replace(/^[
|
|
175
|
+
.map(line => line.replace(/^[[\]",*\-•\s]+/gu, '').trim())
|
|
69
176
|
.filter(Boolean);
|
|
70
177
|
}
|
|
71
178
|
|
|
@@ -77,75 +184,64 @@ async function runGitHubModelsPrompt(config: GitHubModelsConfig, content: string
|
|
|
77
184
|
);
|
|
78
185
|
}
|
|
79
186
|
|
|
80
|
-
const client =
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
model: config.model,
|
|
84
|
-
messages: [{ role: 'user', content }],
|
|
85
|
-
temperature: config.temperature,
|
|
86
|
-
max_tokens: config.maxTokens,
|
|
87
|
-
response_format: { type: 'json_object' },
|
|
88
|
-
},
|
|
187
|
+
const client = new OpenAI({
|
|
188
|
+
baseURL: config.endpoint || DEFAULT_ENDPOINT,
|
|
189
|
+
apiKey: token,
|
|
89
190
|
});
|
|
90
191
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
192
|
+
// Reasoning models (o1, o3, o4, gpt-5) don't support temperature or response_format
|
|
193
|
+
const isReasoningModel = /(?:^|\/)(?:o[1-4]|gpt-5)/iu.test(config.model);
|
|
194
|
+
|
|
195
|
+
// Reasoning models need more tokens because thinking tokens count against the budget
|
|
196
|
+
const maxTokens = isReasoningModel ? Math.max(config.maxTokens * 4, 2000) : config.maxTokens;
|
|
95
197
|
|
|
96
|
-
const
|
|
198
|
+
const completion = await client.chat.completions.create({
|
|
199
|
+
model: config.model,
|
|
200
|
+
messages: [{ role: 'user', content }],
|
|
201
|
+
...(!isReasoningModel && config.temperature !== 1 ? { temperature: config.temperature } : {}),
|
|
202
|
+
max_completion_tokens: maxTokens,
|
|
203
|
+
...(!isReasoningModel ? { response_format: { type: 'json_object' } } : {}),
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
const text = completion.choices?.[0]?.message?.content?.trim();
|
|
97
207
|
if (!text) {
|
|
98
|
-
|
|
208
|
+
const reason = completion.choices?.[0]?.finish_reason;
|
|
209
|
+
throw new Error(`GitHub Models returned empty content (finish_reason: ${reason ?? 'unknown'})`);
|
|
99
210
|
}
|
|
100
211
|
|
|
101
212
|
return text;
|
|
102
213
|
}
|
|
103
214
|
|
|
104
|
-
function
|
|
105
|
-
const
|
|
106
|
-
const
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
.
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
};
|
|
119
|
-
|
|
120
|
-
const flushCurrentChunk = (): void => {
|
|
121
|
-
if (currentChunk.length > 0) {
|
|
122
|
-
chunks.push(currentChunk);
|
|
123
|
-
currentChunk = [];
|
|
124
|
-
currentCharacters = 0;
|
|
125
|
-
}
|
|
126
|
-
};
|
|
127
|
-
|
|
128
|
-
for (const article of articles) {
|
|
129
|
-
const articleCharacters = estimateArticleCharacters(article);
|
|
130
|
-
|
|
131
|
-
if (
|
|
132
|
-
currentChunk.length > 0
|
|
133
|
-
&& (currentChunk.length >= defaultChunkSize || currentCharacters + articleCharacters > maxCharactersPerChunk)
|
|
134
|
-
) {
|
|
135
|
-
flushCurrentChunk();
|
|
136
|
-
}
|
|
215
|
+
async function summarizeArticle(article: ArticleItem, config: Config, sourceType?: string): Promise<string[]> {
|
|
216
|
+
const instruction = resolvePrompt(config.githubModels, sourceType);
|
|
217
|
+
const payload = JSON.stringify({
|
|
218
|
+
instruction,
|
|
219
|
+
maxLength: config.phraseFormatting.maxLength,
|
|
220
|
+
maxPhrasesPerArticle: config.githubModels.maxPhrasesPerArticle,
|
|
221
|
+
items: [{
|
|
222
|
+
title: article.title ?? '',
|
|
223
|
+
source: article.source ?? '',
|
|
224
|
+
time: article.time ?? '',
|
|
225
|
+
content: (article.articleContent ?? article.content ?? '').slice(0, config.githubModels.maxArticleContentLength),
|
|
226
|
+
link: article.link ?? '',
|
|
227
|
+
}],
|
|
228
|
+
});
|
|
137
229
|
|
|
138
|
-
|
|
139
|
-
|
|
230
|
+
logDebug(config, `Sending "${article.title}" (${payload.length} chars) to GitHub Models`);
|
|
231
|
+
const responseText = await runGitHubModelsPrompt(config.githubModels, payload);
|
|
232
|
+
logDebug(config, `Response: ${singleLine(responseText, 220)}`);
|
|
140
233
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
234
|
+
return extractModelPhrases(responseText)
|
|
235
|
+
.map(phrase => singleLine(decodeHtmlEntities(phrase), config.phraseFormatting.maxLength))
|
|
236
|
+
.filter(Boolean)
|
|
237
|
+
.map(phrase => appendSourceSuffix(phrase, article.source, article.time, article.metadata));
|
|
238
|
+
}
|
|
145
239
|
|
|
146
|
-
|
|
240
|
+
const DELAY_BETWEEN_REQUESTS_MS = 2000;
|
|
241
|
+
const MAX_RETRIES = 3;
|
|
147
242
|
|
|
148
|
-
|
|
243
|
+
async function sleep(ms: number): Promise<void> {
|
|
244
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
149
245
|
}
|
|
150
246
|
|
|
151
247
|
export async function buildModelArticlePhrases(
|
|
@@ -153,56 +249,48 @@ export async function buildModelArticlePhrases(
|
|
|
153
249
|
config: Config,
|
|
154
250
|
options: BuildModelArticlePhrasesOptions = {},
|
|
155
251
|
): Promise<string[]> {
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
})
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
options.
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
)
|
|
196
|
-
|
|
197
|
-
const rejectedChunk = settledChunkResults.find(
|
|
198
|
-
(result): result is PromiseRejectedResult => result.status === 'rejected',
|
|
199
|
-
);
|
|
200
|
-
|
|
201
|
-
if (rejectedChunk) {
|
|
202
|
-
throw rejectedChunk.reason;
|
|
252
|
+
options.onProgress?.(`Generating phrases with GitHub Models (${articles.length} article${articles.length === 1 ? '' : 's'})`);
|
|
253
|
+
|
|
254
|
+
const allPhrases: string[] = [];
|
|
255
|
+
let lastError: unknown;
|
|
256
|
+
|
|
257
|
+
for (let i = 0; i < articles.length; i++) {
|
|
258
|
+
const article = articles[i];
|
|
259
|
+
const title = article.title ?? 'untitled';
|
|
260
|
+
options.onProgress?.(`Summarizing (${i + 1}/${articles.length}): ${title.length > 60 ? title.slice(0, 60) + '…' : title}`);
|
|
261
|
+
let phrases: string[] | undefined;
|
|
262
|
+
|
|
263
|
+
for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
|
|
264
|
+
try {
|
|
265
|
+
phrases = await summarizeArticle(article, config, options.sourceType);
|
|
266
|
+
break;
|
|
267
|
+
} catch (error: unknown) {
|
|
268
|
+
const is429 = error instanceof Error && error.message.includes('429');
|
|
269
|
+
if (is429 && attempt < MAX_RETRIES) {
|
|
270
|
+
const backoff = attempt * 5000;
|
|
271
|
+
options.onProgress?.(`Rate limited — waiting ${backoff / 1000}s before retry (${attempt}/${MAX_RETRIES})`);
|
|
272
|
+
await sleep(backoff);
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
lastError = error;
|
|
276
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
277
|
+
logDebug(config, `Failed "${article.title}": ${message}`);
|
|
278
|
+
options.onProgress?.(`GitHub Models failed (${i + 1}/${articles.length}): ${message}`);
|
|
279
|
+
break;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
if (phrases && phrases.length > 0) {
|
|
284
|
+
allPhrases.push(...phrases);
|
|
285
|
+
options.onPhrases?.(phrases);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
options.onProgress?.(`Generated phrases (${i + 1}/${articles.length})`);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
if (allPhrases.length === 0 && lastError) {
|
|
292
|
+
throw lastError;
|
|
203
293
|
}
|
|
204
294
|
|
|
205
|
-
return dedupePhrases(
|
|
206
|
-
settledChunkResults.flatMap(result => (result.status === 'fulfilled' ? result.value : [])),
|
|
207
|
-
);
|
|
295
|
+
return dedupePhrases(allPhrases);
|
|
208
296
|
}
|