Add Guardian context cost and request token telemetry (#44164)

## What changed

- Record per-section text bytes, estimated text tokens, image bytes, and image counts for synchronous reviews and asynchronous scoring without logging evidence payloads.
- Emit estimated request tokens through `codex.guardian.context.request_tokens`. Synchronous estimates include assembled history, instructions, tool definitions, and output format, and measure the full logical request before WebSocket delta generation. Asynchronous estimates cover the assembled input.
- Add shared context budgeting helpers, including conservative image token reservations independent of encoded payload size and model-aware input limit calculation.

## Testing

Add coverage for separate text and image accounting, image estimates independent of encoded size, and section estimates that bound delivered messages. Extend asynchronous scorer and app-server tests to verify cost metric emission.

GitOrigin-RevId: aed45ecd9c23706f88caa51f2a4f2c77872d3bdb
This commit is contained in:
felixxia-oai authored and copyberry committed 2026-09-09 14:48:08 +00:00
1 parent 73a1148c9c
commit d3ffbbed5a
13 files changed
+472 -9

No files matched your search

@@ -179,6 +179,7 @@ struct MockResponsesState {
allow_guardian_review: Notify,
classification_completed: Notify,
truncation_recorded: Notify,
sync_cost_recorded: Notify,
luna_score: f64,
invalid_classification: bool,
review_outcome: ReviewOutcome,
@@ -717,6 +718,21 @@ async fn guardian_v2_routes_scoped_tool_approvals(
"/metrics",
post(
|State(state): State<Arc<MockResponsesState>>, body: String| async move {
if matches!(state.transcript_content, TranscriptContent::MixedEvidence) {
let payload: Value = serde_json::from_str(&body).expect("OTLP JSON");
for metric in payload["resourceMetrics"].as_array().into_iter().flatten()
.flat_map(|resource| resource["scopeMetrics"].as_array().into_iter().flatten())
.flat_map(|scope| scope["metrics"].as_array().into_iter().flatten())
.filter(|metric| metric["name"] == "codex.guardian.context.request_tokens")
{
for point in metric["histogram"]["dataPoints"].as_array().into_iter().flatten() {
let attributes = point["attributes"].as_array().expect("metric attributes");
if attributes.iter().any(|attr| attr["key"] == "target" && attr["value"]["stringValue"] == "sync") {
state.sync_cost_recorded.notify_one();
}
}
}
}
if body.contains("codex.guardian_v2.classification") {
state.classification_completed.notify_one();
}
@@ -1325,6 +1341,11 @@ async fn guardian_v2_routes_scoped_tool_approvals(
assert!(text.contains("Planned action JSON:"));
}
}
if mixed_evidence {
timeout(TIMEOUT, responses_state.sync_cost_recorded.notified())
.await
.expect("sync request cost should be exported");
}
if lifecycle.has_user_answer() {
let reviews = responses_state
.guardian_requests