Route Guardian requests through /responses with identifying headers (#45736)

## What changed

- Replace dedicated Guardian endpoints with `/responses`, sending `x-codex-guardian: reviewer` or `x-codex-guardian: classifier` for eligible Codex backend requests over HTTP and WebSocket.
- Add model-scoped thread headers, recheck backend authentication on each request attempt, and reconnect WebSockets when the applicable headers change.
- Retain `features.guardianv2.free_guardian` for configuration compatibility while removing its routing gate; the backend now controls Guardian billing.

## Testing

Update tests for reviewer and classifier headers, model and authentication scoping, HTTP fallback, WebSocket reuse, and parent-response metadata across retries.

GitOrigin-RevId: 1c2b3c458ab77fb40aae4ee6784f6d827c60e76c
This commit is contained in:
jif
2026-09-15 16:54:10 +00:00
committed by copyberry
parent d4e11a9b97
commit eeded5ba1a
22 changed files with 246 additions and 286 deletions

View File

@@ -25,7 +25,6 @@ pub use realtime_websocket::RealtimeWebsocketEvents;
pub use realtime_websocket::RealtimeWebsocketWriter;
pub use realtime_websocket::session_update_session_json;
pub use responses::ResponsesClient;
pub use responses::ResponsesEndpoint;
pub use responses::ResponsesOptions;
pub use responses_websocket::ResponsesWebsocketClient;
pub use responses_websocket::ResponsesWebsocketClose;

View File

@@ -23,33 +23,9 @@ use std::sync::Arc;
use std::sync::OnceLock;
use tracing::instrument;
/// Responses-compatible inference routes supported by Codex backend.
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub enum ResponsesEndpoint {
/// Regular user-owned model inference.
#[default]
Responses,
/// Full Guardian approval-review agent inference.
Guardian,
/// Lightweight asynchronous Guardian risk classification.
GuardianClassifier,
}
impl ResponsesEndpoint {
/// Returns the provider-relative path for this inference surface.
pub const fn path(self) -> &'static str {
match self {
Self::Responses => "/responses",
Self::Guardian => "/guardian",
Self::GuardianClassifier => "/guardian-classifier",
}
}
}
pub struct ResponsesClient<T: HttpTransport> {
session: EndpointSession<T>,
sse_telemetry: Option<Arc<dyn SseTelemetry>>,
endpoint: ResponsesEndpoint,
}
#[derive(Default)]
@@ -67,16 +43,9 @@ impl<T: HttpTransport> ResponsesClient<T> {
Self {
session: EndpointSession::new(transport, provider, auth),
sse_telemetry: None,
endpoint: ResponsesEndpoint::Responses,
}
}
/// Selects a Responses-compatible backend route for subsequent requests.
pub fn with_endpoint(mut self, endpoint: ResponsesEndpoint) -> Self {
self.endpoint = endpoint;
self
}
pub fn with_telemetry(
self,
request: Option<Arc<dyn RequestTelemetry>>,
@@ -85,7 +54,6 @@ impl<T: HttpTransport> ResponsesClient<T> {
Self {
session: self.session.with_request_telemetry(request),
sse_telemetry: sse,
endpoint: self.endpoint,
}
}
@@ -96,7 +64,7 @@ impl<T: HttpTransport> ResponsesClient<T> {
fields(
transport = "responses_http",
http.method = "POST",
api.path = self.endpoint.path()
api.path = "/responses"
)
)]
pub async fn stream_request(
@@ -135,7 +103,7 @@ impl<T: HttpTransport> ResponsesClient<T> {
fields(
transport = "responses_http",
http.method = "POST",
api.path = self.endpoint.path(),
api.path = "/responses",
turn.has_state = turn_state.is_some()
)
)]
@@ -168,7 +136,7 @@ impl<T: HttpTransport> ResponsesClient<T> {
.session
.stream_encoded_json_with(
Method::POST,
self.endpoint.path(),
"/responses",
extra_headers,
Some(body),
|req| {

View File

@@ -4,7 +4,6 @@ use crate::common::ResponseStream;
use crate::common::ResponsesWsRequest;
use crate::common::SafetyBufferingTreatment;
use crate::common::WS_REQUEST_HEADER_TRACEPARENT_CLIENT_METADATA_KEY;
use crate::endpoint::responses::ResponsesEndpoint;
use crate::error::ApiError;
use crate::provider::Provider;
use crate::rate_limits::parse_rate_limit_event;
@@ -182,7 +181,6 @@ struct ResponsesWebsocketTimingLogContext {
pub struct ResponsesWebsocketConnection {
stream: Arc<Mutex<Option<WsStream>>>,
endpoint: ResponsesEndpoint,
// TODO (pakrym): is this the right place for timeout?
idle_timeout: Duration,
server_reasoning_included: bool,
@@ -194,7 +192,6 @@ impl std::fmt::Debug for ResponsesWebsocketConnection {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ResponsesWebsocketConnection")
.field("stream", &"<ws-stream>")
.field("endpoint", &self.endpoint)
.field("idle_timeout", &self.idle_timeout)
.field("server_reasoning_included", &self.server_reasoning_included)
.field("server_model", &self.server_model)
@@ -210,11 +207,9 @@ impl ResponsesWebsocketConnection {
server_reasoning_included: bool,
server_model: Option<String>,
telemetry: Option<Arc<dyn WebsocketTelemetry>>,
endpoint: ResponsesEndpoint,
) -> Self {
Self {
stream: Arc::new(Mutex::new(Some(stream))),
endpoint,
idle_timeout,
server_reasoning_included,
server_model,
@@ -230,7 +225,7 @@ impl ResponsesWebsocketConnection {
name = "responses_websocket.stream_request",
level = "info",
skip_all,
fields(transport = "responses_websocket", api.path = self.endpoint.path())
fields(transport = "responses_websocket", api.path = "/responses")
)]
pub async fn stream_request(
&self,
@@ -345,7 +340,6 @@ impl ResponsesWebsocketConnection {
pub struct ResponsesWebsocketClient {
provider: Provider,
auth: SharedAuthProvider,
endpoint: ResponsesEndpoint,
}
/// Close frame information captured by a handshake probe.
@@ -375,24 +369,14 @@ pub struct ResponsesWebsocketProbe {
impl ResponsesWebsocketClient {
/// Creates a Responses WebSocket client for an already-resolved provider and auth source.
pub fn new(provider: Provider, auth: SharedAuthProvider) -> Self {
Self {
provider,
auth,
endpoint: ResponsesEndpoint::Responses,
}
}
/// Selects a Responses-compatible backend route for subsequent connections.
pub fn with_endpoint(mut self, endpoint: ResponsesEndpoint) -> Self {
self.endpoint = endpoint;
self
Self { provider, auth }
}
#[instrument(
name = "responses_websocket.connect",
level = "info",
skip_all,
fields(transport = "responses_websocket", api.path = self.endpoint.path())
fields(transport = "responses_websocket", api.path = "/responses")
)]
pub async fn connect(
&self,
@@ -404,7 +388,7 @@ impl ResponsesWebsocketClient {
) -> Result<ResponsesWebsocketConnection, ApiError> {
let ws_url = self
.provider
.websocket_url_for_path(self.endpoint.path())
.websocket_url_for_path("/responses")
.map_err(|err| ApiError::Stream(format!("failed to build websocket URL: {err}")))?;
let mut headers =
@@ -419,7 +403,6 @@ impl ResponsesWebsocketClient {
server_reasoning_included,
server_model,
telemetry,
self.endpoint,
))
}
@@ -439,7 +422,7 @@ impl ResponsesWebsocketClient {
) -> Result<ResponsesWebsocketProbe, ApiError> {
let ws_url = self
.provider
.websocket_url_for_path(self.endpoint.path())
.websocket_url_for_path("/responses")
.map_err(|err| ApiError::Stream(format!("failed to build websocket URL: {err}")))?;
let mut headers =

View File

@@ -65,7 +65,6 @@ pub use crate::endpoint::RealtimeWebsocketConnection;
pub use crate::endpoint::RealtimeWebsocketEvents;
pub use crate::endpoint::RealtimeWebsocketWriter;
pub use crate::endpoint::ResponsesClient;
pub use crate::endpoint::ResponsesEndpoint;
pub use crate::endpoint::ResponsesOptions;
pub use crate::endpoint::ResponsesWebsocketClient;
pub use crate::endpoint::ResponsesWebsocketClose;