Skip to content
Draft
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: 3 additions & 1 deletion docs/src/content/docs/providers/codex.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ Claude Code's `/effort` value maps to Codex `reasoning.effort`: `low`, `medium`,

When reasoning is enabled, the proxy requests an automatic reasoning summary and translates summary deltas into Claude Code thinking blocks. Codex may omit a summary for a simple prompt. `CCP_CODEX_REASONING_SUMMARY=off` suppresses summaries while preserving effort and encrypted continuation content.

Detected auto-review classifier requests default to `low` effort, the effort Codex's own approval reviewer uses, unless the request already sets `output_config.effort`.

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.

## Usage reporting
Expand Down Expand Up @@ -117,7 +119,7 @@ WebSocket setup honors `HTTP_PROXY` for `ws://`, `HTTPS_PROXY` for the default `

An absent, malformed, or ambiguous identity does not reject the HTTP request; that request proceeds without continuation or WebSocket reuse. If the originating socket is missing, dead, or has been replaced, the proxy retries once with the full translated input and without the stale response ID. Continuation and connection state is held only in memory and is lost when the proxy restarts.

Detected auto-review classifier subrequests are intentionally stateless even when valid session and Agent headers are present. They neither consume nor publish continuation or WebSocket ownership.
Detected auto-review classifier subrequests are intentionally stateless even when valid session and Agent headers are present. They neither consume nor publish continuation or WebSocket ownership. Their `prompt_cache_key` is `guardian:<session id>`, the reviewer-scoped key Codex uses for its own approval reviewer, so they do not share the main conversation's cache key.

## Server compaction

Expand Down
2 changes: 1 addition & 1 deletion docs/src/content/docs/reference/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ All keys are optional. An unreadable file, malformed JSON, or incompatible field

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

Auto-review routing resolves the classifier's requested model first, not the main conversation's model. Recognized Claude aliases follow Codex/Kimi session affinity or `aliasProvider`; Grok and other providers do not establish alias affinity. Codex auto-review classifier requests use `gpt-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.
Auto-review routing resolves the classifier's requested model first, not the main conversation's model. Recognized Claude aliases follow Codex/Kimi session affinity or `aliasProvider`; Grok and other providers do not establish alias affinity. Codex auto-review classifier requests use `gpt-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. On Codex, detected classifier requests also default to `low` effort and use a `guardian:<session id>` prompt cache key, as Codex's own approval reviewer does.

## Outbound proxies

Expand Down
4 changes: 4 additions & 0 deletions src/anthropic/schema.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ pub struct MessagesRequest {
pub stream: bool,
#[serde(skip)]
pub bypass_provider_model_override: bool,
#[serde(skip)]
pub prompt_cache_key_override: Option<String>,
#[serde(skip)]
pub omit_reasoning_artifacts: bool,
#[serde(flatten)]
pub extra: serde_json::Map<String, serde_json::Value>,
}
Expand Down
2 changes: 2 additions & 0 deletions src/openai_compat/request.rs
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,8 @@ pub fn parse_request(
messages,
stream: true,
bypass_provider_model_override: false,
prompt_cache_key_override: None,
omit_reasoning_artifacts: false,
extra,
},
requested_model,
Expand Down
6 changes: 5 additions & 1 deletion src/providers/codex/translate/request.rs
Original file line number Diff line number Diff line change
Expand Up @@ -579,6 +579,9 @@ fn translate_request_inner(
if let Some(sid) = opts.session_id {
out.prompt_cache_key = Some(sid);
}
if let Some(key) = req.prompt_cache_key_override.clone() {
out.prompt_cache_key = Some(key);
}

if apply_codex_config {
let service_tier = resolve_service_tier(opts.service_tier, &out.model)?;
Expand All @@ -600,6 +603,7 @@ fn translate_request_inner(
let wants_reasoning = reasoning_requested(resolved_effort.as_ref());
if resolved_effort.is_some() || opts.use_responses_lite {
let summary = if wants_reasoning
&& !req.omit_reasoning_artifacts
&& (!apply_codex_config
|| reasoning_summary_requested(config::codex_reasoning_summary().as_deref()))
{
Expand All @@ -613,7 +617,7 @@ fn translate_request_inner(
context: opts.use_responses_lite.then_some("all_turns".to_string()),
});
}
if wants_reasoning {
if wants_reasoning && !req.omit_reasoning_artifacts {
out.include = Some(vec!["reasoning.encrypted_content".to_string()]);
}

Expand Down
79 changes: 78 additions & 1 deletion src/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,25 @@ fn apply_auto_review_model(
Some(route)
}

/// Mirrors Codex's own approval reviewer: Low effort (`ext/guardian-reviewer/src/model.rs`)
/// and a reviewer-scoped `guardian:{parent}` prompt cache key (`core/src/guardian/review_session.rs`).
fn apply_codex_auto_review_defaults(
body: &mut crate::anthropic::schema::MessagesRequest,
session_id: Option<&str>,
) {
let output_config = body
.extra
.entry("output_config")
.or_insert_with(|| json!({}));
if let Some(config) = output_config.as_object_mut() {
config.entry("effort").or_insert_with(|| json!("low"));
}
body.prompt_cache_key_override = session_id.map(|id| format!("guardian:{id}"));
// The classifier reads only its text verdict; a reasoning summary or encrypted
// reasoning would be generated and returned for nothing.
body.omit_reasoning_artifacts = true;
}

pub struct ServerConfig {
pub bind_address: String,
pub port: u16,
Expand Down Expand Up @@ -1718,6 +1737,9 @@ async fn dispatch_request(
};

body.bypass_provider_model_override = auto_review_route.is_some() && provider.name() == "codex";
if body.bypass_provider_model_override {
apply_codex_auto_review_defaults(&mut body, session_id.as_deref());
}

if let Some(route) = auto_review_route.as_ref() {
log.info(
Expand Down Expand Up @@ -2318,7 +2340,10 @@ mod request_id_header_tests {

#[cfg(test)]
mod auto_review_tests {
use super::{apply_auto_review_model, headers_to_record, is_claude_auto_review_request};
use super::{
apply_auto_review_model, apply_codex_auto_review_defaults, headers_to_record,
is_claude_auto_review_request,
};
use crate::anthropic::schema::MessagesRequest;
use crate::request_identity::{CLAUDE_AGENT_HEADER, CLAUDE_PARENT_AGENT_HEADER};
use http::{HeaderMap, HeaderValue};
Expand Down Expand Up @@ -2379,6 +2404,58 @@ mod auto_review_tests {
assert_eq!(classifier.model.as_deref(), Some("gpt-6-luna"));
}

#[test]
fn codex_classifier_gets_low_effort_and_guardian_cache_key() {
let mut classifier = request(
"You are a security monitor for autonomous AI coding agents.",
false,
json!([]),
);
apply_codex_auto_review_defaults(&mut classifier, Some("sess-1"));
assert_eq!(classifier.extra["output_config"]["effort"], "low");
assert_eq!(
classifier.prompt_cache_key_override.as_deref(),
Some("guardian:sess-1")
);
let translated = crate::providers::codex::translate::request::translate_request(
&classifier,
crate::providers::codex::translate::request::TranslateOptions {
session_id: Some("sess-1".to_string()),
service_tier: None,
model: "gpt-6-luna".to_string(),
use_responses_lite: true,
},
)
.unwrap();
assert_eq!(
translated.prompt_cache_key.as_deref(),
Some("guardian:sess-1")
);
let reasoning = serde_json::to_value(&translated.reasoning).unwrap();
assert_eq!(reasoning["effort"], "low");
assert!(
reasoning
.get("summary")
.is_none_or(serde_json::Value::is_null)
);
assert!(translated.include.is_none());
}

#[test]
fn codex_classifier_keeps_explicit_effort() {
let mut classifier = request(
"You are a security monitor for autonomous AI coding agents.",
false,
json!([]),
);
classifier
.extra
.insert("output_config".to_string(), json!({"effort": "high"}));
apply_codex_auto_review_defaults(&mut classifier, None);
assert_eq!(classifier.extra["output_config"]["effort"], "high");
assert!(classifier.prompt_cache_key_override.is_none());
}

#[test]
fn non_codex_classifier_keeps_requested_model_without_override() {
let mut classifier = request(
Expand Down