Support non interactive mode and model selection

This commit is contained in:
Illia Polosukhin
2026-02-03 14:47:26 -08:00
parent 2cc9aed364
commit 2c26ba8431
17 changed files with 999 additions and 141 deletions
+1 -1
View File
@@ -7,7 +7,7 @@ mod provider;
mod reasoning;
pub mod session;
pub use nearai::NearAiProvider;
pub use nearai::{ModelInfo, NearAiProvider};
pub use provider::{
ChatMessage, CompletionRequest, CompletionResponse, LlmProvider, Role, ToolCall,
ToolCompletionRequest, ToolCompletionResponse, ToolDefinition, ToolResult,
+134
View File
@@ -20,6 +20,17 @@ use crate::llm::provider::{
};
use crate::llm::session::SessionManager;
/// Information about an available model from NEAR AI API.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ModelInfo {
/// Model identifier.
#[serde(alias = "id", alias = "model")]
pub name: String,
/// Optional provider name.
#[serde(default)]
pub provider: Option<String>,
}
/// NEAR AI Chat API provider.
pub struct NearAiProvider {
client: Client,
@@ -50,6 +61,123 @@ impl NearAiProvider {
)
}
/// Fetch available models from the NEAR AI API.
pub async fn list_models(&self) -> Result<Vec<ModelInfo>, LlmError> {
use secrecy::ExposeSecret;
let token = self.session.get_token().await?;
let url = self.api_url("model/list");
tracing::debug!("Fetching models from: {}", url);
let response = self
.client
.get(&url)
.header("Authorization", format!("Bearer {}", token.expose_secret()))
.send()
.await
.map_err(|e| LlmError::RequestFailed {
provider: "nearai".to_string(),
reason: format!("Failed to fetch models: {}", e),
})?;
let status = response.status();
let response_text = response.text().await.unwrap_or_default();
if !status.is_success() {
// Check for session expiration
if status.as_u16() == 401 {
return Err(LlmError::SessionExpired {
provider: "nearai".to_string(),
});
}
return Err(LlmError::RequestFailed {
provider: "nearai".to_string(),
reason: format!("HTTP {}: {}", status, response_text),
});
}
// Parse the response - NEAR AI returns {"limit": N, "models": [...]}
// Each model object may have the name in different fields
#[derive(Deserialize)]
struct ModelMetadata {
#[serde(default)]
name: Option<String>,
#[serde(default, alias = "modelName", alias = "model_name")]
model_name: Option<String>,
}
#[derive(Deserialize)]
struct ModelEntry {
#[serde(default)]
name: Option<String>,
#[serde(default)]
id: Option<String>,
#[serde(default)]
model: Option<String>,
#[serde(default, alias = "modelName", alias = "model_name")]
model_name: Option<String>,
#[serde(default, alias = "modelId", alias = "model_id")]
model_id: Option<String>,
#[serde(default)]
metadata: Option<ModelMetadata>,
}
impl ModelEntry {
fn get_name(&self) -> Option<String> {
self.name
.clone()
.or_else(|| self.id.clone())
.or_else(|| self.model.clone())
.or_else(|| self.model_name.clone())
.or_else(|| self.model_id.clone())
.or_else(|| self.metadata.as_ref().and_then(|m| m.name.clone()))
.or_else(|| self.metadata.as_ref().and_then(|m| m.model_name.clone()))
}
}
#[derive(Deserialize)]
struct ModelsResponse {
#[serde(default)]
models: Option<Vec<ModelEntry>>,
#[serde(default)]
data: Option<Vec<ModelEntry>>,
}
if let Ok(resp) = serde_json::from_str::<ModelsResponse>(&response_text) {
if let Some(entries) = resp.models.or(resp.data) {
let models: Vec<ModelInfo> = entries
.into_iter()
.filter_map(|e| e.get_name().map(|name| ModelInfo { name, provider: None }))
.collect();
if !models.is_empty() {
return Ok(models);
}
}
}
// Try direct array format
if let Ok(entries) = serde_json::from_str::<Vec<ModelEntry>>(&response_text) {
let models: Vec<ModelInfo> = entries
.into_iter()
.filter_map(|e| e.get_name().map(|name| ModelInfo { name, provider: None }))
.collect();
if !models.is_empty() {
return Ok(models);
}
}
// Couldn't find model names in response
Err(LlmError::InvalidResponse {
provider: "nearai".to_string(),
reason: format!(
"No model names found in response: {}",
&response_text[..response_text.len().min(300)]
),
})
}
/// Send a request with automatic session renewal on 401.
async fn send_request<T: Serialize + std::fmt::Debug, R: for<'de> Deserialize<'de>>(
&self,
@@ -431,6 +559,12 @@ impl LlmProvider for NearAiProvider {
// These are approximate and may vary by model
(dec!(0.000003), dec!(0.000015))
}
async fn list_models(&self) -> Result<Vec<String>, LlmError> {
// Use the inherent method and extract IDs
let models = NearAiProvider::list_models(self).await?;
Ok(models.into_iter().map(|m| m.name).collect())
}
}
// NEAR AI API types
+6
View File
@@ -224,6 +224,12 @@ pub trait LlmProvider: Send + Sync {
request: ToolCompletionRequest,
) -> Result<ToolCompletionResponse, LlmError>;
/// List available models from the provider.
/// Default implementation returns empty list.
async fn list_models(&self) -> Result<Vec<String>, LlmError> {
Ok(Vec::new())
}
/// Calculate cost for a completion.
fn calculate_cost(&self, input_tokens: u32, output_tokens: u32) -> Decimal {
let (input_cost, output_cost) = self.cost_per_token();