From 31b952bf01b9a4dd352e3e9167b37b1c6640209f Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 24 Apr 2026 18:43:19 +0000 Subject: [PATCH 1/2] Cut LLM costs: Haiku for background tasks, on-demand summaries, 1 opinion/day MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three changes to reduce the >$1/day Sonnet spend: 1. Switch background tasks to Haiku (claude-haiku-4-5-20251001): - article-summary — was the biggest cost driver - auto-tag-post, auto-tag-note — simple classification - topic-generation, topic-summary — headline extraction - content moderation (llmAnalyzer) — one-word classification These are all low-complexity tasks that don't need Sonnet quality. Haiku is ~10x cheaper. 2. Article summaries on-demand only: Previously, every article in the RSS feed triggered a proactive summary generation even if nobody ever read it. Now summaries are only generated when someone actually clicks into the article detail page. The two proactive requestArticleSummary calls in getMetadata are removed; an on-demand trigger is added in handleArticleView instead. 3. Daily opinions reduced from 2 to 1: maxDailyOpinions changed from 2 to 1. Each opinion post involves multiple Sonnet calls (generate + reflect + review + engage). Expected savings: ~80% reduction in daily token cost. Interactive features (chat, agent, app builder) stay on Sonnet. https://claude.ai/code/session_01GRGLA9yj7BpqKiyi6xFwnm --- blog/opinion.go | 3 +-- chat/chat.go | 10 ++++++---- news/news.go | 31 ++++++++++++++----------------- search/topics.go | 1 + 4 files changed, 22 insertions(+), 23 deletions(-) diff --git a/blog/opinion.go b/blog/opinion.go index d1703f92..05f6ddd7 100644 --- a/blog/opinion.go +++ b/blog/opinion.go @@ -90,8 +90,7 @@ func opinionEngageLoop() { } // maxDailyOpinions limits how many opinion posts are generated per day. -// Reduced from 8 (one per category) to control API costs while no users are active. -const maxDailyOpinions = 2 +const maxDailyOpinions = 1 // publishNextOpinion finds the next category that needs an opinion today // and publishes it, respecting the spacing between posts. diff --git a/chat/chat.go b/chat/chat.go index c6dfebd4..87939e10 100644 --- a/chat/chat.go +++ b/chat/chat.go @@ -1043,11 +1043,12 @@ func Load() { if okUri && okContent && okType { app.Log("chat", "Received summary generation request for %s (%s)", uri, eventType) - // Generate summary using LLM (low priority - background task) + // Generate summary using LLM (low priority - background task, Haiku for cost) prompt := &ai.Prompt{ System: "You are a helpful assistant that creates concise summaries. Provide only the summary content itself without any introductory phrases like 'Here is a summary' or 'This article is about'. Just write 2-3 clear sentences that capture the key points. Report what the article says without editorialising, fact-checking, or adding commentary about whether claims are true or false. Never add disclaimers, caveats, or corrections.", Question: fmt.Sprintf("Summarize this article:\n\n%s", content), Priority: ai.PriorityLow, + Model: "claude-haiku-4-5-20251001", Caller: "article-summary", } @@ -1105,6 +1106,7 @@ func Load() { System: fmt.Sprintf("You are a content categorization assistant. Your task is to categorize posts into ONE of these categories ONLY: %s. If the post does not clearly fit into any of these categories, respond with 'None'. Respond with ONLY the category name or 'None', nothing else.", strings.Join(topics, ", ")), Question: fmt.Sprintf("Categorize this post:\n\nTitle: %s\n\nContent: %s\n\nWhich single category best fits this post?", title, content), Priority: ai.PriorityLow, + Model: "claude-haiku-4-5-20251001", Caller: "auto-tag-post", } @@ -1159,6 +1161,7 @@ func Load() { System: "You are a note organization assistant. Given a note, suggest ONE short tag (1-2 words, lowercase) that best categorizes it. Examples: 'work', 'ideas', 'shopping', 'todo', 'recipe', 'travel', 'health', 'finance'. Respond with ONLY the tag, nothing else. If the note is too short or unclear, respond with 'personal'.", Question: content, Priority: ai.PriorityLow, + Model: "claude-haiku-4-5-20251001", Caller: "auto-tag-note", } @@ -1216,6 +1219,7 @@ func generateSummaries() { Rag: ragContext, Question: prompt, Priority: ai.PriorityMedium, + Model: "claude-haiku-4-5-20251001", Caller: "topic-summary", }) @@ -1589,12 +1593,10 @@ func handlePostChat(w http.ResponseWriter, r *http.Request) { type llmAnalyzer struct{} func (a *llmAnalyzer) Analyze(promptText, question string) (string, error) { - // Create a simple prompt for analysis prompt := &ai.Prompt{ System: promptText, Question: question, - Context: nil, - Rag: nil, + Model: "claude-haiku-4-5-20251001", } return askLLM(prompt) } diff --git a/news/news.go b/news/news.go index 5da8c025..e06dd578 100644 --- a/news/news.go +++ b/news/news.go @@ -528,13 +528,7 @@ func getMetadata(uri string, publishedAt time.Time) (*Metadata, bool, error) { if isHN { age := time.Since(time.Unix(0, cached.Created)) if age < time.Hour { - // Request summary if we don't have one yet, with smart retry - if cached.Summary == "" { - shouldRetry := shouldRequestSummary(cached) - if shouldRetry { - go requestArticleSummary(uri, cached) - } - } + // Summaries generated on-demand when article is viewed. return cached, false, nil // false = from cache } app.Log("news", "HN metadata cache expired for %s (age: %v), refetching comments", uri, age.Round(time.Minute)) @@ -547,13 +541,7 @@ func getMetadata(uri string, publishedAt time.Time) (*Metadata, bool, error) { uri, cachedTime.Format(time.RFC3339), publishedAt.Format(time.RFC3339)) } else { // Cache is still valid - // Request summary if we don't have one yet, with smart retry - if cached.Summary == "" { - shouldRetry := shouldRequestSummary(cached) - if shouldRetry { - go requestArticleSummary(uri, cached) - } - } + // Summaries generated on-demand when article is viewed. return cached, false, nil } } @@ -1555,10 +1543,19 @@ func handleArticleView(w http.ResponseWriter, r *http.Request, articleID string) } } - app.Log("news", "Final title='%s', desc='%s'", title, description) + // On-demand summary generation: if the article has no summary yet, + // request one now that someone is actually reading it. Uses Haiku + // for cost efficiency. Previously this ran proactively for every + // article in the feed. + if summary == "" && articleURL != "" { + if cached, exists := loadCachedMetadata(articleURL); exists && cached.Summary == "" { + if shouldRequestSummary(cached) { + go requestArticleSummary(articleURL, cached) + } + } + } - // Don't fall back to entry.Content for description - that might contain AI-generated summary - // Keep description and summary clearly separated + app.Log("news", "Final title='%s', desc='%s'", title, description) // Build the article page imageSection := "" diff --git a/search/topics.go b/search/topics.go index 60b7966f..e6b94539 100644 --- a/search/topics.go +++ b/search/topics.go @@ -94,6 +94,7 @@ func regenerateTopics() { Return exactly 10 topics, one per line. Nothing else.`, Question: "Extract 10 trending search topics from these recent headlines:\n\n" + strings.Join(headlines, "\n"), Priority: ai.PriorityLow, + Model: "claude-haiku-4-5-20251001", Caller: "topic-generation", } From 5e75904b5d9b51222f2c573c55770c2d3d2f3081 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 25 Apr 2026 05:15:18 +0000 Subject: [PATCH 2/2] Remove weather card, move reminder to top, make console stateless MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Home layout: - Weather card removed — temp is already shown next to the date. The full forecast page at /weather still exists. - Reminder moved to first position in left column — it's the first thing you see on mobile without scrolling. - Card order: Reminder → Blog → News | Markets → Social → Video Strip system events from stream: Removed all auto-publishers (reminder, markets, news breaking) from the stream. The console was getting filled with repetitive system content that nobody asked for. The stream is now user + agent only. Console = stateless command prompt: Completely reworked. No longer a scrolling feed of history. Now: - Centered "Ask Micro anything" placeholder - Prompt at the bottom - Type a question → shows "Thinking..." → polls for agent response → displays the answer in place - Each new question clears the previous answer - Like Alexa: ask, get answer, ask again. No feed, no noise. - Logged-out users see "Log in to use the console" The stream package and /stream endpoint still exist for the MCP tool and direct access, but the home console is now purely a command interface. https://claude.ai/code/session_01GRGLA9yj7BpqKiyi6xFwnm --- home/cards.json | 34 +++++------- home/home.go | 126 ++++++++++++++++++++++++------------------- markets/markets.go | 54 ------------------- reminder/reminder.go | 9 ---- social/social.go | 9 ---- 5 files changed, 84 insertions(+), 148 deletions(-) diff --git a/home/cards.json b/home/cards.json index d5989a13..8fbdcdde 100644 --- a/home/cards.json +++ b/home/cards.json @@ -1,10 +1,18 @@ { "left": [ + { + "id": "reminder", + "title": "Reminder", + "type": "reminder", + "position": 0, + "link": "", + "icon": "/reminder.svg" + }, { "id": "blog", "title": "Blog", "type": "blog", - "position": 0, + "position": 1, "link": "/blog", "icon": "/post.png" }, @@ -12,41 +20,25 @@ "id": "news", "title": "News", "type": "news", - "position": 1, + "position": 2, "link": "/news", "icon": "/news.png" } ], "right": [ - { - "id": "weather", - "title": "Weather", - "type": "weather", - "position": 0, - "link": "/weather", - "icon": "" - }, { "id": "markets", "title": "Markets", "type": "markets", - "position": 1, + "position": 0, "link": "/markets", "icon": "/markets.svg" }, - { - "id": "reminder", - "title": "Reminder", - "type": "reminder", - "position": 2, - "link": "", - "icon": "/reminder.svg" - }, { "id": "social", "title": "Social", "type": "social", - "position": 3, + "position": 1, "link": "/social", "icon": "" }, @@ -54,7 +46,7 @@ "id": "video", "title": "Video", "type": "video", - "position": 4, + "position": 2, "link": "/video", "icon": "/video.png" } diff --git a/home/home.go b/home/home.go index 719a3a40..80d714b6 100644 --- a/home/home.go +++ b/home/home.go @@ -327,19 +327,20 @@ document.getElementById('home-date-weather').textContent=w.temp+'°C '+(e||''); } b.WriteString(``) - // ── Console view (stream) ── - consoleEvents := stream.Recent(stream.StreamLimit, viewerID) - consoleEvents = stream.DedupeAdjacent(consoleEvents) - b.WriteString(`