diff --git a/src/llm/costs.rs b/src/llm/costs.rs index 89c5b529..d7a1860c 100644 --- a/src/llm/costs.rs +++ b/src/llm/costs.rs @@ -17,7 +17,17 @@ pub fn model_cost(model_id: &str) -> Option<(Decimal, Decimal)> { .unwrap_or(model_id); match id { - // OpenAI models -- prices per token (USD) + // OpenAI — GPT-5.x / Codex + "gpt-5.3-codex" | "gpt-5.3-codex-spark" => Some((dec!(0.000002), dec!(0.000008))), + "gpt-5.2-codex" | "gpt-5.2-pro" | "gpt-5.2" => Some((dec!(0.000002), dec!(0.000008))), + "gpt-5.1-codex" | "gpt-5.1-codex-max" | "gpt-5.1" => Some((dec!(0.000002), dec!(0.000008))), + "gpt-5.1-codex-mini" => Some((dec!(0.0000003), dec!(0.0000012))), + "gpt-5-codex" | "gpt-5-pro" | "gpt-5" => Some((dec!(0.000002), dec!(0.000008))), + "gpt-5-mini" | "gpt-5-nano" => Some((dec!(0.0000003), dec!(0.0000012))), + // OpenAI — GPT-4.x + "gpt-4.1" => Some((dec!(0.000002), dec!(0.000008))), + "gpt-4.1-mini" => Some((dec!(0.0000004), dec!(0.0000016))), + "gpt-4.1-nano" => Some((dec!(0.0000001), dec!(0.0000004))), "gpt-4o" | "gpt-4o-2024-11-20" | "gpt-4o-2024-08-06" => { Some((dec!(0.0000025), dec!(0.00001))) } @@ -25,20 +35,36 @@ pub fn model_cost(model_id: &str) -> Option<(Decimal, Decimal)> { "gpt-4-turbo" | "gpt-4-turbo-2024-04-09" => Some((dec!(0.00001), dec!(0.00003))), "gpt-4" | "gpt-4-0613" => Some((dec!(0.00003), dec!(0.00006))), "gpt-3.5-turbo" | "gpt-3.5-turbo-0125" => Some((dec!(0.0000005), dec!(0.0000015))), + // OpenAI — reasoning + "o3" => Some((dec!(0.000002), dec!(0.000008))), + "o3-mini" | "o3-mini-2025-01-31" => Some((dec!(0.0000011), dec!(0.0000044))), + "o4-mini" => Some((dec!(0.0000011), dec!(0.0000044))), "o1" | "o1-2024-12-17" => Some((dec!(0.000015), dec!(0.00006))), "o1-mini" | "o1-mini-2024-09-12" => Some((dec!(0.000003), dec!(0.000012))), - "o3-mini" | "o3-mini-2025-01-31" => Some((dec!(0.0000011), dec!(0.0000044))), - // Anthropic models - "claude-3-5-sonnet-20241022" | "claude-3-5-sonnet-latest" | "claude-sonnet-4-20250514" => { - Some((dec!(0.000003), dec!(0.000015))) - } - "claude-3-5-haiku-20241022" | "claude-3-5-haiku-latest" => { - Some((dec!(0.0000008), dec!(0.000004))) - } - "claude-3-opus-20240229" | "claude-3-opus-latest" | "claude-opus-4-20250514" => { - Some((dec!(0.000015), dec!(0.000075))) - } + // Anthropic + "claude-opus-4-6" + | "claude-opus-4-5" + | "claude-opus-4-5-20251101" + | "claude-opus-4-1" + | "claude-opus-4-1-20250805" + | "claude-opus-4-0" + | "claude-opus-4-20250514" + | "claude-3-opus-20240229" + | "claude-3-opus-latest" => Some((dec!(0.000015), dec!(0.000075))), + "claude-sonnet-4-6" + | "claude-sonnet-4-5" + | "claude-sonnet-4-5-20250929" + | "claude-sonnet-4-0" + | "claude-sonnet-4-20250514" + | "claude-3-7-sonnet-20250219" + | "claude-3-7-sonnet-latest" + | "claude-3-5-sonnet-20241022" + | "claude-3-5-sonnet-latest" => Some((dec!(0.000003), dec!(0.000015))), + "claude-haiku-4-5" + | "claude-haiku-4-5-20251001" + | "claude-3-5-haiku-20241022" + | "claude-3-5-haiku-latest" => Some((dec!(0.0000008), dec!(0.000004))), "claude-3-haiku-20240307" => Some((dec!(0.00000025), dec!(0.00000125))), // Ollama / local models -- free diff --git a/src/setup/wizard.rs b/src/setup/wizard.rs index e91f922b..9dc50fad 100644 --- a/src/setup/wizard.rs +++ b/src/setup/wizard.rs @@ -949,6 +949,11 @@ impl SetupWizard { "anthropic::claude-sonnet-4-20250514".into(), "Claude Sonnet 4 (best quality)".into(), ), + ( + "openai::gpt-5.3-codex".into(), + "GPT-5.3 Codex (flagship)".into(), + ), + ("openai::gpt-5.2".into(), "GPT-5.2".into()), ("openai::gpt-4o".into(), "GPT-4o".into()), ]; @@ -1714,12 +1719,14 @@ fn mask_password_in_url(url: &str) -> String { /// Returns `(model_id, display_label)` pairs. Falls back to static defaults on error. async fn fetch_anthropic_models(cached_key: Option<&str>) -> Vec<(String, String)> { let static_defaults = vec![ - ("claude-sonnet-4-20250514".into(), "Claude Sonnet 4".into()), - ("claude-opus-4-20250514".into(), "Claude Opus 4".into()), ( - "claude-3-5-haiku-20241022".into(), - "Claude 3.5 Haiku (fast)".into(), + "claude-opus-4-6".into(), + "Claude Opus 4.6 (latest flagship)".into(), ), + ("claude-sonnet-4-6".into(), "Claude Sonnet 4.6".into()), + ("claude-opus-4-5".into(), "Claude Opus 4.5".into()), + ("claude-sonnet-4-5".into(), "Claude Sonnet 4.5".into()), + ("claude-haiku-4-5".into(), "Claude Haiku 4.5 (fast)".into()), ]; let api_key = cached_key @@ -1780,10 +1787,21 @@ async fn fetch_anthropic_models(cached_key: Option<&str>) -> Vec<(String, String /// Returns `(model_id, display_label)` pairs. Falls back to static defaults on error. async fn fetch_openai_models(cached_key: Option<&str>) -> Vec<(String, String)> { let static_defaults = vec![ - ("gpt-5".into(), "GPT-5 (flagship)".into()), - ("gpt-5-mini".into(), "GPT-5 Mini (fast)".into()), + ( + "gpt-5.3-codex".into(), + "GPT-5.3 Codex (latest flagship)".into(), + ), + ("gpt-5.2-codex".into(), "GPT-5.2 Codex".into()), + ("gpt-5.2".into(), "GPT-5.2".into()), + ( + "gpt-5.1-codex-mini".into(), + "GPT-5.1 Codex Mini (fast)".into(), + ), + ("gpt-5".into(), "GPT-5".into()), + ("gpt-5-mini".into(), "GPT-5 Mini".into()), ("gpt-4.1".into(), "GPT-4.1".into()), - ("gpt-4o".into(), "GPT-4o".into()), + ("gpt-4.1-mini".into(), "GPT-4.1 Mini".into()), + ("o4-mini".into(), "o4-mini (fast reasoning)".into()), ("o3".into(), "o3 (reasoning)".into()), ]; @@ -1864,11 +1882,15 @@ fn openai_model_priority(model_id: &str) -> usize { let id = model_id.to_ascii_lowercase(); const EXACT_PRIORITY: &[&str] = &[ + "gpt-5.3-codex", + "gpt-5.2-codex", + "gpt-5.2", + "gpt-5.1-codex-mini", "gpt-5", "gpt-5-mini", "gpt-5-nano", - "o3", "o4-mini", + "o3", "o1", "gpt-4.1", "gpt-4.1-mini", @@ -1880,7 +1902,7 @@ fn openai_model_priority(model_id: &str) -> usize { } const PREFIX_PRIORITY: &[&str] = &[ - "gpt-5-", "o3-", "o4-", "o1-", "gpt-4.1-", "gpt-4o-", "gpt-3.5-", "chatgpt-", + "gpt-5.", "gpt-5-", "o3-", "o4-", "o1-", "gpt-4.1-", "gpt-4o-", "gpt-3.5-", "chatgpt-", ]; if let Some(pos) = PREFIX_PRIORITY .iter() @@ -2230,7 +2252,7 @@ mod tests { let _guard = EnvGuard::clear("OPENAI_API_KEY"); let models = fetch_openai_models(None).await; assert!(!models.is_empty()); - assert_eq!(models[0].0, "gpt-5"); + assert_eq!(models[0].0, "gpt-5.3-codex"); assert!( models.iter().any(|(id, _)| id.contains("gpt")), "static defaults should include a GPT model" diff --git a/src/tools/builtin/http.rs b/src/tools/builtin/http.rs index 8b4593c2..aa5d539a 100644 --- a/src/tools/builtin/http.rs +++ b/src/tools/builtin/http.rs @@ -189,8 +189,8 @@ impl Tool for HttpTool { } }, "body": { - "type": "string", - "description": "Request body. Use plain text or serialized JSON." + "type": ["object", "array", "string", "number", "boolean", "null"], + "description": "Request body (for POST/PUT/PATCH)" }, "timeout_secs": { "type": "integer", @@ -361,13 +361,6 @@ impl Tool for HttpTool { mod tests { use super::*; - #[test] - fn test_http_tool_schema_body_has_type() { - let tool = HttpTool::new(); - let schema = tool.parameters_schema(); - assert_eq!(schema["properties"]["body"]["type"], "string"); - } - #[test] fn test_http_tool_schema_headers_is_array() { let tool = HttpTool::new(); @@ -460,4 +453,18 @@ mod tests { ] ); } + + #[test] + fn test_http_tool_schema_body_has_type() { + let schema = HttpTool::new().parameters_schema(); + let body = schema + .get("properties") + .and_then(|p| p.get("body")) + .expect("body schema missing"); + + assert!( + body.get("type").is_some(), + "body schema must include a type for OpenAI-compatible tool validation" + ); + } } diff --git a/src/tools/builtin/json.rs b/src/tools/builtin/json.rs index f6d91a0d..5c077ee7 100644 --- a/src/tools/builtin/json.rs +++ b/src/tools/builtin/json.rs @@ -28,8 +28,8 @@ impl Tool for JsonTool { "description": "The JSON operation to perform" }, "data": { - "type": "string", - "description": "JSON input string. For query/stringify/validate, pass serialized JSON." + "type": ["string", "object", "array", "number", "boolean", "null"], + "description": "JSON input data. Pass a string for parse, any type otherwise." }, "path": { "type": "string", @@ -154,13 +154,6 @@ fn query_json(data: &serde_json::Value, path: &str) -> Result