harness-providers: models.dev catalog + cost display wiring (M3)
- modelsdev.rs: ModelCatalog parses models.dev api.json → ModelInfo keyed by (provider, model); 24h file cache at ~/.cache/ai-harness/models.json with a baked assets/models-snapshot.json fallback so cost/limits work offline. load_cached_or_baked (no network) + refresh/refresh_default_cache (background). - App: loads the catalog at init (cached-or-baked, never blocks), warms the cache in a background task, and passes the session model's pricing into RunConfig.cost so session cost accrues for real. - TUI: AppState tracks session_cost/session_tokens from Session events; status bar shows "<tokens> · $<cost>". Snapshots updated. - 7 modelsdev tests (parse, defaults, unknown-model, baked snapshot, cache TTL, cached-vs-baked load). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user