From 3959805198e867e606f3af94239c5b7ea42ec678 Mon Sep 17 00:00:00 2001 From: David Katz Date: Tue, 17 Feb 2026 11:43:10 -0500 Subject: [PATCH] Use Canonical Models to set context window sizes (#6723) --- crates/goose-acp/src/server.rs | 16 +- crates/goose-acp/src/server_factory.rs | 4 +- crates/goose-acp/tests/common_tests/mod.rs | 2 +- crates/goose-acp/tests/fixtures/mod.rs | 6 +- crates/goose-cli/src/commands/configure.rs | 11 +- crates/goose-cli/src/commands/term.rs | 8 +- crates/goose-cli/src/commands/web.rs | 2 +- .../src/scenario_tests/scenario_runner.rs | 2 +- crates/goose-cli/src/session/builder.rs | 1 + crates/goose-cli/src/session/mod.rs | 2 +- crates/goose-server/src/openapi.rs | 8 +- crates/goose-server/src/routes/agent.rs | 1 + .../src/routes/config_management.rs | 123 ++++------ crates/goose/src/agents/agent.rs | 1 + .../src/agents/platform_extensions/summon.rs | 9 +- crates/goose/src/context_mgmt/mod.rs | 2 +- crates/goose/src/model.rs | 207 +++++------------ crates/goose/src/providers/anthropic.rs | 2 +- crates/goose/src/providers/auto_detect.rs | 2 +- crates/goose/src/providers/base.rs | 8 +- .../src/providers/canonical/name_builder.rs | 4 +- crates/goose/src/providers/claude_code.rs | 4 +- crates/goose/src/providers/databricks.rs | 7 +- .../goose/src/providers/formats/anthropic.rs | 19 +- .../goose/src/providers/formats/databricks.rs | 8 +- crates/goose/src/providers/formats/openai.rs | 6 +- .../goose/src/providers/formats/snowflake.rs | 6 +- crates/goose/src/providers/google.rs | 2 +- crates/goose/src/providers/init.rs | 23 +- crates/goose/src/providers/openai.rs | 2 +- crates/goose/src/providers/openrouter.rs | 2 +- .../goose/src/providers/provider_registry.rs | 4 +- crates/goose/src/providers/provider_test.rs | 1 + crates/goose/src/providers/toolshim.rs | 3 +- crates/goose/src/providers/venice.rs | 5 +- crates/goose/src/scheduler.rs | 3 +- crates/goose/tests/providers.rs | 2 +- crates/goose/tests/tetrate_streaming.rs | 6 +- ui/desktop/openapi.json | 215 +++++++++--------- ui/desktop/src/api/index.ts | 4 +- ui/desktop/src/api/sdk.gen.ts | 20 +- ui/desktop/src/api/types.gen.ts | 74 +++--- ui/desktop/src/components/ChatInput.tsx | 87 +++---- .../src/components/ToolCallWithResponse.tsx | 4 +- .../components/bottom_menu/CostTracker.tsx | 23 +- .../components/sessions/SessionListView.tsx | 8 +- .../extensions/modal/ExtensionModal.tsx | 2 +- .../extensions/modal/HeadersSection.tsx | 4 +- .../models/bottom_bar/ModelsBottomBar.tsx | 5 +- .../models/subcomponents/SwitchModelModal.tsx | 4 +- .../forms/CustomProviderForm.tsx | 11 +- ui/desktop/src/hooks/useCostTracking.ts | 13 +- ui/desktop/src/utils/canonical.ts | 24 ++ ui/desktop/src/utils/pricing.ts | 24 -- 54 files changed, 465 insertions(+), 581 deletions(-) create mode 100644 ui/desktop/src/utils/canonical.ts delete mode 100644 ui/desktop/src/utils/pricing.ts diff --git a/crates/goose-acp/src/server.rs b/crates/goose-acp/src/server.rs index 197fcc92..78b2c83b 100644 --- a/crates/goose-acp/src/server.rs +++ b/crates/goose-acp/src/server.rs @@ -738,7 +738,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?; @@ -956,9 +957,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 96b94559..b2810d85 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 c5471d2e..e6a4fd26 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 a4a295af..ff9c2983 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 4be26fd6..ef7bcb01 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 22fade45..19661a31 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 c3dedac3..09f91bef 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 e872c84b..a1472aa0 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 9807a4ba..6b1e6a0b 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 dc07bc59..853e6398 100644 --- a/crates/goose-cli/src/session/mod.rs +++ b/crates/goose-cli/src/session/mod.rs @@ -1847,7 +1847,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 1fa80ab3..f33db835 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 1c5b9a89..6fc0a266 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 45562d79..032d55e9 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 - )) - })?; +pub async fn get_canonical_model_info( + Json(query): Json, +) -> Json { + let canonical_model = maybe_get_canonical_model(&query.provider, &query.model); - let mut pricing_data = Vec::new(); + 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(), + }); - 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), - }); - } - - 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 20023a0b..9ed9f883 100644 --- a/crates/goose/src/agents/agent.rs +++ b/crates/goose/src/agents/agent.rs @@ -1629,6 +1629,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 c300f08c..1d160583 100644 --- a/crates/goose/src/agents/platform_extensions/summon.rs +++ b/crates/goose/src/agents/platform_extensions/summon.rs @@ -1417,11 +1417,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 b2a53a47..2041f032 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 4adfa7d6..32fb80d1 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; + self.context_limit.unwrap_or(DEFAULT_CONTEXT_LIMIT) + } + + pub fn max_output_tokens(&self) -> i32 { + if let Some(tokens) = self.max_tokens { + return tokens; } - // Otherwise, get the model's default limit - let main_limit = - Self::get_model_specific_limit(&self.model_name).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 - } + // 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 c6dfa100..2bec2118 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 39a9e541..fff59baf 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 b253afea..173dddf6 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 afa7a5b2..3b3b2484 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 9312c5b3..a00d21cd 100644 --- a/crates/goose/src/providers/claude_code.rs +++ b/crates/goose/src/providers/claude_code.rs @@ -1336,7 +1336,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 49709acb..c5509fce 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 67aa5878..f47a387a 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 51a1265c..383aeb22 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 ae5c4595..567ce0cf 100644 --- a/crates/goose/src/providers/formats/openai.rs +++ b/crates/goose/src/providers/formats/openai.rs @@ -1496,7 +1496,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( @@ -1536,7 +1536,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( @@ -1577,7 +1577,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 b3bb6202..cbb561b3 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 31fda4c2..217aa190 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 333362ae..c1f69e61 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 6e7834bd..40fc1ee6 100644 --- a/crates/goose/src/providers/openai.rs +++ b/crates/goose/src/providers/openai.rs @@ -72,7 +72,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 9ce10f10..26d08983 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 45f94d59..8248a7b5 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 b47b59d6..3283b9b7 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 cc9ae121..086543bd 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 3b453094..7a26d47e 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 815f535b..192fa796 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 ba4cbe47..0d3ff3a4 100644 --- a/crates/goose/tests/providers.rs +++ b/crates/goose/tests/providers.rs @@ -283,7 +283,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 ab4663f3..32c4c202 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 7ec30838..57984244 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 7089867c..9a24bb7a 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 3192bcd9..012124b3 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 689d4dce..8371c16f 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 1e4cd8e6..347382d3 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 a4a6ea1e..8e8ddc80 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 4514b44f..5ebf8957 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 0b7301af..ae6028dc 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 4968e8d5..a3ba9ccf 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 5b265529..67b8c58f 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/models/bottom_bar/ModelsBottomBar.tsx b/ui/desktop/src/components/settings/models/bottom_bar/ModelsBottomBar.tsx index f7c97c2d..b482b5e2 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 79af485e..6d294a18 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 78a3ddb4..a70dc497 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 8ea74b47..11773adb 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 00000000..6ee98da6 --- /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 509b9240..00000000 --- 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; - } -}