Skip to content
Open
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
203 changes: 201 additions & 2 deletions src/workers/continuum-core/src/modules/ai_provider.rs
Original file line number Diff line number Diff line change
Expand Up @@ -182,8 +182,91 @@ impl AIProviderModule {
/// - "asked for local but DMR is down" (Docker Desktop needs to be running)
/// - "asked for a specific provider/model that isn't here" (existing message)
///
/// Hoisted out of both `ai/generate` and the convenience `generate_text` so
/// the two paths report the same diagnosis.
/// The set of provider ids that count as "offline / network-
/// independent" for the boot-status `adapter:` line. A host with
/// any of these registered has an inference path that survives a
/// WAN outage and is reported as `Ok` regardless of which cloud
/// providers also registered.
///
/// Sourced from the substrate's actual provider-id constants — NOT
/// a hand-rolled prefix match — so a rename in `LLAMACPP_PROVIDER_ID`
/// or a future `LOCAL_*` constant lands the boot-status check
/// automatically. PR #1553 round 1 reviewer caught two failure
/// modes the prefix shape produced:
///
/// 1. `"docker-model-runner"` (DMR) didn't start with `llamacpp-`,
/// so a host running DMR-only got `Degraded: "cloud only ..."` —
/// factually false. DMR runs against localhost:12434, network-
/// independent.
/// 2. The `register_adapters` flow uses the single
/// `LLAMACPP_PROVIDER_ID = "llamacpp-local"` constant — there is
/// no per-model `"llamacpp-qwen3-coder"` registered name. The
/// prefix matched anyway by coincidence; the prior tests pinned
/// a string shape that production never produces.
///
/// Identifier strings, not a trait check on `dyn AIProviderAdapter`,
/// because the registry's `available()` returns `Vec<&str>` of
/// provider ids — the trait isn't reachable from the helper without
/// holding the registry lock. The trait-method refactor
/// (`AIProviderAdapter::is_offline_path()`) is a follow-up.
const OFFLINE_PROVIDER_IDS: &[&str] = &[
crate::inference::LLAMACPP_PROVIDER_ID,
// DMR registers under "docker-model-runner". Localhost via
// Docker Desktop's model runner socket — survives a WAN
// outage as long as Docker Desktop is running.
"docker-model-runner",
];

/// Pure projection of the AdapterRegistry's `available()` set → a
/// boot-status `(kind, detail)` pair. Card e9f50a36 slice A4.
///
/// Mapping:
/// - **Empty** → `Failed`. The substrate has zero inference
/// capability; persona cognition on `FullCitizen` / `FailFast`
/// will refuse to boot downstream.
/// - **No offline provider registered** → `Degraded`, `"cloud only:
/// <names> (no local fallback if cloud unreachable)"`. The
/// substrate runs but has no offline path — a single network
/// outage takes down inference. Operator wants to know.
/// - **At least one offline provider** (`llamacpp-local`,
/// `docker-model-runner`, etc — see [`OFFLINE_PROVIDER_IDS`]) →
/// `Ok`, `"<names> (N providers)"`. Mixed cloud + offline OR
/// offline-only both land here; substrate has at least one path
/// that survives WAN loss.
///
/// The kind ordering matches the rest of the slice A boot lines:
/// `Ok < Degraded < Failed` per `BootStatusKind`'s derived `Ord`,
/// so a sentinel computing "worst kind across boot" with `.max()`
/// reports the right verdict.
fn render_adapter_boot_status(
available: &[&str],
) -> (crate::runtime::boot_status::BootStatusKind, String) {
use crate::runtime::boot_status::BootStatusKind;
if available.is_empty() {
return (
BootStatusKind::Failed,
"no inference adapter registered — add API keys to \
~/.continuum/config.env or pull a local GGUF"
.to_string(),
);
}
let has_offline = available
.iter()
.any(|name| OFFLINE_PROVIDER_IDS.contains(name));
let names_joined = available.join(", ");
if has_offline {
(
BootStatusKind::Ok,
format!("{names_joined} ({} providers)", available.len()),
)
} else {
(
BootStatusKind::Degraded,
format!("cloud only: {names_joined} (no local fallback if cloud unreachable)"),
)
}
}

fn select_failure_message(
registry: &AdapterRegistry,
requested_provider: Option<&str>,
Expand Down Expand Up @@ -585,6 +668,15 @@ impl AIProviderModule {
.warn("No providers available! Add API keys to ~/.continuum/config.env");
}

// Boot status report (card e9f50a36 slice A4): a single line
// naming the registered adapters so the operator sees what
// inference capabilities the substrate actually has, not
// what *should* be available. Empty = Failed (no inference
// possible). All cloud + no local = Degraded (no offline
// path). Mixed or local-only = Ok.
let (kind, detail) = render_adapter_boot_status(&available);
crate::runtime::boot_status::boot_status("adapter", kind, &detail);

Ok(())
}

Expand Down Expand Up @@ -1120,3 +1212,110 @@ pub async fn generate_text(

Ok(response)
}

#[cfg(test)]
mod boot_status_tests {
use super::*;
use crate::runtime::boot_status::BootStatusKind;

/// Empty registry = no inference at all. The substrate has
/// nothing to route to. Operator action lives in the detail.
#[test]
fn empty_registry_reports_failed_with_remediation() {
let (kind, detail) = render_adapter_boot_status(&[]);
assert_eq!(kind, BootStatusKind::Failed);
assert!(
detail.contains("config.env") && detail.contains("GGUF"),
"detail must name both remediation paths: {detail:?}"
);
}

/// Cloud-only (no offline provider registered) = `Degraded`.
/// The substrate is "working" but a single network blip kills
/// inference for every persona. Operator wants to know.
#[test]
fn cloud_only_reports_degraded_with_no_local_fallback_note() {
let providers = ["anthropic", "openai", "groq"];
let (kind, detail) = render_adapter_boot_status(&providers);
assert_eq!(kind, BootStatusKind::Degraded);
assert!(
detail.contains("cloud only"),
"detail must say cloud-only: {detail:?}"
);
assert!(
detail.contains("no local fallback"),
"detail must call out the missing fallback: {detail:?}"
);
}

/// `LLAMACPP_PROVIDER_ID` registered = `Ok`. PR #1553 round 1
/// reviewer caught the prior test using a fictitious
/// `"llamacpp-qwen3-coder"` id that production never emits —
/// `register_adapters` uses the single `LLAMACPP_PROVIDER_ID`
/// constant ("llamacpp-local") and multiplexes models within
/// the adapter. Pinning the actual constant here protects
/// against a future rename silently changing the boot-status
/// classification.
#[test]
fn llamacpp_local_reports_ok() {
let providers = [
"anthropic",
crate::inference::LLAMACPP_PROVIDER_ID,
"openai",
];
let (kind, detail) = render_adapter_boot_status(&providers);
assert_eq!(kind, BootStatusKind::Ok);
assert!(
detail.contains(crate::inference::LLAMACPP_PROVIDER_ID),
"detail must name the local provider so the operator \
can spot wrong-model-loaded at a glance: {detail:?}"
);
assert!(
detail.contains("3 providers"),
"detail must include count for quick visual scan: {detail:?}"
);
}

/// PR #1553 round 1 reviewer's primary BLOCK: DMR is
/// network-independent (runs against `localhost:12434` via
/// Docker Desktop's model runner socket). The prior prefix
/// match shape misclassified a DMR-only host as `Degraded:
/// "cloud only ..."` — a load-bearing observability lie. With
/// the `OFFLINE_PROVIDER_IDS` set this case correctly reports
/// `Ok`.
///
/// Pins the exact production registration id from
/// `model_registry/catalog.rs:471` so a future provider-id
/// drift fails this test instead of silently misclassifying.
#[test]
fn dmr_only_reports_ok_not_cloud_only() {
let providers = ["docker-model-runner"];
let (kind, detail) = render_adapter_boot_status(&providers);
assert_eq!(
kind,
BootStatusKind::Ok,
"DMR is localhost-only (network-independent) and must \
classify as an offline provider, not as a cloud lie. \
Got {kind:?} for detail {detail:?}"
);
assert!(
!detail.contains("cloud"),
"DMR-only must not surface a cloud-fallback warning: {detail:?}"
);
}

/// Local-only also reports Ok — a host with no API keys but
/// a local GGUF is the "M1 32GB offline" doctrine case Joel's
/// canonical workflow targets.
#[test]
fn local_only_reports_ok_without_cloud_warning() {
let providers = [crate::inference::LLAMACPP_PROVIDER_ID];
let (kind, detail) = render_adapter_boot_status(&providers);
assert_eq!(kind, BootStatusKind::Ok);
assert!(
!detail.contains("cloud"),
"local-only must not mention cloud in the detail (no \
warning to surface): {detail:?}"
);
}
}
Loading