diff --git a/guides/checks.md b/guides/checks.md index acc578b..56fdfd4 100644 --- a/guides/checks.md +++ b/guides/checks.md @@ -216,6 +216,26 @@ provider's native variable and never live in the config file or under the A provider is only selectable when its API key is present; selecting a provider whose key is missing is an error. +### Routing through Fireworks + +Fireworks exposes an Anthropic-compatible Messages endpoint, so it slots into +the `anthropic` provider rather than needing its own provider kind: override +`base_url` to Fireworks and set `ANTHROPIC_API_KEY` to a Fireworks key +(`fw_...`). A repo-root `MultiTool.toml` doing this: + +```toml +[checks] +provider = "anthropic" +model = "accounts/fireworks/routers/glm-5p1-fast" + +[checks.providers.anthropic] +base_url = "https://api.fireworks.ai/inference" +``` + +The `model` must still be a known ID in `ANTHROPIC_MODELS` +(`src/checks/config/models.rs`) — add the Fireworks model ID you want there +before pointing `base_url` at it. + ## ⚠️ MVP constraints - **macOS only** — copy-on-write sandboxing uses APFS `clonefile`. Linux and diff --git a/src/checks/config/models.rs b/src/checks/config/models.rs index bb1d40d..0db182e 100644 --- a/src/checks/config/models.rs +++ b/src/checks/config/models.rs @@ -8,8 +8,17 @@ use super::schema::ProviderKind; -/// Valid Anthropic model IDs. -pub const ANTHROPIC_MODELS: &[&str] = &["claude-opus-4-8", "claude-sonnet-4-6", "claude-haiku-4-5"]; +/// Valid Anthropic model IDs. Also includes Fireworks model IDs usable via +/// Fireworks' Anthropic-compatible Messages endpoint (`[checks.providers.anthropic].base_url` +/// pointed at `https://api.fireworks.ai/inference`) — Fireworks speaks the same +/// wire format, so it slots into the `anthropic` provider rather than needing +/// its own [`ProviderKind`]. +pub const ANTHROPIC_MODELS: &[&str] = &[ + "claude-opus-4-8", + "claude-sonnet-4-6", + "claude-haiku-4-5", + "accounts/fireworks/routers/glm-5p1-fast", +]; /// Valid OpenAI model IDs. pub const OPENAI_MODELS: &[&str] = &["gpt-4o", "gpt-4o-mini"]; diff --git a/src/checks/mod.rs b/src/checks/mod.rs index 554729e..c937064 100644 --- a/src/checks/mod.rs +++ b/src/checks/mod.rs @@ -156,7 +156,7 @@ fn spawn_core( oneshot::Receiver, ) { let (tx, rx) = oneshot::channel(); - let presenter = PresenterActor::spawn(PresenterActor::new(backend)); + let presenter = PresenterActor::spawn(PresenterActor::new(backend, cfg.model.clone())); let reporting = ReportingActor::spawn(ReportingActor::new(tx)); let execution = ExecutionActor::spawn(ExecutionActor::new( executor, diff --git a/src/checks/presenter/heartbeat.rs b/src/checks/presenter/heartbeat.rs index 536fd40..e3d9ab1 100644 --- a/src/checks/presenter/heartbeat.rs +++ b/src/checks/presenter/heartbeat.rs @@ -40,7 +40,8 @@ impl HeartbeatBackend { } } - /// The heartbeat text, e.g. `[multi] 7/12 checks complete · 4 running · 3m12s`. + /// The heartbeat text, e.g. + /// `[multi] 7/12 checks complete · 4 running · 3m12s · claude-sonnet-4-6`. /// Returns `None` before there's anything meaningful to report. fn line(&self, state: &PresenterState) -> Option { let total = state.total?; @@ -49,9 +50,10 @@ impl HeartbeatBackend { } let elapsed = human_elapsed(state.run_started.elapsed()); Some(format!( - "[multi] {}/{total} checks complete · {} running · {elapsed}", + "[multi] {}/{total} checks complete · {} running · {elapsed} · {}", state.done(), state.running(), + state.model, )) } @@ -123,7 +125,7 @@ mod tests { #[test] fn no_line_until_total_is_known() { let backend = HeartbeatBackend::new(false); - let mut state = PresenterState::new(); + let mut state = PresenterState::new("test-model".into()); queued(&mut state, 0); // Total unknown ⇒ nothing to print yet. assert!(backend.line(&state).is_none()); @@ -131,12 +133,13 @@ mod tests { state.apply(&UiEvent::DiscoveryComplete { total_checks: 2 }); let line = backend.line(&state).expect("line once total is known"); assert!(line.starts_with("[multi] 0/2 checks complete"), "{line}"); + assert!(line.ends_with("test-model"), "{line}"); } #[test] fn line_reflects_done_and_running_counts() { let backend = HeartbeatBackend::new(false); - let mut state = PresenterState::new(); + let mut state = PresenterState::new("test-model".into()); queued(&mut state, 0); queued(&mut state, 1); state.apply(&UiEvent::DiscoveryComplete { total_checks: 2 }); @@ -157,7 +160,7 @@ mod tests { #[test] fn empty_suite_emits_nothing() { let backend = HeartbeatBackend::new(true); - let mut state = PresenterState::new(); + let mut state = PresenterState::new("test-model".into()); state.apply(&UiEvent::DiscoveryComplete { total_checks: 0 }); assert!(backend.line(&state).is_none()); } diff --git a/src/checks/presenter/inline.rs b/src/checks/presenter/inline.rs index 61b1347..299d603 100644 --- a/src/checks/presenter/inline.rs +++ b/src/checks/presenter/inline.rs @@ -340,8 +340,9 @@ fn live_lines( .unwrap_or_else(|| "?".to_string()); let elapsed = human_elapsed(state.run_started.elapsed()); lines.push(Line::raw(format!( - "checks {bar} {done}/{total_str} ✓{sat} ✗{failed} ⚠{errored} · {} running · {elapsed}", + "checks {bar} {done}/{total_str} ✓{sat} ✗{failed} ⚠{errored} · {} running · {elapsed} · {}", state.running(), + state.model, ))); // Order: requirements with any running/retrying check first (liveness), then @@ -453,6 +454,16 @@ mod tests { assert_eq!(rendered[1], report_text(&failing)); } + /// The live header names the model in use, so a run against a non-default + /// provider/model is visibly distinguishable from one against the default. + #[test] + fn header_shows_the_model() { + let state = PresenterState::new("claude-sonnet-4-6".into()); + let lines = live_lines(&state, &HashSet::new(), 0, GAUGE_WIDTH); + let header: String = lines[0].spans.iter().map(|s| s.content.as_ref()).collect(); + assert!(header.ends_with("claude-sonnet-4-6"), "{header}"); + } + #[test] fn satisfied_record_is_single_line() { let out = outcome("OK", true, vec![]); diff --git a/src/checks/presenter/mod.rs b/src/checks/presenter/mod.rs index 4e2260e..32258e2 100644 --- a/src/checks/presenter/mod.rs +++ b/src/checks/presenter/mod.rs @@ -106,10 +106,10 @@ pub(crate) struct PresenterActor { } impl PresenterActor { - /// Build the actor over a chosen backend. - pub(crate) fn new(backend: Box) -> Self { + /// Build the actor over a chosen backend, for a run against `model`. + pub(crate) fn new(backend: Box, model: String) -> Self { Self { - state: PresenterState::new(), + state: PresenterState::new(model), backend, ticker: None, log_pump: None, @@ -265,7 +265,8 @@ mod tests { #[tokio::test] async fn actor_folds_events_and_records_them() { let (backend, events) = RecordingBackend::new(); - let presenter = PresenterActor::spawn(PresenterActor::new(Box::new(backend))); + let presenter = + PresenterActor::spawn(PresenterActor::new(Box::new(backend), "test-model".into())); presenter .tell(UiEvent::CheckQueued { diff --git a/src/checks/presenter/state.rs b/src/checks/presenter/state.rs index c89b4a2..0f7cd91 100644 --- a/src/checks/presenter/state.rs +++ b/src/checks/presenter/state.rs @@ -67,6 +67,10 @@ pub(crate) struct CheckRow { /// /// [`PresenterState::apply`]: PresenterState::apply pub(crate) struct PresenterState { + /// The model running this suite's checks, shown in the header so a run + /// against a non-default provider/model doesn't look identical to one + /// against the default. + pub model: String, /// When the run began (for the total-elapsed header counter). pub run_started: Instant, /// Total checks to expect; `None` until `DiscoveryComplete`. @@ -82,8 +86,9 @@ pub(crate) struct PresenterState { impl PresenterState { /// A fresh state stamped with the run's start instant. - pub(crate) fn new() -> Self { + pub(crate) fn new(model: String) -> Self { Self { + model, run_started: Instant::now(), total: None, discovery_complete: false, @@ -240,7 +245,7 @@ mod tests { #[test] fn tallies_recompute_from_rows_across_a_retry() { - let mut s = PresenterState::new(); + let mut s = PresenterState::new("test-model".into()); s.apply(&UiEvent::CheckQueued { id: 0, req_index: 0, @@ -274,7 +279,7 @@ mod tests { #[test] fn requirement_completion_and_outcome_use_shared_aggregation() { - let mut s = PresenterState::new(); + let mut s = PresenterState::new("test-model".into()); for (id, title) in [(0, "a"), (1, "b")] { s.apply(&UiEvent::CheckQueued { id,