mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-10-27 06:15:24 +00:00
Re-pin lithos-llm to 55add4596b861a0623d00c3a54aa5c147c8d504b and
pebble to c91810fe51aece80359b9cd8efea971af0c46925, where token usage
and cost travel together as Usage { tokens: TokenCounts, cost:
Option<Cost> }. Fabro now carries that one type everywhere it used to
carry BilledTokenCounts, BilledModelUsage, UsdMicros, or a token count
beside a cost_usd_micros.
fabro-types: billing.rs is usage.rs with ModelRef, ModelUsage { model,
usage }, sum_usage, and usage_is_empty; billing_rollup.rs is
usage_rollup.rs with ProjectionUsageStage, ProjectionUsageByModel,
ProjectionUsageRollup, and usage_rollup_from_projection. Every usage
field is named usage: StageProjection.usage and usage_by_model,
Outcome<Option<ModelUsage>>, stage.completed and stage.failed usage and
usage_by_model, prompt.completed usage, run.completed and run.failed
usage (total_usd_micros is gone), Conclusion.usage, StageSummary.usage,
Run.usage. RunSize buckets by Cost.
fabro-workflow: model_usage_from_llm prices tokens from the catalog with
a Catalog cost source, with_reported_cost keeps a provider cost, and the
pebble handler's stage_usage groups pebble's accounts by model and sums
rows with Usage::saturating_add, so a total has a cost only when every
priced part was priced. The store fold's live usage is the agent's
usage plus its descendants'.
API: the OpenAPI spec deletes BilledTokenCounts, BilledModelUsage,
CompletionUsage, CompletionCost, TokenUsage, and RunBillingSummary,
adds TokenCounts, Cost, Usage, and ModelUsage, and renames every
billing schema, property, tag, path, and operation to usage. fabro-api
reuses lithos-llm's and fabro-types' types through with_replacement,
with a round-trip test per replacement.
Old stored runs get no migration: their pebble events in the old shape
read back with zero usage, and their rebuilt projections lose agent
usage.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
161 lines
4.1 KiB
Rust
161 lines
4.1 KiB
Rust
use std::collections::HashMap;
|
|
use std::sync::Arc;
|
|
|
|
use anyhow::Result;
|
|
use chrono::{DateTime, Utc};
|
|
use fabro_types::{Run, RunId, RunStatus};
|
|
|
|
use crate::server_client::Client;
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub(crate) struct ServerRunInfo {
|
|
run: Run,
|
|
}
|
|
|
|
impl ServerRunInfo {
|
|
pub(crate) fn from_run(run: Run) -> Self {
|
|
Self { run }
|
|
}
|
|
|
|
pub(crate) fn run_id(&self) -> RunId {
|
|
self.run.id
|
|
}
|
|
|
|
pub(crate) fn parent_id(&self) -> Option<RunId> {
|
|
self.run.parent_id
|
|
}
|
|
|
|
pub(crate) fn workflow_name(&self) -> Option<&str> {
|
|
self.run.workflow.name.as_deref()
|
|
}
|
|
|
|
pub(crate) fn workflow_graph_name(&self) -> Option<&str> {
|
|
self.run.workflow.graph_name.as_deref()
|
|
}
|
|
|
|
pub(crate) fn workflow_slug(&self) -> Option<&str> {
|
|
self.run.workflow.slug.as_deref()
|
|
}
|
|
|
|
pub(crate) fn workflow_display_name(&self) -> String {
|
|
self.run.workflow.display_name().unwrap_or("-").to_string()
|
|
}
|
|
|
|
pub(crate) fn workflow_matches(&self, pattern: &str) -> bool {
|
|
[
|
|
self.workflow_name(),
|
|
self.workflow_graph_name(),
|
|
self.workflow_slug(),
|
|
]
|
|
.into_iter()
|
|
.flatten()
|
|
.any(|value| value.contains(pattern))
|
|
}
|
|
|
|
pub(crate) fn status(&self) -> RunStatus {
|
|
self.run.lifecycle.status
|
|
}
|
|
|
|
pub(crate) fn start_time(&self) -> String {
|
|
self.start_time_dt()
|
|
.map(|time| time.to_rfc3339())
|
|
.unwrap_or_default()
|
|
}
|
|
|
|
pub(crate) fn start_time_dt(&self) -> Option<DateTime<Utc>> {
|
|
self.run
|
|
.timestamps
|
|
.started_at
|
|
.or(Some(self.run.id.created_at()))
|
|
}
|
|
|
|
pub(crate) fn labels(&self) -> &HashMap<String, String> {
|
|
&self.run.labels
|
|
}
|
|
|
|
pub(crate) fn wall_time_ms(&self) -> Option<u64> {
|
|
self.run.timing.as_ref().map(|t| t.wall_time_ms)
|
|
}
|
|
|
|
pub(crate) fn total_usd_micros(&self) -> Option<u64> {
|
|
self.run.usage.cost.map(|cost| cost.usd_micros)
|
|
}
|
|
|
|
pub(crate) fn source_directory(&self) -> Option<&str> {
|
|
self.run.source_directory.as_deref()
|
|
}
|
|
|
|
pub(crate) fn repo_origin_url(&self) -> Option<&str> {
|
|
self.run
|
|
.repository
|
|
.as_ref()
|
|
.and_then(|repository| repository.origin_url.as_deref())
|
|
}
|
|
|
|
pub(crate) fn goal(&self) -> String {
|
|
self.run.goal.clone()
|
|
}
|
|
}
|
|
|
|
pub(crate) struct ServerRunLookup {
|
|
runs: Vec<ServerRunInfo>,
|
|
}
|
|
|
|
impl ServerRunLookup {
|
|
pub(crate) async fn from_client(client: Arc<Client>) -> Result<Self> {
|
|
let runs = client.list_store_runs().await?;
|
|
Ok(Self::from_runs(runs))
|
|
}
|
|
|
|
pub(crate) async fn from_client_by_parent(
|
|
client: Arc<Client>,
|
|
parent_id: RunId,
|
|
) -> Result<Self> {
|
|
let runs = client.list_store_runs_by_parent(parent_id).await?;
|
|
Ok(Self::from_runs(runs))
|
|
}
|
|
|
|
fn from_runs(runs: Vec<Run>) -> Self {
|
|
let mut runs = runs
|
|
.into_iter()
|
|
.map(ServerRunInfo::from_run)
|
|
.collect::<Vec<_>>();
|
|
runs.sort_by(|a, b| {
|
|
b.start_time_dt()
|
|
.cmp(&a.start_time_dt())
|
|
.then_with(|| b.run_id().cmp(&a.run_id()))
|
|
});
|
|
Self { runs }
|
|
}
|
|
|
|
pub(crate) fn runs(&self) -> &[ServerRunInfo] {
|
|
&self.runs
|
|
}
|
|
}
|
|
|
|
pub(crate) fn filter_server_runs(
|
|
runs: &[ServerRunInfo],
|
|
before: Option<&str>,
|
|
workflow: Option<&str>,
|
|
labels: &[(String, String)],
|
|
running_only: bool,
|
|
) -> Vec<ServerRunInfo> {
|
|
runs.iter()
|
|
.filter(|run| !running_only || run.status().is_active())
|
|
.filter(|run| {
|
|
before.is_none_or(|before| {
|
|
let start_time = run.start_time();
|
|
start_time.is_empty() || start_time.as_str() < before
|
|
})
|
|
})
|
|
.filter(|run| workflow.is_none_or(|pattern| run.workflow_matches(pattern)))
|
|
.filter(|run| {
|
|
labels.iter().all(|(key, value)| {
|
|
run.labels()
|
|
.get(key)
|
|
.is_some_and(|current| current == value)
|
|
})
|
|
})
|
|
.cloned()
|
|
.collect()
|
|
}
|