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/reference/http-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ The request `model` selects the provider. The proxy translates supported message

Streaming responses use Anthropic SSE events such as `message_start`, `content_block_start`, `content_block_delta`, `content_block_stop`, `message_delta`, and `message_stop`. Non-streaming requests are accumulated from the provider's stream.

Codex responses carry the quota headers Codex reports for the subscription: `x-codex-plan-type`, `x-codex-active-limit`, `x-codex-credits-*`, and, for every limit family, `-used-percent`, `-window-minutes`, `-reset-at`, `-reset-after-seconds`, `-limit-name`, and `-over-secondary-limit-percent`. Which family a window belongs to and how long that window runs come from the headers themselves: a plan may meter a weekly window as `primary` and no secondary window at all. The values are the ones Codex sent for the request being answered. A response that arrives before any telemetry does carries the previous values, or none.

Unknown models return HTTP 400 with the supported catalog. Missing provider auth returns HTTP 401.

## `POST /v1/messages/count_tokens`
Expand Down
3 changes: 3 additions & 0 deletions src/providers/codex/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2342,6 +2342,8 @@ impl CodexHttpClient {
}
})?;

super::rate_limits::record(super::rate_limits::telemetry_headers(resp.headers()));

Ok((resp, started_at))
}

Expand All @@ -2352,6 +2354,7 @@ impl CodexHttpClient {
ctx: &RequestContext,
) -> Result<CodexResponse, CodexError> {
let status = resp.status().as_u16();
super::rate_limits::record(super::rate_limits::telemetry_headers(resp.headers()));
let headers: Vec<(String, String)> = resp
.headers()
.iter()
Expand Down
20 changes: 17 additions & 3 deletions src/providers/codex/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ pub mod count_tokens;
pub(crate) mod events;
pub mod images;
pub mod native;
pub mod rate_limits;
pub mod request_summary;
pub mod search;
pub mod transcription;
Expand Down Expand Up @@ -534,7 +535,7 @@ impl Provider for CodexProvider {
}

async fn handle_messages(&self, body: MessagesRequest, ctx: RequestContext) -> Response {
self.handle_messages_inner(body, ctx, None).await
rate_limits::stamp_response(self.handle_messages_inner(body, ctx, None).await)
}

async fn handle_messages_with_conversation_identity(
Expand All @@ -543,8 +544,10 @@ impl Provider for CodexProvider {
ctx: RequestContext,
conversation_identity: Option<ConversationIdentity>,
) -> Response {
self.handle_messages_inner(body, ctx, conversation_identity)
.await
rate_limits::stamp_response(
self.handle_messages_inner(body, ctx, conversation_identity)
.await,
)
}

async fn handle_count_tokens(&self, body: MessagesRequest, ctx: RequestContext) -> Response {
Expand Down Expand Up @@ -886,6 +889,7 @@ async fn live_stream_response_once(
generation_started = true;
}
append_upstream_sse_payload(&mut upstream_sse_body, &payload);
record_rate_limit_telemetry(&payload);
let (chunk, terminal) = match translate_live_stream_payload(&mut translator, &payload, None)
{
Ok(result) => result,
Expand Down Expand Up @@ -1107,6 +1111,7 @@ fn remaining_live_stream_response(
match item {
Ok(payload) => {
append_upstream_sse_payload(&mut upstream_sse_body, &payload);
record_rate_limit_telemetry(&payload);
let (chunk, terminal) = match translate_live_stream_payload(
&mut translator,
&payload,
Expand Down Expand Up @@ -1230,6 +1235,15 @@ fn append_upstream_sse_payload(buffer: &mut Vec<u8>, payload: &serde_json::Value
buffer.push(b'\n');
}

/// Keeps the quota numbers a `codex.rate_limits` event carries, so the response
/// to the request after this one can pass them to the client. Every other event
/// leaves the snapshot alone.
fn record_rate_limit_telemetry(payload: &serde_json::Value) {
if payload.get("type").and_then(|value| value.as_str()) == Some("codex.rate_limits") {
rate_limits::record(rate_limits::event_headers(payload));
}
}

fn event_stream_response<S>(stream: S) -> Response
where
S: futures_util::Stream<Item = Result<Bytes, std::io::Error>> + Send + 'static,
Expand Down
Loading