mirror of
https://github.com/zyycn/codex-proxy-rs.git
synced 2026-11-06 16:06:08 +00:00
fix(groups): attribute usage costs by serving account membership
Use the account that served each completed request to attribute group costs.
Multi-group client keys no longer charge groups that did not serve the request.
Historical costs follow the account's current group membership.
(cherry picked from commit 6f16c8efd1)
Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
@@ -529,6 +529,8 @@ async fn group_costs(
|
||||
return Ok(BTreeMap::new());
|
||||
}
|
||||
let completed_usage = completed_usage_fact_predicate("mr");
|
||||
// 用量按实际完成请求的账号统计:账号属于多个分组时计入每个所属分组,
|
||||
// 不再按 Client Key 绑定分组快照归属,避免多分组 Key 的费用重复出现在未承接请求的分组上。
|
||||
let statement = format!(
|
||||
"with requested_groups(group_id) as (
|
||||
select unnest($1::text[])
|
||||
@@ -541,8 +543,10 @@ async fn group_costs(
|
||||
coalesce(sum(mr.cost_amount), 0)::text as retained_total_usd
|
||||
from requested_groups
|
||||
cross join runtime_settings settings
|
||||
left join account_group_accounts membership
|
||||
on membership.account_group_id = requested_groups.group_id
|
||||
left join model_requests mr
|
||||
on mr.routing_group_refs @> array[requested_groups.group_id]::text[]
|
||||
on mr.provider_account_ref = membership.provider_account_id
|
||||
and mr.started_at >= now() - make_interval(days => settings.usage_retention_days::int)
|
||||
and {completed_usage}
|
||||
and mr.cost_currency = 'USD'
|
||||
|
||||
@@ -89,14 +89,24 @@ async fn groups_aggregate_cross_provider_members_and_key_bindings_without_multip
|
||||
.await
|
||||
.expect("create scoped client key");
|
||||
}
|
||||
// 多分组 Key 的请求费用只计入实际承接账号所属的分组,Key 绑定的其他分组不重复计费。
|
||||
seed_group_cost_snapshot(
|
||||
&database.pool,
|
||||
"req_historical_empty_group",
|
||||
"req_dual_group_key",
|
||||
"acct_group_openai",
|
||||
EMPTY_GROUP,
|
||||
&[MIXED_GROUP, EMPTY_GROUP],
|
||||
"1.5",
|
||||
)
|
||||
.await;
|
||||
// 归属跟随完成请求的账号,而不是 Client Key 绑定的分组快照。
|
||||
seed_group_cost_snapshot(
|
||||
&database.pool,
|
||||
"req_account_attribution",
|
||||
"acct_group_xai",
|
||||
&[EMPTY_GROUP],
|
||||
"2.25",
|
||||
)
|
||||
.await;
|
||||
|
||||
let page = groups
|
||||
.list_account_groups(AccountGroupListQuery {
|
||||
@@ -138,14 +148,14 @@ async fn groups_aggregate_cross_provider_members_and_key_bindings_without_multip
|
||||
assert_eq!(mixed.account_summary.total, 0);
|
||||
assert_eq!(mixed.capacity.used_slots, None);
|
||||
assert_eq!(mixed.capacity.total_slots, Some(0));
|
||||
assert_eq!(mixed.usage.today_usd.as_str(), "0");
|
||||
assert_eq!(mixed.usage.retained_total_usd.as_str(), "0");
|
||||
assert_eq!(mixed.usage.today_usd.as_str(), "3.75");
|
||||
assert_eq!(mixed.usage.retained_total_usd.as_str(), "3.75");
|
||||
let empty = by_id.get(EMPTY_GROUP).expect("empty group");
|
||||
assert_eq!(empty.member_count, 0);
|
||||
assert!(empty.provider_counts.is_empty());
|
||||
assert_eq!(empty.client_key_count, 1);
|
||||
assert_eq!(empty.usage.today_usd.as_str(), "1.5");
|
||||
assert_eq!(empty.usage.retained_total_usd.as_str(), "1.5");
|
||||
assert_eq!(empty.usage.today_usd.as_str(), "0");
|
||||
assert_eq!(empty.usage.retained_total_usd.as_str(), "0");
|
||||
|
||||
let all_key = keys
|
||||
.reveal_client_key(&client_key_id("key_all_accounts"))
|
||||
@@ -272,6 +282,7 @@ async fn group_costs_should_include_statusless_websocket_but_reject_statusless_h
|
||||
)
|
||||
.await
|
||||
.expect("create statusless cost group");
|
||||
assign_accounts(&database.pool, EMPTY_GROUP, &["acct_group_statusless"]).await;
|
||||
for (request_id, cost_amount) in [
|
||||
("req_group_http_success", "1.5"),
|
||||
("req_group_statusless_websocket", "2"),
|
||||
@@ -281,7 +292,7 @@ async fn group_costs_should_include_statusless_websocket_but_reject_statusless_h
|
||||
&database.pool,
|
||||
request_id,
|
||||
"acct_group_statusless",
|
||||
EMPTY_GROUP,
|
||||
&[EMPTY_GROUP],
|
||||
cost_amount,
|
||||
)
|
||||
.await;
|
||||
@@ -393,9 +404,13 @@ async fn seed_group_cost_snapshot(
|
||||
pool: &sqlx::PgPool,
|
||||
request_id: &str,
|
||||
account_id: &str,
|
||||
historical_group_id: &str,
|
||||
routing_group_ids: &[&str],
|
||||
cost_amount: &str,
|
||||
) {
|
||||
let routing_group_refs: Vec<String> = routing_group_ids
|
||||
.iter()
|
||||
.map(|id| (*id).to_owned())
|
||||
.collect();
|
||||
sqlx::query(
|
||||
"insert into model_requests (
|
||||
id, client_api_key_ref, config_revision, protocol, operation, endpoint,
|
||||
@@ -411,16 +426,16 @@ async fn seed_group_cost_snapshot(
|
||||
'sent', now(), 'succeeded', 200, 200, 10,
|
||||
'provider_reported', $4::numeric, 'USD', now() - interval '1 minute',
|
||||
now() + interval '5 minutes', now(),
|
||||
'groups', array[$3]::text[], jsonb_build_array($3::text)
|
||||
'groups', $3::text[], to_jsonb($3::text[])
|
||||
)",
|
||||
)
|
||||
.bind(request_id)
|
||||
.bind(account_id)
|
||||
.bind(historical_group_id)
|
||||
.bind(routing_group_refs)
|
||||
.bind(cost_amount)
|
||||
.execute(pool)
|
||||
.await
|
||||
.expect("seed historical group cost snapshot");
|
||||
.expect("seed group cost snapshot");
|
||||
}
|
||||
|
||||
async fn current_revision(pool: &sqlx::PgPool) -> u64 {
|
||||
|
||||
Reference in New Issue
Block a user