mirror of
https://github.com/zyycn/codex-proxy-rs.git
synced 2026-11-07 00:05:49 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3920624862 | ||
|
|
d804bca495 | ||
|
|
ca527d5833 |
@@ -628,23 +628,6 @@ pub struct ProviderQuota {
|
||||
pub provider_data: Option<ProviderDocument>,
|
||||
}
|
||||
|
||||
/// Provider 官方用量统计的账号形态。
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum ProviderUsageStatisticsMode {
|
||||
Workspace,
|
||||
Personal,
|
||||
}
|
||||
|
||||
impl ProviderUsageStatisticsMode {
|
||||
#[must_use]
|
||||
pub const fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
Self::Workspace => "workspace",
|
||||
Self::Personal => "personal",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Provider 官方用量统计的目标额度周期。
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct ProviderUsageStatisticsCycle {
|
||||
@@ -737,7 +720,6 @@ pub struct ProviderUsageStatisticsSummary {
|
||||
/// Provider 已解释、计算并排序的官方用量统计。
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct ProviderUsageStatistics {
|
||||
pub mode: ProviderUsageStatisticsMode,
|
||||
pub cycle: ProviderUsageStatisticsCycle,
|
||||
pub summary: ProviderUsageStatisticsSummary,
|
||||
pub models: Vec<ProviderUsageStatisticsModel>,
|
||||
|
||||
@@ -625,7 +625,6 @@ pub struct AccountQuotaData {
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AccountUsageStatisticsData {
|
||||
pub mode: String,
|
||||
pub cycle: AccountUsageStatisticsCycleView,
|
||||
pub summary: AccountUsageStatisticsSummaryView,
|
||||
pub models: Vec<AccountUsageStatisticsModelView>,
|
||||
@@ -704,7 +703,6 @@ pub struct AccountUsageStatisticsDayView {
|
||||
impl From<ProviderUsageStatistics> for AccountUsageStatisticsData {
|
||||
fn from(statistics: ProviderUsageStatistics) -> Self {
|
||||
Self {
|
||||
mode: statistics.mode.as_str().to_owned(),
|
||||
cycle: AccountUsageStatisticsCycleView {
|
||||
offset: statistics.cycle.offset,
|
||||
start_at: statistics.cycle.start_at.to_rfc3339(),
|
||||
|
||||
@@ -78,9 +78,8 @@ mod usage_statistics {
|
||||
observability::{CurrencyCost, DecimalAmount},
|
||||
provider_credentials::{
|
||||
ProviderUsageStatistics, ProviderUsageStatisticsCycle, ProviderUsageStatisticsDay,
|
||||
ProviderUsageStatisticsMode, ProviderUsageStatisticsModel,
|
||||
ProviderUsageStatisticsServiceTier, ProviderUsageStatisticsSummary,
|
||||
ProviderUsageStatisticsTokens,
|
||||
ProviderUsageStatisticsModel, ProviderUsageStatisticsServiceTier,
|
||||
ProviderUsageStatisticsSummary, ProviderUsageStatisticsTokens,
|
||||
},
|
||||
};
|
||||
use gateway_api::admin::accounts::{AccountUsageStatisticsData, AccountUsageStatisticsQuery};
|
||||
@@ -148,7 +147,6 @@ mod usage_statistics {
|
||||
.single()
|
||||
.expect("cycle end");
|
||||
let response = AccountUsageStatisticsData::from(ProviderUsageStatistics {
|
||||
mode: ProviderUsageStatisticsMode::Personal,
|
||||
cycle: ProviderUsageStatisticsCycle {
|
||||
offset: 0,
|
||||
start_at,
|
||||
@@ -193,6 +191,7 @@ mod usage_statistics {
|
||||
});
|
||||
let value = serde_json::to_value(response).expect("serialize usage statistics");
|
||||
|
||||
assert!(value.get("mode").is_none());
|
||||
assert_eq!(value["cycle"]["offset"], 0);
|
||||
assert_eq!(
|
||||
value["summary"]["estimatedCost"],
|
||||
|
||||
@@ -21,10 +21,9 @@ use gateway_admin::model::provider_credentials::{
|
||||
ProviderExportCredentialInput, ProviderModel, ProviderModels, ProviderQuota,
|
||||
ProviderQuotaRequest, ProviderQuotaWindow, ProviderQuotaWindowRole, ProviderResetCredit,
|
||||
ProviderResetCreditResult, ProviderResetCredits, ProviderUsageStatistics,
|
||||
ProviderUsageStatisticsCycle, ProviderUsageStatisticsDay, ProviderUsageStatisticsMode,
|
||||
ProviderUsageStatisticsModel, ProviderUsageStatisticsRequest,
|
||||
ProviderUsageStatisticsServiceTier, ProviderUsageStatisticsSummary,
|
||||
ProviderUsageStatisticsTokens, QuotaLocalUsageAttribution,
|
||||
ProviderUsageStatisticsCycle, ProviderUsageStatisticsDay, ProviderUsageStatisticsModel,
|
||||
ProviderUsageStatisticsRequest, ProviderUsageStatisticsServiceTier,
|
||||
ProviderUsageStatisticsSummary, ProviderUsageStatisticsTokens, QuotaLocalUsageAttribution,
|
||||
};
|
||||
use gateway_admin::model::{MutationActor, MutationContext, Revision};
|
||||
use gateway_admin::ports::provider::{ProviderAdmin, ProviderAdminError, ProviderAdminErrorKind};
|
||||
@@ -53,9 +52,9 @@ use crate::credential::{
|
||||
CodexOAuthPendingClaimOutcome, CodexOAuthPendingStore, CodexOAuthPendingStoreError,
|
||||
CodexPendingAuthorization, CodexQuotaWindow, CodexQuotaWindowKind, CodexQuotaWindowRole,
|
||||
CodexResetCreditsError, CodexUsageStatistics, CodexUsageStatisticsError,
|
||||
CodexUsageStatisticsMode, CodexUsageStatisticsServiceTier, CodexUsageStatisticsTokens,
|
||||
CompleteCodexOAuthAuthorization, CompletedCodexOAuthCredential, ExportManagedCodexCredential,
|
||||
StartCodexOAuthAuthorization, StoredCodexPendingAuthorization,
|
||||
CodexUsageStatisticsServiceTier, CodexUsageStatisticsTokens, CompleteCodexOAuthAuthorization,
|
||||
CompletedCodexOAuthCredential, ExportManagedCodexCredential, StartCodexOAuthAuthorization,
|
||||
StoredCodexPendingAuthorization,
|
||||
};
|
||||
use crate::credential::{
|
||||
CodexCredentialCodec, CodexOAuthSecret, oauth_owner_ref, parse_access_token_expiration,
|
||||
@@ -861,10 +860,6 @@ fn project_usage_statistics(
|
||||
})
|
||||
.collect::<Result<Vec<_>, ProviderAdminError>>()?;
|
||||
Ok(ProviderUsageStatistics {
|
||||
mode: match statistics.mode {
|
||||
CodexUsageStatisticsMode::Workspace => ProviderUsageStatisticsMode::Workspace,
|
||||
CodexUsageStatisticsMode::Personal => ProviderUsageStatisticsMode::Personal,
|
||||
},
|
||||
cycle: ProviderUsageStatisticsCycle {
|
||||
offset: statistics.cycle.offset,
|
||||
start_at: statistics.cycle.start_at,
|
||||
|
||||
@@ -49,8 +49,8 @@ pub use quota::{
|
||||
CodexQuotaFact, CodexQuotaRefreshPolicy, CodexQuotaSyncSummary, CodexQuotaWindow,
|
||||
CodexQuotaWindowKind, CodexQuotaWindowRole, CodexResetCreditsError, CodexUsageStatistics,
|
||||
CodexUsageStatisticsCycle, CodexUsageStatisticsDay, CodexUsageStatisticsError,
|
||||
CodexUsageStatisticsMode, CodexUsageStatisticsModel, CodexUsageStatisticsServiceTier,
|
||||
CodexUsageStatisticsSummary, CodexUsageStatisticsTokens, parse_codex_quota_usage,
|
||||
CodexUsageStatisticsModel, CodexUsageStatisticsServiceTier, CodexUsageStatisticsSummary,
|
||||
CodexUsageStatisticsTokens, parse_codex_quota_usage,
|
||||
};
|
||||
pub use refresh::{
|
||||
CodexCredentialRefreshError, CodexCredentialRefreshOutcome, CodexCredentialRefreshService,
|
||||
|
||||
@@ -15,8 +15,8 @@ pub use snapshot::{
|
||||
};
|
||||
pub use usage_statistics::{
|
||||
CodexUsageStatistics, CodexUsageStatisticsCycle, CodexUsageStatisticsDay,
|
||||
CodexUsageStatisticsMode, CodexUsageStatisticsModel, CodexUsageStatisticsServiceTier,
|
||||
CodexUsageStatisticsSummary, CodexUsageStatisticsTokens,
|
||||
CodexUsageStatisticsModel, CodexUsageStatisticsServiceTier, CodexUsageStatisticsSummary,
|
||||
CodexUsageStatisticsTokens,
|
||||
};
|
||||
|
||||
use std::collections::{BTreeMap, BTreeSet, HashMap};
|
||||
@@ -320,9 +320,8 @@ enum DailyUsageFetchError {
|
||||
}
|
||||
|
||||
struct FetchedDailyUsage {
|
||||
mode: CodexUsageStatisticsMode,
|
||||
model_breakdown: Value,
|
||||
daily_totals: Option<Value>,
|
||||
daily_totals: Value,
|
||||
}
|
||||
|
||||
impl CodexQuotaSchedulingProjection {
|
||||
@@ -684,13 +683,12 @@ impl CodexCredentialQuotaService {
|
||||
|
||||
Ok(usage_statistics::build_usage_statistics(
|
||||
usage_statistics::BuildUsageStatistics {
|
||||
mode: fetched.mode,
|
||||
period,
|
||||
timezone,
|
||||
current_used_percent: window.used_percent(),
|
||||
now,
|
||||
model_breakdown: &fetched.model_breakdown,
|
||||
daily_totals: fetched.daily_totals.as_ref(),
|
||||
daily_totals: &fetched.daily_totals,
|
||||
max_cycle_offset: MAX_CYCLE_OFFSET,
|
||||
},
|
||||
))
|
||||
@@ -1501,32 +1499,6 @@ async fn fetch_daily_usage_once(
|
||||
.authentication
|
||||
.authorization_header(Utc::now())
|
||||
.map_err(|_| DailyUsageAttemptError::InvalidCredential)?;
|
||||
let workspace_request_id = format!("usage_statistics_{}", Uuid::now_v7().simple());
|
||||
let workspace = client
|
||||
.fetch_daily_usage(
|
||||
CodexRequestContext::auxiliary(
|
||||
authorization.expose_secret(),
|
||||
prepared.account.upstream_account_id(),
|
||||
&workspace_request_id,
|
||||
None,
|
||||
),
|
||||
CodexDailyUsageReport::WorkspaceModelTokens,
|
||||
start_date,
|
||||
end_date,
|
||||
)
|
||||
.await;
|
||||
match workspace {
|
||||
Ok(model_breakdown) => {
|
||||
return Ok(FetchedDailyUsage {
|
||||
mode: CodexUsageStatisticsMode::Workspace,
|
||||
model_breakdown,
|
||||
daily_totals: None,
|
||||
});
|
||||
}
|
||||
Err(error) if workspace_is_unavailable_for_account(&error) => {}
|
||||
Err(error) => return Err(DailyUsageAttemptError::Upstream(error)),
|
||||
}
|
||||
|
||||
let model_request_id = format!("usage_statistics_{}", Uuid::now_v7().simple());
|
||||
let totals_request_id = format!("usage_statistics_{}", Uuid::now_v7().simple());
|
||||
let model_breakdown = client.fetch_daily_usage(
|
||||
@@ -1554,21 +1526,11 @@ async fn fetch_daily_usage_once(
|
||||
let (model_breakdown, daily_totals) = tokio::try_join!(model_breakdown, daily_totals)
|
||||
.map_err(DailyUsageAttemptError::Upstream)?;
|
||||
Ok(FetchedDailyUsage {
|
||||
mode: CodexUsageStatisticsMode::Personal,
|
||||
model_breakdown,
|
||||
daily_totals: Some(daily_totals),
|
||||
daily_totals,
|
||||
})
|
||||
}
|
||||
|
||||
fn workspace_is_unavailable_for_account(error: &CodexClientError) -> bool {
|
||||
matches!(
|
||||
error,
|
||||
CodexClientError::Upstream { status, body, .. }
|
||||
if *status == reqwest::StatusCode::BAD_REQUEST
|
||||
&& body.to_ascii_lowercase().contains("no active workspace")
|
||||
)
|
||||
}
|
||||
|
||||
fn map_statistics_quota_fetch_error(error: CodexQuotaFetchError) -> CodexUsageStatisticsError {
|
||||
match error {
|
||||
CodexQuotaFetchError::InvalidCredential => CodexUsageStatisticsError::InvalidCredentialData,
|
||||
|
||||
@@ -11,12 +11,6 @@ use crate::transport::{OpenAiBillingUsage, openai_aggregate_billing_breakdown};
|
||||
|
||||
const EPSILON: f64 = 1e-9;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum CodexUsageStatisticsMode {
|
||||
Workspace,
|
||||
Personal,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub enum CodexUsageStatisticsServiceTier {
|
||||
Standard,
|
||||
@@ -113,7 +107,6 @@ pub struct CodexUsageStatisticsSummary {
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct CodexUsageStatistics {
|
||||
pub mode: CodexUsageStatisticsMode,
|
||||
pub cycle: CodexUsageStatisticsCycle,
|
||||
pub summary: CodexUsageStatisticsSummary,
|
||||
pub models: Vec<CodexUsageStatisticsModel>,
|
||||
@@ -162,19 +155,17 @@ pub fn statistics_period(
|
||||
}
|
||||
|
||||
pub(super) struct BuildUsageStatistics<'a> {
|
||||
pub mode: CodexUsageStatisticsMode,
|
||||
pub period: CodexUsageStatisticsPeriod,
|
||||
pub timezone: FixedOffset,
|
||||
pub current_used_percent: Option<f64>,
|
||||
pub now: DateTime<Utc>,
|
||||
pub model_breakdown: &'a Value,
|
||||
pub daily_totals: Option<&'a Value>,
|
||||
pub daily_totals: &'a Value,
|
||||
pub max_cycle_offset: u8,
|
||||
}
|
||||
|
||||
pub(super) fn build_usage_statistics(input: BuildUsageStatistics<'_>) -> CodexUsageStatistics {
|
||||
let BuildUsageStatistics {
|
||||
mode,
|
||||
period,
|
||||
timezone,
|
||||
current_used_percent,
|
||||
@@ -183,12 +174,7 @@ pub(super) fn build_usage_statistics(input: BuildUsageStatistics<'_>) -> CodexUs
|
||||
daily_totals,
|
||||
max_cycle_offset,
|
||||
} = input;
|
||||
let all_days = match mode {
|
||||
CodexUsageStatisticsMode::Workspace => build_workspace_days(model_breakdown),
|
||||
CodexUsageStatisticsMode::Personal => {
|
||||
build_personal_days(daily_totals.unwrap_or(&Value::Null), model_breakdown)
|
||||
}
|
||||
};
|
||||
let all_days = build_personal_days(daily_totals, model_breakdown);
|
||||
let selected_days = select_cycle_days(
|
||||
&all_days,
|
||||
period.start_at,
|
||||
@@ -227,7 +213,6 @@ pub(super) fn build_usage_statistics(input: BuildUsageStatistics<'_>) -> CodexUs
|
||||
daily.sort_by_key(|day| Reverse(day.date));
|
||||
|
||||
CodexUsageStatistics {
|
||||
mode,
|
||||
cycle: CodexUsageStatisticsCycle {
|
||||
offset: period.cycle_offset,
|
||||
start_at: period.start_at,
|
||||
@@ -300,46 +285,6 @@ impl MoneySum {
|
||||
}
|
||||
}
|
||||
|
||||
fn build_workspace_days(payload: &Value) -> Vec<DailyUsage> {
|
||||
extract_daily_list(payload)
|
||||
.into_iter()
|
||||
.filter_map(|row| {
|
||||
let date = date_value(row.get("date")?)?;
|
||||
let mut models = value_array(row.get("models"))
|
||||
.iter()
|
||||
.filter_map(direct_model_usage)
|
||||
.collect::<Vec<_>>();
|
||||
let model_tokens = sum_tokens(models.iter().map(|model| model.tokens));
|
||||
let fallback_tokens = token_parts(row.get("totals").unwrap_or(row));
|
||||
let tokens = if model_tokens.is_empty() {
|
||||
fallback_tokens
|
||||
} else {
|
||||
model_tokens
|
||||
};
|
||||
let credit_weight = day_credit_weight(row);
|
||||
if tokens.is_empty() && credit_weight <= EPSILON {
|
||||
return None;
|
||||
}
|
||||
let has_missing_token_data = tokens.is_empty() && credit_weight > EPSILON;
|
||||
if models.is_empty() && !tokens.is_empty() {
|
||||
models.push(unknown_model(tokens, false));
|
||||
}
|
||||
if has_missing_token_data {
|
||||
for model in &mut models {
|
||||
model.has_missing_token_data = true;
|
||||
}
|
||||
}
|
||||
Some(finalize_day(
|
||||
date,
|
||||
credit_weight,
|
||||
tokens,
|
||||
models,
|
||||
has_missing_token_data,
|
||||
))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn build_personal_days(totals_payload: &Value, breakdown_payload: &Value) -> Vec<DailyUsage> {
|
||||
let totals_by_date = rows_by_date(totals_payload);
|
||||
let breakdown_by_date = rows_by_date(breakdown_payload);
|
||||
@@ -430,47 +375,6 @@ fn build_personal_days(totals_payload: &Value, breakdown_payload: &Value) -> Vec
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn direct_model_usage(value: &Value) -> Option<DailyModelUsage> {
|
||||
let tokens = token_parts(value);
|
||||
let credits = non_negative_f64(value.get("credits"));
|
||||
if tokens.is_empty() && credits <= EPSILON {
|
||||
return None;
|
||||
}
|
||||
let model = model_name(value);
|
||||
let service_tier = model_service_tier(value.get("speed").or_else(|| value.get("mode")));
|
||||
let estimated_cost = (!tokens.is_empty())
|
||||
.then(|| aggregate_cost(&model, service_tier, tokens))
|
||||
.flatten();
|
||||
Some(DailyModelUsage {
|
||||
model,
|
||||
service_tier,
|
||||
credits,
|
||||
tokens,
|
||||
has_unknown_pricing: estimated_cost.is_none() && !tokens.is_empty(),
|
||||
estimated_cost,
|
||||
has_estimated_allocation: false,
|
||||
has_rate_fallback: false,
|
||||
has_missing_token_data: tokens.is_empty() && credits > EPSILON,
|
||||
})
|
||||
}
|
||||
|
||||
fn unknown_model(
|
||||
tokens: CodexUsageStatisticsTokens,
|
||||
estimated_allocation: bool,
|
||||
) -> DailyModelUsage {
|
||||
DailyModelUsage {
|
||||
model: "unknown".to_owned(),
|
||||
service_tier: CodexUsageStatisticsServiceTier::Standard,
|
||||
credits: 0.0,
|
||||
tokens,
|
||||
estimated_cost: None,
|
||||
has_unknown_pricing: !tokens.is_empty(),
|
||||
has_estimated_allocation: estimated_allocation,
|
||||
has_rate_fallback: estimated_allocation,
|
||||
has_missing_token_data: false,
|
||||
}
|
||||
}
|
||||
|
||||
fn finalize_day(
|
||||
date: NaiveDate,
|
||||
credit_weight: f64,
|
||||
|
||||
@@ -8,9 +8,6 @@ pub const CODEX_IMAGE_EDITS_PATH: &str = "/codex/images/edits";
|
||||
pub const CODEX_USAGE_API_PATH: &str = "/api/codex/usage";
|
||||
/// `/wham/usage`
|
||||
pub const WHAM_USAGE_PATH: &str = "/wham/usage";
|
||||
/// Workspace 账号每日模型 Token 明细。
|
||||
pub const WHAM_DAILY_WORKSPACE_USER_TOKEN_USAGE_PATH: &str =
|
||||
"/wham/usage/daily-workspace-user-token-usage-breakdown";
|
||||
/// 个人账号每日模型额度权重。
|
||||
pub const WHAM_DAILY_TOKEN_USAGE_PATH: &str = "/wham/usage/daily-token-usage-breakdown";
|
||||
/// 个人账号每日 Token 总量。
|
||||
|
||||
@@ -37,8 +37,8 @@ pub use self::{
|
||||
endpoints::{
|
||||
CODEX_IMAGE_EDITS_PATH, CODEX_IMAGE_GENERATIONS_PATH, CODEX_RESPONSES_PATH,
|
||||
CODEX_USAGE_API_PATH, WHAM_DAILY_TOKEN_USAGE_PATH, WHAM_DAILY_WORKSPACE_USAGE_COUNTS_PATH,
|
||||
WHAM_DAILY_WORKSPACE_USER_TOKEN_USAGE_PATH, WHAM_RATE_LIMIT_RESET_CREDITS_CONSUME_PATH,
|
||||
WHAM_RATE_LIMIT_RESET_CREDITS_PATH, WHAM_USAGE_PATH, endpoint_url, usage_endpoint_url,
|
||||
WHAM_RATE_LIMIT_RESET_CREDITS_CONSUME_PATH, WHAM_RATE_LIMIT_RESET_CREDITS_PATH,
|
||||
WHAM_USAGE_PATH, endpoint_url, usage_endpoint_url,
|
||||
},
|
||||
headers::build_codex_base_headers,
|
||||
request::{CodexRequestEncodeError, encode_generate_request},
|
||||
|
||||
@@ -12,6 +12,7 @@ use crate::transport::protocol::responses::CodexResponsesRequest;
|
||||
const PASSTHROUGH_HEADERS_CONTEXT_KEY: &str = "opaque_request_headers";
|
||||
const THREAD_SPAWN_SUBAGENT_KIND: &str = "thread_spawn";
|
||||
const THREAD_SPAWN_CONVERSATION_PREFIX: &str = "thread-spawn:";
|
||||
const UNSUPPORTED_CODEX_RESPONSES_FIELDS: &[&str] = &["max_output_tokens", "temperature"];
|
||||
|
||||
const CROSS_ACCOUNT_IDENTITY_KEYS: &[&str] = &[
|
||||
"authorization",
|
||||
@@ -90,7 +91,7 @@ pub fn encode_generate_request(
|
||||
return Err(CodexRequestEncodeError::InvalidProtocolPayload);
|
||||
}
|
||||
let mut body = payload.body().clone();
|
||||
body.insert("model".to_owned(), Value::String(upstream_model.to_owned()));
|
||||
adapt_codex_responses_body(&mut body, upstream_model);
|
||||
|
||||
let mut encoded = CodexResponsesRequest::from_body(body);
|
||||
encoded.explicit_prompt_cache_key = encoded.prompt_cache_key().is_some();
|
||||
@@ -99,6 +100,13 @@ pub fn encode_generate_request(
|
||||
Ok(encoded)
|
||||
}
|
||||
|
||||
fn adapt_codex_responses_body(body: &mut Map<String, Value>, upstream_model: &str) {
|
||||
body.insert("model".to_owned(), Value::String(upstream_model.to_owned()));
|
||||
for field in UNSUPPORTED_CODEX_RESPONSES_FIELDS {
|
||||
body.remove(*field);
|
||||
}
|
||||
}
|
||||
|
||||
fn extract_request_context(request: &mut CodexResponsesRequest) {
|
||||
let context = ExtractedRequestContext::from_body(request.body());
|
||||
request.turn_state = context.turn_state;
|
||||
|
||||
@@ -13,8 +13,7 @@ use super::{
|
||||
},
|
||||
diagnostics::CodexUpstreamSendPhase,
|
||||
endpoints::{
|
||||
WHAM_DAILY_TOKEN_USAGE_PATH, WHAM_DAILY_WORKSPACE_USAGE_COUNTS_PATH,
|
||||
WHAM_DAILY_WORKSPACE_USER_TOKEN_USAGE_PATH, endpoint_url,
|
||||
WHAM_DAILY_TOKEN_USAGE_PATH, WHAM_DAILY_WORKSPACE_USAGE_COUNTS_PATH, endpoint_url,
|
||||
},
|
||||
response_meta,
|
||||
};
|
||||
@@ -23,7 +22,6 @@ const MAX_CODEX_DAILY_USAGE_BODY_BYTES: usize = 4 * 1024 * 1024;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub(crate) enum CodexDailyUsageReport {
|
||||
WorkspaceModelTokens,
|
||||
PersonalModelCredits,
|
||||
PersonalTokenTotals,
|
||||
}
|
||||
@@ -31,7 +29,6 @@ pub(crate) enum CodexDailyUsageReport {
|
||||
impl CodexDailyUsageReport {
|
||||
const fn path(self) -> &'static str {
|
||||
match self {
|
||||
Self::WorkspaceModelTokens => WHAM_DAILY_WORKSPACE_USER_TOKEN_USAGE_PATH,
|
||||
Self::PersonalModelCredits => WHAM_DAILY_TOKEN_USAGE_PATH,
|
||||
Self::PersonalTokenTotals => WHAM_DAILY_WORKSPACE_USAGE_COUNTS_PATH,
|
||||
}
|
||||
|
||||
@@ -3,9 +3,7 @@ use std::sync::Arc;
|
||||
|
||||
use chrono::{DateTime, Days, NaiveTime, TimeDelta, Utc};
|
||||
use gateway_core::engine::credential::ProviderAccountId;
|
||||
use provider_openai::credential::{
|
||||
CodexUsageStatisticsError, CodexUsageStatisticsMode, CodexUsageStatisticsServiceTier,
|
||||
};
|
||||
use provider_openai::credential::{CodexUsageStatisticsError, CodexUsageStatisticsServiceTier};
|
||||
use serde_json::json;
|
||||
use wiremock::matchers::{method, path};
|
||||
use wiremock::{Mock, MockServer, ResponseTemplate};
|
||||
@@ -13,7 +11,6 @@ use wiremock::{Mock, MockServer, ResponseTemplate};
|
||||
use super::{create_account, quota_service_with_base_url};
|
||||
use crate::support::MemoryAccountStore;
|
||||
|
||||
const WORKSPACE_REPORT_PATH: &str = "/wham/usage/daily-workspace-user-token-usage-breakdown";
|
||||
const PERSONAL_MODEL_REPORT_PATH: &str = "/wham/usage/daily-token-usage-breakdown";
|
||||
const PERSONAL_TOTALS_REPORT_PATH: &str = "/wham/analytics/daily-workspace-usage-counts";
|
||||
|
||||
@@ -78,11 +75,6 @@ async fn personal_report_allocates_tokens_on_the_server_and_preserves_official_t
|
||||
let server = MockServer::start().await;
|
||||
let (start_at, end_at) = cycle_anchor();
|
||||
let report_date = Utc::now().date_naive().to_string();
|
||||
Mock::given(method("GET"))
|
||||
.and(path(WORKSPACE_REPORT_PATH))
|
||||
.respond_with(ResponseTemplate::new(400).set_body_string("no active workspace"))
|
||||
.mount(&server)
|
||||
.await;
|
||||
Mock::given(method("GET"))
|
||||
.and(path(PERSONAL_MODEL_REPORT_PATH))
|
||||
.respond_with(ResponseTemplate::new(200).set_body_json(json!({"data": [{
|
||||
@@ -115,7 +107,6 @@ async fn personal_report_allocates_tokens_on_the_server_and_preserves_official_t
|
||||
.await
|
||||
.expect("personal statistics");
|
||||
|
||||
assert_eq!(statistics.mode, CodexUsageStatisticsMode::Personal);
|
||||
assert_eq!(statistics.cycle.start_at, start_at);
|
||||
assert_eq!(statistics.summary.tokens.uncached_input, 101);
|
||||
assert_eq!(statistics.summary.tokens.cached_input, 203);
|
||||
@@ -152,95 +143,6 @@ async fn personal_report_allocates_tokens_on_the_server_and_preserves_official_t
|
||||
}));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn workspace_report_keeps_direct_rows_marks_boundary_and_never_prices_unknown_models() {
|
||||
let server = MockServer::start().await;
|
||||
let (start_at, end_at) = cycle_anchor();
|
||||
let report_date = start_at.date_naive().to_string();
|
||||
Mock::given(method("GET"))
|
||||
.and(path(WORKSPACE_REPORT_PATH))
|
||||
.respond_with(ResponseTemplate::new(200).set_body_json(json!({"data": [{
|
||||
"date": report_date,
|
||||
"models": [
|
||||
{
|
||||
"model": "gpt-5.6-sol",
|
||||
"speed": "fast",
|
||||
"uncached_text_input_tokens": 1000000,
|
||||
"cached_text_input_tokens": 1000000,
|
||||
"text_output_tokens": 1000000,
|
||||
"text_total_tokens": 3000000
|
||||
},
|
||||
{
|
||||
"model": "gpt-5.6-terra",
|
||||
"speed": "standard",
|
||||
"uncached_text_input_tokens": 1000000,
|
||||
"text_total_tokens": 1000000
|
||||
},
|
||||
{
|
||||
"model": "future-private-model",
|
||||
"speed": "fast",
|
||||
"uncached_text_input_tokens": 10,
|
||||
"text_total_tokens": 10
|
||||
}
|
||||
]
|
||||
}]})))
|
||||
.mount(&server)
|
||||
.await;
|
||||
let (store, service) = service("acct_workspace_statistics", &server, end_at).await;
|
||||
let account = store
|
||||
.account("acct_workspace_statistics")
|
||||
.expect("workspace account");
|
||||
|
||||
let statistics = service
|
||||
.usage_statistics(account.id(), 0, 0)
|
||||
.await
|
||||
.expect("workspace statistics");
|
||||
|
||||
assert_eq!(statistics.mode, CodexUsageStatisticsMode::Workspace);
|
||||
assert!(statistics.daily[0].is_boundary_day);
|
||||
let sol = statistics
|
||||
.models
|
||||
.iter()
|
||||
.find(|model| model.model == "gpt-5.6-sol")
|
||||
.expect("Sol row");
|
||||
assert_eq!(sol.service_tier, CodexUsageStatisticsServiceTier::Fast);
|
||||
assert_eq!(
|
||||
sol.estimated_cost
|
||||
.expect("published Sol price")
|
||||
.amount()
|
||||
.canonical(),
|
||||
"71"
|
||||
);
|
||||
let terra = statistics
|
||||
.models
|
||||
.iter()
|
||||
.find(|model| model.model == "gpt-5.6-terra")
|
||||
.expect("Terra row");
|
||||
assert_eq!(
|
||||
terra.service_tier,
|
||||
CodexUsageStatisticsServiceTier::Standard
|
||||
);
|
||||
assert_eq!(
|
||||
terra
|
||||
.estimated_cost
|
||||
.expect("published Terra price")
|
||||
.amount()
|
||||
.canonical(),
|
||||
"2"
|
||||
);
|
||||
let unknown = statistics
|
||||
.models
|
||||
.iter()
|
||||
.find(|model| model.model == "future-private-model")
|
||||
.expect("unknown row");
|
||||
assert_eq!(unknown.tokens.total, 10);
|
||||
assert!(unknown.estimated_cost.is_none());
|
||||
assert!(unknown.has_unknown_pricing);
|
||||
assert!(statistics.summary.estimated_cost.is_some());
|
||||
assert!(statistics.summary.has_unknown_pricing);
|
||||
assert!(statistics.summary.projected_cost.is_none());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn cycle_offset_fetches_only_the_selected_cycle_with_boundary_padding() {
|
||||
let server = MockServer::start().await;
|
||||
@@ -250,18 +152,26 @@ async fn cycle_offset_fetches_only_the_selected_cycle_with_boundary_padding() {
|
||||
.checked_sub_days(Days::new(14))
|
||||
.expect("selected date");
|
||||
Mock::given(method("GET"))
|
||||
.and(path(WORKSPACE_REPORT_PATH))
|
||||
.and(path(PERSONAL_MODEL_REPORT_PATH))
|
||||
.respond_with(ResponseTemplate::new(200).set_body_json(json!({"data": [{
|
||||
"date": selected_date.to_string(),
|
||||
"models": [{
|
||||
"model": "gpt-5.6-terra",
|
||||
"speed": "standard",
|
||||
"uncached_text_input_tokens": 10,
|
||||
"text_total_tokens": 10
|
||||
"credits": 10
|
||||
}]
|
||||
}]})))
|
||||
.mount(&server)
|
||||
.await;
|
||||
Mock::given(method("GET"))
|
||||
.and(path(PERSONAL_TOTALS_REPORT_PATH))
|
||||
.respond_with(ResponseTemplate::new(200).set_body_json(json!({"data": [{
|
||||
"date": selected_date.to_string(),
|
||||
"uncached_text_input_tokens": 10,
|
||||
"text_total_tokens": 10
|
||||
}]})))
|
||||
.mount(&server)
|
||||
.await;
|
||||
let (store, service) = service("acct_offset_statistics", &server, current_end_at).await;
|
||||
let account = store
|
||||
.account("acct_offset_statistics")
|
||||
@@ -280,8 +190,8 @@ async fn cycle_offset_fetches_only_the_selected_cycle_with_boundary_padding() {
|
||||
let requests = server.received_requests().await.expect("recorded requests");
|
||||
let report = requests
|
||||
.iter()
|
||||
.find(|request| request.url.path() == WORKSPACE_REPORT_PATH)
|
||||
.expect("workspace report request");
|
||||
.find(|request| request.url.path() == PERSONAL_TOTALS_REPORT_PATH)
|
||||
.expect("personal totals report request");
|
||||
let query = report
|
||||
.url
|
||||
.query_pairs()
|
||||
|
||||
@@ -70,7 +70,7 @@ fn encoder_should_never_hash_prompt_content_into_an_accountless_pool_identity()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn encoder_should_only_patch_model_and_preserve_raw_generate_semantics() {
|
||||
fn encoder_should_patch_model_and_preserve_supported_generate_semantics() {
|
||||
let request = request(Map::from_iter([
|
||||
("model".to_owned(), json!("client-model")),
|
||||
("input".to_owned(), json!("secret prompt")),
|
||||
@@ -78,7 +78,6 @@ fn encoder_should_only_patch_model_and_preserve_raw_generate_semantics() {
|
||||
"tools".to_owned(),
|
||||
json!([{"type": "function", "name": "lookup", "strict": true}]),
|
||||
),
|
||||
("max_output_tokens".to_owned(), json!(512)),
|
||||
("store".to_owned(), json!(false)),
|
||||
(
|
||||
"reasoning".to_owned(),
|
||||
@@ -89,7 +88,6 @@ fn encoder_should_only_patch_model_and_preserve_raw_generate_semantics() {
|
||||
let encoded = encode_generate_request(&request, "gpt-test").expect("encode");
|
||||
assert_eq!(encoded.body().get("model"), Some(&json!("gpt-test")));
|
||||
assert!(encoded.body().get("stream").is_none());
|
||||
assert_eq!(encoded.body().get("max_output_tokens"), Some(&json!(512)));
|
||||
assert_eq!(encoded.body().get("store"), Some(&json!(false)));
|
||||
let body = Value::Object(encoded.body().clone());
|
||||
assert_eq!(body.pointer("/tools/0/strict"), Some(&json!(true)));
|
||||
@@ -97,6 +95,28 @@ fn encoder_should_only_patch_model_and_preserve_raw_generate_semantics() {
|
||||
assert!(!encoded.force_http_sse);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn encoder_should_remove_unsupported_fields_from_upstream_body() {
|
||||
let request = request(Map::from_iter([
|
||||
("model".to_owned(), json!("client-model")),
|
||||
("input".to_owned(), json!("hello")),
|
||||
("max_output_tokens".to_owned(), json!(512)),
|
||||
("max_tokens".to_owned(), json!(256)),
|
||||
("temperature".to_owned(), json!(0.2)),
|
||||
]));
|
||||
|
||||
let encoded = encode_generate_request(&request, "gpt-test").expect("encode");
|
||||
|
||||
assert_eq!(
|
||||
Value::Object(encoded.body().clone()),
|
||||
json!({
|
||||
"model": "gpt-test",
|
||||
"input": "hello",
|
||||
"max_tokens": 256,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn encoder_should_preserve_client_store_intent() {
|
||||
let request = request(Map::from_iter([
|
||||
|
||||
@@ -170,8 +170,6 @@ export interface AccountQuotaResponse {
|
||||
account: Account
|
||||
}
|
||||
|
||||
export type AccountUsageStatisticsMode = 'workspace' | 'personal'
|
||||
|
||||
export interface AccountUsageStatisticsTokens {
|
||||
uncachedInput: number
|
||||
cachedInput: number
|
||||
@@ -209,7 +207,6 @@ export interface AccountUsageStatisticsDay {
|
||||
}
|
||||
|
||||
export interface AccountUsageStatisticsResponse {
|
||||
mode: AccountUsageStatisticsMode
|
||||
cycle: {
|
||||
offset: number
|
||||
startAt: string
|
||||
|
||||
@@ -35,7 +35,6 @@ const accountIdentity = computed(() =>
|
||||
|| props.account.id,
|
||||
)
|
||||
const title = computed(() => `用量统计 · ${accountIdentity.value}`)
|
||||
const modeLabel = computed(() => view.value?.mode === 'personal' ? '个人模式' : '工作区模式')
|
||||
const quotaRailStyle = computed(() => ({
|
||||
width: `${view.value?.cycle.usedPercent ?? 0}%`,
|
||||
}))
|
||||
@@ -121,9 +120,6 @@ function formatProjectedTokens(value: number | null) {
|
||||
<span class="rounded-full bg-cp-info-bg px-2 py-0.75 text-cp-xs font-bold text-cp-info-text">
|
||||
{{ cycleLabel }}
|
||||
</span>
|
||||
<span class="rounded-full bg-cp-fill-tertiary px-2 py-0.75 text-cp-xs font-bold text-cp-text-secondary">
|
||||
{{ modeLabel }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
|
||||
@@ -1 +1 @@
|
||||
version: "3.3.2"
|
||||
version: "3.3.3"
|
||||
|
||||
Reference in New Issue
Block a user