diff --git a/crates/goose-acp/src/server.rs b/crates/goose-acp/src/server.rs index 47908231727c..92a68450f719 100644 --- a/crates/goose-acp/src/server.rs +++ b/crates/goose-acp/src/server.rs @@ -737,7 +737,8 @@ impl GooseAcpAgent { let config_path = self.config_dir.join(CONFIG_YAML_NAME); let config = Config::new(&config_path, "goose")?; let model_id = config.get_goose_model()?; - goose::model::ModelConfig::new(&model_id)? + let provider_name = config.get_goose_provider()?; + goose::model::ModelConfig::new(&model_id)?.with_canonical_limits(&provider_name) } }; let provider = (self.provider_factory)(model_config, Vec::new()).await?; @@ -955,9 +956,18 @@ impl GooseAcpAgent { session_id: &str, model_id: &str, ) -> Result { - let model_config = goose::model::ModelConfig::new(model_id).map_err(|e| { - sacp::Error::invalid_params().data(format!("Invalid model config: {}", e)) + let config_path = self.config_dir.join(CONFIG_YAML_NAME); + let config = Config::new(&config_path, "goose").map_err(|e| { + sacp::Error::internal_error().data(format!("Failed to read config: {}", e)) + })?; + let provider_name = config.get_goose_provider().map_err(|_| { + sacp::Error::internal_error().data("No provider configured".to_string()) })?; + let model_config = goose::model::ModelConfig::new(model_id) + .map_err(|e| { + sacp::Error::invalid_params().data(format!("Invalid model config: {}", e)) + })? + .with_canonical_limits(&provider_name); let provider = (self.provider_factory)(model_config, Vec::new()) .await .map_err(|e| { diff --git a/crates/goose-acp/src/server_factory.rs b/crates/goose-acp/src/server_factory.rs index 96b94559406a..b2810d85648f 100644 --- a/crates/goose-acp/src/server_factory.rs +++ b/crates/goose-acp/src/server_factory.rs @@ -38,9 +38,7 @@ impl AcpServer { Box::pin(async move { let config_path = config_dir.join(goose::config::base::CONFIG_YAML_NAME); let config = goose::config::Config::new(&config_path, "goose")?; - let provider_name = config - .get_goose_provider() - .map_err(|_| anyhow::anyhow!("No provider configured"))?; + let provider_name = config.get_goose_provider()?; goose::providers::create(&provider_name, model_config, extensions).await }) }); diff --git a/crates/goose-acp/tests/common_tests/mod.rs b/crates/goose-acp/tests/common_tests/mod.rs index 679d0f455ae1..1047803cc0e7 100644 --- a/crates/goose-acp/tests/common_tests/mod.rs +++ b/crates/goose-acp/tests/common_tests/mod.rs @@ -25,7 +25,7 @@ pub async fn run_config_mcp() { let mcp = McpFixture::new(Some(expected_session_id.clone())).await; let config_yaml = format!( - "GOOSE_MODEL: {TEST_MODEL}\nextensions:\n mcp-fixture:\n enabled: true\n type: streamable_http\n name: mcp-fixture\n description: MCP fixture\n uri: \"{}\"\n", + "GOOSE_MODEL: {TEST_MODEL}\nGOOSE_PROVIDER: openai\nextensions:\n mcp-fixture:\n enabled: true\n type: streamable_http\n name: mcp-fixture\n description: MCP fixture\n uri: \"{}\"\n", mcp.url ); fs::write(temp_dir.path().join(CONFIG_YAML_NAME), config_yaml).unwrap(); diff --git a/crates/goose-acp/tests/fixtures/mod.rs b/crates/goose-acp/tests/fixtures/mod.rs index a4a295af8869..ff9c298311ad 100644 --- a/crates/goose-acp/tests/fixtures/mod.rs +++ b/crates/goose-acp/tests/fixtures/mod.rs @@ -199,7 +199,11 @@ pub async fn spawn_acp_server_in_process( fs::create_dir_all(data_root).unwrap(); let config_path = data_root.join(goose::config::base::CONFIG_YAML_NAME); if !config_path.exists() { - fs::write(&config_path, format!("GOOSE_MODEL: {TEST_MODEL}\n")).unwrap(); + fs::write( + &config_path, + format!("GOOSE_MODEL: {TEST_MODEL}\nGOOSE_PROVIDER: openai\n"), + ) + .unwrap(); } let provider_factory = provider_factory.unwrap_or_else(|| { let base_url = openai_base_url.to_string(); diff --git a/crates/goose-cli/src/commands/configure.rs b/crates/goose-cli/src/commands/configure.rs index 4be26fd6cfcf..ef7bcb01e33f 100644 --- a/crates/goose-cli/src/commands/configure.rs +++ b/crates/goose-cli/src/commands/configure.rs @@ -327,7 +327,7 @@ async fn handle_oauth_configuration(provider_name: &str, key_name: &str) -> anyh )); // Create a temporary provider instance to handle OAuth - let temp_model = ModelConfig::new("temp")?; + let temp_model = ModelConfig::new("temp")?.with_canonical_limits(provider_name); match create(provider_name, temp_model, Vec::new()).await { Ok(provider) => match provider.configure_oauth().await { Ok(_) => { @@ -682,7 +682,8 @@ pub async fn configure_provider_dialog() -> anyhow::Result { let spin = spinner(); spin.start("Attempting to fetch supported models..."); let models_res = { - let temp_model_config = ModelConfig::new(&provider_meta.default_model)?; + let temp_model_config = + ModelConfig::new(&provider_meta.default_model)?.with_canonical_limits(provider_name); let temp_provider = create(provider_name, temp_model_config, Vec::new()).await?; retry_operation(&RetryConfig::default(), || async { temp_provider.fetch_recommended_models().await @@ -1442,7 +1443,7 @@ pub async fn configure_tool_permissions_dialog() -> anyhow::Result<()> { let model: String = config .get_goose_model() .expect("No model configured. Please set model first"); - let model_config = ModelConfig::new(&model)?; + let model_config = ModelConfig::new(&model)?.with_canonical_limits(&provider_name); let agent = Agent::new(); @@ -1662,7 +1663,7 @@ pub async fn handle_openrouter_auth() -> anyhow::Result<()> { println!("\nTesting configuration..."); let configured_model: String = config.get_goose_model()?; let model_config = match goose::model::ModelConfig::new(&configured_model) { - Ok(config) => config, + Ok(config) => config.with_canonical_limits("openrouter"), Err(e) => { eprintln!("⚠️ Invalid model configuration: {}", e); eprintln!("Your settings have been saved. Please check your model configuration."); @@ -1742,7 +1743,7 @@ pub async fn handle_tetrate_auth() -> anyhow::Result<()> { println!("\nTesting configuration..."); let configured_model: String = config.get_goose_model()?; let model_config = match goose::model::ModelConfig::new(&configured_model) { - Ok(config) => config, + Ok(config) => config.with_canonical_limits("tetrate"), Err(e) => { eprintln!("⚠️ Invalid model configuration: {}", e); eprintln!("Your settings have been saved. Please check your model configuration."); diff --git a/crates/goose-cli/src/commands/term.rs b/crates/goose-cli/src/commands/term.rs index 22fade45a26b..19661a31e326 100644 --- a/crates/goose-cli/src/commands/term.rs +++ b/crates/goose-cli/src/commands/term.rs @@ -291,7 +291,13 @@ pub async fn handle_term_info() -> Result<()> { let context_limit = config .get_goose_model() .ok() - .and_then(|model_name| goose::model::ModelConfig::new(&model_name).ok()) + .and_then(|model_name| { + config.get_goose_provider().ok().and_then(|provider_name| { + goose::model::ModelConfig::new(&model_name) + .ok() + .map(|c| c.with_canonical_limits(&provider_name)) + }) + }) .map(|mc| mc.context_limit()) .unwrap_or(128_000); diff --git a/crates/goose-cli/src/commands/web.rs b/crates/goose-cli/src/commands/web.rs index c3dedac3af31..09f91befb1bc 100644 --- a/crates/goose-cli/src/commands/web.rs +++ b/crates/goose-cli/src/commands/web.rs @@ -168,7 +168,7 @@ fn get_provider_and_model() -> (String, String) { } async fn create_agent(provider_name: &str, model: &str) -> Result { - let model_config = goose::model::ModelConfig::new(model)?; + let model_config = goose::model::ModelConfig::new(model)?.with_canonical_limits(provider_name); let agent = Agent::new(); diff --git a/crates/goose-cli/src/scenario_tests/scenario_runner.rs b/crates/goose-cli/src/scenario_tests/scenario_runner.rs index 431626a4303c..76ebd1d157e7 100644 --- a/crates/goose-cli/src/scenario_tests/scenario_runner.rs +++ b/crates/goose-cli/src/scenario_tests/scenario_runner.rs @@ -188,7 +188,7 @@ where let inner_provider = create( &factory_name, - ModelConfig::new(config.model_name)?, + ModelConfig::new(config.model_name)?.with_canonical_limits(&factory_name), Vec::new(), ) .await?; diff --git a/crates/goose-cli/src/session/builder.rs b/crates/goose-cli/src/session/builder.rs index 9807a4babf56..6b1e6a0b83ad 100644 --- a/crates/goose-cli/src/session/builder.rs +++ b/crates/goose-cli/src/session/builder.rs @@ -386,6 +386,7 @@ fn resolve_provider_and_model( output::render_error(&format!("Failed to create model configuration: {}", e)); process::exit(1); }) + .with_canonical_limits(&provider_name) .with_temperature(temperature) }; diff --git a/crates/goose-cli/src/session/mod.rs b/crates/goose-cli/src/session/mod.rs index 1757d85aeb06..aa03ad507618 100644 --- a/crates/goose-cli/src/session/mod.rs +++ b/crates/goose-cli/src/session/mod.rs @@ -1841,7 +1841,7 @@ async fn get_reasoner() -> Result, anyhow::Error> { }; let model_config = - ModelConfig::new_with_context_env(model, Some("GOOSE_PLANNER_CONTEXT_LIMIT"))?; + ModelConfig::new_with_context_env(model, &provider, Some("GOOSE_PLANNER_CONTEXT_LIMIT"))?; let extensions = goose::config::extensions::get_enabled_extensions_with_config(config); let reasoner = create(&provider, model_config, extensions).await?; diff --git a/crates/goose-server/src/openapi.rs b/crates/goose-server/src/openapi.rs index 1fa80ab31a0e..f33db83520c7 100644 --- a/crates/goose-server/src/openapi.rs +++ b/crates/goose-server/src/openapi.rs @@ -357,7 +357,7 @@ derive_utoipa!(Icon as IconSchema); super::routes::config_management::check_provider, super::routes::config_management::set_config_provider, super::routes::config_management::configure_provider_oauth, - super::routes::config_management::get_pricing, + super::routes::config_management::get_canonical_model_info, super::routes::prompts::get_prompts, super::routes::prompts::get_prompt, super::routes::prompts::save_prompt, @@ -443,9 +443,9 @@ derive_utoipa!(Icon as IconSchema); super::routes::config_management::UpdateCustomProviderRequest, super::routes::config_management::CheckProviderRequest, super::routes::config_management::SetProviderRequest, - super::routes::config_management::PricingQuery, - super::routes::config_management::PricingResponse, - super::routes::config_management::PricingData, + super::routes::config_management::ModelInfoQuery, + super::routes::config_management::ModelInfoResponse, + super::routes::config_management::ModelInfoData, super::routes::prompts::PromptsListResponse, super::routes::prompts::PromptContentResponse, super::routes::prompts::SavePromptRequest, diff --git a/crates/goose-server/src/routes/agent.rs b/crates/goose-server/src/routes/agent.rs index 1c5b9a8952e6..6fc0a26635de 100644 --- a/crates/goose-server/src/routes/agent.rs +++ b/crates/goose-server/src/routes/agent.rs @@ -549,6 +549,7 @@ async fn update_agent_provider( format!("Invalid model config: {}", e), ) })? + .with_canonical_limits(&payload.provider) .with_context_limit(payload.context_limit) .with_request_params(payload.request_params); diff --git a/crates/goose-server/src/routes/config_management.rs b/crates/goose-server/src/routes/config_management.rs index 45562d79791f..032d55e980a1 100644 --- a/crates/goose-server/src/routes/config_management.rs +++ b/crates/goose-server/src/routes/config_management.rs @@ -227,13 +227,6 @@ fn is_valid_provider_name(provider_name: &str) -> bool { pub async fn read_config( Json(query): Json, ) -> Result, ErrorResponse> { - if query.key == "model-limits" { - let limits = ModelConfig::get_all_model_limits(); - return Ok(Json(ConfigValueResponse::Value(serde_json::to_value( - limits, - )?))); - } - let config = Config::global(); let response_value = match config.get(&query.key, query.is_secret) { @@ -386,7 +379,7 @@ pub async fn get_provider_models( ))); } - let model_config = ModelConfig::new(&metadata.default_model)?; + let model_config = ModelConfig::new(&metadata.default_model)?.with_canonical_limits(&name); let provider = goose::providers::create(&name, model_config, Vec::new()).await?; let models_result = provider.fetch_recommended_models().await; @@ -426,66 +419,60 @@ pub async fn get_slash_commands() -> Result, ErrorRe } #[derive(Serialize, ToSchema)] -pub struct PricingData { +pub struct ModelInfoData { pub provider: String, pub model: String, - pub input_token_cost: f64, - pub output_token_cost: f64, + pub context_limit: usize, + pub max_output_tokens: Option, + pub input_token_cost: Option, + pub output_token_cost: Option, + pub cache_read_token_cost: Option, + pub cache_write_token_cost: Option, pub currency: String, - pub context_length: Option, } #[derive(Serialize, ToSchema)] -pub struct PricingResponse { - pub pricing: Vec, +pub struct ModelInfoResponse { + pub model_info: Option, pub source: String, } #[derive(Deserialize, ToSchema)] -pub struct PricingQuery { +pub struct ModelInfoQuery { pub provider: String, pub model: String, } #[utoipa::path( post, - path = "/config/pricing", - request_body = PricingQuery, + path = "/config/canonical-model-info", + request_body = ModelInfoQuery, responses( - (status = 200, description = "Model pricing data retrieved successfully", body = PricingResponse) + (status = 200, description = "Model information retrieved successfully", body = ModelInfoResponse) ) )] -pub async fn get_pricing( - Json(query): Json, -) -> Result, ErrorResponse> { - let canonical_model = - maybe_get_canonical_model(&query.provider, &query.model).ok_or_else(|| { - ErrorResponse::not_found(format!( - "Model '{}/{}' not found", - query.provider, query.model - )) - })?; - - let mut pricing_data = Vec::new(); - - if let (Some(input_cost), Some(output_cost)) = - (canonical_model.cost.input, canonical_model.cost.output) - { - pricing_data.push(PricingData { - provider: query.provider.clone(), - model: query.model.clone(), - // Canonical model costs are per million tokens, convert to per-token - input_token_cost: input_cost / 1_000_000.0, - output_token_cost: output_cost / 1_000_000.0, - currency: "$".to_string(), - context_length: Some(canonical_model.limit.context as u32), - }); - } +pub async fn get_canonical_model_info( + Json(query): Json, +) -> Json { + let canonical_model = maybe_get_canonical_model(&query.provider, &query.model); + + let model_info = canonical_model.map(|canonical_model| ModelInfoData { + provider: query.provider.clone(), + model: query.model.clone(), + context_limit: canonical_model.limit.context, + max_output_tokens: canonical_model.limit.output, + // Costs are per million tokens - client handles division for display + input_token_cost: canonical_model.cost.input, + output_token_cost: canonical_model.cost.output, + cache_read_token_cost: canonical_model.cost.cache_read, + cache_write_token_cost: canonical_model.cost.cache_write, + currency: "$".to_string(), + }); - Ok(Json(PricingResponse { - pricing: pricing_data, + Json(ModelInfoResponse { + model_info, source: "canonical".to_string(), - })) + }) } #[utoipa::path( @@ -807,9 +794,11 @@ pub async fn configure_provider_oauth( ))); } - let temp_model = ModelConfig::new("temp").map_err(|e| { - ErrorResponse::bad_request(format!("Failed to create temporary model config: {}", e)) - })?; + let temp_model = ModelConfig::new("temp") + .map_err(|e| { + ErrorResponse::bad_request(format!("Failed to create temporary model config: {}", e)) + })? + .with_canonical_limits(&provider_name); // OAuth configuration does not use extensions. let provider = create(&provider_name, temp_model, Vec::new()) @@ -849,7 +838,10 @@ pub fn routes(state: Arc) -> Router { .route("/config/providers/{name}/models", get(get_provider_models)) .route("/config/detect-provider", post(detect_provider)) .route("/config/slash_commands", get(get_slash_commands)) - .route("/config/pricing", post(get_pricing)) + .route( + "/config/canonical-model-info", + post(get_canonical_model_info), + ) .route("/config/init", post(init_config)) .route("/config/backup", post(backup_config)) .route("/config/recover", post(recover_config)) @@ -872,33 +864,4 @@ pub fn routes(state: Arc) -> Router { } #[cfg(test)] -mod tests { - use http::HeaderMap; - - use super::*; - - #[tokio::test] - async fn test_read_model_limits() { - let mut headers = HeaderMap::new(); - headers.insert("X-Secret-Key", "test".parse().unwrap()); - - let result = read_config(Json(ConfigKeyQuery { - key: "model-limits".to_string(), - is_secret: false, - })) - .await; - - assert!(result.is_ok()); - let response = match result.unwrap().0 { - ConfigValueResponse::Value(value) => value, - ConfigValueResponse::MaskedValue(_) => panic!("unexpected secret"), - }; - - let limits: Vec = serde_json::from_value(response).unwrap(); - assert!(!limits.is_empty()); - - let gpt4_limit = limits.iter().find(|l| l.pattern == "gpt-4o"); - assert!(gpt4_limit.is_some()); - assert_eq!(gpt4_limit.unwrap().context_limit, 128_000); - } -} +mod tests {} diff --git a/crates/goose/src/agents/agent.rs b/crates/goose/src/agents/agent.rs index 3cfedeb39f45..594f3c8b2e28 100644 --- a/crates/goose/src/agents/agent.rs +++ b/crates/goose/src/agents/agent.rs @@ -1581,6 +1581,7 @@ impl Agent { .ok_or_else(|| anyhow!("Could not configure agent: missing model"))?; crate::model::ModelConfig::new(&model_name) .map_err(|e| anyhow!("Could not configure agent: invalid model {}", e))? + .with_canonical_limits(&provider_name) } }; diff --git a/crates/goose/src/agents/platform_extensions/summon.rs b/crates/goose/src/agents/platform_extensions/summon.rs index 05b207b59329..f624188fa77a 100644 --- a/crates/goose/src/agents/platform_extensions/summon.rs +++ b/crates/goose/src/agents/platform_extensions/summon.rs @@ -1331,11 +1331,10 @@ impl SummonClient { .or_else(|| session.provider_name.clone()) .ok_or_else(|| anyhow::anyhow!("No provider configured"))?; - let mut model_config = session - .model_config - .clone() - .map(Ok) - .unwrap_or_else(|| crate::model::ModelConfig::new("default"))?; + let mut model_config = session.model_config.clone().map(Ok).unwrap_or_else(|| { + crate::model::ModelConfig::new("default") + .map(|c| c.with_canonical_limits(&provider_name)) + })?; if let Some(model) = ¶ms.model { model_config.model_name = model.clone(); diff --git a/crates/goose/src/context_mgmt/mod.rs b/crates/goose/src/context_mgmt/mod.rs index 305b817b60f0..dcdf2f70dffe 100644 --- a/crates/goose/src/context_mgmt/mod.rs +++ b/crates/goose/src/context_mgmt/mod.rs @@ -549,7 +549,7 @@ mod tests { max_tokens: None, toolshim: false, toolshim_model: None, - fast_model: None, + fast_model_config: None, request_params: None, }, max_tool_responses: None, diff --git a/crates/goose/src/model.rs b/crates/goose/src/model.rs index 4adfa7d6ab34..32fb80d18398 100644 --- a/crates/goose/src/model.rs +++ b/crates/goose/src/model.rs @@ -44,65 +44,6 @@ pub enum ConfigError { InvalidRange(String, String), } -static MODEL_SPECIFIC_LIMITS: Lazy> = Lazy::new(|| { - vec![ - // openai - ("gpt-5.2-codex", 400_000), // auto-compacting context - ("gpt-5.2", 400_000), // auto-compacting context - ("gpt-5.1-codex-max", 256_000), - ("gpt-5.1-codex-mini", 256_000), - ("gpt-4-turbo", 128_000), - ("gpt-4.1", 1_000_000), - ("gpt-4-1", 1_000_000), - ("gpt-4o", 128_000), - ("o4-mini", 200_000), - ("o3-mini", 200_000), - ("o3", 200_000), - // anthropic - all 200k - ("claude", 200_000), - // google - ("gemini-1.5-flash", 1_048_576), - ("gemini-1", 128_000), - ("gemini-2", 1_048_576), - ("gemini-3-pro-image", 65_536), - ("gemini-3-pro", 1_048_576), - ("gemini-3-flash", 1_048_576), - ("gemma-3-27b", 128_000), - ("gemma-3-12b", 128_000), - ("gemma-3-4b", 128_000), - ("gemma-3-1b", 32_000), - ("gemma3-27b", 128_000), - ("gemma3-12b", 128_000), - ("gemma3-4b", 128_000), - ("gemma3-1b", 32_000), - ("gemma-2-27b", 8_192), - ("gemma-2-9b", 8_192), - ("gemma-2-2b", 8_192), - ("gemma2-", 8_192), - ("gemma-7b", 8_192), - ("gemma-2b", 8_192), - ("gemma1", 8_192), - ("gemma", 8_192), - // facebook - ("llama-2-1b", 32_000), - ("llama", 128_000), - // qwen - ("qwen3-coder", 262_144), - ("qwen2-7b", 128_000), - ("qwen2-14b", 128_000), - ("qwen2-32b", 131_072), - ("qwen2-70b", 262_144), - ("qwen2", 128_000), - ("qwen3-32b", 131_072), - // xai - ("grok-4", 256_000), - ("grok-code-fast-1", 256_000), - ("grok", 131_072), - // other - ("kimi-k2", 131_072), - ] -}); - #[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] pub struct ModelConfig { pub model_name: String, @@ -111,52 +52,49 @@ pub struct ModelConfig { pub max_tokens: Option, pub toolshim: bool, pub toolshim_model: Option, - pub fast_model: Option, + #[serde(skip)] + pub fast_model_config: Option>, /// Provider-specific request parameters (e.g., anthropic_beta headers) #[serde(default, skip_serializing_if = "Option::is_none")] pub request_params: Option>, } -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct ModelLimitConfig { - pub pattern: String, - pub context_limit: usize, -} - impl ModelConfig { pub fn new(model_name: &str) -> Result { - Self::new_with_context_env(model_name.to_string(), None) + Self::new_base(model_name.to_string(), None) } pub fn new_with_context_env( model_name: String, + provider_name: &str, context_env_var: Option<&str>, ) -> Result { - let predefined = find_predefined_model(&model_name); + let config = Self::new_base(model_name, context_env_var)?; + Ok(config.with_canonical_limits(provider_name)) + } - let context_limit = if let Some(ref pm) = predefined { - if let Some(env_var) = context_env_var { - if let Ok(val) = std::env::var(env_var) { - Some(Self::validate_context_limit(&val, env_var)?) - } else { - pm.context_limit - } - } else if let Ok(val) = std::env::var("GOOSE_CONTEXT_LIMIT") { - Some(Self::validate_context_limit(&val, "GOOSE_CONTEXT_LIMIT")?) + fn new_base(model_name: String, context_env_var: Option<&str>) -> Result { + let context_limit = if let Some(env_var) = context_env_var { + if let Ok(val) = std::env::var(env_var) { + Some(Self::validate_context_limit(&val, env_var)?) } else { - pm.context_limit + None } + } else if let Ok(val) = std::env::var("GOOSE_CONTEXT_LIMIT") { + Some(Self::validate_context_limit(&val, "GOOSE_CONTEXT_LIMIT")?) } else { - Self::parse_context_limit(&model_name, None, context_env_var)? + None }; - let request_params = predefined.and_then(|pm| pm.request_params); - - let temperature = Self::parse_temperature()?; let max_tokens = Self::parse_max_tokens()?; + let temperature = Self::parse_temperature()?; let toolshim = Self::parse_toolshim()?; let toolshim_model = Self::parse_toolshim_model()?; + // Pick up request_params from predefined models (always applies) + let predefined = find_predefined_model(&model_name); + let request_params = predefined.and_then(|pm| pm.request_params); + Ok(Self { model_name, context_limit, @@ -164,43 +102,34 @@ impl ModelConfig { max_tokens, toolshim, toolshim_model, - fast_model: None, + fast_model_config: None, request_params, }) } - fn parse_context_limit( - model_name: &str, - fast_model: Option<&str>, - custom_env_var: Option<&str>, - ) -> Result, ConfigError> { - // First check if there's an explicit environment variable override - if let Some(env_var) = custom_env_var { - if let Ok(val) = std::env::var(env_var) { - return Self::validate_context_limit(&val, env_var).map(Some); + pub fn with_canonical_limits(mut self, provider_name: &str) -> Self { + if self.context_limit.is_none() || self.max_tokens.is_none() { + if let Some(canonical) = crate::providers::canonical::maybe_get_canonical_model( + provider_name, + &self.model_name, + ) { + if self.context_limit.is_none() { + self.context_limit = Some(canonical.limit.context); + } + if self.max_tokens.is_none() { + self.max_tokens = canonical.limit.output.map(|o| o as i32); + } } } - if let Ok(val) = std::env::var("GOOSE_CONTEXT_LIMIT") { - return Self::validate_context_limit(&val, "GOOSE_CONTEXT_LIMIT").map(Some); - } - - // Get the model's limit - let model_limit = Self::get_model_specific_limit(model_name); - - // If there's a fast_model, get its limit and use the minimum - if let Some(fast_model_name) = fast_model { - let fast_model_limit = Self::get_model_specific_limit(fast_model_name); - // Return the minimum of both limits (if both exist) - match (model_limit, fast_model_limit) { - (Some(m), Some(f)) => Ok(Some(m.min(f))), - (Some(m), None) => Ok(Some(m)), - (None, Some(f)) => Ok(Some(f)), - (None, None) => Ok(None), + // Try filling remaining gaps from predefined models + if self.context_limit.is_none() { + if let Some(pm) = find_predefined_model(&self.model_name) { + self.context_limit = pm.context_limit; } - } else { - Ok(model_limit) } + + self } fn validate_context_limit(val: &str, env_var: &str) -> Result { @@ -291,23 +220,6 @@ impl ModelConfig { } } - fn get_model_specific_limit(model_name: &str) -> Option { - MODEL_SPECIFIC_LIMITS - .iter() - .find(|(pattern, _)| model_name.contains(pattern)) - .map(|(_, limit)| *limit) - } - - pub fn get_all_model_limits() -> Vec { - MODEL_SPECIFIC_LIMITS - .iter() - .map(|(pattern, context_limit)| ModelLimitConfig { - pattern: pattern.to_string(), - context_limit: *context_limit, - }) - .collect() - } - pub fn with_context_limit(mut self, limit: Option) -> Self { if limit.is_some() { self.context_limit = limit; @@ -335,9 +247,15 @@ impl ModelConfig { self } - pub fn with_fast(mut self, fast_model: String) -> Self { - self.fast_model = Some(fast_model); - self + pub fn with_fast( + mut self, + fast_model_name: &str, + provider_name: &str, + ) -> Result { + // Create a full ModelConfig for the fast model with proper canonical lookup + let fast_config = ModelConfig::new(fast_model_name)?.with_canonical_limits(provider_name); + self.fast_model_config = Some(Box::new(fast_config)); + Ok(self) } pub fn with_request_params(mut self, params: Option>) -> Self { @@ -346,33 +264,24 @@ impl ModelConfig { } pub fn use_fast_model(&self) -> Self { - if let Some(fast_model) = &self.fast_model { - let mut config = self.clone(); - config.model_name = fast_model.clone(); - config + if let Some(fast_config) = &self.fast_model_config { + *fast_config.clone() } else { self.clone() } } pub fn context_limit(&self) -> usize { - // If we have an explicit context limit set, use it - if let Some(limit) = self.context_limit { - return limit; - } - - // Otherwise, get the model's default limit - let main_limit = - Self::get_model_specific_limit(&self.model_name).unwrap_or(DEFAULT_CONTEXT_LIMIT); + self.context_limit.unwrap_or(DEFAULT_CONTEXT_LIMIT) + } - // If we have a fast_model, also check its limit and use the minimum - if let Some(fast_model) = &self.fast_model { - let fast_limit = - Self::get_model_specific_limit(fast_model).unwrap_or(DEFAULT_CONTEXT_LIMIT); - main_limit.min(fast_limit) - } else { - main_limit + pub fn max_output_tokens(&self) -> i32 { + if let Some(tokens) = self.max_tokens { + return tokens; } + + // Priority 2: Global default + 4_096 } pub fn new_or_fail(model_name: &str) -> ModelConfig { diff --git a/crates/goose/src/providers/anthropic.rs b/crates/goose/src/providers/anthropic.rs index c6dfa1002bab..2bec2118edea 100644 --- a/crates/goose/src/providers/anthropic.rs +++ b/crates/goose/src/providers/anthropic.rs @@ -59,7 +59,7 @@ pub struct AnthropicProvider { impl AnthropicProvider { pub async fn from_env(model: ModelConfig) -> Result { - let model = model.with_fast(ANTHROPIC_DEFAULT_FAST_MODEL.to_string()); + let model = model.with_fast(ANTHROPIC_DEFAULT_FAST_MODEL, ANTHROPIC_PROVIDER_NAME)?; let config = crate::config::Config::global(); let api_key: String = config.get_secret("ANTHROPIC_API_KEY")?; diff --git a/crates/goose/src/providers/auto_detect.rs b/crates/goose/src/providers/auto_detect.rs index 39a9e54118e6..fff59bafde51 100644 --- a/crates/goose/src/providers/auto_detect.rs +++ b/crates/goose/src/providers/auto_detect.rs @@ -21,7 +21,7 @@ pub async fn detect_provider_from_api_key(api_key: &str) -> Option<(String, Vec< let result = match crate::providers::create( provider_name, - ModelConfig::new_or_fail("default"), + ModelConfig::new_or_fail("default").with_canonical_limits(provider_name), Vec::new(), ) .await diff --git a/crates/goose/src/providers/base.rs b/crates/goose/src/providers/base.rs index 05447494bcd0..9e873e643639 100644 --- a/crates/goose/src/providers/base.rs +++ b/crates/goose/src/providers/base.rs @@ -133,9 +133,11 @@ impl ProviderMetadata { default_model: default_model.to_string(), known_models: model_names .iter() - .map(|&name| ModelInfo { - name: name.to_string(), - context_limit: ModelConfig::new_or_fail(name).context_limit(), + .map(|&model_name| ModelInfo { + name: model_name.to_string(), + context_limit: ModelConfig::new_or_fail(model_name) + .with_canonical_limits(name) + .context_limit(), input_token_cost: None, output_token_cost: None, currency: None, diff --git a/crates/goose/src/providers/canonical/name_builder.rs b/crates/goose/src/providers/canonical/name_builder.rs index afa7a5b2f899..3b3b24849e96 100644 --- a/crates/goose/src/providers/canonical/name_builder.rs +++ b/crates/goose/src/providers/canonical/name_builder.rs @@ -81,10 +81,10 @@ pub fn map_to_canonical_model( if let Some(canonical) = registry.get(registry_provider, model) { return Some(canonical.id.clone()); } - return None; + // If direct lookup failed, fall through to inference logic below } - // For hosting/meta-providers do string matching magic to figure out the real provider and model + // For hosting/meta-providers (or unknown providers), do string matching magic to figure out the real provider and model let model_stripped = strip_common_prefixes(model); if let Some(swapped) = swap_claude_word_order(&model_stripped) { diff --git a/crates/goose/src/providers/claude_code.rs b/crates/goose/src/providers/claude_code.rs index 461f97c7c631..e67d48d408d5 100644 --- a/crates/goose/src/providers/claude_code.rs +++ b/crates/goose/src/providers/claude_code.rs @@ -1334,7 +1334,9 @@ mod tests { fn make_provider() -> ClaudeCodeProvider { ClaudeCodeProvider { command: PathBuf::from("claude"), - model: ModelConfig::new(CLAUDE_CODE_DEFAULT_MODEL).unwrap(), + model: ModelConfig::new(CLAUDE_CODE_DEFAULT_MODEL) + .unwrap() + .with_canonical_limits(CLAUDE_CODE_PROVIDER_NAME), name: "claude-code".to_string(), mcp_config_file: None, cli_process: tokio::sync::OnceCell::new(), diff --git a/crates/goose/src/providers/databricks.rs b/crates/goose/src/providers/databricks.rs index 49709acbdb9a..c5509fced491 100644 --- a/crates/goose/src/providers/databricks.rs +++ b/crates/goose/src/providers/databricks.rs @@ -150,7 +150,8 @@ impl DatabricksProvider { fast_retry_config, name: DATABRICKS_PROVIDER_NAME.to_string(), }; - provider.model = model.with_fast(DATABRICKS_DEFAULT_FAST_MODEL.to_string()); + provider.model = + model.with_fast(DATABRICKS_DEFAULT_FAST_MODEL, DATABRICKS_PROVIDER_NAME)?; Ok(provider) } @@ -300,9 +301,9 @@ impl Provider for DatabricksProvider { // Use fast retry config if this is the fast model let is_fast_model = self .model - .fast_model + .fast_model_config .as_ref() - .map(|fast| fast == &model_config.model_name) + .map(|fast| fast.model_name == model_config.model_name) .unwrap_or(false); let retry_config = if is_fast_model { diff --git a/crates/goose/src/providers/formats/anthropic.rs b/crates/goose/src/providers/formats/anthropic.rs index 67aa58780b45..f47a387aa832 100644 --- a/crates/goose/src/providers/formats/anthropic.rs +++ b/crates/goose/src/providers/formats/anthropic.rs @@ -395,30 +395,17 @@ pub fn create_request( let tool_specs = format_tools(tools); let system_spec = format_system(system); - // Check if we have any messages to send if anthropic_messages.is_empty() { return Err(anyhow!("No valid messages to send to Anthropic API")); } - // https://platform.claude.com/docs/en/about-claude/models/overview - // 64k output tokens works for most claude models, but not old opus: - let max_tokens = model_config.max_tokens.unwrap_or_else(|| { - let name = &model_config.model_name; - if name.contains("claude-3-haiku") { - 4096 - } else if name.contains("claude-opus-4-0") || name.contains("claude-opus-4-1") { - 32000 - } else { - 64000 - } - }); + let max_tokens = model_config.max_output_tokens(); let mut payload = json!({ "model": model_config.model_name, "messages": anthropic_messages, "max_tokens": max_tokens, }); - // Add system message if present if !system.is_empty() { payload .as_object_mut() @@ -426,7 +413,6 @@ pub fn create_request( .insert("system".to_string(), json!(system_spec)); } - // Add tools if present if !tool_specs.is_empty() { payload .as_object_mut() @@ -434,7 +420,6 @@ pub fn create_request( .insert("tools".to_string(), json!(tool_specs)); } - // Add temperature if specified and not using extended thinking model if let Some(temp) = model_config.temperature { payload .as_object_mut() @@ -442,10 +427,8 @@ pub fn create_request( .insert("temperature".to_string(), json!(temp)); } - // Add thinking parameters when CLAUDE_THINKING_ENABLED is set let is_thinking_enabled = std::env::var("CLAUDE_THINKING_ENABLED").is_ok(); if is_thinking_enabled { - // Minimum budget_tokens is 1024 let budget_tokens = std::env::var("CLAUDE_THINKING_BUDGET") .unwrap_or_else(|_| "16000".to_string()) .parse() diff --git a/crates/goose/src/providers/formats/databricks.rs b/crates/goose/src/providers/formats/databricks.rs index 51a1265c817e..383aeb225899 100644 --- a/crates/goose/src/providers/formats/databricks.rs +++ b/crates/goose/src/providers/formats/databricks.rs @@ -1056,7 +1056,7 @@ mod tests { max_tokens: Some(1024), toolshim: false, toolshim_model: None, - fast_model: None, + fast_model_config: None, request_params: None, }; let request = create_request(&model_config, "system", &[], &[], &ImageFormat::OpenAi)?; @@ -1088,7 +1088,7 @@ mod tests { max_tokens: Some(1024), toolshim: false, toolshim_model: None, - fast_model: None, + fast_model_config: None, request_params: None, }; let request = create_request(&model_config, "system", &[], &[], &ImageFormat::OpenAi)?; @@ -1440,7 +1440,7 @@ mod tests { max_tokens: Some(8192), toolshim: false, toolshim_model: None, - fast_model: None, + fast_model_config: None, request_params: None, }; @@ -1492,7 +1492,7 @@ mod tests { max_tokens: Some(4096), toolshim: false, toolshim_model: None, - fast_model: None, + fast_model_config: None, request_params: None, }; diff --git a/crates/goose/src/providers/formats/openai.rs b/crates/goose/src/providers/formats/openai.rs index 111bd660ce68..151adb73b754 100644 --- a/crates/goose/src/providers/formats/openai.rs +++ b/crates/goose/src/providers/formats/openai.rs @@ -1489,7 +1489,7 @@ mod tests { max_tokens: Some(1024), toolshim: false, toolshim_model: None, - fast_model: None, + fast_model_config: None, request_params: None, }; let request = create_request( @@ -1529,7 +1529,7 @@ mod tests { max_tokens: Some(1024), toolshim: false, toolshim_model: None, - fast_model: None, + fast_model_config: None, request_params: None, }; let request = create_request( @@ -1570,7 +1570,7 @@ mod tests { max_tokens: Some(1024), toolshim: false, toolshim_model: None, - fast_model: None, + fast_model_config: None, request_params: None, }; let request = create_request( diff --git a/crates/goose/src/providers/formats/snowflake.rs b/crates/goose/src/providers/formats/snowflake.rs index b3bb6202c6f0..cbb561b35746 100644 --- a/crates/goose/src/providers/formats/snowflake.rs +++ b/crates/goose/src/providers/formats/snowflake.rs @@ -568,7 +568,8 @@ data: {"id":"a9537c2c-2017-4906-9817-2456168d89fa","model":"claude-sonnet-4-2025 use crate::conversation::message::Message; use crate::model::ModelConfig; - let model_config = ModelConfig::new_or_fail("claude-4-sonnet"); + let model_config = + ModelConfig::new_or_fail("claude-4-sonnet").with_canonical_limits("snowflake"); let system = "You are a helpful assistant that can use tools to get information."; let messages = vec![Message::user().with_text("What is the stock price of Nvidia?")]; @@ -677,7 +678,8 @@ data: {"id":"a9537c2c-2017-4906-9817-2456168d89fa","model":"claude-sonnet-4-2025 use crate::conversation::message::Message; use crate::model::ModelConfig; - let model_config = ModelConfig::new_or_fail("claude-4-sonnet"); + let model_config = + ModelConfig::new_or_fail("claude-4-sonnet").with_canonical_limits("snowflake"); let system = "Reply with only a description in four words or less"; let messages = vec![Message::user().with_text("Test message")]; let tools = vec![Tool::new( diff --git a/crates/goose/src/providers/google.rs b/crates/goose/src/providers/google.rs index 31fda4c22eee..217aa1905ec2 100644 --- a/crates/goose/src/providers/google.rs +++ b/crates/goose/src/providers/google.rs @@ -69,7 +69,7 @@ pub struct GoogleProvider { impl GoogleProvider { pub async fn from_env(model: ModelConfig) -> Result { - let model = model.with_fast(GOOGLE_DEFAULT_FAST_MODEL.to_string()); + let model = model.with_fast(GOOGLE_DEFAULT_FAST_MODEL, GOOGLE_PROVIDER_NAME)?; let config = crate::config::Config::global(); let api_key: String = config.get_secret("GOOGLE_API_KEY")?; diff --git a/crates/goose/src/providers/init.rs b/crates/goose/src/providers/init.rs index 333362ae05e2..c1f69e61d6b9 100644 --- a/crates/goose/src/providers/init.rs +++ b/crates/goose/src/providers/init.rs @@ -141,7 +141,8 @@ pub async fn create_with_named_model( model_name: &str, extensions: Vec, ) -> Result> { - create(provider_name, ModelConfig::new(model_name)?, extensions).await + let config = ModelConfig::new(model_name)?.with_canonical_limits(provider_name); + create(provider_name, config, extensions).await } async fn create_lead_worker_from_env( @@ -168,10 +169,11 @@ async fn create_lead_worker_from_env( let lead_model_config = ModelConfig::new_with_context_env( lead_model_name.to_string(), + &lead_provider_name, Some("GOOSE_LEAD_CONTEXT_LIMIT"), )?; - let worker_model_config = create_worker_model_config(default_model)?; + let worker_model_config = create_worker_model_config(default_model, default_provider_name)?; let registry = get_registry().await; @@ -207,8 +209,12 @@ async fn create_lead_worker_from_env( ))) } -fn create_worker_model_config(default_model: &ModelConfig) -> Result { +fn create_worker_model_config( + default_model: &ModelConfig, + provider_name: &str, +) -> Result { let mut worker_config = ModelConfig::new_or_fail(&default_model.model_name) + .with_canonical_limits(provider_name) .with_context_limit(default_model.context_limit) .with_temperature(default_model.temperature) .with_max_tokens(default_model.max_tokens) @@ -253,7 +259,7 @@ mod tests { let provider = create( "openai", - ModelConfig::new_or_fail("gpt-4o-mini"), + ModelConfig::new_or_fail("gpt-4o-mini").with_canonical_limits("openai"), Vec::new(), ) .await @@ -282,7 +288,7 @@ mod tests { let provider = create( "openai", - ModelConfig::new_or_fail("gpt-4o-mini"), + ModelConfig::new_or_fail("gpt-4o-mini").with_canonical_limits("openai"), Vec::new(), ) .await @@ -304,10 +310,11 @@ mod tests { ("GOOSE_CONTEXT_LIMIT", global_limit), ]); - let default_model = - ModelConfig::new_or_fail("gpt-3.5-turbo").with_context_limit(Some(16_000)); + let default_model = ModelConfig::new_or_fail("gpt-3.5-turbo") + .with_canonical_limits("openai") + .with_context_limit(Some(16_000)); - let result = create_worker_model_config(&default_model).unwrap(); + let result = create_worker_model_config(&default_model, "openai").unwrap(); assert_eq!(result.context_limit, Some(expected_limit)); } diff --git a/crates/goose/src/providers/openai.rs b/crates/goose/src/providers/openai.rs index 04e8c8851d94..c37240321e39 100644 --- a/crates/goose/src/providers/openai.rs +++ b/crates/goose/src/providers/openai.rs @@ -69,7 +69,7 @@ pub struct OpenAiProvider { impl OpenAiProvider { pub async fn from_env(model: ModelConfig) -> Result { - let model = model.with_fast(OPEN_AI_DEFAULT_FAST_MODEL.to_string()); + let model = model.with_fast(OPEN_AI_DEFAULT_FAST_MODEL, OPEN_AI_PROVIDER_NAME)?; let config = crate::config::Config::global(); let host: String = config diff --git a/crates/goose/src/providers/openrouter.rs b/crates/goose/src/providers/openrouter.rs index 52466c6a34b5..ea7cf634249d 100644 --- a/crates/goose/src/providers/openrouter.rs +++ b/crates/goose/src/providers/openrouter.rs @@ -51,7 +51,7 @@ pub struct OpenRouterProvider { impl OpenRouterProvider { pub async fn from_env(model: ModelConfig) -> Result { - let model = model.with_fast(OPENROUTER_DEFAULT_FAST_MODEL.to_string()); + let model = model.with_fast(OPENROUTER_DEFAULT_FAST_MODEL, OPENROUTER_PROVIDER_NAME)?; let config = crate::config::Config::global(); let api_key: String = config.get_secret("OPENROUTER_API_KEY")?; diff --git a/crates/goose/src/providers/provider_registry.rs b/crates/goose/src/providers/provider_registry.rs index 45f94d594731..8248a7b5016b 100644 --- a/crates/goose/src/providers/provider_registry.rs +++ b/crates/goose/src/providers/provider_registry.rs @@ -25,7 +25,9 @@ impl ProviderEntry { extensions: Vec, ) -> Result> { let default_model = &self.metadata.default_model; - let model_config = ModelConfig::new(default_model.as_str())?; + let provider_name = &self.metadata.name; + let model_config = + ModelConfig::new(default_model.as_str())?.with_canonical_limits(provider_name); (self.constructor)(model_config, extensions).await } } diff --git a/crates/goose/src/providers/provider_test.rs b/crates/goose/src/providers/provider_test.rs index b47b59d62bea..3283b9b7f44c 100644 --- a/crates/goose/src/providers/provider_test.rs +++ b/crates/goose/src/providers/provider_test.rs @@ -10,6 +10,7 @@ pub async fn test_provider_configuration( toolshim_model: Option, ) -> Result<()> { let model_config = ModelConfig::new(model)? + .with_canonical_limits(provider_name) .with_max_tokens(Some(50)) .with_toolshim(toolshim_enabled) .with_toolshim_model(toolshim_model); diff --git a/crates/goose/src/providers/toolshim.rs b/crates/goose/src/providers/toolshim.rs index cc9ae121388e..086543bd7900 100644 --- a/crates/goose/src/providers/toolshim.rs +++ b/crates/goose/src/providers/toolshim.rs @@ -154,7 +154,8 @@ impl OllamaInterpreter { messages.push(user_message); let model_config = ModelConfig::new(model) - .map_err(|e| ProviderError::RequestFailed(format!("Model config error: {e}")))?; + .map_err(|e| ProviderError::RequestFailed(format!("Model config error: {e}")))? + .with_canonical_limits("ollama"); let mut payload = create_request( &model_config, diff --git a/crates/goose/src/providers/venice.rs b/crates/goose/src/providers/venice.rs index 3b4530944b86..7a26d47e0db5 100644 --- a/crates/goose/src/providers/venice.rs +++ b/crates/goose/src/providers/venice.rs @@ -85,7 +85,7 @@ pub struct VeniceProvider { } impl VeniceProvider { - pub async fn from_env(mut model: ModelConfig) -> Result { + pub async fn from_env(model: ModelConfig) -> Result { let config = crate::config::Config::global(); let api_key: String = config.get_secret("VENICE_API_KEY")?; let host: String = config @@ -98,9 +98,6 @@ impl VeniceProvider { .get_param("VENICE_MODELS_PATH") .unwrap_or_else(|_| VENICE_DEFAULT_MODELS_PATH.to_string()); - // Ensure we only keep the bare model id internally - model.model_name = strip_flags(&model.model_name).to_string(); - let auth = AuthMethod::BearerToken(api_key); let api_client = ApiClient::new(host, auth)?; diff --git a/crates/goose/src/scheduler.rs b/crates/goose/src/scheduler.rs index 815f535b387c..192fa7966ac5 100644 --- a/crates/goose/src/scheduler.rs +++ b/crates/goose/src/scheduler.rs @@ -739,7 +739,8 @@ async fn execute_job( let config = Config::global(); let provider_name = config.get_goose_provider()?; let model_name = config.get_goose_model()?; - let model_config = crate::model::ModelConfig::new(&model_name)?; + let model_config = + crate::model::ModelConfig::new(&model_name)?.with_canonical_limits(&provider_name); let session = agent .config diff --git a/crates/goose/tests/providers.rs b/crates/goose/tests/providers.rs index bf515c6a31a1..b17757c95ab9 100644 --- a/crates/goose/tests/providers.rs +++ b/crates/goose/tests/providers.rs @@ -280,7 +280,7 @@ impl ProviderTester { .model_switch_name .as_deref() .expect("model_switch_name required for test_model_switch"); - let alt_config = goose::model::ModelConfig::new(alt)?; + let alt_config = goose::model::ModelConfig::new(alt)?.with_canonical_limits(&self.name); let message = Message::user().with_text("Just say hello!"); let (response, _) = self diff --git a/crates/goose/tests/tetrate_streaming.rs b/crates/goose/tests/tetrate_streaming.rs index ab4663f38cee..32c4c2028c27 100644 --- a/crates/goose/tests/tetrate_streaming.rs +++ b/crates/goose/tests/tetrate_streaming.rs @@ -15,7 +15,8 @@ mod tetrate_streaming_tests { async fn create_test_provider() -> Result { // Create a test provider with the default model - let model_config = ModelConfig::new("claude-3-5-sonnet-latest")?; + let model_config = + ModelConfig::new("claude-3-5-sonnet-latest")?.with_canonical_limits("tetrate"); TetrateProvider::from_env(model_config).await } @@ -237,7 +238,8 @@ mod tetrate_streaming_tests { // Test with invalid API key to ensure error handling works std::env::set_var("TETRATE_API_KEY", "invalid-key-for-testing"); - let model_config = ModelConfig::new("claude-3-5-sonnet-latest")?; + let model_config = + ModelConfig::new("claude-3-5-sonnet-latest")?.with_canonical_limits("tetrate"); let provider = TetrateProvider::from_env(model_config).await?; let messages = vec![Message::user().with_text("Hello")]; diff --git a/ui/desktop/openapi.json b/ui/desktop/openapi.json index 36f07d7ae446..a518b9f2ee6b 100644 --- a/ui/desktop/openapi.json +++ b/ui/desktop/openapi.json @@ -741,6 +741,36 @@ } } }, + "/config/canonical-model-info": { + "post": { + "tags": [ + "super::routes::config_management" + ], + "operationId": "get_canonical_model_info", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ModelInfoQuery" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Model information retrieved successfully", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ModelInfoResponse" + } + } + } + } + } + } + }, "/config/check_provider": { "post": { "tags": [ @@ -1095,36 +1125,6 @@ } } }, - "/config/pricing": { - "post": { - "tags": [ - "super::routes::config_management" - ], - "operationId": "get_pricing", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/PricingQuery" - } - } - }, - "required": true - }, - "responses": { - "200": { - "description": "Model pricing data retrieved successfully", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/PricingResponse" - } - } - } - } - } - } - }, "/config/prompts": { "get": { "tags": [ @@ -5261,10 +5261,6 @@ "nullable": true, "minimum": 0 }, - "fast_model": { - "type": "string", - "nullable": true - }, "max_tokens": { "type": "integer", "format": "int32", @@ -5334,6 +5330,89 @@ } } }, + "ModelInfoData": { + "type": "object", + "required": [ + "provider", + "model", + "context_limit", + "currency" + ], + "properties": { + "cache_read_token_cost": { + "type": "number", + "format": "double", + "nullable": true + }, + "cache_write_token_cost": { + "type": "number", + "format": "double", + "nullable": true + }, + "context_limit": { + "type": "integer", + "minimum": 0 + }, + "currency": { + "type": "string" + }, + "input_token_cost": { + "type": "number", + "format": "double", + "nullable": true + }, + "max_output_tokens": { + "type": "integer", + "nullable": true, + "minimum": 0 + }, + "model": { + "type": "string" + }, + "output_token_cost": { + "type": "number", + "format": "double", + "nullable": true + }, + "provider": { + "type": "string" + } + } + }, + "ModelInfoQuery": { + "type": "object", + "required": [ + "provider", + "model" + ], + "properties": { + "model": { + "type": "string" + }, + "provider": { + "type": "string" + } + } + }, + "ModelInfoResponse": { + "type": "object", + "required": [ + "source" + ], + "properties": { + "model_info": { + "allOf": [ + { + "$ref": "#/components/schemas/ModelInfoData" + } + ], + "nullable": true + }, + "source": { + "type": "string" + } + } + }, "ParseRecipeRequest": { "type": "object", "required": [ @@ -5397,74 +5476,6 @@ } } }, - "PricingData": { - "type": "object", - "required": [ - "provider", - "model", - "input_token_cost", - "output_token_cost", - "currency" - ], - "properties": { - "context_length": { - "type": "integer", - "format": "int32", - "nullable": true, - "minimum": 0 - }, - "currency": { - "type": "string" - }, - "input_token_cost": { - "type": "number", - "format": "double" - }, - "model": { - "type": "string" - }, - "output_token_cost": { - "type": "number", - "format": "double" - }, - "provider": { - "type": "string" - } - } - }, - "PricingQuery": { - "type": "object", - "required": [ - "provider", - "model" - ], - "properties": { - "model": { - "type": "string" - }, - "provider": { - "type": "string" - } - } - }, - "PricingResponse": { - "type": "object", - "required": [ - "pricing", - "source" - ], - "properties": { - "pricing": { - "type": "array", - "items": { - "$ref": "#/components/schemas/PricingData" - } - }, - "source": { - "type": "string" - } - } - }, "PrincipalType": { "type": "string", "enum": [ diff --git a/ui/desktop/src/api/index.ts b/ui/desktop/src/api/index.ts index 7089867c1eab..9a24bb7ac0de 100644 --- a/ui/desktop/src/api/index.ts +++ b/ui/desktop/src/api/index.ts @@ -1,4 +1,4 @@ // This file is auto-generated by @hey-api/openapi-ts -export { addExtension, agentAddExtension, agentRemoveExtension, backupConfig, callTool, cancelDownload, checkProvider, configureProviderOauth, confirmToolAction, createCustomProvider, createRecipe, createSchedule, decodeRecipe, deleteModel, deleteRecipe, deleteSchedule, deleteSession, detectProvider, diagnostics, downloadModel, encodeRecipe, exportApp, exportSession, forkSession, getCustomProvider, getDictationConfig, getDownloadProgress, getExtensions, getPricing, getPrompt, getPrompts, getProviderModels, getSession, getSessionExtensions, getSessionInsights, getSlashCommands, getTools, getTunnelStatus, importApp, importSession, initConfig, inspectRunningJob, killRunningJob, listApps, listModels, listRecipes, listSchedules, listSessions, mcpUiProxy, type Options, parseRecipe, pauseSchedule, providers, readAllConfig, readConfig, readResource, recipeToYaml, recoverConfig, removeConfig, removeCustomProvider, removeExtension, reply, resetPrompt, restartAgent, resumeAgent, runNowHandler, savePrompt, saveRecipe, scanRecipe, scheduleRecipe, searchSessions, sendTelemetryEvent, sessionsHandler, setConfigProvider, setRecipeSlashCommand, startAgent, startOpenrouterSetup, startTetrateSetup, startTunnel, status, stopAgent, stopTunnel, systemInfo, transcribeDictation, unpauseSchedule, updateAgentProvider, updateCustomProvider, updateFromSession, updateSchedule, updateSessionName, updateSessionUserRecipeValues, updateWorkingDir, upsertConfig, upsertPermissions, validateConfig } from './sdk.gen'; -export type { ActionRequired, ActionRequiredData, AddExtensionData, AddExtensionErrors, AddExtensionRequest, AddExtensionResponse, AddExtensionResponses, AgentAddExtensionData, AgentAddExtensionErrors, AgentAddExtensionResponse, AgentAddExtensionResponses, AgentRemoveExtensionData, AgentRemoveExtensionErrors, AgentRemoveExtensionResponse, AgentRemoveExtensionResponses, Annotations, Author, AuthorRequest, BackupConfigData, BackupConfigErrors, BackupConfigResponse, BackupConfigResponses, CallToolData, CallToolErrors, CallToolRequest, CallToolResponse, CallToolResponse2, CallToolResponses, CancelDownloadData, CancelDownloadErrors, CancelDownloadResponses, ChatRequest, CheckProviderData, CheckProviderRequest, ClientOptions, CommandType, ConfigKey, ConfigKeyQuery, ConfigResponse, ConfigureProviderOauthData, ConfigureProviderOauthErrors, ConfigureProviderOauthResponses, ConfirmToolActionData, ConfirmToolActionErrors, ConfirmToolActionRequest, ConfirmToolActionResponses, Content, Conversation, CreateCustomProviderData, CreateCustomProviderErrors, CreateCustomProviderResponse, CreateCustomProviderResponses, CreateRecipeData, CreateRecipeErrors, CreateRecipeRequest, CreateRecipeResponse, CreateRecipeResponse2, CreateRecipeResponses, CreateScheduleData, CreateScheduleErrors, CreateScheduleRequest, CreateScheduleResponse, CreateScheduleResponses, CspMetadata, DeclarativeProviderConfig, DecodeRecipeData, DecodeRecipeErrors, DecodeRecipeRequest, DecodeRecipeResponse, DecodeRecipeResponse2, DecodeRecipeResponses, DeleteModelData, DeleteModelErrors, DeleteModelResponses, DeleteRecipeData, DeleteRecipeErrors, DeleteRecipeRequest, DeleteRecipeResponse, DeleteRecipeResponses, DeleteScheduleData, DeleteScheduleErrors, DeleteScheduleResponse, DeleteScheduleResponses, DeleteSessionData, DeleteSessionErrors, DeleteSessionResponses, DetectProviderData, DetectProviderErrors, DetectProviderRequest, DetectProviderResponse, DetectProviderResponse2, DetectProviderResponses, DiagnosticsData, DiagnosticsErrors, DiagnosticsResponse, DiagnosticsResponses, DictationProvider, DictationProviderStatus, DownloadModelData, DownloadModelErrors, DownloadModelResponses, DownloadProgress, DownloadStatus, EmbeddedResource, EncodeRecipeData, EncodeRecipeErrors, EncodeRecipeRequest, EncodeRecipeResponse, EncodeRecipeResponse2, EncodeRecipeResponses, Envs, ErrorResponse, ExportAppData, ExportAppError, ExportAppErrors, ExportAppResponse, ExportAppResponses, ExportSessionData, ExportSessionErrors, ExportSessionResponse, ExportSessionResponses, ExtensionConfig, ExtensionData, ExtensionEntry, ExtensionLoadResult, ExtensionQuery, ExtensionResponse, ForkRequest, ForkResponse, ForkSessionData, ForkSessionErrors, ForkSessionResponse, ForkSessionResponses, FrontendToolRequest, GetCustomProviderData, GetCustomProviderErrors, GetCustomProviderResponse, GetCustomProviderResponses, GetDictationConfigData, GetDictationConfigResponse, GetDictationConfigResponses, GetDownloadProgressData, GetDownloadProgressErrors, GetDownloadProgressResponse, GetDownloadProgressResponses, GetExtensionsData, GetExtensionsErrors, GetExtensionsResponse, GetExtensionsResponses, GetPricingData, GetPricingResponse, GetPricingResponses, GetPromptData, GetPromptErrors, GetPromptResponse, GetPromptResponses, GetPromptsData, GetPromptsResponse, GetPromptsResponses, GetProviderModelsData, GetProviderModelsErrors, GetProviderModelsResponse, GetProviderModelsResponses, GetSessionData, GetSessionErrors, GetSessionExtensionsData, GetSessionExtensionsErrors, GetSessionExtensionsResponse, GetSessionExtensionsResponses, GetSessionInsightsData, GetSessionInsightsErrors, GetSessionInsightsResponse, GetSessionInsightsResponses, GetSessionResponse, GetSessionResponses, GetSlashCommandsData, GetSlashCommandsResponse, GetSlashCommandsResponses, GetToolsData, GetToolsErrors, GetToolsQuery, GetToolsResponse, GetToolsResponses, GetTunnelStatusData, GetTunnelStatusResponse, GetTunnelStatusResponses, GooseApp, Icon, ImageContent, ImportAppData, ImportAppError, ImportAppErrors, ImportAppRequest, ImportAppResponse, ImportAppResponse2, ImportAppResponses, ImportSessionData, ImportSessionErrors, ImportSessionRequest, ImportSessionResponse, ImportSessionResponses, InitConfigData, InitConfigErrors, InitConfigResponse, InitConfigResponses, InspectJobResponse, InspectRunningJobData, InspectRunningJobErrors, InspectRunningJobResponse, InspectRunningJobResponses, JsonObject, KillJobResponse, KillRunningJobData, KillRunningJobResponses, ListAppsData, ListAppsError, ListAppsErrors, ListAppsRequest, ListAppsResponse, ListAppsResponse2, ListAppsResponses, ListModelsData, ListModelsResponse, ListModelsResponses, ListRecipeResponse, ListRecipesData, ListRecipesErrors, ListRecipesResponse, ListRecipesResponses, ListSchedulesData, ListSchedulesErrors, ListSchedulesResponse, ListSchedulesResponse2, ListSchedulesResponses, ListSessionsData, ListSessionsErrors, ListSessionsResponse, ListSessionsResponses, LoadedProvider, McpAppResource, McpUiProxyData, McpUiProxyErrors, McpUiProxyResponses, Message, MessageContent, MessageEvent, MessageMetadata, ModelConfig, ModelInfo, ParseRecipeData, ParseRecipeError, ParseRecipeErrors, ParseRecipeRequest, ParseRecipeResponse, ParseRecipeResponse2, ParseRecipeResponses, PauseScheduleData, PauseScheduleErrors, PauseScheduleResponse, PauseScheduleResponses, Permission, PermissionLevel, PermissionsMetadata, PricingData, PricingQuery, PricingResponse, PrincipalType, PromptContentResponse, PromptsListResponse, ProviderDetails, ProviderEngine, ProviderMetadata, ProvidersData, ProvidersResponse, ProvidersResponse2, ProvidersResponses, ProviderType, RawAudioContent, RawEmbeddedResource, RawImageContent, RawResource, RawTextContent, ReadAllConfigData, ReadAllConfigResponse, ReadAllConfigResponses, ReadConfigData, ReadConfigErrors, ReadConfigResponses, ReadResourceData, ReadResourceErrors, ReadResourceRequest, ReadResourceResponse, ReadResourceResponse2, ReadResourceResponses, ReasoningContent, Recipe, RecipeManifest, RecipeParameter, RecipeParameterInputType, RecipeParameterRequirement, RecipeToYamlData, RecipeToYamlError, RecipeToYamlErrors, RecipeToYamlRequest, RecipeToYamlResponse, RecipeToYamlResponse2, RecipeToYamlResponses, RecoverConfigData, RecoverConfigErrors, RecoverConfigResponse, RecoverConfigResponses, RedactedThinkingContent, RemoveConfigData, RemoveConfigErrors, RemoveConfigResponse, RemoveConfigResponses, RemoveCustomProviderData, RemoveCustomProviderErrors, RemoveCustomProviderResponse, RemoveCustomProviderResponses, RemoveExtensionData, RemoveExtensionErrors, RemoveExtensionRequest, RemoveExtensionResponse, RemoveExtensionResponses, ReplyData, ReplyErrors, ReplyResponse, ReplyResponses, ResetPromptData, ResetPromptErrors, ResetPromptResponse, ResetPromptResponses, ResourceContents, ResourceMetadata, Response, RestartAgentData, RestartAgentErrors, RestartAgentRequest, RestartAgentResponse, RestartAgentResponse2, RestartAgentResponses, ResumeAgentData, ResumeAgentErrors, ResumeAgentRequest, ResumeAgentResponse, ResumeAgentResponse2, ResumeAgentResponses, RetryConfig, Role, RunNowHandlerData, RunNowHandlerErrors, RunNowHandlerResponse, RunNowHandlerResponses, RunNowResponse, SavePromptData, SavePromptErrors, SavePromptRequest, SavePromptResponse, SavePromptResponses, SaveRecipeData, SaveRecipeError, SaveRecipeErrors, SaveRecipeRequest, SaveRecipeResponse, SaveRecipeResponse2, SaveRecipeResponses, ScanRecipeData, ScanRecipeRequest, ScanRecipeResponse, ScanRecipeResponse2, ScanRecipeResponses, ScheduledJob, ScheduleRecipeData, ScheduleRecipeErrors, ScheduleRecipeRequest, ScheduleRecipeResponses, SearchSessionsData, SearchSessionsErrors, SearchSessionsResponse, SearchSessionsResponses, SendTelemetryEventData, SendTelemetryEventResponses, Session, SessionDisplayInfo, SessionExtensionsResponse, SessionInsights, SessionListResponse, SessionsHandlerData, SessionsHandlerErrors, SessionsHandlerResponse, SessionsHandlerResponses, SessionsQuery, SessionType, SetConfigProviderData, SetProviderRequest, SetRecipeSlashCommandData, SetRecipeSlashCommandErrors, SetRecipeSlashCommandResponses, SetSlashCommandRequest, Settings, SetupResponse, SlashCommand, SlashCommandsResponse, StartAgentData, StartAgentError, StartAgentErrors, StartAgentRequest, StartAgentResponse, StartAgentResponses, StartOpenrouterSetupData, StartOpenrouterSetupResponse, StartOpenrouterSetupResponses, StartTetrateSetupData, StartTetrateSetupResponse, StartTetrateSetupResponses, StartTunnelData, StartTunnelError, StartTunnelErrors, StartTunnelResponse, StartTunnelResponses, StatusData, StatusResponse, StatusResponses, StopAgentData, StopAgentErrors, StopAgentRequest, StopAgentResponse, StopAgentResponses, StopTunnelData, StopTunnelError, StopTunnelErrors, StopTunnelResponses, SubRecipe, SuccessCheck, SystemInfo, SystemInfoData, SystemInfoResponse, SystemInfoResponses, SystemNotificationContent, SystemNotificationType, TaskSupport, TelemetryEventRequest, Template, TextContent, ThinkingContent, TokenState, Tool, ToolAnnotations, ToolConfirmationRequest, ToolExecution, ToolInfo, ToolPermission, ToolRequest, ToolResponse, TranscribeDictationData, TranscribeDictationErrors, TranscribeDictationResponse, TranscribeDictationResponses, TranscribeRequest, TranscribeResponse, TunnelInfo, TunnelState, UiMetadata, UnpauseScheduleData, UnpauseScheduleErrors, UnpauseScheduleResponse, UnpauseScheduleResponses, UpdateAgentProviderData, UpdateAgentProviderErrors, UpdateAgentProviderResponses, UpdateCustomProviderData, UpdateCustomProviderErrors, UpdateCustomProviderRequest, UpdateCustomProviderResponse, UpdateCustomProviderResponses, UpdateFromSessionData, UpdateFromSessionErrors, UpdateFromSessionRequest, UpdateFromSessionResponses, UpdateProviderRequest, UpdateScheduleData, UpdateScheduleErrors, UpdateScheduleRequest, UpdateScheduleResponse, UpdateScheduleResponses, UpdateSessionNameData, UpdateSessionNameErrors, UpdateSessionNameRequest, UpdateSessionNameResponses, UpdateSessionUserRecipeValuesData, UpdateSessionUserRecipeValuesError, UpdateSessionUserRecipeValuesErrors, UpdateSessionUserRecipeValuesRequest, UpdateSessionUserRecipeValuesResponse, UpdateSessionUserRecipeValuesResponse2, UpdateSessionUserRecipeValuesResponses, UpdateWorkingDirData, UpdateWorkingDirErrors, UpdateWorkingDirRequest, UpdateWorkingDirResponses, UpsertConfigData, UpsertConfigErrors, UpsertConfigQuery, UpsertConfigResponse, UpsertConfigResponses, UpsertPermissionsData, UpsertPermissionsErrors, UpsertPermissionsQuery, UpsertPermissionsResponse, UpsertPermissionsResponses, ValidateConfigData, ValidateConfigErrors, ValidateConfigResponse, ValidateConfigResponses, WhisperModelResponse, WindowProps } from './types.gen'; +export { addExtension, agentAddExtension, agentRemoveExtension, backupConfig, callTool, cancelDownload, checkProvider, configureProviderOauth, confirmToolAction, createCustomProvider, createRecipe, createSchedule, decodeRecipe, deleteModel, deleteRecipe, deleteSchedule, deleteSession, detectProvider, diagnostics, downloadModel, encodeRecipe, exportApp, exportSession, forkSession, getCanonicalModelInfo, getCustomProvider, getDictationConfig, getDownloadProgress, getExtensions, getPrompt, getPrompts, getProviderModels, getSession, getSessionExtensions, getSessionInsights, getSlashCommands, getTools, getTunnelStatus, importApp, importSession, initConfig, inspectRunningJob, killRunningJob, listApps, listModels, listRecipes, listSchedules, listSessions, mcpUiProxy, type Options, parseRecipe, pauseSchedule, providers, readAllConfig, readConfig, readResource, recipeToYaml, recoverConfig, removeConfig, removeCustomProvider, removeExtension, reply, resetPrompt, restartAgent, resumeAgent, runNowHandler, savePrompt, saveRecipe, scanRecipe, scheduleRecipe, searchSessions, sendTelemetryEvent, sessionsHandler, setConfigProvider, setRecipeSlashCommand, startAgent, startOpenrouterSetup, startTetrateSetup, startTunnel, status, stopAgent, stopTunnel, systemInfo, transcribeDictation, unpauseSchedule, updateAgentProvider, updateCustomProvider, updateFromSession, updateSchedule, updateSessionName, updateSessionUserRecipeValues, updateWorkingDir, upsertConfig, upsertPermissions, validateConfig } from './sdk.gen'; +export type { ActionRequired, ActionRequiredData, AddExtensionData, AddExtensionErrors, AddExtensionRequest, AddExtensionResponse, AddExtensionResponses, AgentAddExtensionData, AgentAddExtensionErrors, AgentAddExtensionResponse, AgentAddExtensionResponses, AgentRemoveExtensionData, AgentRemoveExtensionErrors, AgentRemoveExtensionResponse, AgentRemoveExtensionResponses, Annotations, Author, AuthorRequest, BackupConfigData, BackupConfigErrors, BackupConfigResponse, BackupConfigResponses, CallToolData, CallToolErrors, CallToolRequest, CallToolResponse, CallToolResponse2, CallToolResponses, CancelDownloadData, CancelDownloadErrors, CancelDownloadResponses, ChatRequest, CheckProviderData, CheckProviderRequest, ClientOptions, CommandType, ConfigKey, ConfigKeyQuery, ConfigResponse, ConfigureProviderOauthData, ConfigureProviderOauthErrors, ConfigureProviderOauthResponses, ConfirmToolActionData, ConfirmToolActionErrors, ConfirmToolActionRequest, ConfirmToolActionResponses, Content, Conversation, CreateCustomProviderData, CreateCustomProviderErrors, CreateCustomProviderResponse, CreateCustomProviderResponses, CreateRecipeData, CreateRecipeErrors, CreateRecipeRequest, CreateRecipeResponse, CreateRecipeResponse2, CreateRecipeResponses, CreateScheduleData, CreateScheduleErrors, CreateScheduleRequest, CreateScheduleResponse, CreateScheduleResponses, CspMetadata, DeclarativeProviderConfig, DecodeRecipeData, DecodeRecipeErrors, DecodeRecipeRequest, DecodeRecipeResponse, DecodeRecipeResponse2, DecodeRecipeResponses, DeleteModelData, DeleteModelErrors, DeleteModelResponses, DeleteRecipeData, DeleteRecipeErrors, DeleteRecipeRequest, DeleteRecipeResponse, DeleteRecipeResponses, DeleteScheduleData, DeleteScheduleErrors, DeleteScheduleResponse, DeleteScheduleResponses, DeleteSessionData, DeleteSessionErrors, DeleteSessionResponses, DetectProviderData, DetectProviderErrors, DetectProviderRequest, DetectProviderResponse, DetectProviderResponse2, DetectProviderResponses, DiagnosticsData, DiagnosticsErrors, DiagnosticsResponse, DiagnosticsResponses, DictationProvider, DictationProviderStatus, DownloadModelData, DownloadModelErrors, DownloadModelResponses, DownloadProgress, DownloadStatus, EmbeddedResource, EncodeRecipeData, EncodeRecipeErrors, EncodeRecipeRequest, EncodeRecipeResponse, EncodeRecipeResponse2, EncodeRecipeResponses, Envs, ErrorResponse, ExportAppData, ExportAppError, ExportAppErrors, ExportAppResponse, ExportAppResponses, ExportSessionData, ExportSessionErrors, ExportSessionResponse, ExportSessionResponses, ExtensionConfig, ExtensionData, ExtensionEntry, ExtensionLoadResult, ExtensionQuery, ExtensionResponse, ForkRequest, ForkResponse, ForkSessionData, ForkSessionErrors, ForkSessionResponse, ForkSessionResponses, FrontendToolRequest, GetCanonicalModelInfoData, GetCanonicalModelInfoResponse, GetCanonicalModelInfoResponses, GetCustomProviderData, GetCustomProviderErrors, GetCustomProviderResponse, GetCustomProviderResponses, GetDictationConfigData, GetDictationConfigResponse, GetDictationConfigResponses, GetDownloadProgressData, GetDownloadProgressErrors, GetDownloadProgressResponse, GetDownloadProgressResponses, GetExtensionsData, GetExtensionsErrors, GetExtensionsResponse, GetExtensionsResponses, GetPromptData, GetPromptErrors, GetPromptResponse, GetPromptResponses, GetPromptsData, GetPromptsResponse, GetPromptsResponses, GetProviderModelsData, GetProviderModelsErrors, GetProviderModelsResponse, GetProviderModelsResponses, GetSessionData, GetSessionErrors, GetSessionExtensionsData, GetSessionExtensionsErrors, GetSessionExtensionsResponse, GetSessionExtensionsResponses, GetSessionInsightsData, GetSessionInsightsErrors, GetSessionInsightsResponse, GetSessionInsightsResponses, GetSessionResponse, GetSessionResponses, GetSlashCommandsData, GetSlashCommandsResponse, GetSlashCommandsResponses, GetToolsData, GetToolsErrors, GetToolsQuery, GetToolsResponse, GetToolsResponses, GetTunnelStatusData, GetTunnelStatusResponse, GetTunnelStatusResponses, GooseApp, Icon, ImageContent, ImportAppData, ImportAppError, ImportAppErrors, ImportAppRequest, ImportAppResponse, ImportAppResponse2, ImportAppResponses, ImportSessionData, ImportSessionErrors, ImportSessionRequest, ImportSessionResponse, ImportSessionResponses, InitConfigData, InitConfigErrors, InitConfigResponse, InitConfigResponses, InspectJobResponse, InspectRunningJobData, InspectRunningJobErrors, InspectRunningJobResponse, InspectRunningJobResponses, JsonObject, KillJobResponse, KillRunningJobData, KillRunningJobResponses, ListAppsData, ListAppsError, ListAppsErrors, ListAppsRequest, ListAppsResponse, ListAppsResponse2, ListAppsResponses, ListModelsData, ListModelsResponse, ListModelsResponses, ListRecipeResponse, ListRecipesData, ListRecipesErrors, ListRecipesResponse, ListRecipesResponses, ListSchedulesData, ListSchedulesErrors, ListSchedulesResponse, ListSchedulesResponse2, ListSchedulesResponses, ListSessionsData, ListSessionsErrors, ListSessionsResponse, ListSessionsResponses, LoadedProvider, McpAppResource, McpUiProxyData, McpUiProxyErrors, McpUiProxyResponses, Message, MessageContent, MessageEvent, MessageMetadata, ModelConfig, ModelInfo, ModelInfoData, ModelInfoQuery, ModelInfoResponse, ParseRecipeData, ParseRecipeError, ParseRecipeErrors, ParseRecipeRequest, ParseRecipeResponse, ParseRecipeResponse2, ParseRecipeResponses, PauseScheduleData, PauseScheduleErrors, PauseScheduleResponse, PauseScheduleResponses, Permission, PermissionLevel, PermissionsMetadata, PrincipalType, PromptContentResponse, PromptsListResponse, ProviderDetails, ProviderEngine, ProviderMetadata, ProvidersData, ProvidersResponse, ProvidersResponse2, ProvidersResponses, ProviderType, RawAudioContent, RawEmbeddedResource, RawImageContent, RawResource, RawTextContent, ReadAllConfigData, ReadAllConfigResponse, ReadAllConfigResponses, ReadConfigData, ReadConfigErrors, ReadConfigResponses, ReadResourceData, ReadResourceErrors, ReadResourceRequest, ReadResourceResponse, ReadResourceResponse2, ReadResourceResponses, ReasoningContent, Recipe, RecipeManifest, RecipeParameter, RecipeParameterInputType, RecipeParameterRequirement, RecipeToYamlData, RecipeToYamlError, RecipeToYamlErrors, RecipeToYamlRequest, RecipeToYamlResponse, RecipeToYamlResponse2, RecipeToYamlResponses, RecoverConfigData, RecoverConfigErrors, RecoverConfigResponse, RecoverConfigResponses, RedactedThinkingContent, RemoveConfigData, RemoveConfigErrors, RemoveConfigResponse, RemoveConfigResponses, RemoveCustomProviderData, RemoveCustomProviderErrors, RemoveCustomProviderResponse, RemoveCustomProviderResponses, RemoveExtensionData, RemoveExtensionErrors, RemoveExtensionRequest, RemoveExtensionResponse, RemoveExtensionResponses, ReplyData, ReplyErrors, ReplyResponse, ReplyResponses, ResetPromptData, ResetPromptErrors, ResetPromptResponse, ResetPromptResponses, ResourceContents, ResourceMetadata, Response, RestartAgentData, RestartAgentErrors, RestartAgentRequest, RestartAgentResponse, RestartAgentResponse2, RestartAgentResponses, ResumeAgentData, ResumeAgentErrors, ResumeAgentRequest, ResumeAgentResponse, ResumeAgentResponse2, ResumeAgentResponses, RetryConfig, Role, RunNowHandlerData, RunNowHandlerErrors, RunNowHandlerResponse, RunNowHandlerResponses, RunNowResponse, SavePromptData, SavePromptErrors, SavePromptRequest, SavePromptResponse, SavePromptResponses, SaveRecipeData, SaveRecipeError, SaveRecipeErrors, SaveRecipeRequest, SaveRecipeResponse, SaveRecipeResponse2, SaveRecipeResponses, ScanRecipeData, ScanRecipeRequest, ScanRecipeResponse, ScanRecipeResponse2, ScanRecipeResponses, ScheduledJob, ScheduleRecipeData, ScheduleRecipeErrors, ScheduleRecipeRequest, ScheduleRecipeResponses, SearchSessionsData, SearchSessionsErrors, SearchSessionsResponse, SearchSessionsResponses, SendTelemetryEventData, SendTelemetryEventResponses, Session, SessionDisplayInfo, SessionExtensionsResponse, SessionInsights, SessionListResponse, SessionsHandlerData, SessionsHandlerErrors, SessionsHandlerResponse, SessionsHandlerResponses, SessionsQuery, SessionType, SetConfigProviderData, SetProviderRequest, SetRecipeSlashCommandData, SetRecipeSlashCommandErrors, SetRecipeSlashCommandResponses, SetSlashCommandRequest, Settings, SetupResponse, SlashCommand, SlashCommandsResponse, StartAgentData, StartAgentError, StartAgentErrors, StartAgentRequest, StartAgentResponse, StartAgentResponses, StartOpenrouterSetupData, StartOpenrouterSetupResponse, StartOpenrouterSetupResponses, StartTetrateSetupData, StartTetrateSetupResponse, StartTetrateSetupResponses, StartTunnelData, StartTunnelError, StartTunnelErrors, StartTunnelResponse, StartTunnelResponses, StatusData, StatusResponse, StatusResponses, StopAgentData, StopAgentErrors, StopAgentRequest, StopAgentResponse, StopAgentResponses, StopTunnelData, StopTunnelError, StopTunnelErrors, StopTunnelResponses, SubRecipe, SuccessCheck, SystemInfo, SystemInfoData, SystemInfoResponse, SystemInfoResponses, SystemNotificationContent, SystemNotificationType, TaskSupport, TelemetryEventRequest, Template, TextContent, ThinkingContent, TokenState, Tool, ToolAnnotations, ToolConfirmationRequest, ToolExecution, ToolInfo, ToolPermission, ToolRequest, ToolResponse, TranscribeDictationData, TranscribeDictationErrors, TranscribeDictationResponse, TranscribeDictationResponses, TranscribeRequest, TranscribeResponse, TunnelInfo, TunnelState, UiMetadata, UnpauseScheduleData, UnpauseScheduleErrors, UnpauseScheduleResponse, UnpauseScheduleResponses, UpdateAgentProviderData, UpdateAgentProviderErrors, UpdateAgentProviderResponses, UpdateCustomProviderData, UpdateCustomProviderErrors, UpdateCustomProviderRequest, UpdateCustomProviderResponse, UpdateCustomProviderResponses, UpdateFromSessionData, UpdateFromSessionErrors, UpdateFromSessionRequest, UpdateFromSessionResponses, UpdateProviderRequest, UpdateScheduleData, UpdateScheduleErrors, UpdateScheduleRequest, UpdateScheduleResponse, UpdateScheduleResponses, UpdateSessionNameData, UpdateSessionNameErrors, UpdateSessionNameRequest, UpdateSessionNameResponses, UpdateSessionUserRecipeValuesData, UpdateSessionUserRecipeValuesError, UpdateSessionUserRecipeValuesErrors, UpdateSessionUserRecipeValuesRequest, UpdateSessionUserRecipeValuesResponse, UpdateSessionUserRecipeValuesResponse2, UpdateSessionUserRecipeValuesResponses, UpdateWorkingDirData, UpdateWorkingDirErrors, UpdateWorkingDirRequest, UpdateWorkingDirResponses, UpsertConfigData, UpsertConfigErrors, UpsertConfigQuery, UpsertConfigResponse, UpsertConfigResponses, UpsertPermissionsData, UpsertPermissionsErrors, UpsertPermissionsQuery, UpsertPermissionsResponse, UpsertPermissionsResponses, ValidateConfigData, ValidateConfigErrors, ValidateConfigResponse, ValidateConfigResponses, WhisperModelResponse, WindowProps } from './types.gen'; diff --git a/ui/desktop/src/api/sdk.gen.ts b/ui/desktop/src/api/sdk.gen.ts index 3192bcd9e8bd..012124b3b4f3 100644 --- a/ui/desktop/src/api/sdk.gen.ts +++ b/ui/desktop/src/api/sdk.gen.ts @@ -2,7 +2,7 @@ import type { Client, Options as Options2, TDataShape } from './client'; import { client } from './client.gen'; -import type { AddExtensionData, AddExtensionErrors, AddExtensionResponses, AgentAddExtensionData, AgentAddExtensionErrors, AgentAddExtensionResponses, AgentRemoveExtensionData, AgentRemoveExtensionErrors, AgentRemoveExtensionResponses, BackupConfigData, BackupConfigErrors, BackupConfigResponses, CallToolData, CallToolErrors, CallToolResponses, CancelDownloadData, CancelDownloadErrors, CancelDownloadResponses, CheckProviderData, ConfigureProviderOauthData, ConfigureProviderOauthErrors, ConfigureProviderOauthResponses, ConfirmToolActionData, ConfirmToolActionErrors, ConfirmToolActionResponses, CreateCustomProviderData, CreateCustomProviderErrors, CreateCustomProviderResponses, CreateRecipeData, CreateRecipeErrors, CreateRecipeResponses, CreateScheduleData, CreateScheduleErrors, CreateScheduleResponses, DecodeRecipeData, DecodeRecipeErrors, DecodeRecipeResponses, DeleteModelData, DeleteModelErrors, DeleteModelResponses, DeleteRecipeData, DeleteRecipeErrors, DeleteRecipeResponses, DeleteScheduleData, DeleteScheduleErrors, DeleteScheduleResponses, DeleteSessionData, DeleteSessionErrors, DeleteSessionResponses, DetectProviderData, DetectProviderErrors, DetectProviderResponses, DiagnosticsData, DiagnosticsErrors, DiagnosticsResponses, DownloadModelData, DownloadModelErrors, DownloadModelResponses, EncodeRecipeData, EncodeRecipeErrors, EncodeRecipeResponses, ExportAppData, ExportAppErrors, ExportAppResponses, ExportSessionData, ExportSessionErrors, ExportSessionResponses, ForkSessionData, ForkSessionErrors, ForkSessionResponses, GetCustomProviderData, GetCustomProviderErrors, GetCustomProviderResponses, GetDictationConfigData, GetDictationConfigResponses, GetDownloadProgressData, GetDownloadProgressErrors, GetDownloadProgressResponses, GetExtensionsData, GetExtensionsErrors, GetExtensionsResponses, GetPricingData, GetPricingResponses, GetPromptData, GetPromptErrors, GetPromptResponses, GetPromptsData, GetPromptsResponses, GetProviderModelsData, GetProviderModelsErrors, GetProviderModelsResponses, GetSessionData, GetSessionErrors, GetSessionExtensionsData, GetSessionExtensionsErrors, GetSessionExtensionsResponses, GetSessionInsightsData, GetSessionInsightsErrors, GetSessionInsightsResponses, GetSessionResponses, GetSlashCommandsData, GetSlashCommandsResponses, GetToolsData, GetToolsErrors, GetToolsResponses, GetTunnelStatusData, GetTunnelStatusResponses, ImportAppData, ImportAppErrors, ImportAppResponses, ImportSessionData, ImportSessionErrors, ImportSessionResponses, InitConfigData, InitConfigErrors, InitConfigResponses, InspectRunningJobData, InspectRunningJobErrors, InspectRunningJobResponses, KillRunningJobData, KillRunningJobResponses, ListAppsData, ListAppsErrors, ListAppsResponses, ListModelsData, ListModelsResponses, ListRecipesData, ListRecipesErrors, ListRecipesResponses, ListSchedulesData, ListSchedulesErrors, ListSchedulesResponses, ListSessionsData, ListSessionsErrors, ListSessionsResponses, McpUiProxyData, McpUiProxyErrors, McpUiProxyResponses, ParseRecipeData, ParseRecipeErrors, ParseRecipeResponses, PauseScheduleData, PauseScheduleErrors, PauseScheduleResponses, ProvidersData, ProvidersResponses, ReadAllConfigData, ReadAllConfigResponses, ReadConfigData, ReadConfigErrors, ReadConfigResponses, ReadResourceData, ReadResourceErrors, ReadResourceResponses, RecipeToYamlData, RecipeToYamlErrors, RecipeToYamlResponses, RecoverConfigData, RecoverConfigErrors, RecoverConfigResponses, RemoveConfigData, RemoveConfigErrors, RemoveConfigResponses, RemoveCustomProviderData, RemoveCustomProviderErrors, RemoveCustomProviderResponses, RemoveExtensionData, RemoveExtensionErrors, RemoveExtensionResponses, ReplyData, ReplyErrors, ReplyResponses, ResetPromptData, ResetPromptErrors, ResetPromptResponses, RestartAgentData, RestartAgentErrors, RestartAgentResponses, ResumeAgentData, ResumeAgentErrors, ResumeAgentResponses, RunNowHandlerData, RunNowHandlerErrors, RunNowHandlerResponses, SavePromptData, SavePromptErrors, SavePromptResponses, SaveRecipeData, SaveRecipeErrors, SaveRecipeResponses, ScanRecipeData, ScanRecipeResponses, ScheduleRecipeData, ScheduleRecipeErrors, ScheduleRecipeResponses, SearchSessionsData, SearchSessionsErrors, SearchSessionsResponses, SendTelemetryEventData, SendTelemetryEventResponses, SessionsHandlerData, SessionsHandlerErrors, SessionsHandlerResponses, SetConfigProviderData, SetRecipeSlashCommandData, SetRecipeSlashCommandErrors, SetRecipeSlashCommandResponses, StartAgentData, StartAgentErrors, StartAgentResponses, StartOpenrouterSetupData, StartOpenrouterSetupResponses, StartTetrateSetupData, StartTetrateSetupResponses, StartTunnelData, StartTunnelErrors, StartTunnelResponses, StatusData, StatusResponses, StopAgentData, StopAgentErrors, StopAgentResponses, StopTunnelData, StopTunnelErrors, StopTunnelResponses, SystemInfoData, SystemInfoResponses, TranscribeDictationData, TranscribeDictationErrors, TranscribeDictationResponses, UnpauseScheduleData, UnpauseScheduleErrors, UnpauseScheduleResponses, UpdateAgentProviderData, UpdateAgentProviderErrors, UpdateAgentProviderResponses, UpdateCustomProviderData, UpdateCustomProviderErrors, UpdateCustomProviderResponses, UpdateFromSessionData, UpdateFromSessionErrors, UpdateFromSessionResponses, UpdateScheduleData, UpdateScheduleErrors, UpdateScheduleResponses, UpdateSessionNameData, UpdateSessionNameErrors, UpdateSessionNameResponses, UpdateSessionUserRecipeValuesData, UpdateSessionUserRecipeValuesErrors, UpdateSessionUserRecipeValuesResponses, UpdateWorkingDirData, UpdateWorkingDirErrors, UpdateWorkingDirResponses, UpsertConfigData, UpsertConfigErrors, UpsertConfigResponses, UpsertPermissionsData, UpsertPermissionsErrors, UpsertPermissionsResponses, ValidateConfigData, ValidateConfigErrors, ValidateConfigResponses } from './types.gen'; +import type { AddExtensionData, AddExtensionErrors, AddExtensionResponses, AgentAddExtensionData, AgentAddExtensionErrors, AgentAddExtensionResponses, AgentRemoveExtensionData, AgentRemoveExtensionErrors, AgentRemoveExtensionResponses, BackupConfigData, BackupConfigErrors, BackupConfigResponses, CallToolData, CallToolErrors, CallToolResponses, CancelDownloadData, CancelDownloadErrors, CancelDownloadResponses, CheckProviderData, ConfigureProviderOauthData, ConfigureProviderOauthErrors, ConfigureProviderOauthResponses, ConfirmToolActionData, ConfirmToolActionErrors, ConfirmToolActionResponses, CreateCustomProviderData, CreateCustomProviderErrors, CreateCustomProviderResponses, CreateRecipeData, CreateRecipeErrors, CreateRecipeResponses, CreateScheduleData, CreateScheduleErrors, CreateScheduleResponses, DecodeRecipeData, DecodeRecipeErrors, DecodeRecipeResponses, DeleteModelData, DeleteModelErrors, DeleteModelResponses, DeleteRecipeData, DeleteRecipeErrors, DeleteRecipeResponses, DeleteScheduleData, DeleteScheduleErrors, DeleteScheduleResponses, DeleteSessionData, DeleteSessionErrors, DeleteSessionResponses, DetectProviderData, DetectProviderErrors, DetectProviderResponses, DiagnosticsData, DiagnosticsErrors, DiagnosticsResponses, DownloadModelData, DownloadModelErrors, DownloadModelResponses, EncodeRecipeData, EncodeRecipeErrors, EncodeRecipeResponses, ExportAppData, ExportAppErrors, ExportAppResponses, ExportSessionData, ExportSessionErrors, ExportSessionResponses, ForkSessionData, ForkSessionErrors, ForkSessionResponses, GetCanonicalModelInfoData, GetCanonicalModelInfoResponses, GetCustomProviderData, GetCustomProviderErrors, GetCustomProviderResponses, GetDictationConfigData, GetDictationConfigResponses, GetDownloadProgressData, GetDownloadProgressErrors, GetDownloadProgressResponses, GetExtensionsData, GetExtensionsErrors, GetExtensionsResponses, GetPromptData, GetPromptErrors, GetPromptResponses, GetPromptsData, GetPromptsResponses, GetProviderModelsData, GetProviderModelsErrors, GetProviderModelsResponses, GetSessionData, GetSessionErrors, GetSessionExtensionsData, GetSessionExtensionsErrors, GetSessionExtensionsResponses, GetSessionInsightsData, GetSessionInsightsErrors, GetSessionInsightsResponses, GetSessionResponses, GetSlashCommandsData, GetSlashCommandsResponses, GetToolsData, GetToolsErrors, GetToolsResponses, GetTunnelStatusData, GetTunnelStatusResponses, ImportAppData, ImportAppErrors, ImportAppResponses, ImportSessionData, ImportSessionErrors, ImportSessionResponses, InitConfigData, InitConfigErrors, InitConfigResponses, InspectRunningJobData, InspectRunningJobErrors, InspectRunningJobResponses, KillRunningJobData, KillRunningJobResponses, ListAppsData, ListAppsErrors, ListAppsResponses, ListModelsData, ListModelsResponses, ListRecipesData, ListRecipesErrors, ListRecipesResponses, ListSchedulesData, ListSchedulesErrors, ListSchedulesResponses, ListSessionsData, ListSessionsErrors, ListSessionsResponses, McpUiProxyData, McpUiProxyErrors, McpUiProxyResponses, ParseRecipeData, ParseRecipeErrors, ParseRecipeResponses, PauseScheduleData, PauseScheduleErrors, PauseScheduleResponses, ProvidersData, ProvidersResponses, ReadAllConfigData, ReadAllConfigResponses, ReadConfigData, ReadConfigErrors, ReadConfigResponses, ReadResourceData, ReadResourceErrors, ReadResourceResponses, RecipeToYamlData, RecipeToYamlErrors, RecipeToYamlResponses, RecoverConfigData, RecoverConfigErrors, RecoverConfigResponses, RemoveConfigData, RemoveConfigErrors, RemoveConfigResponses, RemoveCustomProviderData, RemoveCustomProviderErrors, RemoveCustomProviderResponses, RemoveExtensionData, RemoveExtensionErrors, RemoveExtensionResponses, ReplyData, ReplyErrors, ReplyResponses, ResetPromptData, ResetPromptErrors, ResetPromptResponses, RestartAgentData, RestartAgentErrors, RestartAgentResponses, ResumeAgentData, ResumeAgentErrors, ResumeAgentResponses, RunNowHandlerData, RunNowHandlerErrors, RunNowHandlerResponses, SavePromptData, SavePromptErrors, SavePromptResponses, SaveRecipeData, SaveRecipeErrors, SaveRecipeResponses, ScanRecipeData, ScanRecipeResponses, ScheduleRecipeData, ScheduleRecipeErrors, ScheduleRecipeResponses, SearchSessionsData, SearchSessionsErrors, SearchSessionsResponses, SendTelemetryEventData, SendTelemetryEventResponses, SessionsHandlerData, SessionsHandlerErrors, SessionsHandlerResponses, SetConfigProviderData, SetRecipeSlashCommandData, SetRecipeSlashCommandErrors, SetRecipeSlashCommandResponses, StartAgentData, StartAgentErrors, StartAgentResponses, StartOpenrouterSetupData, StartOpenrouterSetupResponses, StartTetrateSetupData, StartTetrateSetupResponses, StartTunnelData, StartTunnelErrors, StartTunnelResponses, StatusData, StatusResponses, StopAgentData, StopAgentErrors, StopAgentResponses, StopTunnelData, StopTunnelErrors, StopTunnelResponses, SystemInfoData, SystemInfoResponses, TranscribeDictationData, TranscribeDictationErrors, TranscribeDictationResponses, UnpauseScheduleData, UnpauseScheduleErrors, UnpauseScheduleResponses, UpdateAgentProviderData, UpdateAgentProviderErrors, UpdateAgentProviderResponses, UpdateCustomProviderData, UpdateCustomProviderErrors, UpdateCustomProviderResponses, UpdateFromSessionData, UpdateFromSessionErrors, UpdateFromSessionResponses, UpdateScheduleData, UpdateScheduleErrors, UpdateScheduleResponses, UpdateSessionNameData, UpdateSessionNameErrors, UpdateSessionNameResponses, UpdateSessionUserRecipeValuesData, UpdateSessionUserRecipeValuesErrors, UpdateSessionUserRecipeValuesResponses, UpdateWorkingDirData, UpdateWorkingDirErrors, UpdateWorkingDirResponses, UpsertConfigData, UpsertConfigErrors, UpsertConfigResponses, UpsertPermissionsData, UpsertPermissionsErrors, UpsertPermissionsResponses, ValidateConfigData, ValidateConfigErrors, ValidateConfigResponses } from './types.gen'; export type Options = Options2 & { /** @@ -145,6 +145,15 @@ export const readAllConfig = (options?: Op export const backupConfig = (options?: Options) => (options?.client ?? client).post({ url: '/config/backup', ...options }); +export const getCanonicalModelInfo = (options: Options) => (options.client ?? client).post({ + url: '/config/canonical-model-info', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options.headers + } +}); + export const checkProvider = (options: Options) => (options.client ?? client).post({ url: '/config/check_provider', ...options, @@ -209,15 +218,6 @@ export const upsertPermissions = (options: } }); -export const getPricing = (options: Options) => (options.client ?? client).post({ - url: '/config/pricing', - ...options, - headers: { - 'Content-Type': 'application/json', - ...options.headers - } -}); - export const getPrompts = (options?: Options) => (options?.client ?? client).get({ url: '/config/prompts', ...options }); export const resetPrompt = (options: Options) => (options.client ?? client).delete({ url: '/config/prompts/{name}', ...options }); diff --git a/ui/desktop/src/api/types.gen.ts b/ui/desktop/src/api/types.gen.ts index 689d4dce6176..8371c16f6f75 100644 --- a/ui/desktop/src/api/types.gen.ts +++ b/ui/desktop/src/api/types.gen.ts @@ -620,7 +620,6 @@ export type MessageMetadata = { export type ModelConfig = { context_limit?: number | null; - fast_model?: string | null; max_tokens?: number | null; model_name: string; /** @@ -664,6 +663,28 @@ export type ModelInfo = { supports_cache_control?: boolean | null; }; +export type ModelInfoData = { + cache_read_token_cost?: number | null; + cache_write_token_cost?: number | null; + context_limit: number; + currency: string; + input_token_cost?: number | null; + max_output_tokens?: number | null; + model: string; + output_token_cost?: number | null; + provider: string; +}; + +export type ModelInfoQuery = { + model: string; + provider: string; +}; + +export type ModelInfoResponse = { + model_info?: ModelInfoData | null; + source: string; +}; + export type ParseRecipeRequest = { content: string; }; @@ -703,25 +724,6 @@ export type PermissionsMetadata = { microphone?: boolean; }; -export type PricingData = { - context_length?: number | null; - currency: string; - input_token_cost: number; - model: string; - output_token_cost: number; - provider: string; -}; - -export type PricingQuery = { - model: string; - provider: string; -}; - -export type PricingResponse = { - pricing: Array; - source: string; -}; - export type PrincipalType = 'Extension' | 'Tool'; export type PromptContentResponse = { @@ -1972,6 +1974,22 @@ export type BackupConfigResponses = { export type BackupConfigResponse = BackupConfigResponses[keyof BackupConfigResponses]; +export type GetCanonicalModelInfoData = { + body: ModelInfoQuery; + path?: never; + query?: never; + url: '/config/canonical-model-info'; +}; + +export type GetCanonicalModelInfoResponses = { + /** + * Model information retrieved successfully + */ + 200: ModelInfoResponse; +}; + +export type GetCanonicalModelInfoResponse = GetCanonicalModelInfoResponses[keyof GetCanonicalModelInfoResponses]; + export type CheckProviderData = { body: CheckProviderRequest; path?: never; @@ -2245,22 +2263,6 @@ export type UpsertPermissionsResponses = { export type UpsertPermissionsResponse = UpsertPermissionsResponses[keyof UpsertPermissionsResponses]; -export type GetPricingData = { - body: PricingQuery; - path?: never; - query?: never; - url: '/config/pricing'; -}; - -export type GetPricingResponses = { - /** - * Model pricing data retrieved successfully - */ - 200: PricingResponse; -}; - -export type GetPricingResponse = GetPricingResponses[keyof GetPricingResponses]; - export type GetPromptsData = { body?: never; path?: never; diff --git a/ui/desktop/src/components/ChatInput.tsx b/ui/desktop/src/components/ChatInput.tsx index 1e4cd8e6661e..347382d30f3d 100644 --- a/ui/desktop/src/components/ChatInput.tsx +++ b/ui/desktop/src/components/ChatInput.tsx @@ -41,6 +41,7 @@ import { import { getNavigationShortcutText } from '../utils/keyboardShortcuts'; import { UserInput, ImageData } from '../types/message'; import { compressImageDataUrl } from '../utils/conversionUtils'; +import { fetchCanonicalModelInfo } from '../utils/canonical'; interface PastedImage { id: string; @@ -58,11 +59,6 @@ const TOOLS_MAX_SUGGESTED = 60; // max number of tools before we show a warning // Manual compact trigger message - must match backend constant const MANUAL_COMPACT_TRIGGER = '/compact'; -interface ModelLimit { - pattern: string; - context_limit: number; -} - interface ChatInputProps { sessionId: string | null; handleSubmit: (input: UserInput) => void; @@ -142,7 +138,7 @@ export default function ChatInput({ const dropdownRef: React.RefObject = useRef( null ) as React.RefObject; - const { getProviders, read } = useConfig(); + const { getProviders } = useConfig(); const { getCurrentModelAndProvider, currentModel, currentProvider } = useModelAndProvider(); const [tokenLimit, setTokenLimit] = useState(TOKEN_LIMIT_DEFAULT); const [isTokenLimitLoaded, setIsTokenLimitLoaded] = useState(false); @@ -367,28 +363,6 @@ export default function ChatInput({ } }, [textAreaRef]); - // Load model limits from the API - const getModelLimits = async () => { - try { - const response = await read('model-limits', false); - if (response) { - // The response is already parsed, no need for JSON.parse - return response as ModelLimit[]; - } - } catch (err) { - console.error('Error fetching model limits:', err); - } - return []; - }; - - const findModelLimit = (modelName: string, modelLimits: ModelLimit[]): number | null => { - if (!modelName) return null; - const matchingLimit = modelLimits.find((limit) => - modelName.toLowerCase().includes(limit.pattern.toLowerCase()) - ); - return matchingLimit ? matchingLimit.context_limit : null; - }; - // Load providers and get current model's token limit const loadProviderDetails = async () => { try { @@ -403,7 +377,7 @@ export default function ChatInput({ return; } - // First, check predefined models from environment (highest priority) + // Priority 1: Check predefined models from environment const predefinedModels = getPredefinedModelsFromEnv(); const predefinedModel = predefinedModels.find((m) => m.name === model); if (predefinedModel?.context_limit) { @@ -412,12 +386,18 @@ export default function ChatInput({ return; } - const providers = await getProviders(true); + // Priority 2: Check canonical model info (source of truth) + const canonicalInfo = await fetchCanonicalModelInfo(provider, model); + if (canonicalInfo?.context_limit) { + setTokenLimit(canonicalInfo.context_limit); + setIsTokenLimitLoaded(true); + return; + } - // Find the provider details for the current provider + // Priority 3: Fall back to provider metadata known_models (may be outdated) + const providers = await getProviders(true); const currentProvider = providers.find((p) => p.name === provider); if (currentProvider?.metadata?.known_models) { - // Find the model's token limit from the backend response const modelConfig = currentProvider.metadata.known_models.find((m) => m.name === model); if (modelConfig?.context_limit) { setTokenLimit(modelConfig.context_limit); @@ -426,16 +406,7 @@ export default function ChatInput({ } } - // Fallback: Use pattern matching logic if no exact model match was found - const modelLimit = await getModelLimits(); - const fallbackLimit = findModelLimit(model as string, modelLimit); - if (fallbackLimit !== null) { - setTokenLimit(fallbackLimit); - setIsTokenLimitLoaded(true); - return; - } - - // If no match found, use the default model limit + // Priority 4: Use default if nothing else found setTokenLimit(TOKEN_LIMIT_DEFAULT); setIsTokenLimitLoaded(true); } catch (err) { @@ -1190,11 +1161,13 @@ export default function ChatInput({ return (
{ }} + onClick={() => {}} disabled={true} className="bg-slate-600 text-white cursor-not-allowed opacity-50 border-slate-600 rounded-full px-6 py-2" > @@ -1310,12 +1283,13 @@ export default function ChatInput({ } }} disabled={isTranscribing} - className={`rounded-full px-6 py-2 ${isRecording - ? 'bg-red-500 text-white hover:bg-red-600 border-red-500' - : isTranscribing - ? 'bg-slate-600 text-white cursor-not-allowed animate-pulse border-slate-600' - : 'bg-slate-600 text-white hover:bg-slate-700 border-slate-600' - }`} + className={`rounded-full px-6 py-2 ${ + isRecording + ? 'bg-red-500 text-white hover:bg-red-600 border-red-500' + : isTranscribing + ? 'bg-slate-600 text-white cursor-not-allowed animate-pulse border-slate-600' + : 'bg-slate-600 text-white hover:bg-slate-700 border-slate-600' + }`} > @@ -1353,10 +1327,11 @@ export default function ChatInput({ shape="round" variant="outline" disabled={isSubmitButtonDisabled} - className={`rounded-full px-10 py-2 flex items-center gap-2 ${isSubmitButtonDisabled - ? 'bg-slate-600 text-white cursor-not-allowed opacity-50 border-slate-600' - : 'bg-slate-600 text-white hover:bg-slate-700 border-slate-600 hover:cursor-pointer' - }`} + className={`rounded-full px-10 py-2 flex items-center gap-2 ${ + isSubmitButtonDisabled + ? 'bg-slate-600 text-white cursor-not-allowed opacity-50 border-slate-600' + : 'bg-slate-600 text-white hover:bg-slate-700 border-slate-600 hover:cursor-pointer' + }`} > Send diff --git a/ui/desktop/src/components/ToolCallWithResponse.tsx b/ui/desktop/src/components/ToolCallWithResponse.tsx index a4a6ea1ead98..8e8ddc80422b 100644 --- a/ui/desktop/src/components/ToolCallWithResponse.tsx +++ b/ui/desktop/src/components/ToolCallWithResponse.tsx @@ -120,9 +120,7 @@ function McpAppWrapper({ const resultWithMeta = toolResponse?.toolResult as ToolResultWithMeta | undefined; const toolResult = - resultWithMeta?.status === 'success' && resultWithMeta.value - ? resultWithMeta.value - : undefined; + resultWithMeta?.status === 'success' && resultWithMeta.value ? resultWithMeta.value : undefined; if (!resourceUri) return null; if (requestWithMeta.toolCall.status !== 'success') return null; diff --git a/ui/desktop/src/components/bottom_menu/CostTracker.tsx b/ui/desktop/src/components/bottom_menu/CostTracker.tsx index 4514b44fdde4..5ebf895746c6 100644 --- a/ui/desktop/src/components/bottom_menu/CostTracker.tsx +++ b/ui/desktop/src/components/bottom_menu/CostTracker.tsx @@ -2,8 +2,8 @@ import { useState, useEffect } from 'react'; import { useModelAndProvider } from '../ModelAndProviderContext'; import { CoinIcon } from '../icons'; import { Tooltip, TooltipContent, TooltipTrigger } from '../ui/Tooltip'; -import { fetchModelPricing } from '../../utils/pricing'; -import { PricingData } from '../../api'; +import { fetchCanonicalModelInfo } from '../../utils/canonical'; +import type { ModelInfoData } from '../../api'; interface CostTrackerProps { inputTokens?: number; @@ -19,7 +19,7 @@ interface CostTrackerProps { export function CostTracker({ inputTokens = 0, outputTokens = 0, sessionCosts }: CostTrackerProps) { const { currentModel, currentProvider } = useModelAndProvider(); - const [costInfo, setCostInfo] = useState(null); + const [costInfo, setCostInfo] = useState(null); const [isLoading, setIsLoading] = useState(true); const [showPricing, setShowPricing] = useState(true); const [pricingFailed, setPricingFailed] = useState(false); @@ -45,7 +45,7 @@ export function CostTracker({ inputTokens = 0, outputTokens = 0, sessionCosts }: setIsLoading(true); try { - const costData = await fetchModelPricing(currentProvider, currentModel); + const costData = await fetchCanonicalModelInfo(currentProvider, currentModel); if (costData) { setCostInfo(costData); setPricingFailed(false); @@ -84,8 +84,8 @@ export function CostTracker({ inputTokens = 0, outputTokens = 0, sessionCosts }: costInfo && (costInfo.input_token_cost !== undefined || costInfo.output_token_cost !== undefined) ) { - const currentInputCost = inputTokens * (costInfo.input_token_cost || 0); - const currentOutputCost = outputTokens * (costInfo.output_token_cost || 0); + const currentInputCost = (inputTokens * (costInfo.input_token_cost || 0)) / 1_000_000; + const currentOutputCost = (outputTokens * (costInfo.output_token_cost || 0)) / 1_000_000; totalCost += currentInputCost + currentOutputCost; } @@ -100,8 +100,8 @@ export function CostTracker({ inputTokens = 0, outputTokens = 0, sessionCosts }: return 0; } - const inputCost = inputTokens * (costInfo.input_token_cost || 0); - const outputCost = outputTokens * (costInfo.output_token_cost || 0); + const inputCost = (inputTokens * (costInfo.input_token_cost || 0)) / 1_000_000; + const outputCost = (outputTokens * (costInfo.output_token_cost || 0)) / 1_000_000; const total = inputCost + outputCost; return total; @@ -201,8 +201,9 @@ export function CostTracker({ inputTokens = 0, outputTokens = 0, sessionCosts }: // Add current model if it has costs if (costInfo && (inputTokens > 0 || outputTokens > 0)) { const currentCost = - inputTokens * (costInfo.input_token_cost || 0) + - outputTokens * (costInfo.output_token_cost || 0); + (inputTokens * (costInfo.input_token_cost || 0) + + outputTokens * (costInfo.output_token_cost || 0)) / + 1_000_000; if (currentCost > 0) { tooltip += `${currentProvider}/${currentModel} (current): ${costInfo.currency || '$'}${currentCost.toFixed(6)} (${inputTokens.toLocaleString()} in, ${outputTokens.toLocaleString()} out)\n`; } @@ -213,7 +214,7 @@ export function CostTracker({ inputTokens = 0, outputTokens = 0, sessionCosts }: } // Default tooltip for single model - return `Input: ${inputTokens.toLocaleString()} tokens (${costInfo?.currency || '$'}${(inputTokens * (costInfo?.input_token_cost || 0)).toFixed(6)}) | Output: ${outputTokens.toLocaleString()} tokens (${costInfo?.currency || '$'}${(outputTokens * (costInfo?.output_token_cost || 0)).toFixed(6)})`; + return `Input: ${inputTokens.toLocaleString()} tokens (${costInfo?.currency || '$'}${((inputTokens * (costInfo?.input_token_cost || 0)) / 1_000_000).toFixed(6)}) | Output: ${outputTokens.toLocaleString()} tokens (${costInfo?.currency || '$'}${((outputTokens * (costInfo?.output_token_cost || 0)) / 1_000_000).toFixed(6)})`; }; return ( diff --git a/ui/desktop/src/components/sessions/SessionListView.tsx b/ui/desktop/src/components/sessions/SessionListView.tsx index 0b7301af41d6..ae6028dccbd9 100644 --- a/ui/desktop/src/components/sessions/SessionListView.tsx +++ b/ui/desktop/src/components/sessions/SessionListView.tsx @@ -358,15 +358,17 @@ const SessionListView: React.FC = React.memo( const resp = await searchSessions({ query: { query: debouncedSearchTerm }, }); - + if (resp.data) { // Response is Vec - sessions that match the search const matchedSessionIds = new Set(resp.data.map((s: { id: string }) => s.id)); const filtered = sessions.filter((session) => matchedSessionIds.has(session.id)); - + startTransition(() => { setFilteredSessions(filtered); - setSearchResults(filtered.length > 0 ? { count: filtered.length, currentIndex: 1 } : null); + setSearchResults( + filtered.length > 0 ? { count: filtered.length, currentIndex: 1 } : null + ); }); } }; diff --git a/ui/desktop/src/components/settings/extensions/modal/ExtensionModal.tsx b/ui/desktop/src/components/settings/extensions/modal/ExtensionModal.tsx index 4968e8d566fd..a3ba9ccfc15b 100644 --- a/ui/desktop/src/components/settings/extensions/modal/ExtensionModal.tsx +++ b/ui/desktop/src/components/settings/extensions/modal/ExtensionModal.tsx @@ -163,7 +163,7 @@ export default function ExtensionModal({ const trimmedNewKey = value.trim(); const normalizedNewKey = trimmedNewKey.toLowerCase(); const isDuplicate = formData.headers.some( - (h, i) => i !== index && h.key.trim().toLowerCase() === normalizedNewKey, + (h, i) => i !== index && h.key.trim().toLowerCase() === normalizedNewKey ); if (isDuplicate && trimmedNewKey !== '') { return; diff --git a/ui/desktop/src/components/settings/extensions/modal/HeadersSection.tsx b/ui/desktop/src/components/settings/extensions/modal/HeadersSection.tsx index 5b265529f424..67b8c58fff5b 100644 --- a/ui/desktop/src/components/settings/extensions/modal/HeadersSection.tsx +++ b/ui/desktop/src/components/settings/extensions/modal/HeadersSection.tsx @@ -45,9 +45,7 @@ export default function HeadersSection({ const valueEmpty = !newValue.trim(); const keyHasSpaces = newKey.includes(' '); const normalizedNewKey = newKey.trim().toLowerCase(); - const isDuplicate = headers.some( - h => h.key.trim().toLowerCase() === normalizedNewKey - ); + const isDuplicate = headers.some((h) => h.key.trim().toLowerCase() === normalizedNewKey); if (keyEmpty || valueEmpty) { setInvalidFields({ diff --git a/ui/desktop/src/components/settings/extensions/utils.ts b/ui/desktop/src/components/settings/extensions/utils.ts index 3ea8bb04cff0..466ce90ea7b6 100644 --- a/ui/desktop/src/components/settings/extensions/utils.ts +++ b/ui/desktop/src/components/settings/extensions/utils.ts @@ -100,8 +100,8 @@ export function extensionToFormData(extension: FixedExtensionEntry): ExtensionFo description: extension.description || '', type: extension.type === 'frontend' || - extension.type === 'inline_python' || - extension.type === 'platform' + extension.type === 'inline_python' || + extension.type === 'platform' ? 'stdio' : extension.type, cmd: extension.type === 'stdio' ? quoteShell([extension.cmd, ...extension.args]) : undefined, diff --git a/ui/desktop/src/components/settings/models/bottom_bar/ModelsBottomBar.tsx b/ui/desktop/src/components/settings/models/bottom_bar/ModelsBottomBar.tsx index f7c97c2dd522..b482b5e2a255 100644 --- a/ui/desktop/src/components/settings/models/bottom_bar/ModelsBottomBar.tsx +++ b/ui/desktop/src/components/settings/models/bottom_bar/ModelsBottomBar.tsx @@ -30,10 +30,7 @@ export default function ModelsBottomBar({ setView, alerts, }: ModelsBottomBarProps) { - const { - currentModel, - currentProvider, - } = useModelAndProvider(); + const { currentModel, currentProvider } = useModelAndProvider(); const currentModelInfo = useCurrentModelInfo(); const { read, getProviders } = useConfig(); const [displayProvider, setDisplayProvider] = useState(null); diff --git a/ui/desktop/src/components/settings/models/subcomponents/SwitchModelModal.tsx b/ui/desktop/src/components/settings/models/subcomponents/SwitchModelModal.tsx index 79af485e2d06..6d294a18b9ee 100644 --- a/ui/desktop/src/components/settings/models/subcomponents/SwitchModelModal.tsx +++ b/ui/desktop/src/components/settings/models/subcomponents/SwitchModelModal.tsx @@ -85,7 +85,9 @@ export const SwitchModelModal = ({ const [providerOptions, setProviderOptions] = useState<{ value: string; label: string }[]>([]); type ModelOption = { value: string; label: string; provider: string; isDisabled?: boolean }; const [modelOptions, setModelOptions] = useState<{ options: ModelOption[] }[]>([]); - const [provider, setProvider] = useState(initialProvider || currentProvider || null); + const [provider, setProvider] = useState( + initialProvider || currentProvider || null + ); const [model, setModel] = useState(currentModel || ''); const [isCustomModel, setIsCustomModel] = useState(false); const [validationErrors, setValidationErrors] = useState({ diff --git a/ui/desktop/src/components/settings/providers/modal/subcomponents/forms/CustomProviderForm.tsx b/ui/desktop/src/components/settings/providers/modal/subcomponents/forms/CustomProviderForm.tsx index 78a3ddb433e9..a70dc4976c1b 100644 --- a/ui/desktop/src/components/settings/providers/modal/subcomponents/forms/CustomProviderForm.tsx +++ b/ui/desktop/src/components/settings/providers/modal/subcomponents/forms/CustomProviderForm.tsx @@ -78,7 +78,7 @@ export default function CustomProviderForm({ const valueEmpty = !newHeaderValue.trim(); const keyHasSpaces = newHeaderKey.includes(' '); const normalizedNewKey = newHeaderKey.trim().toLowerCase(); - const isDuplicate = headers.some(h => h.key.trim().toLowerCase() === normalizedNewKey); + const isDuplicate = headers.some((h) => h.key.trim().toLowerCase() === normalizedNewKey); if (keyEmpty || valueEmpty) { setInvalidHeaderFields({ @@ -125,7 +125,7 @@ export default function CustomProviderForm({ } const normalizedValue = value.trim().toLowerCase(); const isDuplicate = headers.some( - (h, i) => i !== index && h.key.trim().toLowerCase() === normalizedValue, + (h, i) => i !== index && h.key.trim().toLowerCase() === normalizedValue ); if (isDuplicate && normalizedValue !== '') { return; @@ -177,9 +177,7 @@ export default function CustomProviderForm({ if (newHeaderKey.trim() && newHeaderValue.trim()) { const keyHasSpaces = newHeaderKey.includes(' '); const normalizedPendingKey = newHeaderKey.trim().toLowerCase(); - const isDuplicate = headers.some( - (h) => h.key.trim().toLowerCase() === normalizedPendingKey, - ); + const isDuplicate = headers.some((h) => h.key.trim().toLowerCase() === normalizedPendingKey); if (!keyHasSpaces && !isDuplicate) { allHeaders.push({ key: newHeaderKey, value: newHeaderValue }); @@ -387,7 +385,8 @@ export default function CustomProviderForm({ Custom Headers

- Add custom HTTP headers to include in requests to the provider. Click the "+" button to add after filling both fields. + Add custom HTTP headers to include in requests to the provider. Click the "+" button + to add after filling both fields.

{headers.map((header, index) => ( diff --git a/ui/desktop/src/hooks/useCostTracking.ts b/ui/desktop/src/hooks/useCostTracking.ts index 8ea74b47a9f5..11773adbf65c 100644 --- a/ui/desktop/src/hooks/useCostTracking.ts +++ b/ui/desktop/src/hooks/useCostTracking.ts @@ -1,6 +1,6 @@ import { useEffect, useRef, useState } from 'react'; import { useModelAndProvider } from '../components/ModelAndProviderContext'; -import { fetchModelPricing } from '../utils/pricing'; +import { fetchCanonicalModelInfo } from '../utils/canonical'; import { Session } from '../api'; interface UseCostTrackingProps { @@ -42,13 +42,18 @@ export const useCostTracking = ({ const prevKey = `${prevProviderRef.current}/${prevModelRef.current}`; // Get pricing info for the previous model - const prevCostInfo = await fetchModelPricing(prevProviderRef.current, prevModelRef.current); + const prevCostInfo = await fetchCanonicalModelInfo( + prevProviderRef.current, + prevModelRef.current + ); if (prevCostInfo) { const prevInputCost = - (sessionInputTokens || localInputTokens) * (prevCostInfo.input_token_cost || 0); + ((sessionInputTokens || localInputTokens) * (prevCostInfo.input_token_cost || 0)) / + 1_000_000; const prevOutputCost = - (sessionOutputTokens || localOutputTokens) * (prevCostInfo.output_token_cost || 0); + ((sessionOutputTokens || localOutputTokens) * (prevCostInfo.output_token_cost || 0)) / + 1_000_000; const prevTotalCost = prevInputCost + prevOutputCost; // Save the accumulated costs for this model diff --git a/ui/desktop/src/utils/canonical.ts b/ui/desktop/src/utils/canonical.ts new file mode 100644 index 000000000000..6ee98da6052b --- /dev/null +++ b/ui/desktop/src/utils/canonical.ts @@ -0,0 +1,24 @@ +/** + * Utilities for fetching canonical model information from the backend + */ + +import { getCanonicalModelInfo, type ModelInfoData } from '../api'; + +/** + * Fetch canonical model info (pricing + context limits) for a specific provider/model + */ +export async function fetchCanonicalModelInfo( + provider: string, + model: string +): Promise { + try { + const response = await getCanonicalModelInfo({ + body: { provider, model }, + throwOnError: true, + }); + + return response.data.model_info ?? null; + } catch { + return null; + } +} diff --git a/ui/desktop/src/utils/pricing.ts b/ui/desktop/src/utils/pricing.ts deleted file mode 100644 index 509b92409851..000000000000 --- a/ui/desktop/src/utils/pricing.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { getPricing, PricingData } from '../api'; - -/** - * Fetch pricing for a specific provider/model from the backend - */ -export async function fetchModelPricing( - provider: string, - model: string -): Promise { - try { - const response = await getPricing({ - body: { provider, model }, - throwOnError: false, - }); - - if (!response.data) { - return null; - } - - return response.data.pricing?.[0] ?? null; - } catch { - return null; - } -}