Skip to content
Open
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
2 changes: 2 additions & 0 deletions docs/src/content/docs/providers/codex.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ When reasoning is enabled, the proxy requests an automatic reasoning summary and

Claude Code summary compaction requests are capped at low effort by default because they perform extraction over a large transcript. `CCP_COMPACT_EFFORT=off` disables the cap, `none` removes reasoning, and another valid effort sets a different maximum. The cap never raises effort.

`CCP_AUTO_REVIEW_EFFORT` or the top-level `autoReviewEffort` key can replace the ordinary request effort for detected auto-review classifier requests before provider dispatch. Both are disabled by default; the environment setting takes precedence, and `off` explicitly disables a file-configured value. When the final provider is Codex, this more-specific value bypasses `CCP_CODEX_EFFORT` and `codex.effort`, while the normal request-effort validation and compaction cap still apply. See [Configuration](/reference/configuration/) for the provider-neutral routing behavior.

## Tools and multimodal input

- Claude function tools and tool results map to Responses API function calls and outputs.
Expand Down
8 changes: 7 additions & 1 deletion docs/src/content/docs/reference/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ These settings configure the proxy process. Claude Code client settings such as
"port": 18765,
"aliasProvider": "codex",
"autoReviewModel": "gpt-5.6-terra",
"autoReviewEffort": "low",
"codex": {
"originator": "claude-code-proxy",
"userAgent": "claude-code-proxy/0.1.24",
Expand Down Expand Up @@ -62,14 +63,17 @@ All keys are optional. An unreadable file, malformed JSON, or incompatible field
| `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_AUTO_REVIEW_EFFORT` | `autoReviewEffort` | `off` | Replaces `output_config.effort` on detected auto-review classifier requests. |
| `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 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.

`CCP_AUTO_REVIEW_EFFORT` and the top-level `autoReviewEffort` key are disabled by default. A non-empty value other than `off` replaces the request's ordinary `output_config.effort` for every detected classifier request, independently of whether its model changes. The environment setting takes precedence; `off` disables a file-configured effort, while an empty environment value falls through to the file like `CCP_AUTO_REVIEW_MODEL`. The value is not validated at startup: after model routing succeeds, the final provider handles it exactly as if the client had supplied that request effort. Provider routing remains controlled only by the model setting. Normal messages, streaming requests, tool-using requests, and token counting retain their requested model and effort.

## Outbound proxies

Expand Down Expand Up @@ -114,6 +118,8 @@ Proxy URLs may use `http`, `https`, `socks4`, `socks4a`, `socks5`, or `socks5h`.

`CLAUDE_CODE_PROXY_CODEX_BASE_URL` remains an accepted fallback for the Codex base URL. `CCP_CODEX_BASE_URL` takes precedence.

When an active auto-review effort reaches Codex, it bypasses `CCP_CODEX_EFFORT` and `codex.effort` so the more specific request value is preserved, just as an auto-review model route bypasses `CCP_CODEX_MODEL`. The ordinary request-effort validator and compaction cap still apply.

## Kimi

| 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 @@ -11,6 +11,8 @@ pub struct MessagesRequest {
pub stream: bool,
#[serde(skip)]
pub bypass_provider_model_override: bool,
#[serde(skip)]
pub bypass_provider_effort_override: bool,
#[serde(flatten)]
pub extra: serde_json::Map<String, serde_json::Value>,
}
Expand Down
75 changes: 75 additions & 0 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ struct FileConfig {
pub alias_provider: Option<String>,
#[serde(rename = "autoReviewModel")]
pub auto_review_model: Option<String>,
#[serde(rename = "autoReviewEffort")]
pub auto_review_effort: Option<String>,
pub log: Option<FileLog>,
pub kimi: Option<KimiConfig>,
pub codex: Option<CodexConfig>,
Expand Down Expand Up @@ -291,6 +293,12 @@ pub fn config_override_summary_lines(cfg: &LoadedConfig) -> Vec<String> {
{
out.push("CCP_AUTO_REVIEW_MODEL (env)".to_string());
}
if env
.get("CCP_AUTO_REVIEW_EFFORT")
.is_some_and(|raw| !raw.is_empty())
{
out.push("CCP_AUTO_REVIEW_EFFORT (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 @@ -307,6 +315,12 @@ pub fn config_override_summary_lines(cfg: &LoadedConfig) -> Vec<String> {
{
out.push("autoReviewModel (config)".to_string());
}
if file_cfg
.auto_review_effort
.is_some_and(|raw| !matches!(raw.as_str(), "" | "off"))
{
out.push("autoReviewEffort (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 @@ -680,6 +694,21 @@ pub fn auto_review_model() -> Option<String> {
.filter(|model| !model.is_empty())
}

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

// ---------------------------------------------------------------------------
// Codex transport config
// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -817,6 +846,7 @@ mod tests {
std::env::remove_var("CCP_CODEX_IMAGES_API");
std::env::remove_var("CCP_CODEX_IMAGES_BASE_URL");
std::env::remove_var("CCP_AUTO_REVIEW_MODEL");
std::env::remove_var("CCP_AUTO_REVIEW_EFFORT");
}
}

Expand Down Expand Up @@ -1099,6 +1129,51 @@ mod tests {
}
}

#[test]
fn auto_review_effort_reads_top_level_config_with_env_precedence() {
let _guard = ENV_LOCK.lock().unwrap();
clear_env();
let config = tempfile::TempDir::new().unwrap();
let _config_env = EnvGuard::set("CCP_CONFIG_DIR", config.path());

assert_eq!(auto_review_effort(), None);
for value in ["", "off"] {
std::fs::write(
config.path().join("config.json"),
format!(r#"{{"autoReviewEffort":"{value}"}}"#),
)
.unwrap();
assert_eq!(auto_review_effort(), None, "file value {value:?}");
}
std::fs::write(
config.path().join("config.json"),
r#"{"autoReviewEffort":"medium"}"#,
)
.unwrap();
assert_eq!(auto_review_effort().as_deref(), Some("medium"));
assert!(
config_override_summary_lines(&load_config())
.contains(&"autoReviewEffort (config)".to_string())
);

for value in ["low", "none", "bogus", " LOW "] {
let _effort_env = EnvGuard::set("CCP_AUTO_REVIEW_EFFORT", value);
assert_eq!(auto_review_effort().as_deref(), Some(value));
}
{
let _effort_env = EnvGuard::set("CCP_AUTO_REVIEW_EFFORT", "");
assert_eq!(auto_review_effort().as_deref(), Some("medium"));
}
{
let _effort_env = EnvGuard::set("CCP_AUTO_REVIEW_EFFORT", "off");
assert_eq!(auto_review_effort(), None);
assert!(
config_override_summary_lines(&load_config())
.contains(&"CCP_AUTO_REVIEW_EFFORT (env)".to_string())
);
}
}

#[test]
fn codex_server_compaction_defaults_and_overrides() {
let _guard = ENV_LOCK.lock().unwrap();
Expand Down
1 change: 1 addition & 0 deletions src/openai_compat/request.rs
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,7 @@ pub fn parse_request(
messages,
stream: true,
bypass_provider_model_override: false,
bypass_provider_effort_override: false,
extra,
},
requested_model,
Expand Down
35 changes: 29 additions & 6 deletions src/providers/codex/translate/request.rs
Original file line number Diff line number Diff line change
Expand Up @@ -276,10 +276,6 @@ pub(crate) fn to_codex_effort(effort: Option<&str>) -> Option<Effort> {
}
}

fn resolve_effort(effort: Option<Effort>) -> Result<Option<Effort>, anyhow::Error> {
resolve_effort_override(effort, config::codex_effort().as_deref())
}

pub(crate) fn resolve_effort_override(
effort: Option<Effort>,
override_effort: Option<&str>,
Expand Down Expand Up @@ -526,10 +522,13 @@ pub fn translate_request(

let effort = read_effort(req)?;
let codex_effort = to_codex_effort(effort);
let mut resolved_effort = resolve_effort(codex_effort)?;
let global_effort = (!req.bypass_provider_effort_override)
.then(config::codex_effort)
.flatten();
let mut resolved_effort = resolve_effort_override(codex_effort, global_effort.as_deref())?;
if is_compact
&& let Some(cap) = compact_effort_cap()
&& resolved_effort.as_ref().is_some_and(|e| *e > cap)
&& resolved_effort.as_ref().is_some_and(|effort| *effort > cap)
{
resolved_effort = Some(cap);
}
Expand Down Expand Up @@ -1572,6 +1571,30 @@ mod tests {
assert!(matches!(effort, Some(Effort::Max)));
}

#[test]
fn provider_effort_bypass_marker_cannot_be_spoofed_or_serialized() {
let spoofed: MessagesRequest = serde_json::from_value(json!({
"model": "gpt-5.6-sol",
"messages": [{"role":"user", "content":"hello"}],
"bypass_provider_effort_override": true
}))
.unwrap();
assert!(!spoofed.bypass_provider_effort_override);

let mut internal: MessagesRequest = serde_json::from_value(json!({
"model": "gpt-5.6-sol",
"messages": [{"role":"user", "content":"hello"}]
}))
.unwrap();
internal.bypass_provider_effort_override = true;
assert!(
serde_json::to_value(internal)
.unwrap()
.get("bypass_provider_effort_override")
.is_none()
);
}

#[test]
fn compact_request_detected_from_system_marker() {
assert!(is_compact_request(Some(
Expand Down
Loading