mirror of
https://github.com/openai/codex.git
synced 2026-06-02 11:22:01 +00:00
feat(app-server): expose account token usage
This commit is contained in:
@@ -5921,6 +5921,29 @@
|
||||
"title": "Account/rateLimits/readRequest",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
"$ref": "#/definitions/RequestId"
|
||||
},
|
||||
"method": {
|
||||
"enum": [
|
||||
"account/tokenUsage/read"
|
||||
],
|
||||
"title": "Account/tokenUsage/readRequestMethod",
|
||||
"type": "string"
|
||||
},
|
||||
"params": {
|
||||
"type": "null"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"method"
|
||||
],
|
||||
"title": "Account/tokenUsage/readRequest",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
|
||||
@@ -1833,6 +1833,29 @@
|
||||
"title": "Account/rateLimits/readRequest",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
"$ref": "#/definitions/v2/RequestId"
|
||||
},
|
||||
"method": {
|
||||
"enum": [
|
||||
"account/tokenUsage/read"
|
||||
],
|
||||
"title": "Account/tokenUsage/readRequestMethod",
|
||||
"type": "string"
|
||||
},
|
||||
"params": {
|
||||
"type": "null"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"method"
|
||||
],
|
||||
"title": "Account/tokenUsage/readRequest",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
@@ -5731,6 +5754,62 @@
|
||||
"title": "AccountRateLimitsUpdatedNotification",
|
||||
"type": "object"
|
||||
},
|
||||
"AccountTokenUsageDailyBucket": {
|
||||
"properties": {
|
||||
"startDate": {
|
||||
"type": "string"
|
||||
},
|
||||
"tokens": {
|
||||
"format": "int64",
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"startDate",
|
||||
"tokens"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"AccountTokenUsageSummary": {
|
||||
"properties": {
|
||||
"currentStreakDays": {
|
||||
"format": "int64",
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"lifetimeTokens": {
|
||||
"format": "int64",
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"longestRunningTurnSec": {
|
||||
"format": "int64",
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"longestStreakDays": {
|
||||
"format": "int64",
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"peakDailyTokens": {
|
||||
"format": "int64",
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"AccountUpdatedNotification": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
@@ -9474,6 +9553,28 @@
|
||||
"title": "GetAccountResponse",
|
||||
"type": "object"
|
||||
},
|
||||
"GetAccountTokenUsageResponse": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
"dailyUsageBuckets": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/v2/AccountTokenUsageDailyBucket"
|
||||
},
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"summary": {
|
||||
"$ref": "#/definitions/v2/AccountTokenUsageSummary"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"summary"
|
||||
],
|
||||
"title": "GetAccountTokenUsageResponse",
|
||||
"type": "object"
|
||||
},
|
||||
"GitInfo": {
|
||||
"properties": {
|
||||
"branch": {
|
||||
|
||||
@@ -103,6 +103,62 @@
|
||||
"title": "AccountRateLimitsUpdatedNotification",
|
||||
"type": "object"
|
||||
},
|
||||
"AccountTokenUsageDailyBucket": {
|
||||
"properties": {
|
||||
"startDate": {
|
||||
"type": "string"
|
||||
},
|
||||
"tokens": {
|
||||
"format": "int64",
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"startDate",
|
||||
"tokens"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"AccountTokenUsageSummary": {
|
||||
"properties": {
|
||||
"currentStreakDays": {
|
||||
"format": "int64",
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"lifetimeTokens": {
|
||||
"format": "int64",
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"longestRunningTurnSec": {
|
||||
"format": "int64",
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"longestStreakDays": {
|
||||
"format": "int64",
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"peakDailyTokens": {
|
||||
"format": "int64",
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"AccountUpdatedNotification": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
@@ -2581,6 +2637,29 @@
|
||||
"title": "Account/rateLimits/readRequest",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
"$ref": "#/definitions/RequestId"
|
||||
},
|
||||
"method": {
|
||||
"enum": [
|
||||
"account/tokenUsage/read"
|
||||
],
|
||||
"title": "Account/tokenUsage/readRequestMethod",
|
||||
"type": "string"
|
||||
},
|
||||
"params": {
|
||||
"type": "null"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"method"
|
||||
],
|
||||
"title": "Account/tokenUsage/readRequest",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
@@ -5954,6 +6033,28 @@
|
||||
"title": "GetAccountResponse",
|
||||
"type": "object"
|
||||
},
|
||||
"GetAccountTokenUsageResponse": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
"dailyUsageBuckets": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/AccountTokenUsageDailyBucket"
|
||||
},
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"summary": {
|
||||
"$ref": "#/definitions/AccountTokenUsageSummary"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"summary"
|
||||
],
|
||||
"title": "GetAccountTokenUsageResponse",
|
||||
"type": "object"
|
||||
},
|
||||
"GitInfo": {
|
||||
"properties": {
|
||||
"branch": {
|
||||
|
||||
80
codex-rs/app-server-protocol/schema/json/v2/GetAccountTokenUsageResponse.json
generated
Normal file
80
codex-rs/app-server-protocol/schema/json/v2/GetAccountTokenUsageResponse.json
generated
Normal file
@@ -0,0 +1,80 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"definitions": {
|
||||
"AccountTokenUsageDailyBucket": {
|
||||
"properties": {
|
||||
"startDate": {
|
||||
"type": "string"
|
||||
},
|
||||
"tokens": {
|
||||
"format": "int64",
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"startDate",
|
||||
"tokens"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"AccountTokenUsageSummary": {
|
||||
"properties": {
|
||||
"currentStreakDays": {
|
||||
"format": "int64",
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"lifetimeTokens": {
|
||||
"format": "int64",
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"longestRunningTurnSec": {
|
||||
"format": "int64",
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"longestStreakDays": {
|
||||
"format": "int64",
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"peakDailyTokens": {
|
||||
"format": "int64",
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"properties": {
|
||||
"dailyUsageBuckets": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/AccountTokenUsageDailyBucket"
|
||||
},
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"summary": {
|
||||
"$ref": "#/definitions/AccountTokenUsageSummary"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"summary"
|
||||
],
|
||||
"title": "GetAccountTokenUsageResponse",
|
||||
"type": "object"
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
5
codex-rs/app-server-protocol/schema/typescript/v2/AccountTokenUsageDailyBucket.ts
generated
Normal file
5
codex-rs/app-server-protocol/schema/typescript/v2/AccountTokenUsageDailyBucket.ts
generated
Normal file
@@ -0,0 +1,5 @@
|
||||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type AccountTokenUsageDailyBucket = { startDate: string, tokens: bigint, };
|
||||
5
codex-rs/app-server-protocol/schema/typescript/v2/AccountTokenUsageSummary.ts
generated
Normal file
5
codex-rs/app-server-protocol/schema/typescript/v2/AccountTokenUsageSummary.ts
generated
Normal file
@@ -0,0 +1,5 @@
|
||||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type AccountTokenUsageSummary = { lifetimeTokens: bigint | null, peakDailyTokens: bigint | null, longestRunningTurnSec: bigint | null, currentStreakDays: bigint | null, longestStreakDays: bigint | null, };
|
||||
7
codex-rs/app-server-protocol/schema/typescript/v2/GetAccountTokenUsageResponse.ts
generated
Normal file
7
codex-rs/app-server-protocol/schema/typescript/v2/GetAccountTokenUsageResponse.ts
generated
Normal file
@@ -0,0 +1,7 @@
|
||||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { AccountTokenUsageDailyBucket } from "./AccountTokenUsageDailyBucket";
|
||||
import type { AccountTokenUsageSummary } from "./AccountTokenUsageSummary";
|
||||
|
||||
export type GetAccountTokenUsageResponse = { summary: AccountTokenUsageSummary, dailyUsageBuckets: Array<AccountTokenUsageDailyBucket> | null, };
|
||||
@@ -3,6 +3,8 @@
|
||||
export type { Account } from "./Account";
|
||||
export type { AccountLoginCompletedNotification } from "./AccountLoginCompletedNotification";
|
||||
export type { AccountRateLimitsUpdatedNotification } from "./AccountRateLimitsUpdatedNotification";
|
||||
export type { AccountTokenUsageDailyBucket } from "./AccountTokenUsageDailyBucket";
|
||||
export type { AccountTokenUsageSummary } from "./AccountTokenUsageSummary";
|
||||
export type { AccountUpdatedNotification } from "./AccountUpdatedNotification";
|
||||
export type { ActivePermissionProfile } from "./ActivePermissionProfile";
|
||||
export type { AddCreditsNudgeCreditType } from "./AddCreditsNudgeCreditType";
|
||||
@@ -139,6 +141,7 @@ export type { FsWriteFileResponse } from "./FsWriteFileResponse";
|
||||
export type { GetAccountParams } from "./GetAccountParams";
|
||||
export type { GetAccountRateLimitsResponse } from "./GetAccountRateLimitsResponse";
|
||||
export type { GetAccountResponse } from "./GetAccountResponse";
|
||||
export type { GetAccountTokenUsageResponse } from "./GetAccountTokenUsageResponse";
|
||||
export type { GitInfo } from "./GitInfo";
|
||||
export type { GrantedPermissionProfile } from "./GrantedPermissionProfile";
|
||||
export type { GuardianApprovalReview } from "./GuardianApprovalReview";
|
||||
|
||||
@@ -931,6 +931,12 @@ client_request_definitions! {
|
||||
response: v2::GetAccountRateLimitsResponse,
|
||||
},
|
||||
|
||||
GetAccountTokenUsage => "account/tokenUsage/read" {
|
||||
params: #[ts(type = "undefined")] #[serde(skip_serializing_if = "Option::is_none")] Option<()>,
|
||||
serialization: None,
|
||||
response: v2::GetAccountTokenUsageResponse,
|
||||
},
|
||||
|
||||
SendAddCreditsNudgeEmail => "account/sendAddCreditsNudgeEmail" {
|
||||
params: v2::SendAddCreditsNudgeEmailParams,
|
||||
serialization: global("account-auth"),
|
||||
@@ -2318,6 +2324,24 @@ mod tests {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serialize_get_account_token_usage() -> Result<()> {
|
||||
let request = ClientRequest::GetAccountTokenUsage {
|
||||
request_id: RequestId::Integer(1),
|
||||
params: None,
|
||||
};
|
||||
assert_eq!(request.id(), &RequestId::Integer(1));
|
||||
assert_eq!(request.method(), "account/tokenUsage/read");
|
||||
assert_eq!(
|
||||
json!({
|
||||
"method": "account/tokenUsage/read",
|
||||
"id": 1,
|
||||
}),
|
||||
serde_json::to_value(&request)?,
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serialize_client_response() -> Result<()> {
|
||||
let cwd = absolute_path("/tmp");
|
||||
|
||||
@@ -185,6 +185,33 @@ pub struct GetAccountRateLimitsResponse {
|
||||
pub rate_limits_by_limit_id: Option<HashMap<String, RateLimitSnapshot>>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct GetAccountTokenUsageResponse {
|
||||
pub summary: AccountTokenUsageSummary,
|
||||
pub daily_usage_buckets: Option<Vec<AccountTokenUsageDailyBucket>>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct AccountTokenUsageSummary {
|
||||
pub lifetime_tokens: Option<i64>,
|
||||
pub peak_daily_tokens: Option<i64>,
|
||||
pub longest_running_turn_sec: Option<i64>,
|
||||
pub current_streak_days: Option<i64>,
|
||||
pub longest_streak_days: Option<i64>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct AccountTokenUsageDailyBucket {
|
||||
pub start_date: String,
|
||||
pub tokens: i64,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
|
||||
@@ -1760,6 +1760,7 @@ Codex supports these authentication modes. The current mode is surfaced in `acco
|
||||
- `account/logout` — sign out; triggers `account/updated`.
|
||||
- `account/updated` (notify) — emitted whenever auth mode changes (`authMode`: `apikey`, `chatgpt`, or `null`) and includes the current ChatGPT `planType` when available.
|
||||
- `account/rateLimits/read` — fetch ChatGPT rate limits; updates arrive via `account/rateLimits/updated` (notify).
|
||||
- `account/tokenUsage/read` — fetch ChatGPT account token-activity summary and daily buckets.
|
||||
- `account/rateLimits/updated` (notify) — emitted whenever a user's ChatGPT rate limits change.
|
||||
- `account/sendAddCreditsNudgeEmail` — ask ChatGPT to email the workspace owner about depleted credits or a reached usage limit.
|
||||
- `mcpServer/oauthLogin/completed` (notify) — emitted after a `mcpServer/oauth/login` flow finishes for a server; payload includes `{ name, success, error? }`.
|
||||
|
||||
@@ -1280,6 +1280,9 @@ impl MessageProcessor {
|
||||
ClientRequest::GetAccountRateLimits { .. } => {
|
||||
self.account_processor.get_account_rate_limits().await
|
||||
}
|
||||
ClientRequest::GetAccountTokenUsage { .. } => {
|
||||
self.account_processor.get_account_token_usage().await
|
||||
}
|
||||
ClientRequest::SendAddCreditsNudgeEmail { params, .. } => {
|
||||
self.account_processor
|
||||
.send_add_credits_nudge_email(params)
|
||||
|
||||
@@ -22,6 +22,8 @@ use codex_analytics::InputError;
|
||||
use codex_analytics::TurnSteerRequestError;
|
||||
use codex_app_server_protocol::Account;
|
||||
use codex_app_server_protocol::AccountLoginCompletedNotification;
|
||||
use codex_app_server_protocol::AccountTokenUsageDailyBucket;
|
||||
use codex_app_server_protocol::AccountTokenUsageSummary;
|
||||
use codex_app_server_protocol::AccountUpdatedNotification;
|
||||
use codex_app_server_protocol::AddCreditsNudgeCreditType;
|
||||
use codex_app_server_protocol::AddCreditsNudgeEmailStatus;
|
||||
@@ -63,6 +65,7 @@ use codex_app_server_protocol::FeedbackUploadResponse;
|
||||
use codex_app_server_protocol::GetAccountParams;
|
||||
use codex_app_server_protocol::GetAccountRateLimitsResponse;
|
||||
use codex_app_server_protocol::GetAccountResponse;
|
||||
use codex_app_server_protocol::GetAccountTokenUsageResponse;
|
||||
use codex_app_server_protocol::GetAuthStatusParams;
|
||||
use codex_app_server_protocol::GetAuthStatusResponse;
|
||||
use codex_app_server_protocol::GetConversationSummaryParams;
|
||||
@@ -265,6 +268,7 @@ use codex_app_server_protocol::WindowsSandboxSetupStartResponse;
|
||||
use codex_arg0::Arg0DispatchPaths;
|
||||
use codex_backend_client::AddCreditsNudgeCreditType as BackendAddCreditsNudgeCreditType;
|
||||
use codex_backend_client::Client as BackendClient;
|
||||
use codex_backend_client::TokenUsageProfile;
|
||||
use codex_chatgpt::connectors;
|
||||
use codex_chatgpt::workspace_settings;
|
||||
use codex_config::CloudRequirementsLoadError;
|
||||
|
||||
@@ -2,6 +2,7 @@ use super::*;
|
||||
|
||||
// Duration before a browser ChatGPT login attempt is abandoned.
|
||||
const LOGIN_CHATGPT_TIMEOUT: Duration = Duration::from_secs(10 * 60);
|
||||
const ACCOUNT_TOKEN_USAGE_FETCH_TIMEOUT: Duration = Duration::from_secs(/*secs*/ 10);
|
||||
// The override is intentionally available only in debug builds, matching the login path below.
|
||||
#[cfg(debug_assertions)]
|
||||
const LOGIN_ISSUER_OVERRIDE_ENV_VAR: &str = "CODEX_APP_SERVER_LOGIN_ISSUER";
|
||||
@@ -131,6 +132,14 @@ impl AccountRequestProcessor {
|
||||
.map(|response| Some(response.into()))
|
||||
}
|
||||
|
||||
pub(crate) async fn get_account_token_usage(
|
||||
&self,
|
||||
) -> Result<Option<ClientResponsePayload>, JSONRPCErrorError> {
|
||||
self.get_account_token_usage_response()
|
||||
.await
|
||||
.map(|response| Some(response.into()))
|
||||
}
|
||||
|
||||
pub(crate) async fn send_add_credits_nudge_email(
|
||||
&self,
|
||||
params: SendAddCreditsNudgeEmailParams,
|
||||
@@ -848,6 +857,55 @@ impl AccountRequestProcessor {
|
||||
)
|
||||
}
|
||||
|
||||
async fn get_account_token_usage_response(
|
||||
&self,
|
||||
) -> Result<GetAccountTokenUsageResponse, JSONRPCErrorError> {
|
||||
let Some(auth) = self.auth_manager.auth().await else {
|
||||
return Err(invalid_request(
|
||||
"codex account authentication required to read token usage",
|
||||
));
|
||||
};
|
||||
|
||||
if !auth.uses_codex_backend() {
|
||||
return Err(invalid_request(
|
||||
"chatgpt authentication required to read token usage",
|
||||
));
|
||||
}
|
||||
|
||||
let client = BackendClient::from_auth(self.config.chatgpt_base_url.clone(), &auth)
|
||||
.map_err(|err| internal_error(format!("failed to construct backend client: {err}")))?;
|
||||
let profile = tokio::time::timeout(
|
||||
ACCOUNT_TOKEN_USAGE_FETCH_TIMEOUT,
|
||||
client.get_token_usage_profile(),
|
||||
)
|
||||
.await
|
||||
.map_err(|_| internal_error("token usage profile fetch timed out"))?
|
||||
.map_err(|err| internal_error(format!("failed to fetch token usage profile: {err}")))?;
|
||||
Ok(Self::account_token_usage_response(profile))
|
||||
}
|
||||
|
||||
fn account_token_usage_response(profile: TokenUsageProfile) -> GetAccountTokenUsageResponse {
|
||||
let stats = profile.stats;
|
||||
GetAccountTokenUsageResponse {
|
||||
summary: AccountTokenUsageSummary {
|
||||
lifetime_tokens: stats.lifetime_tokens,
|
||||
peak_daily_tokens: stats.peak_daily_tokens,
|
||||
longest_running_turn_sec: stats.longest_running_turn_sec,
|
||||
current_streak_days: stats.current_streak_days,
|
||||
longest_streak_days: stats.longest_streak_days,
|
||||
},
|
||||
daily_usage_buckets: stats.daily_usage_buckets.map(|buckets| {
|
||||
buckets
|
||||
.into_iter()
|
||||
.map(|bucket| AccountTokenUsageDailyBucket {
|
||||
start_date: bucket.start_date,
|
||||
tokens: bucket.tokens,
|
||||
})
|
||||
.collect()
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
async fn send_add_credits_nudge_email_response(
|
||||
&self,
|
||||
params: SendAddCreditsNudgeEmailParams,
|
||||
@@ -952,3 +1010,45 @@ impl AccountRequestProcessor {
|
||||
Ok((primary, rate_limits_by_limit_id))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use codex_backend_client::TokenUsageProfileDailyBucket;
|
||||
use codex_backend_client::TokenUsageProfileStats;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn account_token_usage_response_maps_profile_stats_and_daily_buckets() {
|
||||
let response = AccountRequestProcessor::account_token_usage_response(TokenUsageProfile {
|
||||
stats: TokenUsageProfileStats {
|
||||
lifetime_tokens: Some(123),
|
||||
peak_daily_tokens: Some(45),
|
||||
longest_running_turn_sec: Some(67),
|
||||
current_streak_days: Some(8),
|
||||
longest_streak_days: Some(9),
|
||||
daily_usage_buckets: Some(vec![TokenUsageProfileDailyBucket {
|
||||
start_date: "2026-05-29".to_string(),
|
||||
tokens: 10,
|
||||
}]),
|
||||
},
|
||||
});
|
||||
|
||||
assert_eq!(
|
||||
response,
|
||||
GetAccountTokenUsageResponse {
|
||||
summary: AccountTokenUsageSummary {
|
||||
lifetime_tokens: Some(123),
|
||||
peak_daily_tokens: Some(45),
|
||||
longest_running_turn_sec: Some(67),
|
||||
current_streak_days: Some(8),
|
||||
longest_streak_days: Some(9),
|
||||
},
|
||||
daily_usage_buckets: Some(vec![AccountTokenUsageDailyBucket {
|
||||
start_date: "2026-05-29".to_string(),
|
||||
tokens: 10,
|
||||
}]),
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ use crate::types::ConfigFileResponse;
|
||||
use crate::types::PaginatedListTaskListItem;
|
||||
use crate::types::RateLimitReachedKind as BackendRateLimitReachedKind;
|
||||
use crate::types::RateLimitStatusPayload;
|
||||
use crate::types::TokenUsageProfile;
|
||||
use crate::types::TurnAttemptsSiblingTurnsResponse;
|
||||
use anyhow::Result;
|
||||
use codex_api::SharedAuthProvider;
|
||||
@@ -301,6 +302,20 @@ impl Client {
|
||||
Ok(Self::rate_limit_snapshots_from_payload(payload))
|
||||
}
|
||||
|
||||
pub async fn get_token_usage_profile(&self) -> Result<TokenUsageProfile> {
|
||||
let url = self.token_usage_profile_url();
|
||||
let req = self.http.get(&url).headers(self.headers());
|
||||
let (body, ct) = self.exec_request(req, "GET", &url).await?;
|
||||
self.decode_json(&url, &ct, &body)
|
||||
}
|
||||
|
||||
fn token_usage_profile_url(&self) -> String {
|
||||
match self.path_style {
|
||||
PathStyle::CodexApi => format!("{}/api/codex/profiles/me", self.base_url),
|
||||
PathStyle::ChatGptApi => format!("{}/wham/profiles/me", self.base_url),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn send_add_credits_nudge_email(
|
||||
&self,
|
||||
credit_type: AddCreditsNudgeCreditType,
|
||||
@@ -862,4 +877,35 @@ mod tests {
|
||||
serde_json::json!({ "credit_type": "usage_limit" })
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn token_usage_profile_uses_expected_paths() {
|
||||
let codex_client = Client {
|
||||
base_url: "https://example.test".to_string(),
|
||||
http: reqwest::Client::new(),
|
||||
auth_provider: codex_model_provider::unauthenticated_auth_provider(),
|
||||
user_agent: None,
|
||||
chatgpt_account_id: None,
|
||||
chatgpt_account_is_fedramp: false,
|
||||
path_style: PathStyle::CodexApi,
|
||||
};
|
||||
assert_eq!(
|
||||
codex_client.token_usage_profile_url(),
|
||||
"https://example.test/api/codex/profiles/me"
|
||||
);
|
||||
|
||||
let chatgpt_client = Client {
|
||||
base_url: "https://chatgpt.com/backend-api".to_string(),
|
||||
http: reqwest::Client::new(),
|
||||
auth_provider: codex_model_provider::unauthenticated_auth_provider(),
|
||||
user_agent: None,
|
||||
chatgpt_account_id: None,
|
||||
chatgpt_account_is_fedramp: false,
|
||||
path_style: PathStyle::ChatGptApi,
|
||||
};
|
||||
assert_eq!(
|
||||
chatgpt_client.token_usage_profile_url(),
|
||||
"https://chatgpt.com/backend-api/wham/profiles/me"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,4 +9,7 @@ pub use types::CodeTaskDetailsResponseExt;
|
||||
pub use types::ConfigFileResponse;
|
||||
pub use types::PaginatedListTaskListItem;
|
||||
pub use types::TaskListItem;
|
||||
pub use types::TokenUsageProfile;
|
||||
pub use types::TokenUsageProfileDailyBucket;
|
||||
pub use types::TokenUsageProfileStats;
|
||||
pub use types::TurnAttemptsSiblingTurnsResponse;
|
||||
|
||||
@@ -318,6 +318,27 @@ pub struct TurnAttemptsSiblingTurnsResponse {
|
||||
pub sibling_turns: Vec<HashMap<String, Value>>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, PartialEq, Eq)]
|
||||
pub struct TokenUsageProfile {
|
||||
pub stats: TokenUsageProfileStats,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, PartialEq, Eq)]
|
||||
pub struct TokenUsageProfileStats {
|
||||
pub lifetime_tokens: Option<i64>,
|
||||
pub peak_daily_tokens: Option<i64>,
|
||||
pub longest_running_turn_sec: Option<i64>,
|
||||
pub current_streak_days: Option<i64>,
|
||||
pub longest_streak_days: Option<i64>,
|
||||
pub daily_usage_buckets: Option<Vec<TokenUsageProfileDailyBucket>>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, PartialEq, Eq)]
|
||||
pub struct TokenUsageProfileDailyBucket {
|
||||
pub start_date: String,
|
||||
pub tokens: i64,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
Reference in New Issue
Block a user