From d9ed423df55fb534158bdea631754a7dbb737b7f Mon Sep 17 00:00:00 2001 From: Ryan Date: Sat, 8 Aug 2026 17:26:31 +0800 Subject: [PATCH 1/3] Honor configured provider when serving --- src/main_auth.rs | 4 +++ src/main_tests.rs | 91 +++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 93 insertions(+), 2 deletions(-) diff --git a/src/main_auth.rs b/src/main_auth.rs index 5bf7a1d..5ddfc7f 100644 --- a/src/main_auth.rs +++ b/src/main_auth.rs @@ -95,6 +95,10 @@ fn resolve_served_providers( return Ok(vec![value.parse()?]); } + if let Some(provider) = config.and_then(|item| item.provider) { + return Ok(vec![provider]); + } + let mut providers = store .load_all()? .into_iter() diff --git a/src/main_tests.rs b/src/main_tests.rs index 6c14966..327045d 100644 --- a/src/main_tests.rs +++ b/src/main_tests.rs @@ -5,11 +5,44 @@ use super::{ build_update_command, credential_subject, daemon_endpoint_lines, format_bind_addresses, format_login_provider_choice, format_models, new_provider_daemon_restart_hint, parse_bind_hosts, parse_login_provider_choice, resolve_model_fallback, - resolve_status_providers, rotom_version_line, token_expiry_message, + resolve_served_providers, resolve_status_providers, rotom_version_line, token_expiry_message, }; use clap::Parser; -use rotom::config::{Credentials, Provider, now_unix}; +use rotom::config::{AuthStore, Credentials, Provider, now_unix}; use rotom::timefmt::format_duration; +use std::{ + fs, + path::PathBuf, + time::{SystemTime, UNIX_EPOCH}, +}; + +struct TestAuthStore { + store: AuthStore, + path: PathBuf, +} + +impl TestAuthStore { + fn new() -> Self { + let path = std::env::temp_dir().join(format!( + "rotom-main-test-{}-{}.json", + std::process::id(), + SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_nanos() + )); + Self { + store: AuthStore::new(path.clone()), + path, + } + } +} + +impl Drop for TestAuthStore { + fn drop(&mut self) { + let _ = fs::remove_file(&self.path); + } +} #[test] fn reuses_shared_duration_formatting() { @@ -161,6 +194,60 @@ fn prefers_explicit_model_fallback_over_default() { ); } +#[test] +fn served_provider_prefers_cli_provider() { + let auth = TestAuthStore::new(); + save_provider_credentials(&auth.store, Provider::Codex); + save_provider_credentials(&auth.store, Provider::Cursor); + let config = AppConfig { + provider: Some(Provider::Cursor), + ..AppConfig::default() + }; + + let providers = + resolve_served_providers(&auth.store, Some("grok".into()), Some(&config)).unwrap(); + + assert_eq!(providers, [Provider::Grok]); +} + +#[test] +fn served_provider_prefers_config_provider_over_saved_providers() { + let auth = TestAuthStore::new(); + save_provider_credentials(&auth.store, Provider::Codex); + save_provider_credentials(&auth.store, Provider::Cursor); + let config = AppConfig { + provider: Some(Provider::Codex), + ..AppConfig::default() + }; + + let providers = resolve_served_providers(&auth.store, None, Some(&config)).unwrap(); + + assert_eq!(providers, [Provider::Codex]); +} + +#[test] +fn served_provider_uses_saved_providers_without_selection() { + let auth = TestAuthStore::new(); + save_provider_credentials(&auth.store, Provider::Cursor); + save_provider_credentials(&auth.store, Provider::Codex); + + let providers = resolve_served_providers(&auth.store, None, None).unwrap(); + + assert_eq!(providers, [Provider::Codex, Provider::Cursor]); +} + +fn save_provider_credentials(store: &AuthStore, provider: Provider) { + store + .save(&Credentials { + provider, + access_token: "access".into(), + refresh_token: "refresh".into(), + expires_at: now_unix() + 90, + account_id: String::new(), + }) + .unwrap(); +} + #[test] fn formats_models_grouped_by_provider() { let output = format_models(&[Provider::Codex, Provider::Grok]); From e801fea41f3711bfaf7d6052de4229db84fa97fc Mon Sep 17 00:00:00 2001 From: Ryan Date: Fri, 14 Aug 2026 01:48:43 +0800 Subject: [PATCH 2/3] Support GPT-5.6 Codex models --- Cargo.lock | 2 +- Cargo.toml | 2 +- README.md | 17 ++++++++------- src/codex/convert.rs | 5 +++-- src/main_tests.rs | 3 ++- src/models.rs | 51 ++++++++++++++++++++++++++++++-------------- 6 files changed, 51 insertions(+), 29 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4f233a8..59ad854 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1085,7 +1085,7 @@ dependencies = [ [[package]] name = "rotom" -version = "1.5.6" +version = "1.5.7" dependencies = [ "async-stream", "axum", diff --git a/Cargo.toml b/Cargo.toml index adc240e..1ce8778 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rotom" -version = "1.5.6" +version = "1.5.7" edition = "2024" rust-version = "1.86" description = "OpenAI- and Anthropic-compatible local API gateway backed by OAuth providers." diff --git a/README.md b/README.md index 7ccfa2e..5cde986 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ Claude Code running through Grok and GPT: Claude Code using grok-4.3 through rotom

- Claude Code using gpt-5.5 through rotom + Claude Code using GPT through rotom

## Quick Start @@ -38,7 +38,7 @@ Point Claude Code (or any Anthropic-compatible client) at the gateway: ```bash export ANTHROPIC_BASE_URL=http://127.0.0.1:14550 export ANTHROPIC_AUTH_TOKEN=local-secret -export ANTHROPIC_MODEL="gpt-5.5" +export ANTHROPIC_MODEL="gpt-5.6-sol" claude ``` @@ -51,7 +51,7 @@ claude -p "Reply with the single word OK" > Point `ANTHROPIC_BASE_URL` at the server root, **not** `/v1` — clients append > `/v1/messages` themselves. `ANTHROPIC_AUTH_TOKEN` is your local `--api-key`, -> not an upstream token. Use a model that `/v1/models` lists (e.g. `gpt-5.5`). +> not an upstream token. Use a model that `/v1/models` lists (e.g. `gpt-5.6-sol`). ## Logging In @@ -86,9 +86,10 @@ rotom models # everything rotom exposes rotom models --provider grok # one provider ``` -Common ids include `gpt-5.5`, `grok-4.3`, Kiro's `claude-*` family, and -`cursor/auto`. rotom fetches the live registry where the provider supports it -and falls back to built-in aliases otherwise. +Common ids include `gpt-5.6-sol`, `gpt-5.6-terra`, `gpt-5.6-luna`, +`grok-4.3`, Kiro's `claude-*` family, and `cursor/auto`. rotom fetches the +live registry where the provider supports it and falls back to built-in aliases +otherwise. Unknown Anthropic ids like `claude-sonnet-*` are rewritten to a fallback (default `gpt-5.5`). Override with `--model-fallback` or `ROTOM_MODEL_FALLBACK`. @@ -101,7 +102,7 @@ OpenAI-compatible: curl http://127.0.0.1:14550/v1/chat/completions \ -H 'content-type: application/json' \ -H 'authorization: Bearer local-secret' \ - -d '{"model": "gpt-5.5", "messages": [{"role": "user", "content": "hello"}]}' + -d '{"model": "gpt-5.6-sol", "messages": [{"role": "user", "content": "hello"}]}' ``` Anthropic-compatible: @@ -111,7 +112,7 @@ curl http://127.0.0.1:14550/v1/messages \ -H 'content-type: application/json' \ -H 'x-api-key: local-secret' \ -H 'anthropic-version: 2023-06-01' \ - -d '{"model": "gpt-5.5", "max_tokens": 1024, "messages": [{"role": "user", "content": "hello"}]}' + -d '{"model": "gpt-5.6-sol", "max_tokens": 1024, "messages": [{"role": "user", "content": "hello"}]}' ``` ## Running As a Service diff --git a/src/codex/convert.rs b/src/codex/convert.rs index c534973..8de36e4 100644 --- a/src/codex/convert.rs +++ b/src/codex/convert.rs @@ -386,7 +386,8 @@ fn clamp_reasoning_effort(model: &str, effort: &str) -> String { if (id.starts_with("gpt-5.2") || id.starts_with("gpt-5.3") || id.starts_with("gpt-5.4") - || id.starts_with("gpt-5.5")) + || id.starts_with("gpt-5.5") + || id.starts_with("gpt-5.6")) && effort == "minimal" { "low".to_owned() @@ -486,7 +487,7 @@ mod tests { #[test] fn does_not_forward_unsupported_sampling_controls() { let body = to_codex_request(&request(json!({ - "model": "gpt-5.5", + "model": "gpt-5.6-terra", "messages": [{"role": "user", "content": "hello"}], "temperature": 0.2, "top_p": 0.7, diff --git a/src/main_tests.rs b/src/main_tests.rs index 327045d..4690a90 100644 --- a/src/main_tests.rs +++ b/src/main_tests.rs @@ -253,6 +253,7 @@ fn formats_models_grouped_by_provider() { let output = format_models(&[Provider::Codex, Provider::Grok]); assert!(output.contains("OpenAI (codex)\n gpt-5.1")); + assert!(output.contains(" gpt-5.6-sol\n")); assert!(output.contains("Grok (grok)\n grok-4.3")); assert!(!output.contains("Kiro (kiro)")); assert!(output.contains("\n\nGrok (grok)")); @@ -264,7 +265,7 @@ fn formats_single_provider_models() { assert!(output.starts_with("Grok (grok)\n")); assert!(output.contains(" grok-4\n")); - assert!(!output.contains("gpt-5.5")); + assert!(!output.contains("gpt-5.6-sol")); } #[test] diff --git a/src/models.rs b/src/models.rs index 47bfe29..b8729bf 100644 --- a/src/models.rs +++ b/src/models.rs @@ -13,6 +13,9 @@ pub const OPENCLAW_CODEX_MODELS: &[&str] = &[ "gpt-5.4", "gpt-5.4-mini", "gpt-5.5", + "gpt-5.6-sol", + "gpt-5.6-terra", + "gpt-5.6-luna", ]; /// Default Grok model identifiers exposed by the project. @@ -210,13 +213,7 @@ fn highlight_model_rank(provider: Provider, id: &str) -> Option { let (major, minor) = strongest_version(normalized)?; let family = match provider { - Provider::Codex => { - if normalized.starts_with("gpt-") { - 80 - } else { - return None; - } - } + Provider::Codex => codex_family_rank(normalized)?, Provider::Grok => { if normalized.starts_with("grok-") { 80 @@ -286,6 +283,22 @@ fn is_lightweight_highlight_variant(id: &str) -> bool { || id.contains("-spark") } +fn codex_family_rank(id: &str) -> Option { + if !id.starts_with("gpt-") { + return None; + } + + if id.starts_with("gpt-5.6-sol") { + Some(100) + } else if id.starts_with("gpt-5.6-terra") { + Some(95) + } else if id.starts_with("gpt-5.6-luna") { + Some(90) + } else { + Some(80) + } +} + fn kiro_family_rank(id: &str) -> Option { if id.starts_with("claude-fable-") { Some(105) @@ -413,11 +426,14 @@ mod tests { } #[test] - fn defaults_include_gpt_55_models() { + fn defaults_include_gpt_56_models() { let ids = resolve_model_ids(); - assert!(ids.iter().any(|id| id == "gpt-5.5")); - assert!(!ids.iter().any(|id| id == "gpt-5.5-mini")); + assert!(ids.iter().any(|id| id == "gpt-5.6-sol")); + assert!(ids.iter().any(|id| id == "gpt-5.6-terra")); + assert!(ids.iter().any(|id| id == "gpt-5.6-luna")); + assert!(!ids.iter().any(|id| id == "gpt-5.6")); + assert!(!ids.iter().any(|id| id == "gpt-5.6-mini")); } #[test] @@ -425,7 +441,7 @@ mod tests { let ids = resolve_model_ids_for_provider(Provider::Grok); assert!(ids.iter().any(|id| id == "grok-4.3")); - assert!(!ids.iter().any(|id| id == "gpt-5.5")); + assert!(!ids.iter().any(|id| id == "gpt-5.6-sol")); } #[test] @@ -454,10 +470,10 @@ mod tests { assert_eq!( highlight_model_ids_for_provider(Provider::Codex, &ids), vec![ + "gpt-5.6-sol".to_owned(), + "gpt-5.6-terra".to_owned(), + "gpt-5.6-luna".to_owned(), "gpt-5.5".to_owned(), - "gpt-5.4".to_owned(), - "gpt-5.3-codex".to_owned(), - "gpt-5.2-codex".to_owned(), ] ); } @@ -549,7 +565,10 @@ mod tests { assert_eq!(provider_for_model("xai/grok-4.3"), Provider::Grok); assert_eq!(provider_for_model("grok/grok-4.3"), Provider::Grok); assert_eq!(provider_for_model("openai-codex/grok-4.3"), Provider::Grok); - assert_eq!(provider_for_model("openai-codex/gpt-5.5"), Provider::Codex); + assert_eq!( + provider_for_model("openai-codex/gpt-5.6-luna"), + Provider::Codex + ); assert_eq!(provider_for_model("kiro/auto"), Provider::Kiro); assert_eq!(provider_for_model("cursor/gpt-5"), Provider::Cursor); assert_eq!(provider_for_model("sonnet-4"), Provider::Cursor); @@ -574,7 +593,7 @@ mod tests { .map(|model| model.owned_by) }; - assert_eq!(owner_for("gpt-5.5"), Some("openai-codex")); + assert_eq!(owner_for("gpt-5.6-sol"), Some("openai-codex")); assert_eq!(owner_for("grok-4.3"), Some("xai")); assert_eq!(owner_for("auto"), Some("kiro")); assert_eq!(owner_for("cursor/gpt-5"), Some("cursor")); From 49cea9c1b53089a639459c0cd6cfa960748ead7f Mon Sep 17 00:00:00 2001 From: Ryan Date: Fri, 14 Aug 2026 04:26:37 +0800 Subject: [PATCH 3/3] Fix cursor clippy warning --- src/codex/cursor.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/codex/cursor.rs b/src/codex/cursor.rs index 1aaa0b3..aa9307b 100644 --- a/src/codex/cursor.rs +++ b/src/codex/cursor.rs @@ -1098,15 +1098,14 @@ fn cursor_builtin_exec_decline(exec: &ExecServerMessage) -> Option