Skip to main content

ralph_workflow/agents/opencode_api/
mod.rs

1//! `OpenCode` API catalog module.
2//!
3//! This module handles fetching, caching, and querying the `OpenCode` model catalog
4//! from <https://models.dev/api.json>. The catalog contains available providers and models
5//! that `OpenCode` supports, enabling dynamic agent configuration.
6//!
7//! # Module Structure
8//!
9//! - `types` - API catalog data structures
10//! - `cache` - File-based caching with TTL
11//! - `fetch` - HTTP fetching logic
12//!
13//! # Dependency Injection
14//!
15//! The [`CatalogLoader`] trait enables dependency injection for testing.
16//! Production code uses [`RealCatalogLoader`] which fetches from the network,
17//! while tests can provide mock implementations.
18
19mod boundary;
20mod cache;
21mod fetch;
22mod types;
23
24pub use cache::{load_api_catalog, CacheError, CacheWarning};
25pub use fetch::{CatalogHttpClient, RealCatalogFetcher};
26pub use types::{ApiCatalog, Model, Provider};
27
28use std::sync::Arc;
29
30/// `OpenCode` API endpoint for model catalog.
31pub const API_URL: &str = "https://models.dev/api.json";
32
33/// Default cache TTL in seconds (24 hours).
34pub const DEFAULT_CACHE_TTL_SECONDS: u64 = 24 * 60 * 60;
35
36/// Environment variable for customizing cache TTL.
37pub const CACHE_TTL_ENV_VAR: &str = "RALPH_OPENCODE_CACHE_TTL_SECONDS";
38
39/// Trait for loading the `OpenCode` API catalog.
40///
41/// This trait enables dependency injection for catalog loading, allowing
42/// tests to provide mock implementations that don't make network calls.
43pub trait CatalogLoader: Send + Sync {
44    /// Load the API catalog.
45    ///
46    /// Returns the catalog or an error if loading fails.
47    ///
48    /// # Errors
49    ///
50    /// Returns error if the operation fails.
51    fn load(&self) -> Result<ApiCatalog, CacheError>;
52}
53
54/// Production implementation of [`CatalogLoader`] that fetches from the network.
55///
56/// This loader uses the standard caching mechanism:
57/// 1. Check for a valid cached catalog
58/// 2. If cache is missing or expired, fetch from the API
59/// 3. Cache the fetched result for future use
60
61#[derive(Clone)]
62pub struct RealCatalogLoader {
63    fetcher: Arc<dyn fetch::CatalogHttpClient>,
64}
65
66impl RealCatalogLoader {
67    /// Create a new catalog loader with the given HTTP client.
68    pub fn new(fetcher: Arc<dyn fetch::CatalogHttpClient>) -> Self {
69        Self { fetcher }
70    }
71
72    /// Convenience constructor that wraps the client in an [`Arc`].
73    pub fn with_fetcher<F>(fetcher: F) -> Self
74    where
75        F: fetch::CatalogHttpClient + 'static,
76    {
77        Self::new(Arc::new(fetcher))
78    }
79}
80
81impl CatalogLoader for RealCatalogLoader {
82    fn load(&self) -> Result<ApiCatalog, CacheError> {
83        let (catalog, warnings) = cache::load_api_catalog(self.fetcher.as_ref())?;
84        warnings.into_iter().for_each(|warning| {
85            match warning {
86                CacheWarning::StaleCacheUsed { stale_days, error } => {
87                    eprintln!("Warning: Failed to fetch fresh OpenCode API catalog ({error}), using stale cache from {stale_days} days ago");
88                }
89                CacheWarning::CacheSaveFailed { error } => {
90                    eprintln!("Warning: Failed to cache OpenCode API catalog: {error}");
91                }
92            }
93        });
94        Ok(catalog)
95    }
96}