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:
-
+
## 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