mirror of
https://github.com/openai/codex.git
synced 2026-10-27 18:48:37 +00:00
Show native dollar thread usage for ENT26 plans (#57251)
## Why ENT26 plans are billed directly in dollars, so credit estimates and credit-weighted breakdowns do not describe their thread usage. ## What changed - Expose optional `nativeUsageUsdMicros` in app-server `ThreadUsage` responses and generated schemas and SDK types. Missing native usage means unknown, not zero. - Enable ENT26 usage fetching and display native dollar amounts in `/status`, the status line, terminal titles, and the agents overview. Hide credits and omit credit-weighted `/status` breakdowns for these plans. - Use native dollar amounts when tracking post-turn usage settlement for ENT26 plans. ## Testing Add coverage for native usage forwarding, ENT26 billing displays, and settlement polling with stale, missing, or zero dollar amounts. GitOrigin-RevId: ca8ea98141a66bdf0a27feb9f58f67822b821f5f
This commit is contained in:
+8
@@ -25899,6 +25899,14 @@
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"nativeUsageUsdMicros": {
|
||||
"description": "Native dollar usage, independent of credit estimates. None means unknown, not zero.",
|
||||
"format": "int64",
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"threadId": {
|
||||
"type": "string"
|
||||
}
|
||||
|
||||
+8
@@ -23613,6 +23613,14 @@
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"nativeUsageUsdMicros": {
|
||||
"description": "Native dollar usage, independent of credit estimates. None means unknown, not zero.",
|
||||
"format": "int64",
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"threadId": {
|
||||
"type": "string"
|
||||
}
|
||||
|
||||
+8
@@ -76,6 +76,14 @@
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"nativeUsageUsdMicros": {
|
||||
"description": "Native dollar usage, independent of credit estimates. None means unknown, not zero.",
|
||||
"format": "int64",
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"threadId": {
|
||||
"type": "string"
|
||||
}
|
||||
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
@@ -3,4 +3,8 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { ThreadUsageBreakdownGroup } from "./ThreadUsageBreakdownGroup";
|
||||
|
||||
export type ThreadUsage = { threadId: string, estimatedUsageCreditsMicros: bigint, estimatedUsageUsdMicros: bigint | null, groups: Array<ThreadUsageBreakdownGroup>, };
|
||||
export type ThreadUsage = { threadId: string, estimatedUsageCreditsMicros: bigint, estimatedUsageUsdMicros: bigint | null,
|
||||
/**
|
||||
* Native dollar usage, independent of credit estimates. None means unknown, not zero.
|
||||
*/
|
||||
nativeUsageUsdMicros?: bigint | null, groups: Array<ThreadUsageBreakdownGroup>, };
|
||||
|
||||
@@ -2409,6 +2409,8 @@ mod tests {
|
||||
let legacy_optional_nullable_field = (path
|
||||
== Path::new("v2/GetAccountTokenUsageResponse.ts")
|
||||
&& field_prefix.trim() == "threadUsage?")
|
||||
|| (path == Path::new("v2/ThreadUsage.ts")
|
||||
&& field_prefix.trim() == "nativeUsageUsdMicros?")
|
||||
|| (path == Path::new("v2/McpServerOauthLoginCompletedNotification.ts")
|
||||
&& field_prefix.trim() == "loginId?")
|
||||
|| (path == Path::new("v2/MisalignmentErrorDetails.ts")
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
//! Thread billing amounts, keeping native dollars separate from credit estimates.
|
||||
|
||||
use crate::JsonSchema;
|
||||
use crate::TS;
|
||||
use serde::Deserialize;
|
||||
@@ -10,6 +12,9 @@ pub struct ThreadUsage {
|
||||
pub thread_id: String,
|
||||
pub estimated_usage_credits_micros: i64,
|
||||
pub estimated_usage_usd_micros: Option<i64>,
|
||||
/// Native dollar usage, independent of credit estimates. None means unknown, not zero.
|
||||
#[ts(optional, as = "Option<Option<i64>>")]
|
||||
pub native_usage_usd_micros: Option<i64>,
|
||||
pub groups: Vec<ThreadUsageBreakdownGroup>,
|
||||
}
|
||||
|
||||
|
||||
@@ -1295,6 +1295,7 @@ impl AccountRequestProcessor {
|
||||
thread_id: usage.thread_id,
|
||||
estimated_usage_credits_micros: usage.estimated_usage_credits_micros,
|
||||
estimated_usage_usd_micros: usage.estimated_usage_usd_micros,
|
||||
native_usage_usd_micros: usage.native_usage_usd_micros,
|
||||
groups: usage
|
||||
.groups
|
||||
.into_iter()
|
||||
|
||||
@@ -103,6 +103,7 @@ async fn account_thread_usage_uses_active_workspace_and_canonical_thread_ids() -
|
||||
thread_id: thread_id.to_string(),
|
||||
estimated_usage_credits_micros: 46_000_000,
|
||||
estimated_usage_usd_micros: None,
|
||||
native_usage_usd_micros: None,
|
||||
groups: vec![ThreadUsageBreakdownGroup {
|
||||
model: Some("gpt-5.4".to_string()),
|
||||
reasoning_effort: Some("high".to_string()),
|
||||
@@ -132,7 +133,7 @@ async fn account_thread_usage_supports_externally_managed_authentication() -> Re
|
||||
let access_token = encode_id_token(
|
||||
&ChatGptIdTokenClaims::new()
|
||||
.email("external@example.com")
|
||||
.plan_type("business")
|
||||
.plan_type("ent26")
|
||||
.chatgpt_account_id("external-workspace"),
|
||||
)?;
|
||||
Mock::given(method("GET"))
|
||||
@@ -149,7 +150,7 @@ async fn account_thread_usage_supports_externally_managed_authentication() -> Re
|
||||
.send_chatgpt_auth_tokens_login_request(
|
||||
access_token.clone(),
|
||||
"external-workspace".to_string(),
|
||||
Some("business".to_string()),
|
||||
Some("ent26".to_string()),
|
||||
)
|
||||
.await?;
|
||||
let login: LoginAccountResponse =
|
||||
@@ -164,8 +165,9 @@ async fn account_thread_usage_supports_externally_managed_authentication() -> Re
|
||||
.respond_with(ResponseTemplate::new(/*s*/ 200).set_body_json(json!({
|
||||
"threads": [{
|
||||
"thread_id": thread_id,
|
||||
"estimated_usage_credits_micros": 21_000_000,
|
||||
"estimated_usage_usd_micros": 840_000
|
||||
"estimated_usage_credits_micros": 0,
|
||||
"estimated_usage_usd_micros": 0,
|
||||
"native_usage_usd_micros": 750_000
|
||||
}]
|
||||
})))
|
||||
.expect(/*r*/ 1)
|
||||
@@ -181,8 +183,9 @@ async fn account_thread_usage_supports_externally_managed_authentication() -> Re
|
||||
response.thread_usage,
|
||||
Some(ThreadUsage {
|
||||
thread_id: thread_id.to_string(),
|
||||
estimated_usage_credits_micros: 21_000_000,
|
||||
estimated_usage_usd_micros: Some(840_000),
|
||||
estimated_usage_credits_micros: 0,
|
||||
estimated_usage_usd_micros: Some(0),
|
||||
native_usage_usd_micros: Some(750_000),
|
||||
groups: Vec::new(),
|
||||
})
|
||||
);
|
||||
|
||||
@@ -224,7 +224,12 @@ impl App {
|
||||
usage_lines: ThreadId::from_string(&root.id)
|
||||
.ok()
|
||||
.and_then(|id| self.agents_overview.usage.get(&id))
|
||||
.map(super::agents_overview_usage::usage_lines)
|
||||
.map(|usage| {
|
||||
super::agents_overview_usage::usage_lines(
|
||||
usage,
|
||||
self.chat_widget.current_plan_type(),
|
||||
)
|
||||
})
|
||||
.unwrap_or_default(),
|
||||
last_message,
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ use crate::chatwidget::ThreadUsageOutcome;
|
||||
use crate::status::format_credit_micros;
|
||||
use crate::status::format_estimated_usd_micros;
|
||||
use crate::status::format_tokens_compact;
|
||||
use crate::status::thread_cost_micros;
|
||||
use crate::tui::FrameRequester;
|
||||
use codex_app_server_protocol::ThreadUsage;
|
||||
use codex_app_server_protocol::ThreadUsageBreakdownGroup;
|
||||
@@ -46,7 +47,8 @@ impl App {
|
||||
|| !matches!(
|
||||
self.chat_widget.current_plan_type(),
|
||||
Some(
|
||||
PlanType::Business
|
||||
PlanType::Ent26
|
||||
| PlanType::Business
|
||||
| PlanType::EnterpriseCbpUsageBased
|
||||
| PlanType::EnterpriseCbpAutomation
|
||||
)
|
||||
@@ -144,7 +146,10 @@ impl App {
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn usage_lines(usage: &AgentsOverviewUsage) -> Vec<Line<'static>> {
|
||||
pub(super) fn usage_lines(
|
||||
usage: &AgentsOverviewUsage,
|
||||
plan_type: Option<PlanType>,
|
||||
) -> Vec<Line<'static>> {
|
||||
let sum = |count: fn(&ThreadUsageBreakdownGroup) -> Option<i64>| {
|
||||
let groups = &usage.estimate.as_ref()?.groups;
|
||||
if groups.is_empty() {
|
||||
@@ -179,15 +184,14 @@ pub(super) fn usage_lines(usage: &AgentsOverviewUsage) -> Vec<Line<'static>> {
|
||||
}
|
||||
if let Some(estimate) = &usage.estimate {
|
||||
let mut values = Vec::new();
|
||||
if estimate.estimated_usage_credits_micros >= 0 {
|
||||
if plan_type != Some(PlanType::Ent26) && estimate.estimated_usage_credits_micros >= 0 {
|
||||
values.push(format!(
|
||||
"{} credits",
|
||||
format_credit_micros(estimate.estimated_usage_credits_micros)
|
||||
));
|
||||
}
|
||||
if let Some(cost) = estimate
|
||||
.estimated_usage_usd_micros
|
||||
.and_then(format_estimated_usd_micros)
|
||||
if let Some(cost) =
|
||||
thread_cost_micros(estimate, plan_type).and_then(format_estimated_usd_micros)
|
||||
{
|
||||
values.push(cost);
|
||||
}
|
||||
|
||||
@@ -199,6 +199,47 @@ async fn selected_usage_is_cached_and_account_changes_discard_old_results() -> R
|
||||
assert_eq!(app.agents_overview.pending_usage, None);
|
||||
assert!(!app.agents_overview.usage_disabled);
|
||||
}
|
||||
app.chat_widget.update_account_state(
|
||||
/*status_account_display*/ None,
|
||||
Some(PlanType::Ent26),
|
||||
/*has_chatgpt_account*/ false,
|
||||
/*has_codex_backend_auth*/ true,
|
||||
);
|
||||
let view = app.agents_overview_view(threads.to_vec(), Some(selected));
|
||||
app.agents_overview.visible_thread_ids = view.thread_ids();
|
||||
app.chat_widget.show_bottom_pane_view(Box::new(view));
|
||||
app.refresh_agents_overview_usage(&app_server, tui.frame_requester());
|
||||
let (thread_id, request_id) = app
|
||||
.agents_overview
|
||||
.pending_usage
|
||||
.expect("request ENT26 usage");
|
||||
assert_eq!(thread_id, selected);
|
||||
let mut native_usage = expected.clone();
|
||||
native_usage.estimated_usage_credits_micros = 0;
|
||||
native_usage.estimated_usage_usd_micros = Some(0);
|
||||
native_usage.native_usage_usd_micros = Some(750_000);
|
||||
app.finish_agents_overview_usage(
|
||||
thread_id,
|
||||
request_id,
|
||||
Ok(ThreadUsageOutcome::Available(native_usage)),
|
||||
);
|
||||
let view = app.agents_overview_view(threads.to_vec(), Some(selected));
|
||||
let rendered = view
|
||||
.rows
|
||||
.iter()
|
||||
.find(|row| row.thread_id == selected)
|
||||
.unwrap()
|
||||
.details
|
||||
.usage_lines
|
||||
.iter()
|
||||
.map(ToString::to_string)
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
insta::assert_snapshot!(rendered, @"
|
||||
Tokens: 12K in · 3K out
|
||||
Est. usage: ~$0.75
|
||||
");
|
||||
|
||||
app.agents_overview.usage_disabled = true;
|
||||
app.agents_overview.pending_usage = Some((thread_id, request_id));
|
||||
app.app_server_target = AppServerTarget::LocalDaemon {
|
||||
@@ -231,7 +272,7 @@ fn incomplete_billing_groups_do_not_display_partial_token_totals() {
|
||||
.unwrap();
|
||||
let mut usage = AgentsOverviewUsage::default();
|
||||
usage.estimate = Some(estimate);
|
||||
let text = usage_lines(&usage)
|
||||
let text = usage_lines(&usage, Some(PlanType::Business))
|
||||
.into_iter()
|
||||
.map(|line| line.to_string())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
@@ -1428,8 +1428,9 @@ mod tests {
|
||||
serde_json::json!({
|
||||
"threads": [{
|
||||
"thread_id": thread_id.to_string(),
|
||||
"estimated_usage_credits_micros": 46_000_000,
|
||||
"estimated_usage_usd_micros": 1_820_000
|
||||
"estimated_usage_credits_micros": 0,
|
||||
"estimated_usage_usd_micros": 0,
|
||||
"native_usage_usd_micros": 750_000
|
||||
}]
|
||||
}),
|
||||
))
|
||||
@@ -1442,9 +1443,10 @@ mod tests {
|
||||
.await
|
||||
.expect("read thread usage through the authenticated app server"),
|
||||
ThreadUsageOutcome::Available(ThreadUsage {
|
||||
native_usage_usd_micros: Some(750_000),
|
||||
thread_id: thread_id.to_string(),
|
||||
estimated_usage_credits_micros: 46_000_000,
|
||||
estimated_usage_usd_micros: Some(1_820_000),
|
||||
estimated_usage_credits_micros: 0,
|
||||
estimated_usage_usd_micros: Some(0),
|
||||
groups: Vec::new(),
|
||||
})
|
||||
);
|
||||
|
||||
@@ -56,6 +56,7 @@ fn successful_thread_usage(thread_id: ThreadId, request_id: u64) -> AppEvent {
|
||||
thread_id,
|
||||
request_id,
|
||||
result: Ok(ThreadUsageOutcome::Available(ThreadUsage {
|
||||
native_usage_usd_micros: None,
|
||||
thread_id: thread_id.to_string(),
|
||||
estimated_usage_credits_micros: 50_000_000,
|
||||
estimated_usage_usd_micros: Some(1_820_000),
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
//! history-facing `/status` surface.
|
||||
|
||||
use super::*;
|
||||
use crate::status::thread_cost_micros;
|
||||
|
||||
impl ChatWidget {
|
||||
/// Update the status indicator header and details.
|
||||
@@ -385,7 +386,7 @@ impl ChatWidget {
|
||||
|
||||
if self
|
||||
.estimated_thread_usage()
|
||||
.is_some_and(|usage| usage.estimated_usage_usd_micros.is_none())
|
||||
.is_some_and(|usage| thread_cost_micros(usage, self.plan_type).is_none())
|
||||
{
|
||||
preview_data.suppress_placeholder(StatusSurfacePreviewItem::EstimatedThreadCost);
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ use crate::model_catalog::LUNA_RESERVE_MODEL;
|
||||
use crate::status::format_credit_micros;
|
||||
use crate::status::format_estimated_usd_micros;
|
||||
use crate::status::format_tokens_compact;
|
||||
use crate::status::thread_cost_micros;
|
||||
use crate::terminal_program_status::ProgramStatus;
|
||||
use codex_app_server_protocol::AskForApproval;
|
||||
use codex_config::ConfigLayerSource;
|
||||
@@ -807,11 +808,12 @@ impl ChatWidget {
|
||||
}),
|
||||
StatusLineItem::ThreadCredits => self
|
||||
.estimated_thread_usage()
|
||||
.filter(|_| self.plan_type != Some(PlanType::Ent26))
|
||||
.map(|usage| usage.estimated_usage_credits_micros)
|
||||
.map(|credits| format!("{} credits", format_credit_micros(credits))),
|
||||
StatusLineItem::EstimatedThreadCost => self
|
||||
.estimated_thread_usage()
|
||||
.and_then(|usage| usage.estimated_usage_usd_micros)
|
||||
.and_then(|usage| thread_cost_micros(usage, self.plan_type))
|
||||
.and_then(format_estimated_usd_micros),
|
||||
StatusLineItem::SessionId => self.thread_id.map(|id| id.to_string()),
|
||||
StatusLineItem::FastMode => self
|
||||
|
||||
@@ -3117,6 +3117,7 @@ async fn status_line_estimated_thread_cost_fetches_and_renders_backend_estimate(
|
||||
thread_id,
|
||||
request_id,
|
||||
Ok(ThreadUsageOutcome::Available(ThreadUsage {
|
||||
native_usage_usd_micros: None,
|
||||
thread_id: thread_id.to_string(),
|
||||
estimated_usage_credits_micros: 46_000_000,
|
||||
estimated_usage_usd_micros: Some(1_820_000),
|
||||
@@ -3153,6 +3154,7 @@ async fn status_line_thread_credits_fetches_and_renders_fractional_credits() {
|
||||
thread_id,
|
||||
request_id,
|
||||
Ok(ThreadUsageOutcome::Available(ThreadUsage {
|
||||
native_usage_usd_micros: None,
|
||||
thread_id: thread_id.to_string(),
|
||||
estimated_usage_credits_micros: 5_240_000,
|
||||
estimated_usage_usd_micros: Some(210_000),
|
||||
@@ -3186,6 +3188,7 @@ async fn status_line_thread_credits_and_cost_share_one_backend_request() {
|
||||
thread_id,
|
||||
request_id,
|
||||
Ok(ThreadUsageOutcome::Available(ThreadUsage {
|
||||
native_usage_usd_micros: None,
|
||||
thread_id: thread_id.to_string(),
|
||||
estimated_usage_credits_micros: 5_200_000,
|
||||
estimated_usage_usd_micros: Some(210_000),
|
||||
@@ -3220,6 +3223,7 @@ async fn status_line_thread_credits_remain_visible_when_usd_is_unavailable() {
|
||||
thread_id,
|
||||
request_id,
|
||||
Ok(ThreadUsageOutcome::Available(ThreadUsage {
|
||||
native_usage_usd_micros: None,
|
||||
thread_id: thread_id.to_string(),
|
||||
estimated_usage_credits_micros: 5_200_000,
|
||||
estimated_usage_usd_micros: None,
|
||||
@@ -3262,6 +3266,7 @@ async fn terminal_title_thread_usage_fetches_and_renders_without_status_line() {
|
||||
thread_id,
|
||||
request_id,
|
||||
Ok(ThreadUsageOutcome::Available(ThreadUsage {
|
||||
native_usage_usd_micros: None,
|
||||
thread_id: thread_id.to_string(),
|
||||
estimated_usage_credits_micros: 5_200_000,
|
||||
estimated_usage_usd_micros: Some(210_000),
|
||||
@@ -3299,6 +3304,7 @@ async fn terminal_title_thread_credits_remain_visible_when_usd_is_unavailable()
|
||||
thread_id,
|
||||
request_id,
|
||||
Ok(ThreadUsageOutcome::Available(ThreadUsage {
|
||||
native_usage_usd_micros: None,
|
||||
thread_id: thread_id.to_string(),
|
||||
estimated_usage_credits_micros: 5_200_000,
|
||||
estimated_usage_usd_micros: None,
|
||||
@@ -3336,6 +3342,7 @@ async fn terminal_title_and_status_line_share_one_thread_usage_request() {
|
||||
thread_id,
|
||||
request_id,
|
||||
Ok(ThreadUsageOutcome::Available(ThreadUsage {
|
||||
native_usage_usd_micros: None,
|
||||
thread_id: thread_id.to_string(),
|
||||
estimated_usage_credits_micros: 5_200_000,
|
||||
estimated_usage_usd_micros: Some(210_000),
|
||||
@@ -3409,6 +3416,7 @@ async fn status_line_estimated_thread_cost_preserves_cached_amount_during_settle
|
||||
thread_id,
|
||||
initial_request_id,
|
||||
Ok(ThreadUsageOutcome::Available(ThreadUsage {
|
||||
native_usage_usd_micros: None,
|
||||
thread_id: thread_id.to_string(),
|
||||
estimated_usage_credits_micros: 46_000_000,
|
||||
estimated_usage_usd_micros: Some(1_820_000),
|
||||
@@ -3425,6 +3433,7 @@ async fn status_line_estimated_thread_cost_preserves_cached_amount_during_settle
|
||||
thread_id,
|
||||
settlement_request_id,
|
||||
Ok(ThreadUsageOutcome::Available(ThreadUsage {
|
||||
native_usage_usd_micros: None,
|
||||
thread_id: thread_id.to_string(),
|
||||
estimated_usage_credits_micros: 0,
|
||||
estimated_usage_usd_micros: Some(0),
|
||||
@@ -3457,6 +3466,7 @@ async fn completed_turn_refreshes_estimated_thread_cost() {
|
||||
thread_id,
|
||||
initial_request_id,
|
||||
Ok(ThreadUsageOutcome::Available(ThreadUsage {
|
||||
native_usage_usd_micros: None,
|
||||
thread_id: thread_id.to_string(),
|
||||
estimated_usage_credits_micros: 46_000_000,
|
||||
estimated_usage_usd_micros: Some(1_820_000),
|
||||
@@ -3481,6 +3491,7 @@ async fn completed_turn_refreshes_estimated_thread_cost() {
|
||||
thread_id,
|
||||
request_id,
|
||||
Ok(ThreadUsageOutcome::Available(ThreadUsage {
|
||||
native_usage_usd_micros: None,
|
||||
thread_id: thread_id.to_string(),
|
||||
estimated_usage_credits_micros: 50_000_000,
|
||||
estimated_usage_usd_micros: Some(2_100_000),
|
||||
@@ -3509,6 +3520,7 @@ async fn completed_turn_refreshes_credits_only_terminal_title() {
|
||||
thread_id,
|
||||
initial_request_id,
|
||||
Ok(ThreadUsageOutcome::Available(ThreadUsage {
|
||||
native_usage_usd_micros: None,
|
||||
thread_id: thread_id.to_string(),
|
||||
estimated_usage_credits_micros: 5_200_000,
|
||||
estimated_usage_usd_micros: None,
|
||||
@@ -3533,6 +3545,7 @@ async fn completed_turn_refreshes_credits_only_terminal_title() {
|
||||
thread_id,
|
||||
request_id,
|
||||
Ok(ThreadUsageOutcome::Available(ThreadUsage {
|
||||
native_usage_usd_micros: None,
|
||||
thread_id: thread_id.to_string(),
|
||||
estimated_usage_credits_micros: 6_200_000,
|
||||
estimated_usage_usd_micros: None,
|
||||
@@ -3560,6 +3573,7 @@ async fn interrupted_turn_refreshes_estimated_thread_usage() {
|
||||
thread_id,
|
||||
initial_request_id,
|
||||
Ok(ThreadUsageOutcome::Available(ThreadUsage {
|
||||
native_usage_usd_micros: None,
|
||||
thread_id: thread_id.to_string(),
|
||||
estimated_usage_credits_micros: 46_000_000,
|
||||
estimated_usage_usd_micros: Some(1_820_000),
|
||||
@@ -3601,6 +3615,7 @@ async fn failed_turn_refreshes_estimated_thread_usage() {
|
||||
thread_id,
|
||||
initial_request_id,
|
||||
Ok(ThreadUsageOutcome::Available(ThreadUsage {
|
||||
native_usage_usd_micros: None,
|
||||
thread_id: thread_id.to_string(),
|
||||
estimated_usage_credits_micros: 46_000_000,
|
||||
estimated_usage_usd_micros: Some(1_820_000),
|
||||
@@ -3652,6 +3667,7 @@ async fn status_line_estimated_thread_cost_rejects_stale_thread_completions() {
|
||||
previous_thread_id,
|
||||
previous_request_id,
|
||||
Ok(ThreadUsageOutcome::Available(ThreadUsage {
|
||||
native_usage_usd_micros: None,
|
||||
thread_id: previous_thread_id.to_string(),
|
||||
estimated_usage_credits_micros: 46_000_000,
|
||||
estimated_usage_usd_micros: Some(1_820_000),
|
||||
@@ -3664,6 +3680,7 @@ async fn status_line_estimated_thread_cost_rejects_stale_thread_completions() {
|
||||
active_thread_id,
|
||||
active_request_id,
|
||||
Ok(ThreadUsageOutcome::Available(ThreadUsage {
|
||||
native_usage_usd_micros: None,
|
||||
thread_id: active_thread_id.to_string(),
|
||||
estimated_usage_credits_micros: 1_000_000,
|
||||
estimated_usage_usd_micros: Some(400),
|
||||
@@ -3698,6 +3715,7 @@ async fn status_line_estimated_thread_cost_footer_snapshot() {
|
||||
thread_id,
|
||||
request_id,
|
||||
Ok(ThreadUsageOutcome::Available(ThreadUsage {
|
||||
native_usage_usd_micros: None,
|
||||
thread_id: thread_id.to_string(),
|
||||
estimated_usage_credits_micros: 5_200_000,
|
||||
estimated_usage_usd_micros: Some(210_000),
|
||||
|
||||
@@ -243,6 +243,7 @@ async fn status_command_renders_immediately_and_updates_first_card_without_statu
|
||||
thread_id,
|
||||
request_id,
|
||||
Ok(ThreadUsageOutcome::Available(ThreadUsage {
|
||||
native_usage_usd_micros: None,
|
||||
thread_id: thread_id.to_string(),
|
||||
estimated_usage_credits_micros: 50_000_000,
|
||||
estimated_usage_usd_micros: Some(1_820_000),
|
||||
@@ -302,10 +303,9 @@ async fn status_command_renders_immediately_and_updates_first_card_without_statu
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn status_command_omits_thread_usage_for_non_cbp_enterprise_plans() {
|
||||
async fn status_command_omits_thread_usage_for_unsupported_plans() {
|
||||
for plan_type in [
|
||||
Some(PlanType::Enterprise),
|
||||
Some(PlanType::Ent26),
|
||||
Some(PlanType::Pro),
|
||||
Some(PlanType::Edu),
|
||||
Some(PlanType::SelfServeBusinessUsageBased),
|
||||
@@ -373,6 +373,7 @@ async fn status_command_stays_visible_and_updates_after_thread_usage_retry() {
|
||||
thread_id,
|
||||
retry_request_id,
|
||||
Ok(ThreadUsageOutcome::Available(ThreadUsage {
|
||||
native_usage_usd_micros: None,
|
||||
thread_id: thread_id.to_string(),
|
||||
estimated_usage_credits_micros: 50_000_000,
|
||||
estimated_usage_usd_micros: Some(1_820_000),
|
||||
@@ -410,6 +411,7 @@ async fn status_command_renders_credits_and_breakdowns_without_usd_estimate() {
|
||||
thread_id,
|
||||
request_id,
|
||||
Ok(ThreadUsageOutcome::Available(ThreadUsage {
|
||||
native_usage_usd_micros: None,
|
||||
thread_id: thread_id.to_string(),
|
||||
estimated_usage_credits_micros: 50_000_000,
|
||||
estimated_usage_usd_micros: None,
|
||||
@@ -475,6 +477,7 @@ async fn status_command_drops_stale_usd_when_updated_usage_has_only_credits() {
|
||||
thread_id,
|
||||
request_id,
|
||||
Ok(ThreadUsageOutcome::Available(ThreadUsage {
|
||||
native_usage_usd_micros: None,
|
||||
thread_id: thread_id.to_string(),
|
||||
estimated_usage_credits_micros: 40_000_000,
|
||||
estimated_usage_usd_micros: Some(1_820_000),
|
||||
@@ -500,6 +503,7 @@ async fn status_command_drops_stale_usd_when_updated_usage_has_only_credits() {
|
||||
thread_id,
|
||||
request_id,
|
||||
Ok(ThreadUsageOutcome::Available(ThreadUsage {
|
||||
native_usage_usd_micros: None,
|
||||
thread_id: thread_id.to_string(),
|
||||
estimated_usage_credits_micros: 50_000_000,
|
||||
estimated_usage_usd_micros: None,
|
||||
@@ -585,6 +589,7 @@ async fn status_command_remains_visible_when_account_changes_during_usage_refres
|
||||
thread_id,
|
||||
request_id,
|
||||
Ok(ThreadUsageOutcome::Available(ThreadUsage {
|
||||
native_usage_usd_micros: None,
|
||||
thread_id: thread_id.to_string(),
|
||||
estimated_usage_credits_micros: 50_000_000,
|
||||
estimated_usage_usd_micros: Some(1_820_000),
|
||||
@@ -617,6 +622,7 @@ async fn status_command_remains_visible_when_thread_changes_during_usage_refresh
|
||||
thread_id,
|
||||
request_id,
|
||||
Ok(ThreadUsageOutcome::Available(ThreadUsage {
|
||||
native_usage_usd_micros: None,
|
||||
thread_id: thread_id.to_string(),
|
||||
estimated_usage_credits_micros: 50_000_000,
|
||||
estimated_usage_usd_micros: Some(1_820_000),
|
||||
|
||||
@@ -304,6 +304,7 @@ async fn status_surface_preview_lines_thread_usage_snapshot() {
|
||||
thread_id,
|
||||
request_id,
|
||||
Ok(ThreadUsageOutcome::Available(ThreadUsage {
|
||||
native_usage_usd_micros: None,
|
||||
thread_id: thread_id.to_string(),
|
||||
estimated_usage_credits_micros: 5_200_000,
|
||||
estimated_usage_usd_micros: Some(210_000),
|
||||
@@ -351,6 +352,7 @@ async fn status_surface_thread_usage_previews_omit_unavailable_usd_estimates() {
|
||||
thread_id,
|
||||
request_id,
|
||||
Ok(ThreadUsageOutcome::Available(ThreadUsage {
|
||||
native_usage_usd_micros: None,
|
||||
thread_id: thread_id.to_string(),
|
||||
estimated_usage_credits_micros: 5_200_000,
|
||||
estimated_usage_usd_micros: None,
|
||||
|
||||
@@ -9,6 +9,7 @@ use super::StatusLineItem;
|
||||
use super::TerminalTitleItem;
|
||||
use super::ThreadId;
|
||||
use crate::status::StatusHistoryHandle;
|
||||
use crate::status::thread_cost_micros;
|
||||
use codex_app_server_protocol::ThreadUsage;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
@@ -98,7 +99,7 @@ impl ChatWidget {
|
||||
.thread_usage
|
||||
.estimate
|
||||
.as_ref()
|
||||
.and_then(|estimate| estimate.estimated_usage_usd_micros);
|
||||
.and_then(|estimate| thread_cost_micros(estimate, self.plan_type));
|
||||
self.schedule_next_thread_usage_settlement();
|
||||
self.request_thread_usage();
|
||||
}
|
||||
@@ -252,7 +253,7 @@ impl ChatWidget {
|
||||
usage.groups.clone_from(&previous_usage.groups);
|
||||
}
|
||||
let estimated_credits_micros = usage.estimated_usage_credits_micros;
|
||||
let estimated_usd_micros = usage.estimated_usage_usd_micros;
|
||||
let estimated_usd_micros = thread_cost_micros(&usage, self.plan_type);
|
||||
self.thread_usage.estimate = Some(usage);
|
||||
if let Some(baseline) = self.thread_usage.settlement_baseline_credits_micros {
|
||||
if !post_turn_request {
|
||||
@@ -268,10 +269,10 @@ impl ChatWidget {
|
||||
} else {
|
||||
let mut selected = self.selected_thread_usage();
|
||||
if !self.thread_usage.status_history_handles.is_empty() {
|
||||
selected.credits = true;
|
||||
selected.estimated_cost |=
|
||||
self.thread_usage.settlement_baseline_usd_micros.is_some()
|
||||
|| estimated_usd_micros.is_some();
|
||||
selected.credits = self.plan_type != Some(PlanType::Ent26);
|
||||
selected.estimated_cost |= self.plan_type == Some(PlanType::Ent26)
|
||||
|| self.thread_usage.settlement_baseline_usd_micros.is_some()
|
||||
|| estimated_usd_micros.is_some();
|
||||
}
|
||||
let credits_settled =
|
||||
!selected.credits || estimated_credits_micros > baseline;
|
||||
@@ -361,7 +362,8 @@ impl ChatWidget {
|
||||
&& matches!(
|
||||
self.plan_type,
|
||||
Some(
|
||||
PlanType::Business
|
||||
PlanType::Ent26
|
||||
| PlanType::Business
|
||||
| PlanType::EnterpriseCbpUsageBased
|
||||
| PlanType::EnterpriseCbpAutomation
|
||||
)
|
||||
@@ -389,6 +391,7 @@ impl ChatWidget {
|
||||
Ok(_) | Err(_) => {}
|
||||
}
|
||||
}
|
||||
selected.credits &= self.plan_type != Some(PlanType::Ent26);
|
||||
selected
|
||||
}
|
||||
|
||||
|
||||
@@ -35,6 +35,7 @@ fn thread_usage(
|
||||
estimated_usage_usd_micros: Option<i64>,
|
||||
) -> ThreadUsage {
|
||||
ThreadUsage {
|
||||
native_usage_usd_micros: None,
|
||||
thread_id: thread_id.to_string(),
|
||||
estimated_usage_credits_micros,
|
||||
estimated_usage_usd_micros,
|
||||
@@ -472,27 +473,44 @@ async fn transient_zero_credits_preserves_credits_only_estimates() {
|
||||
|
||||
#[tokio::test]
|
||||
async fn cost_settlement_waits_for_new_or_missing_usd_estimates() {
|
||||
for (previous_usd_micros, stale_usd_micros) in [
|
||||
(Some(1_820_000), Some(1_820_000)),
|
||||
(None, None),
|
||||
(None, Some(0)),
|
||||
for (plan_type, previous_usd_micros, stale_usd_micros) in [
|
||||
(PlanType::Business, Some(1_820_000), Some(1_820_000)),
|
||||
(PlanType::Business, None, None),
|
||||
(PlanType::Business, None, Some(0)),
|
||||
(PlanType::Ent26, Some(1_820_000), Some(1_820_000)),
|
||||
(PlanType::Ent26, None, None),
|
||||
(PlanType::Ent26, None, Some(0)),
|
||||
(PlanType::Ent26, Some(1_820_000), Some(0)),
|
||||
] {
|
||||
let (mut chat, _sender, mut rx, _op_rx) = make_chatwidget_manual_with_sender().await;
|
||||
let thread_id = ThreadId::new();
|
||||
let [previous_usage, stale_usage, settled_usage] = [
|
||||
(40_000_000, previous_usd_micros),
|
||||
(50_000_000, stale_usd_micros),
|
||||
(50_000_000, Some(2_100_000)),
|
||||
]
|
||||
.map(|(credits, cost)| {
|
||||
let mut usage = thread_usage(thread_id, credits, cost);
|
||||
usage.native_usage_usd_micros = cost;
|
||||
if plan_type == PlanType::Ent26 {
|
||||
usage.estimated_usage_credits_micros = 0;
|
||||
usage.estimated_usage_usd_micros = Some(0);
|
||||
}
|
||||
usage
|
||||
});
|
||||
let previous_credits_micros = previous_usage.estimated_usage_credits_micros;
|
||||
chat.thread_id = Some(thread_id);
|
||||
chat.has_codex_backend_auth = true;
|
||||
chat.plan_type = Some(PlanType::Business);
|
||||
chat.plan_type = Some(plan_type);
|
||||
chat.local_settings.tui.status_line = Some(vec!["estimated-thread-cost".to_string()]);
|
||||
chat.local_settings.tui.terminal_title =
|
||||
(plan_type == PlanType::Ent26).then(|| vec!["thread-credits".to_string()]);
|
||||
chat.refresh_status_line();
|
||||
let request_id = thread_usage_request_id(&mut rx);
|
||||
assert!(chat.finish_thread_usage_refresh(
|
||||
thread_id,
|
||||
request_id,
|
||||
Ok(ThreadUsageOutcome::Available(thread_usage(
|
||||
thread_id,
|
||||
/*estimated_usage_credits_micros*/ 40_000_000,
|
||||
previous_usd_micros,
|
||||
))),
|
||||
Ok(ThreadUsageOutcome::Available(previous_usage)),
|
||||
));
|
||||
|
||||
chat.refresh_thread_usage_after_turn();
|
||||
@@ -500,15 +518,17 @@ async fn cost_settlement_waits_for_new_or_missing_usd_estimates() {
|
||||
assert!(chat.finish_thread_usage_refresh(
|
||||
thread_id,
|
||||
request_id,
|
||||
Ok(ThreadUsageOutcome::Available(thread_usage(
|
||||
thread_id,
|
||||
/*estimated_usage_credits_micros*/ 50_000_000,
|
||||
stale_usd_micros,
|
||||
))),
|
||||
Ok(ThreadUsageOutcome::Available(stale_usage)),
|
||||
));
|
||||
if plan_type == PlanType::Ent26 {
|
||||
assert_eq!(
|
||||
chat.status_line_text(),
|
||||
stale_usd_micros.and_then(format_estimated_usd_micros)
|
||||
);
|
||||
}
|
||||
assert_eq!(
|
||||
chat.thread_usage.settlement_baseline_credits_micros,
|
||||
Some(40_000_000)
|
||||
Some(previous_credits_micros)
|
||||
);
|
||||
assert_eq!(
|
||||
chat.thread_usage.settlement_baseline_usd_micros,
|
||||
@@ -522,11 +542,7 @@ async fn cost_settlement_waits_for_new_or_missing_usd_estimates() {
|
||||
assert!(chat.finish_thread_usage_refresh(
|
||||
thread_id,
|
||||
request_id,
|
||||
Ok(ThreadUsageOutcome::Available(thread_usage(
|
||||
thread_id,
|
||||
/*estimated_usage_credits_micros*/ 50_000_000,
|
||||
Some(2_100_000),
|
||||
))),
|
||||
Ok(ThreadUsageOutcome::Available(settled_usage)),
|
||||
));
|
||||
|
||||
assert_eq!(chat.status_line_text(), Some("~$2.10".to_string()));
|
||||
@@ -635,6 +651,7 @@ async fn billing_surfaces_render_for_every_supported_enterprise_plan() {
|
||||
PlanType::Business,
|
||||
PlanType::EnterpriseCbpUsageBased,
|
||||
PlanType::EnterpriseCbpAutomation,
|
||||
PlanType::Ent26,
|
||||
] {
|
||||
let (mut chat, _sender, mut rx, _op_rx) = make_chatwidget_manual_with_sender().await;
|
||||
let thread_id = ThreadId::new();
|
||||
@@ -651,13 +668,45 @@ async fn billing_surfaces_render_for_every_supported_enterprise_plan() {
|
||||
assert!(chat.finish_thread_usage_refresh(
|
||||
thread_id,
|
||||
request_id,
|
||||
Ok(ThreadUsageOutcome::Available(thread_usage(
|
||||
thread_id,
|
||||
/*estimated_usage_credits_micros*/ 5_200_000,
|
||||
Some(210_000),
|
||||
))),
|
||||
Ok(ThreadUsageOutcome::Available(ThreadUsage {
|
||||
native_usage_usd_micros: Some(750_000),
|
||||
..thread_usage(
|
||||
thread_id,
|
||||
if plan_type == PlanType::Ent26 {
|
||||
0
|
||||
} else {
|
||||
5_200_000
|
||||
},
|
||||
Some(if plan_type == PlanType::Ent26 {
|
||||
0
|
||||
} else {
|
||||
210_000
|
||||
}),
|
||||
)
|
||||
})),
|
||||
));
|
||||
|
||||
assert!(rx.try_recv().is_err(), "billing surfaces share a request");
|
||||
if plan_type == PlanType::Ent26 {
|
||||
insta::assert_snapshot!(chat.status_line_text().unwrap(), @"~$0.75");
|
||||
insta::assert_snapshot!(chat.last_terminal_title.as_ref().unwrap(), @"~$0.75");
|
||||
chat.add_status_output(
|
||||
/*refreshing_rate_limits*/ false, /*request_id*/ None,
|
||||
);
|
||||
let status_cell = match rx.try_recv() {
|
||||
Ok(AppEvent::InsertHistoryCell(cell)) => cell,
|
||||
event => panic!("expected /status card, got {event:?}"),
|
||||
};
|
||||
let usage_row = status_cell
|
||||
.display_lines(/*width*/ 90)
|
||||
.into_iter()
|
||||
.map(|line| line.to_string())
|
||||
.find(|line| line.contains("Thread usage:"))
|
||||
.unwrap();
|
||||
insta::assert_snapshot!(usage_row, @" Thread usage: ~$0.75");
|
||||
continue;
|
||||
}
|
||||
|
||||
assert_eq!(
|
||||
chat.status_line_text(),
|
||||
Some("5.2 credits · ~$0.21".to_string())
|
||||
@@ -666,7 +715,6 @@ async fn billing_surfaces_render_for_every_supported_enterprise_plan() {
|
||||
chat.last_terminal_title,
|
||||
Some("5.2 credits | ~$0.21".to_string())
|
||||
);
|
||||
assert!(rx.try_recv().is_err(), "billing surfaces share a request");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -726,6 +774,7 @@ async fn unchanged_thread_usage_has_bounded_settlement_refreshes() {
|
||||
thread_id,
|
||||
initial_request_id,
|
||||
Ok(ThreadUsageOutcome::Available(ThreadUsage {
|
||||
native_usage_usd_micros: None,
|
||||
thread_id: thread_id.to_string(),
|
||||
estimated_usage_credits_micros: 46_000_000,
|
||||
estimated_usage_usd_micros: Some(1_820_000),
|
||||
@@ -745,6 +794,7 @@ async fn unchanged_thread_usage_has_bounded_settlement_refreshes() {
|
||||
thread_id,
|
||||
request_id,
|
||||
Ok(ThreadUsageOutcome::Available(ThreadUsage {
|
||||
native_usage_usd_micros: None,
|
||||
thread_id: thread_id.to_string(),
|
||||
estimated_usage_credits_micros: 46_000_000,
|
||||
estimated_usage_usd_micros: Some(1_820_000),
|
||||
@@ -783,6 +833,7 @@ async fn early_draw_rearms_thread_usage_settlement_after_immediate_redraw() {
|
||||
event => panic!("expected initial thread usage request, got {event:?}"),
|
||||
};
|
||||
let usage = ThreadUsage {
|
||||
native_usage_usd_micros: None,
|
||||
thread_id: thread_id.to_string(),
|
||||
estimated_usage_credits_micros: 46_000_000,
|
||||
estimated_usage_usd_micros: Some(1_820_000),
|
||||
@@ -848,6 +899,7 @@ async fn thread_usage_settlement_requires_request_started_after_turn_completion(
|
||||
thread_id,
|
||||
initial_request_id,
|
||||
Ok(ThreadUsageOutcome::Available(ThreadUsage {
|
||||
native_usage_usd_micros: None,
|
||||
thread_id: thread_id.to_string(),
|
||||
estimated_usage_credits_micros: 40_000_000,
|
||||
estimated_usage_usd_micros: Some(1_600_000),
|
||||
@@ -872,6 +924,7 @@ async fn thread_usage_settlement_requires_request_started_after_turn_completion(
|
||||
thread_id,
|
||||
pre_turn_request_id,
|
||||
Ok(ThreadUsageOutcome::Available(ThreadUsage {
|
||||
native_usage_usd_micros: None,
|
||||
thread_id: thread_id.to_string(),
|
||||
estimated_usage_credits_micros: 46_000_000,
|
||||
estimated_usage_usd_micros: Some(1_820_000),
|
||||
@@ -892,6 +945,7 @@ async fn thread_usage_settlement_requires_request_started_after_turn_completion(
|
||||
thread_id,
|
||||
post_turn_request_id,
|
||||
Ok(ThreadUsageOutcome::Available(ThreadUsage {
|
||||
native_usage_usd_micros: None,
|
||||
thread_id: thread_id.to_string(),
|
||||
estimated_usage_credits_micros: 46_000_000,
|
||||
estimated_usage_usd_micros: Some(1_820_000),
|
||||
@@ -910,6 +964,7 @@ async fn thread_usage_settlement_requires_request_started_after_turn_completion(
|
||||
thread_id,
|
||||
settlement_request_id,
|
||||
Ok(ThreadUsageOutcome::Available(ThreadUsage {
|
||||
native_usage_usd_micros: None,
|
||||
thread_id: thread_id.to_string(),
|
||||
estimated_usage_credits_micros: 50_000_000,
|
||||
estimated_usage_usd_micros: Some(2_100_000),
|
||||
@@ -939,6 +994,7 @@ async fn deselecting_billing_surfaces_cancels_polling_and_preserves_status_cache
|
||||
thread_id,
|
||||
initial_request_id,
|
||||
Ok(ThreadUsageOutcome::Available(ThreadUsage {
|
||||
native_usage_usd_micros: None,
|
||||
thread_id: thread_id.to_string(),
|
||||
estimated_usage_credits_micros: 46_000_000,
|
||||
estimated_usage_usd_micros: Some(1_820_000),
|
||||
@@ -981,6 +1037,7 @@ async fn deselecting_billing_surfaces_cancels_polling_and_preserves_status_cache
|
||||
assert_eq!(
|
||||
chat.estimated_thread_usage(),
|
||||
Some(&ThreadUsage {
|
||||
native_usage_usd_micros: None,
|
||||
thread_id: thread_id.to_string(),
|
||||
estimated_usage_credits_micros: 46_000_000,
|
||||
estimated_usage_usd_micros: Some(1_820_000),
|
||||
@@ -991,6 +1048,7 @@ async fn deselecting_billing_surfaces_cancels_polling_and_preserves_status_cache
|
||||
thread_id,
|
||||
settlement_request_id,
|
||||
Ok(ThreadUsageOutcome::Available(ThreadUsage {
|
||||
native_usage_usd_micros: None,
|
||||
thread_id: thread_id.to_string(),
|
||||
estimated_usage_credits_micros: 50_000_000,
|
||||
estimated_usage_usd_micros: Some(2_100_000),
|
||||
|
||||
@@ -140,6 +140,7 @@ struct StatusHistoryCell {
|
||||
token_usage: StatusTokenUsageData,
|
||||
rate_limit_state: Arc<RwLock<StatusRateLimitState>>,
|
||||
thread_usage: StatusThreadUsage,
|
||||
plan_type: Option<PlanType>,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -399,6 +400,7 @@ impl StatusHistoryCell {
|
||||
agents_summary,
|
||||
rate_limit_state,
|
||||
thread_usage,
|
||||
plan_type: _plan_type,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -862,7 +864,9 @@ impl StatusHistoryCell {
|
||||
}
|
||||
|
||||
lines.extend(self.rate_limit_lines(&rate_limit_state, available_width, &formatter));
|
||||
let thread_usage_lines = self.thread_usage.lines(&formatter, value_width);
|
||||
let thread_usage_lines = self
|
||||
.thread_usage
|
||||
.lines(&formatter, value_width, self.plan_type);
|
||||
if !thread_usage_lines.is_empty() {
|
||||
lines.push(Line::from(Vec::<Span<'static>>::new()));
|
||||
lines.extend(thread_usage_lines);
|
||||
|
||||
@@ -33,6 +33,7 @@ pub(crate) use rate_limits::rate_limit_snapshot_display;
|
||||
pub(crate) use rate_limits::rate_limit_snapshot_display_for_limit;
|
||||
pub(crate) use thread_usage::format_credit_micros;
|
||||
pub(crate) use thread_usage::format_estimated_usd_micros;
|
||||
pub(crate) use thread_usage::thread_cost_micros;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
@@ -7,6 +7,7 @@ use crate::wrapping::RtOptions;
|
||||
use crate::wrapping::word_wrap_lines;
|
||||
use codex_app_server_protocol::ThreadUsage;
|
||||
use codex_app_server_protocol::ThreadUsageBreakdownGroup;
|
||||
use codex_protocol::account::PlanType;
|
||||
use ratatui::prelude::Line;
|
||||
use ratatui::style::Stylize;
|
||||
use std::collections::BTreeMap;
|
||||
@@ -32,6 +33,15 @@ const REASONING_LABEL: &str = " Reasoning";
|
||||
const SPEED_LABEL: &str = " Speed";
|
||||
const BILLED_TOKENS_LABEL: &str = " Billed tokens";
|
||||
|
||||
/// ENT26 is billed directly in dollars; missing native usage is not a zero estimate.
|
||||
pub(crate) fn thread_cost_micros(usage: &ThreadUsage, plan_type: Option<PlanType>) -> Option<i64> {
|
||||
if plan_type == Some(PlanType::Ent26) {
|
||||
usage.native_usage_usd_micros
|
||||
} else {
|
||||
usage.estimated_usage_usd_micros
|
||||
}
|
||||
}
|
||||
|
||||
/// Shared state updates a committed `/status` card when its asynchronous estimate arrives.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub(crate) struct StatusThreadUsage {
|
||||
@@ -87,6 +97,7 @@ impl StatusThreadUsage {
|
||||
&self,
|
||||
formatter: &FieldFormatter,
|
||||
value_width: usize,
|
||||
plan_type: Option<PlanType>,
|
||||
) -> Vec<Line<'static>> {
|
||||
#[expect(clippy::expect_used)]
|
||||
let stored_estimate = self
|
||||
@@ -97,6 +108,14 @@ impl StatusThreadUsage {
|
||||
return Vec::new();
|
||||
};
|
||||
|
||||
if plan_type == Some(PlanType::Ent26) {
|
||||
// Credit-weighted breakdowns do not describe native dollar usage.
|
||||
return thread_cost_micros(estimate, plan_type)
|
||||
.and_then(format_estimated_usd_micros)
|
||||
.map(|cost| vec![formatter.line("Thread usage", vec![cost.into()])])
|
||||
.unwrap_or_default();
|
||||
}
|
||||
|
||||
let credits_micros = estimate.estimated_usage_credits_micros;
|
||||
let mut usage = vec![format!("{} credits", format_credit_micros(credits_micros)).into()];
|
||||
if let Some(cost) = estimate
|
||||
|
||||
@@ -10784,6 +10784,13 @@ class ThreadUsage(BaseModel):
|
||||
estimated_usage_credits_micros: Annotated[int, Field(alias="estimatedUsageCreditsMicros")]
|
||||
estimated_usage_usd_micros: Annotated[int | None, Field(alias="estimatedUsageUsdMicros")] = None
|
||||
groups: list[ThreadUsageBreakdownGroup]
|
||||
native_usage_usd_micros: Annotated[
|
||||
int | None,
|
||||
Field(
|
||||
alias="nativeUsageUsdMicros",
|
||||
description="Native dollar usage, independent of credit estimates. None means unknown, not zero.",
|
||||
),
|
||||
] = None
|
||||
thread_id: Annotated[str, Field(alias="threadId")]
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user