use super::*;
use std::collections::BTreeSet;
use mobius::backend::model::provider::{ProviderAuth, provider};
use mobius::middleware::session_files::session_file_limits;
use crate::config::{ConfigStore, GatewayConfig};
use crate::wire::ProviderUsage;
pub(super) async fn gateway_session_summaries(
checkpoints: &Arc<dyn CheckpointStore>,
) -> Result<Vec<SessionSummary>> {
let mut cursor = None;
let mut sessions = Vec::new();
loop {
let page = checkpoints
.list_sessions_page(SessionPageRequest {
bot_id: None,
cursor,
limit: SESSION_PAGE_SIZE,
})
.await?;
sessions.extend(page.sessions);
let Some(next) = page.next_cursor else {
return Ok(sessions);
};
cursor = Some(next);
}
}
pub(super) async fn provider_usage(
config: &GatewayConfig,
store: &ConfigStore,
) -> Result<Vec<ProviderUsage>> {
let provider_ids = config
.configured_providers
.values()
.map(|configured| configured.selection.provider.clone())
.collect::<BTreeSet<_>>();
let auth_path = store.provider_auth_path();
let mut requests = Vec::new();
for provider_id in provider_ids {
let definition = provider(&provider_id)?;
let ProviderAuth::Browser(auth) = definition.auth() else {
continue;
};
if !auth
.configured(&auth_path)
.is_ok_and(|configured| configured)
{
continue;
}
let Some(fetch) = auth.usage_limits(&auth_path) else {
continue;
};
requests.push((provider_id, fetch));
}
Ok(
futures_util::future::join_all(requests.into_iter().map(|(provider, fetch)| async move {
match fetch.await {
Ok(limits) => ProviderUsage {
provider,
limits: Some(limits),
error: None,
},
Err(error) => ProviderUsage {
provider,
limits: None,
error: Some(error.to_string()),
},
}
}))
.await,
)
}
pub(super) fn session_tree_ids(
root_session_id: &str,
sessions: &[SessionSummary],
) -> Option<Vec<String>> {
sessions
.iter()
.any(|session| session.session_id == root_session_id)
.then_some(())?;
let mut seen = HashSet::from([root_session_id.to_owned()]);
let mut ordered = vec![root_session_id.to_owned()];
loop {
let mut changed = false;
for session in sessions {
if seen.contains(&session.session_id)
|| !session
.parent_session_id
.as_ref()
.is_some_and(|parent| seen.contains(parent))
{
continue;
}
seen.insert(session.session_id.clone());
ordered.push(session.session_id.clone());
changed = true;
}
if !changed {
ordered.reverse();
return Some(ordered);
}
}
}
pub(super) fn gateway_run_stats(sessions: &[SessionSummary]) -> Result<RunStats> {
let mut totals = RunStats::default();
for session in sessions {
totals
.completed
.checked_add(&session.execution_stats)
.ok_or_else(|| {
Error::Config("gateway execution statistics exceed the supported range".into())
})?;
}
Ok(totals)
}
pub(super) fn run_summary(record: ExecutionRecord) -> RunSummary {
RunSummary {
session_id: record.session_id,
submission_id: record.submission_id,
turn_id: record.turn_id,
started_at_ms: record.started_at_ms,
finished_at_ms: Some(record.finished_at_ms),
elapsed_ms: record.elapsed_ms,
outcome: Some(record.outcome),
model_calls: record.model_calls,
tool_calls: record.tool_calls,
failed_tool_calls: record.failed_tool_calls,
usage: record.usage,
}
}
pub(super) fn recent_run_groups(
records: Vec<ExecutionRecord>,
sessions: &[SessionSummary],
metadata: &SessionCatalogMetadata,
) -> Vec<SessionRunGroup> {
let sessions_by_id = sessions
.iter()
.map(|session| (session.session_id.as_str(), session))
.collect::<HashMap<_, _>>();
let mut groups: Vec<SessionRunGroup> = Vec::new();
for record in records {
let Some(root) = visible_session(&record.session_id, &sessions_by_id) else {
continue;
};
if metadata
.get(&root.session_id)
.is_some_and(|item| item.hidden)
{
continue;
}
let run = run_summary(record);
if let Some(group) = groups
.iter_mut()
.find(|group| group.session_id == root.session_id)
{
group.runs.push(run);
} else {
groups.push(SessionRunGroup {
session_id: root.session_id.clone(),
title: session_run_group_title(root, metadata),
runs: vec![run],
});
}
}
groups
}
pub(super) fn visible_session<'a>(
session_id: &str,
sessions: &'a HashMap<&str, &'a SessionSummary>,
) -> Option<&'a SessionSummary> {
let mut session = *sessions.get(session_id)?;
for _ in 0..sessions.len() {
if session.catalog_visible {
return Some(session);
}
session = *sessions.get(session.parent_session_id.as_deref()?)?;
}
None
}
pub(super) fn session_run_group_title(
session: &SessionSummary,
metadata: &SessionCatalogMetadata,
) -> String {
metadata
.get(&session.session_id)
.and_then(|item| item.title.as_deref())
.map(str::trim)
.filter(|title| !title.is_empty())
.or_else(|| {
session
.first_user_message
.as_deref()
.map(str::trim)
.filter(|title| !title.is_empty())
})
.unwrap_or("Untitled")
.to_owned()
}
pub(super) fn active_run_summary(session_id: &str, active: &ActiveExecution) -> RunSummary {
let elapsed_ms = Utc::now()
.timestamp_millis()
.checked_sub(active.started_at_ms)
.and_then(|elapsed| u64::try_from(elapsed).ok())
.unwrap_or_default();
RunSummary {
session_id: session_id.into(),
submission_id: active.submission_id.clone(),
turn_id: active.turn_id.clone(),
started_at_ms: active.started_at_ms,
finished_at_ms: None,
elapsed_ms,
outcome: None,
model_calls: active.model_calls,
tool_calls: active.tool_calls,
failed_tool_calls: active.failed_tool_calls,
usage: active.usage.clone(),
}
}
pub(super) async fn gateway_ready(
state: &GatewayState,
) -> std::result::Result<ReadyPayload, Rejection> {
let config = state
.config
.lock()
.map_err(|_| internal("gateway configuration lock is poisoned"))?
.clone();
let routes =
configured_model_routes(&config, &state.store, &state.credentials).map_err(internal)?;
let models: Vec<_> = routes.iter().map(|route| route.choice.clone()).collect();
let model_providers = routes
.into_iter()
.map(|route| (route.choice.route, route.provider.instance))
.collect();
let middleware_features = crate::middleware_manifest::features(&models);
let extensions = crate::extensions::records(&config);
let mut contributions = state.contributions.clone();
contributions.push(
state
.scratchpad
.global_contribution()
.await
.map_err(internal)?,
);
Ok(ReadyPayload {
machine_name: local_machine_name().map_err(internal)?,
bots: state.bots.bots().map_err(internal)?,
sessions: session_catalog(&state.checkpoints, &state.activities)
.await
.map_err(internal)?,
background_approvals: background_approvals(&state.checkpoints, &state.activities)
.await
.map_err(internal)?,
swarm_attentions: state.swarm.pending_attentions().await.map_err(internal)?,
swarms: state.swarm.records().await.map_err(internal)?,
providers: provider_statuses(),
provider_instances: provider_instances(&config, &state.store, &state.credentials)
.map_err(internal)?,
models,
model_providers,
bot_defaults: config.bot_defaults,
middleware_features,
extensions,
contributions,
max_active_sessions: MAX_ACTIVE_SESSIONS,
session_file_limits: session_file_limits(),
})
}
pub(super) fn local_machine_name() -> Result<String> {
let name = nix::unistd::gethostname()
.map_err(|error| Error::Config(format!("failed to read the machine hostname: {error}")))?
.into_string()
.map_err(|_| Error::Config("the machine hostname is not valid UTF-8".into()))?;
let name = name.trim();
if name.is_empty() || name.len() > 255 || name.chars().any(char::is_control) {
return Err(Error::Config("the machine hostname is invalid".into()));
}
Ok(name.to_owned())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::wire::ProviderEndpointAuth;
#[tokio::test]
async fn provider_usage_requires_configured_browser_credentials() {
let root = tempfile::tempdir().expect("state directory");
let listen = "127.0.0.1:8741".parse().expect("listen address");
let (store, config) =
ConfigStore::initialize(root.path().join("state"), listen, None).expect("config");
let api = ProviderConfig {
instance: "openrouter".into(),
provider: "openrouter".into(),
model: "openai/gpt-5".into(),
base_url: Some("https://connector.example/v1".into()),
endpoint_auth: ProviderEndpointAuth::Credentialless,
reasoning_effort: None,
web_search: mobius::backend::model::provider::HostedWebSearch::Off,
};
let codex = ProviderConfig {
instance: "codex".into(),
provider: "openai_codex".into(),
model: "gpt-5.6-sol".into(),
base_url: None,
endpoint_auth: ProviderEndpointAuth::ProviderDefault,
reasoning_effort: None,
web_search: mobius::backend::model::provider::HostedWebSearch::Off,
};
let config = config
.registering_provider(
api,
"API".into(),
Default::default(),
vec!["openai/gpt-5".into()],
Vec::new(),
)
.expect("API provider")
.registering_provider(
codex,
"Codex".into(),
Default::default(),
Vec::new(),
Vec::new(),
)
.expect("Codex provider");
assert!(
provider_usage(&config, &store)
.await
.expect("usage")
.is_empty()
);
}
}