feat: Handle analyze retry with failure marker
When a previous analysis attempt fails and leaves a failure marker, the retry mechanism now correctly identifies this state. It removes the stale input file, allowing the retry to proceed without a conflict error. This ensures users can recover from analysis failures more gracefully.
This commit is contained in:
@@ -119,6 +119,11 @@ impl LlmBackend {
|
||||
self.format_instruction = Some(instruction.into());
|
||||
self
|
||||
}
|
||||
|
||||
fn with_max_completion_tokens(mut self, tokens: u32) -> Self {
|
||||
self.max_completion_tokens = tokens;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// Ionos provider: shared endpoint, `IONOS_API_KEY` env var as bearer token.
|
||||
@@ -149,15 +154,17 @@ pub fn backends() -> &'static [LlmBackend] {
|
||||
static B: OnceLock<Vec<LlmBackend>> = OnceLock::new();
|
||||
B.get_or_init(|| {
|
||||
vec![
|
||||
// gpt-oss-120b: schema OFF on purpose. With `reasoning_effort:
|
||||
// medium`, reasoning tokens count against `max_completion_tokens`
|
||||
// (4096); on large bodies (e.g. 6 recordings, ~3.8k chars) the
|
||||
// reasoning eats the budget and the model returns
|
||||
// `choices[0].message.content == null` — the LlmError::Parse path.
|
||||
// Pre-multi-backend (commit bf6464d) gpt-oss ran without schema
|
||||
// and worked fine; this restores that body byte-for-byte.
|
||||
// gpt-oss-120b: schema OFF on purpose. Reasoning tokens count
|
||||
// against `max_completion_tokens`; with `high` reasoning the
|
||||
// CoT alone can eat several thousand tokens, so the budget is
|
||||
// doubled to 8192 to leave room for the actual answer. Without
|
||||
// headroom the model returns `choices[0].message.content == null`
|
||||
// — the LlmError::Parse path. Pre-multi-backend (commit bf6464d)
|
||||
// gpt-oss ran without schema and worked fine; this restores
|
||||
// that body byte-for-byte except for the larger budget.
|
||||
ionos_backend("gpt_oss_120b", "GPT OSS 120b", "openai/gpt-oss-120b")
|
||||
.with_reasoning_effort("medium")
|
||||
.with_reasoning_effort("high")
|
||||
.with_max_completion_tokens(8192)
|
||||
.with_use_json_schema(false),
|
||||
ionos_backend(
|
||||
"llama_3_1_405b",
|
||||
@@ -234,7 +241,7 @@ mod tests {
|
||||
fn reasoning_effort_is_set_per_model_family() {
|
||||
assert_eq!(
|
||||
find_backend("gpt_oss_120b").unwrap().reasoning_effort,
|
||||
Some("medium".to_string())
|
||||
Some("high".to_string())
|
||||
);
|
||||
assert!(
|
||||
find_backend("llama_3_1_405b")
|
||||
@@ -304,11 +311,11 @@ mod tests {
|
||||
}
|
||||
|
||||
/// gpt-oss-120b runs in plain-text mode (no `response_format`, no
|
||||
/// `top_p`, no second system message). Reason: with
|
||||
/// `reasoning_effort: medium`, schema-constrained decoding plus reasoning
|
||||
/// can exhaust `max_completion_tokens` on large bodies and return
|
||||
/// `content: null`. This pin guarantees the body sent to Ionos stays
|
||||
/// byte-identical to the pre-multi-backend path (commit bf6464d).
|
||||
/// `top_p`, no second system message). Reason: schema-constrained
|
||||
/// decoding plus any non-zero `reasoning_effort` can exhaust
|
||||
/// `max_completion_tokens` on large bodies and return `content: null`.
|
||||
/// This pin guarantees the body sent to Ionos stays byte-identical to
|
||||
/// the pre-multi-backend path (commit bf6464d).
|
||||
#[test]
|
||||
fn gpt_oss_runs_in_plain_text_mode_no_schema_no_top_p_no_format_hint() {
|
||||
let b = find_backend("gpt_oss_120b").unwrap();
|
||||
|
||||
@@ -14,6 +14,7 @@ use tracing::{info, warn};
|
||||
use doctate_common::timestamp::{filename_stem_to_recorded_at, now_rfc3339};
|
||||
use doctate_common::{RECORDING_META_SUFFIX, TranscriptState};
|
||||
|
||||
use crate::analyze::auto_trigger::FAILURE_MARKER_FILE;
|
||||
use crate::analyze::backend::{any_backend_available, find_backend};
|
||||
use crate::analyze::{
|
||||
ANALYSIS_INPUT_FILE, AnalysisInput, AnalyzeJob, AnalyzeSender, DOCUMENT_FILE, RecordingInput,
|
||||
@@ -107,6 +108,17 @@ pub async fn handle_analyze_case(
|
||||
|
||||
let input = build_analysis_input(&case_dir, &backend_id).await?;
|
||||
let input_path = case_dir.join(ANALYSIS_INPUT_FILE);
|
||||
|
||||
// Manual retry path: a failure marker means the worker has explicitly
|
||||
// given up on the prior input. Drop the stale sentinel so create_new
|
||||
// does not reject the legitimate retry as "Analyse läuft bereits".
|
||||
// The marker itself is left in place — the worker overwrites it on
|
||||
// re-failure and removes it on success, so the UI's "analyzing" flag
|
||||
// (driven by input file presence) takes over from the next render.
|
||||
if case_dir.join(FAILURE_MARKER_FILE).exists() {
|
||||
remove_if_exists(&input_path).await?;
|
||||
}
|
||||
|
||||
write_input_create_new(&input_path, &input).await?;
|
||||
|
||||
// Unbounded send: only fails if the worker has gone away (programmer error
|
||||
|
||||
Reference in New Issue
Block a user