From d3ffbbed5a5bec8393268860c46ad0198890601f Mon Sep 17 00:00:00 2001 From: felixxia-oai Date: Wed, 9 Sep 2026 14:38:19 +0000 Subject: [PATCH] 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 --- .../app-server/tests/suite/v2/guardian_v2.rs | 21 ++ codex-rs/core/src/client.rs | 9 + codex-rs/core/src/guardian/mod.rs | 2 + codex-rs/core/src/guardian/prompt.rs | 18 +- codex-rs/core/src/guardian/request_budget.rs | 33 +++ .../guardian-v2/src/async_scorer/extension.rs | 15 ++ .../src/async_scorer/extension_tests.rs | 24 ++- .../guardian-v2/src/async_scorer/sampler.rs | 17 ++ .../src/async_scorer/sampler_tests.rs | 40 +++- codex-rs/guardian-context/src/budget.rs | 200 ++++++++++++++++++ codex-rs/guardian-context/src/budget_tests.rs | 92 ++++++++ codex-rs/guardian-context/src/composition.rs | 2 +- codex-rs/guardian-context/src/lib.rs | 8 + 13 files changed, 472 insertions(+), 9 deletions(-) create mode 100644 codex-rs/core/src/guardian/request_budget.rs create mode 100644 codex-rs/guardian-context/src/budget.rs create mode 100644 codex-rs/guardian-context/src/budget_tests.rs diff --git a/codex-rs/app-server/tests/suite/v2/guardian_v2.rs b/codex-rs/app-server/tests/suite/v2/guardian_v2.rs index 8cb99e9ac2..75374ccb9e 100644 --- a/codex-rs/app-server/tests/suite/v2/guardian_v2.rs +++ b/codex-rs/app-server/tests/suite/v2/guardian_v2.rs @@ -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>, 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 diff --git a/codex-rs/core/src/client.rs b/codex-rs/core/src/client.rs index a813022548..786831d1c4 100644 --- a/codex-rs/core/src/client.rs +++ b/codex-rs/core/src/client.rs @@ -1640,6 +1640,9 @@ impl ModelClientSession { ); self.client .prepare_response_items_for_request(&mut request.input); + if crate::guardian::is_basic_session_source(&self.client.state.session_source) { + crate::guardian::observe_guardian_request(session_telemetry, &request); + } let request_session_telemetry = session_telemetry_for_request(session_telemetry, &request); let inference_trace_attempt = inference_trace.start_attempt(); @@ -1830,6 +1833,12 @@ impl ModelClientSession { Err(err) => return Err(provider.map_api_error(err)), } + // Measure the complete logical request, not only the websocket delta. + if !warmup + && crate::guardian::is_basic_session_source(&self.client.state.session_source) + { + crate::guardian::observe_guardian_request(session_telemetry, &request); + } let (incremental_request, previous_response_id_from_untraced_warmup) = self.prepare_websocket_request(&request); let inference_trace_attempt = if warmup { diff --git a/codex-rs/core/src/guardian/mod.rs b/codex-rs/core/src/guardian/mod.rs index 943649536a..f04041bf0b 100644 --- a/codex-rs/core/src/guardian/mod.rs +++ b/codex-rs/core/src/guardian/mod.rs @@ -9,6 +9,8 @@ mod decision; mod feedback; mod metrics; mod prompt; +mod request_budget; +pub(crate) use request_budget::observe as observe_guardian_request; mod review; mod review_session; mod reviewer_config; diff --git a/codex-rs/core/src/guardian/prompt.rs b/codex-rs/core/src/guardian/prompt.rs index 0f88ec67d2..9f5aea8daf 100644 --- a/codex-rs/core/src/guardian/prompt.rs +++ b/codex-rs/core/src/guardian/prompt.rs @@ -210,9 +210,21 @@ pub(crate) async fn build_guardian_prompt_items_with_parent_turn( if transcript_entries.is_empty() { transcript.items.push(placeholder.to_owned()); } - let items = sections - .compose(presentation, transcript)? - .into_user_inputs()?; + let context = sections.compose(presentation, transcript)?; + for (section, cost) in context.section_costs() { + for (measurement, value) in cost.measurements() { + session.services.session_telemetry.histogram( + codex_guardian_context::SECTION_COST_METRIC, + i64::try_from(value).unwrap_or(i64::MAX), + &[ + ("target", "sync"), + ("section", section), + ("measurement", measurement), + ], + ); + } + } + let items = context.into_user_inputs()?; Ok(GuardianPromptItems { items, transcript_cursor, diff --git a/codex-rs/core/src/guardian/request_budget.rs b/codex-rs/core/src/guardian/request_budget.rs new file mode 100644 index 0000000000..50b356a435 --- /dev/null +++ b/codex-rs/core/src/guardian/request_budget.rs @@ -0,0 +1,33 @@ +//! Measures the complete synchronous request after image admission and wire +//! prefix assembly. Includes reused history, tool definitions and output format. + +use codex_guardian_context::REQUEST_TOKENS_METRIC; +use codex_protocol::protocol::TruncationPolicy; + +use crate::context_manager::estimate_item_token_count; +use codex_api::ResponsesApiRequest; +use codex_otel::SessionTelemetry; + +pub(crate) fn observe(telemetry: &SessionTelemetry, request: &ResponsesApiRequest) -> usize { + let input = request + .input + .iter() + .map(estimate_item_token_count) + .fold(0i64, i64::saturating_add); + let instructions = TruncationPolicy::Bytes(request.instructions.len()).token_budget(); + let metadata = serde_json::to_vec(&(&request.tools, &request.text)) + .map(|bytes| TruncationPolicy::Bytes(bytes.len()).token_budget()) + .unwrap_or(usize::MAX); + let total = usize::try_from(input) + .unwrap_or(usize::MAX) + .saturating_add(instructions) + .saturating_add(metadata); + // The assembled input already includes inherited history and the current + // review. Do not report a guessed old/new split after context injection. + telemetry.histogram( + REQUEST_TOKENS_METRIC, + i64::try_from(total).unwrap_or(i64::MAX), + &[("target", "sync"), ("component", "total")], + ); + total +} diff --git a/codex-rs/ext/guardian-v2/src/async_scorer/extension.rs b/codex-rs/ext/guardian-v2/src/async_scorer/extension.rs index 3259bc54b4..54bdf39a79 100644 --- a/codex-rs/ext/guardian-v2/src/async_scorer/extension.rs +++ b/codex-rs/ext/guardian-v2/src/async_scorer/extension.rs @@ -600,6 +600,21 @@ impl GuardianV2Extension { drop(history); drop(node_repl_images); truncations.extend(std::mem::take(&mut transcript.truncations)); + if let Some(metrics) = metrics.as_deref() { + for (section, cost) in transcript.section_costs() { + for (measurement, value) in cost.measurements() { + metrics.histogram( + codex_guardian_context::SECTION_COST_METRIC, + i64::try_from(value).unwrap_or(i64::MAX), + &[ + ("target", "async"), + ("section", section), + ("measurement", measurement), + ], + ); + } + } + } let classification_input = transcript.into_messages(); let mut failure_reason = "invalid_output"; let mut classification_risk = None; diff --git a/codex-rs/ext/guardian-v2/src/async_scorer/extension_tests.rs b/codex-rs/ext/guardian-v2/src/async_scorer/extension_tests.rs index e3a7cc6314..c41025a8f6 100644 --- a/codex-rs/ext/guardian-v2/src/async_scorer/extension_tests.rs +++ b/codex-rs/ext/guardian-v2/src/async_scorer/extension_tests.rs @@ -349,6 +349,14 @@ fn fast_decision_metric(decision: &str, reason: &str) -> RecordedMetric { #[derive(Default)] struct RecordingMetrics(Mutex>); +impl RecordingMetrics { + fn classification_samples(&self) -> Vec { + self.0.lock().unwrap().iter().filter(|sample| { + !matches!(sample, RecordedMetric::Histogram(name, _, _) if name == codex_guardian_context::SECTION_COST_METRIC || name == codex_guardian_context::REQUEST_TOKENS_METRIC) + }).cloned().collect() + } +} + impl ExtensionMetrics for RecordingMetrics { fn counter(&self, name: &str, inc: i64, tags: &[(&str, &str)]) { self.0.lock().unwrap().push(RecordedMetric::Counter( @@ -1436,12 +1444,22 @@ max_recent_non_user_entries = 8 .latest_scored_tool_call .load(Ordering::Acquire) == 0 - || metrics.0.lock().unwrap().len() < 14 + || metrics.classification_samples().len() < 14 { tokio::task::yield_now().await; } }) .await?; + assert!(metrics.0.lock().unwrap().iter().any(|sample| { + matches!(sample, RecordedMetric::Histogram(name, value, tags) + if name == codex_guardian_context::SECTION_COST_METRIC + && *value > 0 + && tags == &[ + ("target".to_owned(), "async".to_owned()), + ("section".to_owned(), "conversation_transcript".to_owned()), + ("measurement".to_owned(), "text_bytes".to_owned()), + ]) + })); thread_store.insert(SecurityRiskScore { scores: BTreeMap::from([("action_risk".to_owned(), 0.65)]), call_id: None, @@ -1535,7 +1553,7 @@ max_recent_non_user_entries = 8 Some(ReviewDecision::Approved) ); - let samples = initial_metrics.0.lock().unwrap(); + let samples = initial_metrics.classification_samples(); let classification_duration_ms = match &samples[9] { RecordedMetric::Histogram(name, duration_ms, _) if name == CLASSIFICATION_DURATION_METRIC => @@ -1545,7 +1563,7 @@ max_recent_non_user_entries = 8 sample => panic!("expected classification duration metric, got {sample:?}"), }; assert_eq!( - *samples, + samples, [ ("total", 150), ("input", 120), diff --git a/codex-rs/ext/guardian-v2/src/async_scorer/sampler.rs b/codex-rs/ext/guardian-v2/src/async_scorer/sampler.rs index 295647013d..3474c2471b 100644 --- a/codex-rs/ext/guardian-v2/src/async_scorer/sampler.rs +++ b/codex-rs/ext/guardian-v2/src/async_scorer/sampler.rs @@ -292,6 +292,23 @@ impl LunaSampler { item.set_id(Some(ResponseItemId::new(prefix))); } } + let total_tokens = input + .iter() + .map(codex_guardian_context::estimate_input_tokens) + .fold(0usize, usize::saturating_add); + if let Some(metrics) = self.config.metrics.as_deref() { + for (component, tokens) in [ + ("existing_context", 0), + ("new_input", total_tokens), + ("total", total_tokens), + ] { + metrics.histogram( + codex_guardian_context::REQUEST_TOKENS_METRIC, + i64::try_from(tokens).unwrap_or(i64::MAX), + &[("target", "async"), ("component", component)], + ); + } + } let mut request = ResponsesApiRequest { model: MODEL.to_owned(), instructions: String::new(), diff --git a/codex-rs/ext/guardian-v2/src/async_scorer/sampler_tests.rs b/codex-rs/ext/guardian-v2/src/async_scorer/sampler_tests.rs index f4e29afcd1..c822c51944 100644 --- a/codex-rs/ext/guardian-v2/src/async_scorer/sampler_tests.rs +++ b/codex-rs/ext/guardian-v2/src/async_scorer/sampler_tests.rs @@ -276,14 +276,29 @@ async fn sampler_records_token_usage_after_returning_an_early_classification() - assert_eq!(sampler.sample(sample_request("turn-1")).await?, "low"); tokio::time::timeout(Duration::from_secs(2), async { - while metrics.0.lock().unwrap().len() < 7 { + while metrics + .0 + .lock() + .unwrap() + .iter() + .filter(|metric| metric.0 == CLASSIFICATION_TOKEN_USAGE_METRIC) + .count() + < 7 + { tokio::task::yield_now().await; } }) .await?; assert_eq!( - *metrics.0.lock().unwrap(), + metrics + .0 + .lock() + .unwrap() + .iter() + .filter(|metric| metric.0 == CLASSIFICATION_TOKEN_USAGE_METRIC) + .cloned() + .collect::>(), [ ("total", 37), ("input", 37), @@ -300,6 +315,27 @@ async fn sampler_records_token_usage_after_returning_an_early_classification() - )) ); + let request = server + .wait_for_request( + /*connection_index*/ INITIAL_WEBSOCKET_CONNECTIONS - 1, + /*request_index*/ 0, + ) + .await + .body_json(); + let input: Vec = serde_json::from_value(request["input"].clone())?; + let estimated = input + .iter() + .map(codex_guardian_context::estimate_input_tokens) + .sum::(); + assert!(metrics.0.lock().unwrap().contains(&( + codex_guardian_context::REQUEST_TOKENS_METRIC.to_owned(), + i64::try_from(estimated)?, + vec![ + ("target".to_owned(), "async".to_owned()), + ("component".to_owned(), "total".to_owned()), + ], + ))); + Ok(()) } diff --git a/codex-rs/guardian-context/src/budget.rs b/codex-rs/guardian-context/src/budget.rs new file mode 100644 index 0000000000..6131d9bfd9 --- /dev/null +++ b/codex-rs/guardian-context/src/budget.rs @@ -0,0 +1,200 @@ +//! Payload-free accounting for composed sections and complete Guardian requests. +//! Text and image bytes stay separate. Hosts provide existing-context estimates; +//! the shared wire estimate reserves image tokens without counting base64 as text. + +use std::io::Write; + +use codex_protocol::models::ContentItem; +use codex_protocol::models::ResponseItem; +use codex_protocol::protocol::TruncationPolicy; + +use crate::ComposedContext; +use crate::composition::SectionDelivery; +use crate::composition::SectionOutput; + +pub const SECTION_COST_METRIC: &str = "codex.guardian.context.section_cost"; +pub const REQUEST_TOKENS_METRIC: &str = "codex.guardian.context.request_tokens"; +// A conservative reservation matching the existing original-image patch ceiling. +const IMAGE_TOKEN_RESERVATION: usize = 10_000; +/// Conservative input ceiling when the model catalog has no authoritative window. +pub const DEFAULT_MAX_INPUT_TOKENS: usize = 128_000; + +/// Applies the same configured-window cap and effective percentage as sync review. +/// Async callers supply no parent-model override. +pub fn effective_input_token_limit( + model: &codex_protocol::openai_models::ModelInfo, + configured_window: Option, +) -> usize { + let supported = model + .resolved_context_window() + .unwrap_or(DEFAULT_MAX_INPUT_TOKENS as i64); + let supported = if model.used_fallback_model_metadata { + supported.min(DEFAULT_MAX_INPUT_TOKENS as i64) + } else { + supported + }; + let limit = configured_window + .unwrap_or(supported) + .min(supported) + .saturating_mul(model.effective_context_window_percent.clamp(0, 100)) + / 100; + usize::try_from(limit.max(0)).unwrap_or(usize::MAX) +} + +/// Complete input allowance after the host resolves its model and configuration. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct RequestBudget { + pub max_input_tokens: usize, + pub existing_context_tokens: usize, +} + +/// Size of section evidence, excluding enclosing message serialization overhead. +#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] +pub struct SectionCost { + pub text_bytes: usize, + pub image_bytes: usize, + pub image_count: usize, +} + +impl SectionCost { + pub fn measurements(self) -> [(&'static str, usize); 4] { + [ + ("text_bytes", self.text_bytes), + ( + "estimated_text_tokens", + TruncationPolicy::Bytes(self.text_bytes).token_budget(), + ), + ("image_bytes", self.image_bytes), + ("image_count", self.image_count), + ] + } +} + +impl ComposedContext { + /// Conservative cost of composed evidence, including message wrappers. + /// Shared user sections each reserve a wrapper, bounding their coalesced cost. + pub fn estimated_tokens(&self) -> usize { + self.sections + .iter() + .map(section_tokens) + .fold(0usize, usize::saturating_add) + } + + /// Stable section names and numeric costs; never exposes evidence in diagnostics. + pub fn section_costs(&self) -> impl Iterator + '_ { + self.sections.iter().map(|section| { + let content = match §ion.delivery { + SectionDelivery::UserContent(content) => content, + SectionDelivery::Message(message) => match message.as_ref() { + ResponseItem::Message { content, .. } => content, + item => { + return ( + section.id, + SectionCost { + text_bytes: ByteCount::item(item), + ..SectionCost::default() + }, + ); + } + }, + }; + let mut cost = SectionCost::default(); + for item in content { + match item { + ContentItem::InputText { text } | ContentItem::OutputText { text } => { + cost.text_bytes = cost.text_bytes.saturating_add(text.len()); + } + ContentItem::InputImage { image_url, .. } => { + cost.image_bytes = cost.image_bytes.saturating_add(image_url.len()); + cost.image_count = cost.image_count.saturating_add(1); + } + ContentItem::InputAudio { audio_url } => { + // Guardian currently has no audio contributor. Count a future opaque + // payload conservatively until its consumer supplies modality costs. + cost.text_bytes = cost.text_bytes.saturating_add(audio_url.len()); + } + } + } + (section.id, cost) + }) + } +} + +/// Conservative estimate including roles, framing, metadata and opaque checkpoints. +/// Images reserve tokens separately from their wire bytes. This is a budgeting +/// heuristic, not the model's tokenizer or a measurement of server-side usage. +pub fn estimate_input_tokens(item: &ResponseItem) -> usize { + let content = match item { + ResponseItem::Message { content, .. } => content.as_slice(), + _ => &[], + }; + adjusted_tokens(ByteCount::item(item), content) +} + +pub(super) fn content_tokens(item: &ContentItem) -> usize { + let bytes = ByteCount::measure(|counter| serde_json::to_writer(counter, item)); + adjusted_tokens(bytes, std::slice::from_ref(item)) +} + +pub(super) fn section_tokens(section: &SectionOutput) -> usize { + match §ion.delivery { + SectionDelivery::UserContent(content) if content.is_empty() => 0, + SectionDelivery::UserContent(content) => { + let separators = + TruncationPolicy::Bytes(content.len().saturating_sub(1)).token_budget(); + content.iter().map(content_tokens).fold( + estimate_input_tokens(&crate::composition::user_message(Vec::new())) + .saturating_add(separators), + usize::saturating_add, + ) + } + SectionDelivery::Message(message) => estimate_input_tokens(message), + } +} + +fn adjusted_tokens(mut bytes: usize, content: &[ContentItem]) -> usize { + for item in content { + if let ContentItem::InputImage { image_url, .. } = item { + let payload = ByteCount::measure(|counter| serde_json::to_writer(counter, image_url)); + if payload == usize::MAX { + return usize::MAX; + } + bytes = bytes + .saturating_sub(payload.saturating_sub(2)) + .saturating_add(TruncationPolicy::Tokens(IMAGE_TOKEN_RESERVATION).byte_budget()); + } + } + TruncationPolicy::Bytes(bytes).token_budget() +} + +#[derive(Default)] +struct ByteCount(usize); + +impl ByteCount { + fn item(item: &ResponseItem) -> usize { + Self::measure(|counter| serde_json::to_writer(counter, item)) + } + + fn measure(serialize: impl FnOnce(&mut Self) -> serde_json::Result<()>) -> usize { + let mut count = Self::default(); + if serialize(&mut count).is_err() { + return usize::MAX; + } + count.0 + } +} + +impl Write for ByteCount { + fn write(&mut self, bytes: &[u8]) -> std::io::Result { + self.0 = self.0.saturating_add(bytes.len()); + Ok(bytes.len()) + } + + fn flush(&mut self) -> std::io::Result<()> { + Ok(()) + } +} + +#[cfg(test)] +#[path = "budget_tests.rs"] +mod tests; diff --git a/codex-rs/guardian-context/src/budget_tests.rs b/codex-rs/guardian-context/src/budget_tests.rs new file mode 100644 index 0000000000..b66ec2ea37 --- /dev/null +++ b/codex-rs/guardian-context/src/budget_tests.rs @@ -0,0 +1,92 @@ +//! Costs distinguish evidence payloads from complete model request estimates. + +use super::*; +use crate::composition::SectionOutput; +use crate::composition::user_message as message; +use pretty_assertions::assert_eq; + +#[test] +fn section_costs_keep_multimodal_payloads_separate() { + let context = ComposedContext { + sections: vec![ + SectionOutput { + id: "transcript", + delivery: SectionDelivery::UserContent(vec![ + ContentItem::InputText { + text: "évidence".to_owned(), + }, + ContentItem::InputImage { + image_url: "data:image/png;base64,AAAA".to_owned(), + detail: None, + }, + ]), + }, + SectionOutput { + id: "trusted", + delivery: SectionDelivery::Message(Box::new(message(vec![ + ContentItem::InputText { + text: "verified".to_owned(), + }, + ]))), + }, + ], + truncations: Vec::new(), + }; + assert_eq!( + context.section_costs().collect::>(), + vec![ + ( + "transcript", + SectionCost { + text_bytes: "évidence".len(), + image_bytes: "data:image/png;base64,AAAA".len(), + image_count: 1 + } + ), + ( + "trusted", + SectionCost { + text_bytes: "verified".len(), + ..SectionCost::default() + } + ), + ] + ); +} + +#[test] +fn request_estimate_reserves_images_independently_of_encoded_size() { + let image = |payload: &str| { + message(vec![ContentItem::InputImage { + image_url: format!("data:image/png;base64,{payload}"), + detail: None, + }]) + }; + let short = estimate_input_tokens(&image("AAAA")); + assert_eq!(short, estimate_input_tokens(&image(&"A".repeat(200_000)))); + assert!(short >= IMAGE_TOKEN_RESERVATION); +} + +#[test] +fn section_estimate_bounds_the_delivered_message() { + // Individually rounded costs do not always leave room for JSON separators. + for text in ["x", "quoted \"text\"", "évidence"] { + for count in 1..=40 { + let context = ComposedContext { + sections: vec![SectionOutput { + id: "transcript", + delivery: SectionDelivery::UserContent(vec![ + ContentItem::InputText { + text: text.to_owned(), + }; + count + ]), + }], + truncations: Vec::new(), + }; + let estimate = context.estimated_tokens(); + let delivered = context.into_messages(); + assert!(estimate >= estimate_input_tokens(&delivered[0])); + } + } +} diff --git a/codex-rs/guardian-context/src/composition.rs b/codex-rs/guardian-context/src/composition.rs index eb74bc8b82..f6db22a21b 100644 --- a/codex-rs/guardian-context/src/composition.rs +++ b/codex-rs/guardian-context/src/composition.rs @@ -270,7 +270,7 @@ impl ComposedContext { } } -fn user_message(content: Vec) -> ResponseItem { +pub(super) fn user_message(content: Vec) -> ResponseItem { ResponseItem::Message { id: None, role: "user".to_owned(), diff --git a/codex-rs/guardian-context/src/lib.rs b/codex-rs/guardian-context/src/lib.rs index 906174012e..e33f6562b3 100644 --- a/codex-rs/guardian-context/src/lib.rs +++ b/codex-rs/guardian-context/src/lib.rs @@ -48,7 +48,15 @@ pub use verified_answers::render_verified_answers; mod retained_instructions; mod action; +mod budget; mod composition; +pub use budget::DEFAULT_MAX_INPUT_TOKENS; +pub use budget::REQUEST_TOKENS_METRIC; +pub use budget::RequestBudget; +pub use budget::SECTION_COST_METRIC; +pub use budget::SectionCost; +pub use budget::effective_input_token_limit; +pub use budget::estimate_input_tokens; mod profile; pub use composition::CollectedContext; pub use composition::ComposedContext;