M3: models.dev catalog and cost display wiring

Adds a bundled models.dev snapshot plus harness-providers::modelsdev to resolve model metadata, and wires live cost display into the TUI's input bar and message state, updating the render snapshots accordingly.
This commit is contained in:
2026-07-10 16:19:59 +02:00
parent a99edc116a
commit ab0269df1a
16 changed files with 428 additions and 19 deletions
+20 -4
View File
@@ -19,7 +19,7 @@ use harness_core::tool::ToolRegistry;
use harness_core::types::{
Message, MessageId, ModelRef, Part, PartBody, PartId, Session, SessionId,
};
use harness_providers::{AnthropicProvider, OpenAiProvider, ProviderRegistry};
use harness_providers::{AnthropicProvider, ModelCatalog, OpenAiProvider, ProviderRegistry};
use tokio::task::JoinHandle;
use tokio_util::sync::CancellationToken;
@@ -83,6 +83,7 @@ struct EngineInner {
permissions: Arc<PermissionService>,
tools: ToolRegistry,
providers: ProviderRegistry,
catalog: ModelCatalog,
cwd: PathBuf,
data_dir: PathBuf,
runs: Mutex<HashMap<SessionId, RunHandle>>,
@@ -97,7 +98,9 @@ impl EngineHandle {
pub fn init(cwd: PathBuf) -> Result<Self, AppError> {
let path = db_path(&cwd);
let store = Store::open(&path)?;
Self::new(cwd, store)
let handle = Self::new(cwd, store)?;
handle.spawn_catalog_refresh();
Ok(handle)
}
/// In-memory store — useful for tests and ephemeral sessions.
@@ -143,6 +146,10 @@ impl EngineHandle {
.join("ai-harness")
.join("tool-output");
// No network at construction: use the cached copy if fresh, else the baked snapshot.
// `init` warms the cache in the background for the next launch.
let catalog = ModelCatalog::load_cached_or_baked(&ModelCatalog::default_cache_path());
Ok(Self {
inner: Arc::new(EngineInner {
config,
@@ -151,6 +158,7 @@ impl EngineHandle {
permissions,
tools,
providers,
catalog,
cwd,
data_dir,
runs: Mutex::new(HashMap::new()),
@@ -158,6 +166,15 @@ impl EngineHandle {
})
}
/// Fire-and-forget refresh of the models.dev cache so the next launch has current pricing.
fn spawn_catalog_refresh(&self) {
tokio::spawn(async {
if let Err(e) = ModelCatalog::refresh_default_cache().await {
tracing::debug!(error = %e, "models.dev refresh failed; using cached/baked metadata");
}
});
}
pub fn bus(&self) -> EventBus {
self.inner.bus.clone()
}
@@ -254,8 +271,7 @@ impl EngineHandle {
temperature: None,
max_steps: DEFAULT_MAX_STEPS,
instructions: self.inner.config.instructions.clone(),
// Populated from models.dev metadata in the follow-up increment.
cost: None,
cost: Some(self.inner.catalog.cost(provider_id, model_id)),
};
// Reserve the run slot *before* spawning. If we inserted after spawning, a run that