Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions guides/checks.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
13 changes: 11 additions & 2 deletions src/checks/config/models.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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"];
Expand Down
2 changes: 1 addition & 1 deletion src/checks/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ fn spawn_core(
oneshot::Receiver<RunResult>,
) {
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,
Expand Down
13 changes: 8 additions & 5 deletions src/checks/presenter/heartbeat.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String> {
let total = state.total?;
Expand All @@ -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,
))
}

Expand Down Expand Up @@ -123,20 +125,21 @@ 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());

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 });
Expand All @@ -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());
}
Expand Down
13 changes: 12 additions & 1 deletion src/checks/presenter/inline.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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![]);
Expand Down
9 changes: 5 additions & 4 deletions src/checks/presenter/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -106,10 +106,10 @@ pub(crate) struct PresenterActor {
}

impl PresenterActor {
/// Build the actor over a chosen backend.
pub(crate) fn new(backend: Box<dyn RenderBackend>) -> Self {
/// Build the actor over a chosen backend, for a run against `model`.
pub(crate) fn new(backend: Box<dyn RenderBackend>, model: String) -> Self {
Self {
state: PresenterState::new(),
state: PresenterState::new(model),
backend,
ticker: None,
log_pump: None,
Expand Down Expand Up @@ -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 {
Expand Down
11 changes: 8 additions & 3 deletions src/checks/presenter/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand All @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down