Bisect provider-rejected batches in triage and deep assessment

DeepSeek's content filter rejects a whole request (400 "Content Exists
Risk") when one article trips it, which cost the other articles in the
batch their assessment and retried them every run. New curate/batch.rs
runs both stages through a bisecting runner: a rejected batch is split
until the offending article is isolated, that article is retried once on
the editor provider when it is a different one, and a still-rejected
article is recorded as a provider_rejected assessment row so it is not
retried for assessment_reuse_days. Cache reuse accepts rows from either
configured model. Each stage logs reused/requested/rejected counts, the
curation: line shows rejections when non-zero, explain prints them, and
the llm_assess span reports the deep-set size.

Implemented by a Claude agent from an orchestrator brief; verified
fmt/clippy(-W dead_code)/test green (354 lib tests).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A1rCLQeKBgnBo3oTgHuTMe
This commit is contained in:
thalladaandClaude Fable 5.1 committed 2026-09-02 20:44:28 +00:00
1 parent 7a842b61b6
commit af2a3ceb49
14 files changed
+1711 -127

No files matched your search

+12 -4
View File
@@ -12,6 +12,7 @@
pub mod admit;
pub mod assess;
pub mod batch;
pub mod editor;
pub mod editorial;
pub mod embedding;
@@ -50,23 +51,30 @@ impl Curator {
/// A no-op under `--skip-llm`: like triage, nothing is read or written and
/// utility falls back to the present signals (§12.3, §17). When the bulk
/// provider is down or its budget trips, cached rows are still reused and
/// the failed batches simply stay unassessed.
/// the failed batches simply stay unassessed. A batch the provider's
/// content filter rejects is bisected, and a rejected single article is
/// retried on the editor when that is another provider (`curate::batch`).
pub async fn assess(
&self,
candidates: &mut [Candidate],
rescore: bool,
profile_version: Option<i64>,
assessed_at: Timestamp,
) -> anyhow::Result<usize> {
) -> anyhow::Result<batch::StageSummary> {
let Some(bulk) = self.llms.bulk.as_ref() else {
tracing::info!("--skip-llm: deep assessment skipped");
return Ok(0);
return Ok(batch::StageSummary::default());
};
let span = tracing::info_span!("llm_assess", candidates = candidates.len());
let admitted = candidates
.iter()
.filter(|candidate| candidate.stage == "admitted")
.count();
let span = tracing::info_span!("llm_assess", candidates = admitted);
let _guard = span.enter();
assess::run(
&self.db,
Some(bulk),
self.llms.editor.as_ref(),
&bulk.model,
candidates,
self.config.llm.deep_batch_size,