feat(models): add GPT-5.3 Codex, full GPT-5.x family, Claude 4.x series, o4-mini (#197)

Fixes #184 — updates model selection, priority sort, and cost table to
match current OpenAI and Anthropic model catalogs.

OpenAI: GPT-5.3 Codex, GPT-5.2 Codex/Pro, GPT-5.1 Codex/Mini/Max,
GPT-5/Mini/Nano, GPT-4.1/Mini/Nano, o4-mini, o3/Pro
Anthropic: Claude Opus 4.6/4.5/4.1/4.0, Claude Sonnet 4.6/4.5/4.0,
Claude Haiku 4.5, Claude 3.7 Sonnet, Claude 3.5 Haiku

Also resolves stale merge-conflict markers in http.rs and json.rs.
This commit is contained in:
bigguybobby
2026-02-20 01:16:13 +00:00
committed by GitHub
parent fa64df05ff
commit dae26d640e
4 changed files with 102 additions and 40 deletions
+38 -12
View File
@@ -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
+32 -10
View File
@@ -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"
+16 -9
View File
@@ -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"
);
}
}
+16 -9
View File
@@ -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<serde_json::Value,
mod tests {
use super::*;
#[test]
fn test_json_tool_schema_data_has_type() {
let tool = JsonTool;
let schema = tool.parameters_schema();
assert_eq!(schema["properties"]["data"]["type"], "string");
}
#[test]
fn test_query_json() {
let data = serde_json::json!({
@@ -197,4 +190,18 @@ mod tests {
let err = parse_json_input(&input).unwrap_err();
assert!(err.to_string().contains("invalid JSON input"));
}
#[test]
fn test_json_tool_schema_data_has_type() {
let schema = JsonTool.parameters_schema();
let data = schema
.get("properties")
.and_then(|p| p.get("data"))
.expect("data schema missing");
assert!(
data.get("type").is_some(),
"data schema must include a type for OpenAI-compatible tool validation"
);
}
}