Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions docs/src/content/docs/reference/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ These settings configure the proxy process. Claude Code client settings such as
"bindAddress": "127.0.0.1",
"port": 18765,
"aliasProvider": "codex",
"autoReviewModel": "gpt-5.6-terra",
"codex": {
"originator": "claude-code-proxy",
"userAgent": "claude-code-proxy/0.1.24",
Expand Down Expand Up @@ -58,13 +59,16 @@ All keys are optional. An unreadable file, malformed JSON, or incompatible field
| `PORT` | `port` | `18765` | Listener port. |
| `CCP_CONFIG_DIR` | none | Platform config directory | Replaces the configuration and file-backed auth root. |
| `CCP_ALIAS_PROVIDER` | `aliasProvider` | `codex` | Routes recognized Anthropic-style aliases through `codex` or `kimi`. |
| `CCP_AUTO_REVIEW_MODEL` | `autoReviewModel` | `gpt-5.6-luna` for Codex | Routes Claude Code's non-streaming, tool-free Bash security-review classifier through a registered model. |
| `CCP_LOG_STDERR` | `log.stderr` | `false` | Mirrors logs to stderr when present in the environment, regardless of its value. |
| `CCP_LOG_VERBOSE` | `log.verbose` | `false` | Preserves full string fields in structured logs when present, regardless of its value. |
| `CCP_TRAFFIC_LOG` | none | `false` | Enables full request captures for `1`, `true`, or `yes`. |
| `XDG_STATE_HOME` | none | `~/.local/state` | State base on macOS and Linux. |

`CCP_CONFIG_DIR` affects `config.json` and file-backed provider auth. It does not relocate the state directory.

Codex auto-review classifier requests use `gpt-5.6-luna` by default. Requests routed through other providers retain their requested model. `CCP_AUTO_REVIEW_MODEL` or `autoReviewModel` selects an explicit registered model for all detected classifier requests without changing the session's provider affinity. Normal messages, streaming requests, tool-using requests, and token counting retain their requested model.

## Codex

| Environment | Config key | Default | Purpose |
Expand Down
2 changes: 2 additions & 0 deletions src/anthropic/schema.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ pub struct MessagesRequest {
pub messages: Vec<Message>,
#[serde(default)]
pub stream: bool,
#[serde(skip)]
pub bypass_provider_model_override: bool,
#[serde(flatten)]
pub extra: serde_json::Map<String, serde_json::Value>,
}
Expand Down
51 changes: 51 additions & 0 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ struct FileConfig {
pub port: Option<u16>,
#[serde(rename = "aliasProvider")]
pub alias_provider: Option<String>,
#[serde(rename = "autoReviewModel")]
pub auto_review_model: Option<String>,
pub log: Option<FileLog>,
pub kimi: Option<KimiConfig>,
pub codex: Option<CodexConfig>,
Expand Down Expand Up @@ -254,6 +256,12 @@ pub fn config_override_summary_lines(cfg: &LoadedConfig) -> Vec<String> {
if env.contains_key("CCP_CODEX_SERVER_COMPACTION") {
out.push("CCP_CODEX_SERVER_COMPACTION (env)".to_string());
}
if env
.get("CCP_AUTO_REVIEW_MODEL")
.is_some_and(|raw| !raw.is_empty())
{
out.push("CCP_AUTO_REVIEW_MODEL (env)".to_string());
}
if let Some(file_cfg) = file {
if let Some(bind_address) = file_cfg.bind_address {
out.push(format!("bindAddress: {bind_address}"));
Expand All @@ -264,6 +272,12 @@ pub fn config_override_summary_lines(cfg: &LoadedConfig) -> Vec<String> {
if let Some(alias) = file_cfg.alias_provider {
out.push(format!("aliasProvider: {alias}"));
}
if file_cfg
.auto_review_model
.is_some_and(|model| !model.is_empty())
{
out.push("autoReviewModel (config)".to_string());
}
if let Some(log) = file_cfg.log {
if let Some(v) = log.verbose {
out.push(format!("log.verbose: {v}"));
Expand Down Expand Up @@ -588,6 +602,19 @@ pub fn codex_model() -> Option<String> {
None
}

pub fn auto_review_model() -> Option<String> {
let env: HashMap<_, _> = std::env::vars().collect();
if let Some(raw) = env
.get("CCP_AUTO_REVIEW_MODEL")
.filter(|raw| !raw.is_empty())
{
return Some(raw.clone());
}
read_file_config(&paths::config_dir())
.and_then(|file| file.auto_review_model)
.filter(|model| !model.is_empty())
}

// ---------------------------------------------------------------------------
// Codex transport config
// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -702,6 +729,7 @@ mod tests {
std::env::remove_var("CCP_CODEX_REASONING_SUMMARY");
std::env::remove_var("CCP_CODEX_SERVER_COMPACTION");
std::env::remove_var("CCP_CODEX_RESPONSES_API");
std::env::remove_var("CCP_AUTO_REVIEW_MODEL");
}
}

Expand Down Expand Up @@ -933,6 +961,29 @@ mod tests {
}
}

#[test]
fn auto_review_model_reads_top_level_config_and_env_takes_precedence() {
let _guard = ENV_LOCK.lock().unwrap();
clear_env();
let config = tempfile::TempDir::new().unwrap();
std::fs::write(
config.path().join("config.json"),
r#"{"autoReviewModel":"grok-4.5"}"#,
)
.unwrap();
let _config_env = EnvGuard::set("CCP_CONFIG_DIR", config.path());

assert_eq!(auto_review_model().as_deref(), Some("grok-4.5"));
{
let _model_env = EnvGuard::set("CCP_AUTO_REVIEW_MODEL", "gpt-5.6-terra");
assert_eq!(auto_review_model().as_deref(), Some("gpt-5.6-terra"));
}
{
let _model_env = EnvGuard::set("CCP_AUTO_REVIEW_MODEL", "");
assert_eq!(auto_review_model().as_deref(), Some("grok-4.5"));
}
}

#[test]
fn codex_server_compaction_defaults_and_overrides() {
let _guard = ENV_LOCK.lock().unwrap();
Expand Down
9 changes: 6 additions & 3 deletions src/providers/codex/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,8 @@ use self::count_tokens::count_translated_tokens;
use self::translate::accumulate::accumulate_response_with_traffic;
use self::translate::live_stream::LiveStreamTranslator;
use self::translate::model_allowlist::{
assert_allowed_model, full_lane_web_search_model, resolve_model_request, uses_responses_lite,
assert_allowed_model, full_lane_web_search_model, resolve_model_request_with_config_override,
uses_responses_lite,
};
use self::translate::reducer::finish_metadata_from_upstream;
use self::translate::request::{
Expand Down Expand Up @@ -106,7 +107,8 @@ impl Provider for CodexProvider {
let want_stream = body.stream;
let model = body.model.as_deref().unwrap_or("gpt-5.6-sol");

let mut resolved = resolve_model_request(model);
let mut resolved =
resolve_model_request_with_config_override(model, !body.bypass_provider_model_override);
if let Err(e) = assert_allowed_model(&resolved.model) {
return json_error(
StatusCode::BAD_REQUEST,
Expand Down Expand Up @@ -321,7 +323,8 @@ impl Provider for CodexProvider {

async fn handle_count_tokens(&self, body: MessagesRequest, ctx: RequestContext) -> Response {
let model = body.model.as_deref().unwrap_or("gpt-5.6-sol");
let mut resolved = resolve_model_request(model);
let mut resolved =
resolve_model_request_with_config_override(model, !body.bypass_provider_model_override);
if let Err(e) = assert_allowed_model(&resolved.model) {
return json_error(
StatusCode::BAD_REQUEST,
Expand Down
9 changes: 8 additions & 1 deletion src/providers/codex/translate/model_allowlist.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,13 @@ fn resolve_fast_model_alias(model: &str) -> ResolvedModel {
}

pub fn resolve_model_request(model: &str) -> ResolvedModel {
resolve_model_request_with_config_override(model, true)
}

pub fn resolve_model_request_with_config_override(
model: &str,
apply_config_override: bool,
) -> ResolvedModel {
let alias = MODEL_ALIASES
.iter()
.find(|(alias, _)| *alias == model)
Expand All @@ -65,7 +72,7 @@ pub fn resolve_model_request(model: &str) -> ResolvedModel {

let requested = resolve_fast_model_alias(alias);

let override_model = config::codex_model();
let override_model = apply_config_override.then(config::codex_model).flatten();
let resolved = match override_model {
Some(ref val) if !val.is_empty() => resolve_fast_model_alias(val),
_ => requested.clone(),
Expand Down
Loading