Curation v2 step 2: Claude Opus 5 editor, the Brief, per-provider budgets
- AnthropicBackend (Messages API, cached system block, output_config.effort,
server-side fallbacks, refusal surfaced as an error); Llms { bulk, editor }
with editor_or_bulk(); PriceTable-based UsageMeter per provider.
- [anthropic], [editorial], deepseek.max_concurrent_requests and
curation.max_article_count config; startup logs resolved providers.
- Budget day is the UTC date of started_at, preloaded from
runs.provider_costs_json; finish_run writes provider_costs_json and
config_json. Stage A batches run concurrently with per-batch budget checks.
- Editor prompt with one-line "why" per pick; no minimum lineup size;
--max-articles is a ceiling; top-up branch deleted; why stored on picks and
issue_articles.why and rendered in chapters and In this issue.
- Summaries on the editor client (3k-token input, concurrency 4, bulk then
excerpt fallback); "The Brief" replaces From the Editor; section intros gone.
- Colophon carries per-provider costs and models.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A1rCLQeKBgnBo3oTgHuTMe
This commit is contained in:
+28
-12
@@ -39,6 +39,7 @@ struct IndexEntry {
|
||||
source: String,
|
||||
reading_minutes: i64,
|
||||
summary: String,
|
||||
why: Option<String>,
|
||||
}
|
||||
|
||||
struct IndexSection {
|
||||
@@ -59,7 +60,6 @@ struct InThisIssue {
|
||||
struct SectionPage {
|
||||
title: String,
|
||||
name: String,
|
||||
intro: Option<String>,
|
||||
}
|
||||
|
||||
struct RatingLinks {
|
||||
@@ -76,6 +76,7 @@ struct ArticleChapter {
|
||||
byline: Option<String>,
|
||||
meta_line: String,
|
||||
social_line: Option<String>,
|
||||
why: Option<String>,
|
||||
summary: Option<String>,
|
||||
excerpt_only: bool,
|
||||
body_html: String,
|
||||
@@ -108,7 +109,10 @@ struct ColophonChapter {
|
||||
issue_number: i64,
|
||||
display_date: String,
|
||||
generated_at: String,
|
||||
model: String,
|
||||
bulk_model: String,
|
||||
editor_model: String,
|
||||
summaries_model: String,
|
||||
provider_costs: Vec<ProviderCostLine>,
|
||||
entries_fetched: i64,
|
||||
feeds_seen: i64,
|
||||
candidates: i64,
|
||||
@@ -120,6 +124,11 @@ struct ColophonChapter {
|
||||
generator_version: String,
|
||||
}
|
||||
|
||||
struct ProviderCostLine {
|
||||
provider: String,
|
||||
cost: String,
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Chapters (§3.10)
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -274,6 +283,7 @@ pub fn render_in_this_issue(issue: &Issue) -> Result<Chapter, EpubError> {
|
||||
source: pick.article.feed_title.clone(),
|
||||
reading_minutes: pick.article.reading_minutes(),
|
||||
summary: summary_for(issue, pick).unwrap_or_default().to_string(),
|
||||
why: pick.why.clone(),
|
||||
})
|
||||
.collect();
|
||||
sections.push(IndexSection { name, entries });
|
||||
@@ -287,6 +297,7 @@ pub fn render_in_this_issue(issue: &Issue) -> Result<Chapter, EpubError> {
|
||||
source: "Wikipedia Current Events".into(),
|
||||
reading_minutes: 3,
|
||||
summary: "The day's events, as recorded by the Current Events portal.".into(),
|
||||
why: None,
|
||||
}],
|
||||
});
|
||||
}
|
||||
@@ -304,14 +315,11 @@ pub fn render_in_this_issue(issue: &Issue) -> Result<Chapter, EpubError> {
|
||||
})
|
||||
}
|
||||
|
||||
/// A section title page: name + LLM intro (§3.10).
|
||||
pub fn render_section_page(name: &str, intro: Option<&str>) -> Result<Chapter, EpubError> {
|
||||
/// A section title page: the name only (§14.2 removed the LLM intros).
|
||||
pub fn render_section_page(name: &str) -> Result<Chapter, EpubError> {
|
||||
let tpl = SectionPage {
|
||||
title: name.to_string(),
|
||||
name: name.to_string(),
|
||||
intro: intro
|
||||
.map(|s| s.trim().to_string())
|
||||
.filter(|s| !s.is_empty()),
|
||||
};
|
||||
Ok(Chapter {
|
||||
id: format!("sec-{name}"),
|
||||
@@ -364,6 +372,7 @@ pub fn render_article(
|
||||
byline: article.author.as_ref().map(|a| format!("By {a}")),
|
||||
meta_line: meta_parts.join(" \u{00b7} "),
|
||||
social_line: social_line(&article.social),
|
||||
why: pick.why.clone(),
|
||||
summary: summary_for(issue, pick).map(str::to_string),
|
||||
excerpt_only: article.excerpt_only,
|
||||
body_html: prepare_body(&article.content_html, images_),
|
||||
@@ -429,16 +438,23 @@ pub fn render_world_briefing(issue: &Issue) -> Result<Option<Chapter>, EpubError
|
||||
/// Colophon: generation timestamp, models used, token cost, feed counts (§3.10).
|
||||
pub fn render_colophon(issue: &Issue) -> Result<Chapter, EpubError> {
|
||||
let colophon = &issue.colophon;
|
||||
let provider_costs = colophon
|
||||
.provider_costs
|
||||
.iter()
|
||||
.map(|(provider, cost)| ProviderCostLine {
|
||||
provider: provider.clone(),
|
||||
cost: format!("${cost:.4}"),
|
||||
})
|
||||
.collect();
|
||||
let tpl = ColophonChapter {
|
||||
title: "Colophon".into(),
|
||||
issue_number: issue.meta.issue_number,
|
||||
display_date: issue.meta.display_date.clone(),
|
||||
generated_at: issue.meta.generated_at.to_string(),
|
||||
model: if colophon.model.is_empty() {
|
||||
"none (heuristic selection)".into()
|
||||
} else {
|
||||
colophon.model.clone()
|
||||
},
|
||||
bulk_model: colophon.models.bulk.clone(),
|
||||
editor_model: colophon.models.editor.clone(),
|
||||
summaries_model: colophon.models.summaries.clone(),
|
||||
provider_costs,
|
||||
entries_fetched: colophon.entries_fetched,
|
||||
feeds_seen: colophon.feeds_seen,
|
||||
candidates: colophon.candidates,
|
||||
|
||||
Reference in New Issue
Block a user