mirror of
https://github.com/fawney19/Aether.git
synced 2026-10-09 10:57:03 +08:00
feat: add selectable routing groups and composite billing
Support per-model provider enablement and compact model editing. Capture request-time billing factors, charge customer costs separately, and preserve historical statistics without backfills.
This commit is contained in:
+154
@@ -0,0 +1,154 @@
|
||||
-- Customer charges use the immutable request-time factor snapshot. Provider
|
||||
-- procurement cost remains in actual_total_cost_usd for legacy reporting.
|
||||
CREATE OR REPLACE FUNCTION public.usage_customer_billable_amount(
|
||||
metadata jsonb, base_cost numeric, legacy_cost numeric
|
||||
) RETURNS numeric LANGUAGE plpgsql IMMUTABLE PARALLEL SAFE AS $$
|
||||
DECLARE factor jsonb; multiplier numeric; amount numeric;
|
||||
factor_name text; factor_value jsonb; factor_number double precision;
|
||||
expected_multiplier double precision := 1.0; factor_count integer := 0;
|
||||
has_zero boolean := false;
|
||||
BEGIN
|
||||
IF metadata ? 'billing_multiplier_snapshot' THEN
|
||||
IF jsonb_typeof(metadata->'billing_multiplier_snapshot') <> 'object'
|
||||
OR metadata #> '{billing_multiplier_snapshot,version}' IS DISTINCT FROM '1'::jsonb
|
||||
OR jsonb_typeof(metadata #> '{billing_multiplier_snapshot,factors}') IS DISTINCT FROM 'object'
|
||||
THEN RETURN NULL; END IF;
|
||||
factor := metadata #> '{billing_multiplier_snapshot,multiplier}';
|
||||
FOR factor_name, factor_value IN
|
||||
SELECT key, value FROM jsonb_each(metadata #> '{billing_multiplier_snapshot,factors}') ORDER BY key COLLATE "C"
|
||||
LOOP
|
||||
factor_count := factor_count + 1;
|
||||
IF factor_count > 16 OR factor_name = '' OR length(factor_name) > 64
|
||||
OR factor_name !~ '^[A-Za-z0-9_]+$'
|
||||
OR jsonb_typeof(factor_value) IS DISTINCT FROM 'number'
|
||||
THEN RETURN NULL; END IF;
|
||||
factor_number := factor_value::text::double precision;
|
||||
IF factor_number < 0 OR factor_number > 1.7976931348623157e308::double precision
|
||||
THEN RETURN NULL; END IF;
|
||||
has_zero := has_zero OR factor_number = 0;
|
||||
END LOOP;
|
||||
-- Rust short-circuits zero before multiplying any of the other factors.
|
||||
IF has_zero THEN expected_multiplier := 0;
|
||||
ELSE
|
||||
FOR factor_name, factor_value IN
|
||||
SELECT key, value FROM jsonb_each(metadata #> '{billing_multiplier_snapshot,factors}') ORDER BY key COLLATE "C"
|
||||
LOOP
|
||||
factor_number := factor_value::text::double precision;
|
||||
BEGIN
|
||||
expected_multiplier := expected_multiplier * factor_number;
|
||||
EXCEPTION WHEN numeric_value_out_of_range THEN
|
||||
-- PostgreSQL raises on float underflow; Rust rounds that product to 0.
|
||||
IF expected_multiplier::numeric * factor_number::numeric > 1.7976931348623157e308::numeric
|
||||
THEN RETURN NULL; END IF;
|
||||
expected_multiplier := 0;
|
||||
END;
|
||||
END LOOP;
|
||||
END IF;
|
||||
ELSIF metadata ? 'routing_group_billing_multiplier' THEN
|
||||
factor := metadata->'routing_group_billing_multiplier';
|
||||
expected_multiplier := NULL;
|
||||
ELSE
|
||||
RETURN CASE WHEN legacy_cost NOT IN ('NaN'::numeric,'Infinity'::numeric,'-Infinity'::numeric)
|
||||
THEN round(legacy_cost,8) END;
|
||||
END IF;
|
||||
IF jsonb_typeof(factor) IS DISTINCT FROM 'number' THEN RETURN NULL; END IF;
|
||||
multiplier := factor::text::numeric;
|
||||
factor_number := factor::text::double precision;
|
||||
IF factor_number < 0
|
||||
OR factor_number > 1.7976931348623157e308::double precision
|
||||
OR (expected_multiplier IS NOT NULL AND factor_number <> expected_multiplier)
|
||||
OR multiplier < 0 OR multiplier > 1.7976931348623157e308::numeric
|
||||
OR base_cost IS NULL OR base_cost < 0
|
||||
OR base_cost IN ('NaN'::numeric,'Infinity'::numeric,'-Infinity'::numeric)
|
||||
THEN RETURN NULL; END IF;
|
||||
amount := base_cost * multiplier;
|
||||
IF amount > 1.7976931348623157e308::numeric THEN RETURN NULL; END IF;
|
||||
RETURN round(amount,8);
|
||||
EXCEPTION WHEN numeric_value_out_of_range OR invalid_text_representation THEN
|
||||
-- Corrupt captured pricing must not abort an entire analytics query.
|
||||
RETURN NULL;
|
||||
END $$;
|
||||
|
||||
CREATE OR REPLACE VIEW public.usage_analytics_facts_v1 AS
|
||||
SELECT u.request_id, COALESCE(u.id, u.request_id) AS id, u.created_at,
|
||||
CASE WHEN identity.owner_id IS NOT NULL AND identity.is_standalone=false THEN identity.owner_id END AS actor_user_id,
|
||||
identity.owner_id AS credential_owner_id,
|
||||
CASE WHEN identity.owner_id IS NULL THEN 'unknown' WHEN identity.is_standalone THEN 'standalone'
|
||||
WHEN NOT identity.is_standalone THEN 'employee' ELSE 'unknown' END AS attribution_kind,
|
||||
CASE WHEN identity.owner_id IS NULL THEN 'unknown' WHEN identity.is_standalone THEN 'standalone_key'
|
||||
WHEN NOT identity.is_standalone THEN 'user_account' ELSE 'unknown' END AS attribution_source,
|
||||
COALESCE(a.record_kind, 'request') AS record_kind, a.parent_request_id,
|
||||
u.api_key_id, u.model, u.target_model, u.provider_id, u.provider_name,
|
||||
u.api_format, u.endpoint_kind, u.request_type, u.is_stream, u.has_format_conversion,
|
||||
u.status, u.status_code, u.error_category, u.failure_origin, u.failure_stage, u.failure_reason,
|
||||
u.failure_schema_version, u.response_time_ms, u.first_byte_time_ms,
|
||||
COALESCE(s.billing_status, u.billing_status) AS settlement_status,
|
||||
COALESCE(metadata.value->'usage_available', 'true'::jsonb) <> 'false'::jsonb AS usage_available,
|
||||
COALESCE(metadata.value->'usage_pricing_available', 'true'::jsonb) <> 'false'::jsonb
|
||||
AND (s.billing_total_cost_usd IS NOT NULL OR COALESCE(s.billing_status, u.billing_status) = 'settled') AS pricing_available,
|
||||
CASE WHEN COALESCE(metadata.value->'usage_available', 'true'::jsonb) <> 'false'::jsonb
|
||||
THEN b.input_tokens END AS input_tokens,
|
||||
CASE WHEN COALESCE(metadata.value->'usage_available', 'true'::jsonb) <> 'false'::jsonb
|
||||
THEN b.output_tokens END AS output_tokens,
|
||||
CASE WHEN COALESCE(metadata.value->'usage_available', 'true'::jsonb) <> 'false'::jsonb
|
||||
THEN b.total_tokens END AS total_tokens,
|
||||
CASE WHEN COALESCE(metadata.value->'usage_available', 'true'::jsonb) <> 'false'::jsonb
|
||||
THEN b.cache_read_input_tokens END AS cache_read_input_tokens,
|
||||
CASE WHEN COALESCE(metadata.value->'usage_available', 'true'::jsonb) <> 'false'::jsonb
|
||||
THEN b.cache_creation_input_tokens END AS cache_creation_input_tokens,
|
||||
CASE WHEN COALESCE(metadata.value->'usage_pricing_available', 'true'::jsonb) <> 'false'::jsonb
|
||||
AND (s.billing_total_cost_usd IS NOT NULL OR COALESCE(s.billing_status, u.billing_status) = 'settled')
|
||||
THEN round(COALESCE(s.billing_total_cost_usd::numeric, u.total_cost_usd::numeric), 8) END AS rated_amount,
|
||||
CASE WHEN COALESCE(metadata.value->'usage_pricing_available', 'true'::jsonb) <> 'false'::jsonb
|
||||
AND (s.billing_actual_total_cost_usd IS NOT NULL OR COALESCE(s.billing_status, u.billing_status) = 'settled')
|
||||
THEN public.usage_customer_billable_amount(metadata.value,
|
||||
COALESCE(s.billing_total_cost_usd::numeric, u.total_cost_usd::numeric),
|
||||
COALESCE(s.billing_actual_total_cost_usd::numeric, u.actual_total_cost_usd::numeric)) END AS billable_amount,
|
||||
s.quota_covered_amount_usd AS quota_covered_amount,
|
||||
s.wallet_consumed_amount_usd AS wallet_consumed_amount,
|
||||
s.wallet_debit_amount_usd AS wallet_debit_amount,
|
||||
s.wallet_recharge_debit_usd AS wallet_recharge_debit_amount,
|
||||
s.wallet_gift_debit_usd AS wallet_gift_debit_amount,
|
||||
s.wallet_overdraft_usd AS wallet_overdraft_amount,
|
||||
s.allocation_status, s.finalized_at AS settled_at,
|
||||
CASE WHEN s.billing_total_cost_usd IS NOT NULL THEN 'settlement_snapshot' ELSE 'legacy_float' END AS amount_source,
|
||||
b.upstream_is_stream,
|
||||
CASE WHEN metadata.value #>> '{analytics_measurement,source}' IN ('reported','estimated','mixed')
|
||||
THEN metadata.value #>> '{analytics_measurement,source}' ELSE 'unknown' END AS token_source,
|
||||
CASE WHEN COALESCE(metadata.value->'usage_available','true'::jsonb) <> 'false'::jsonb
|
||||
AND COALESCE(metadata.value->'usage_pricing_available','true'::jsonb) <> 'false'::jsonb
|
||||
AND s.input_price_per_1m IS NOT NULL AND s.billing_cache_read_cost_usd IS NOT NULL
|
||||
THEN round(s.input_price_per_1m::numeric * b.cache_read_input_tokens::numeric / 1000000,8) END AS cache_estimated_full_cost_amount,
|
||||
CASE WHEN COALESCE(metadata.value->'usage_available','true'::jsonb) <> 'false'::jsonb
|
||||
AND COALESCE(metadata.value->'usage_pricing_available','true'::jsonb) <> 'false'::jsonb
|
||||
AND s.input_price_per_1m IS NOT NULL AND s.billing_cache_read_cost_usd IS NOT NULL
|
||||
THEN round(s.billing_cache_read_cost_usd::numeric,8) END AS cache_read_cost_amount,
|
||||
CASE WHEN COALESCE(metadata.value->'usage_available','true'::jsonb) <> 'false'::jsonb
|
||||
AND COALESCE(metadata.value->'usage_pricing_available','true'::jsonb) <> 'false'::jsonb
|
||||
AND s.input_price_per_1m IS NOT NULL AND s.billing_cache_creation_cost_usd IS NOT NULL
|
||||
THEN round(s.billing_cache_creation_cost_usd::numeric,8) END AS cache_creation_cost_amount
|
||||
FROM public.usage u
|
||||
-- OFFSET 0 keeps this projection from being flattened: large metadata is
|
||||
-- detoasted and parsed once per request, rather than once per metric expression.
|
||||
CROSS JOIN LATERAL (SELECT u.request_metadata::jsonb AS value OFFSET 0) metadata
|
||||
LEFT JOIN public.usage_settlement_snapshots s USING (request_id)
|
||||
LEFT JOIN public.usage_attribution_snapshots a USING (request_id)
|
||||
JOIN public.usage_billing_facts b USING (request_id)
|
||||
LEFT JOIN public.api_keys k ON k.id=u.api_key_id
|
||||
CROSS JOIN LATERAL (
|
||||
SELECT CASE WHEN a.request_id IS NOT NULL THEN a.credential_owner_id
|
||||
WHEN EXISTS (SELECT 1 FROM public.users WHERE id=u.user_id AND NOT is_deleted) THEN u.user_id END AS owner_id,
|
||||
COALESCE(k.is_standalone,
|
||||
CASE WHEN jsonb_typeof(metadata.value #> '{analytics_attribution,is_standalone}')='boolean'
|
||||
THEN (metadata.value #>> '{analytics_attribution,is_standalone}')::boolean END,
|
||||
CASE WHEN jsonb_typeof(metadata.value->'api_key_is_standalone')='boolean'
|
||||
THEN (metadata.value->>'api_key_is_standalone')::boolean END,
|
||||
CASE WHEN a.attribution_source='user_account' THEN false
|
||||
WHEN a.attribution_source='standalone_key' THEN true END,
|
||||
CASE WHEN u.api_key_id IS NULL THEN false END) AS is_standalone
|
||||
) identity;
|
||||
|
||||
-- Do not backfill existing rows or scan historical usage during the upgrade.
|
||||
-- Historical daily totals retain their legacy charge through the read fallback;
|
||||
-- normal daily aggregation writes billing_cost for newly aggregated days.
|
||||
ALTER TABLE public.stats_daily ADD COLUMN IF NOT EXISTS billing_cost numeric(20,8);
|
||||
@@ -561,7 +561,18 @@ SET
|
||||
rate_limit = CASE WHEN $7 THEN $8 ELSE rate_limit END,
|
||||
concurrent_limit = CASE WHEN $9 THEN $10 ELSE concurrent_limit END,
|
||||
ip_rules = CASE WHEN $11 THEN $12::jsonb ELSE ip_rules END,
|
||||
feature_settings = CASE WHEN $13 THEN $14::jsonb ELSE feature_settings END,
|
||||
feature_settings = CASE WHEN $16 THEN
|
||||
NULLIF(
|
||||
(COALESCE(CASE WHEN $13 THEN $14::jsonb ELSE feature_settings END, '{}'::jsonb)
|
||||
- 'routing_group_id' - 'routing_group_name')
|
||||
|| CASE WHEN $17 THEN
|
||||
CASE WHEN $18::text IS NULL THEN '{}'::jsonb
|
||||
ELSE jsonb_build_object('routing_group_id', $18::text) END
|
||||
WHEN jsonb_typeof(feature_settings->'routing_group_id') = 'string' THEN
|
||||
jsonb_build_object('routing_group_id', feature_settings->'routing_group_id')
|
||||
ELSE '{}'::jsonb END,
|
||||
'{}'::jsonb)
|
||||
ELSE CASE WHEN $13 THEN $14::jsonb ELSE feature_settings END END,
|
||||
updated_at = NOW()
|
||||
WHERE user_id = $1
|
||||
AND id = $2
|
||||
@@ -1357,6 +1368,18 @@ impl AuthApiKeyWriteRepository for SqlxAuthApiKeySnapshotReadRepository {
|
||||
.bind(record.feature_settings.is_some())
|
||||
.bind(feature_settings)
|
||||
.bind(false)
|
||||
.bind(record.routing_group_selection.is_some())
|
||||
.bind(
|
||||
record
|
||||
.routing_group_selection
|
||||
.as_ref()
|
||||
.is_some_and(|patch| patch.group_id.is_some()),
|
||||
)
|
||||
.bind(
|
||||
record
|
||||
.routing_group_selection
|
||||
.and_then(|patch| patch.group_id.flatten()),
|
||||
)
|
||||
.fetch_optional(&self.pool)
|
||||
.await
|
||||
.map_postgres_err()?;
|
||||
@@ -1418,6 +1441,18 @@ WHERE id = $2
|
||||
.bind(record.feature_settings.is_some())
|
||||
.bind(feature_settings)
|
||||
.bind(true)
|
||||
.bind(record.routing_group_selection.is_some())
|
||||
.bind(
|
||||
record
|
||||
.routing_group_selection
|
||||
.as_ref()
|
||||
.is_some_and(|patch| patch.group_id.is_some()),
|
||||
)
|
||||
.bind(
|
||||
record
|
||||
.routing_group_selection
|
||||
.and_then(|patch| patch.group_id.flatten()),
|
||||
)
|
||||
.fetch_optional(&self.pool)
|
||||
.await
|
||||
.map_postgres_err()?;
|
||||
@@ -2143,12 +2178,126 @@ mod tests {
|
||||
.contains("key_encrypted = CASE WHEN $3 THEN $4 ELSE key_encrypted END"));
|
||||
assert!(UPDATE_USER_API_KEY_BASIC_SQL
|
||||
.contains("ip_rules = CASE WHEN $11 THEN $12::jsonb ELSE ip_rules END"));
|
||||
assert!(UPDATE_USER_API_KEY_BASIC_SQL.contains(
|
||||
"feature_settings = CASE WHEN $13 THEN $14::jsonb ELSE feature_settings END"
|
||||
));
|
||||
assert!(UPDATE_USER_API_KEY_BASIC_SQL
|
||||
.contains("CASE WHEN $13 THEN $14::jsonb ELSE feature_settings END"));
|
||||
assert!(UPDATE_USER_API_KEY_BASIC_SQL.contains("AND ($15 = FALSE OR is_locked = FALSE)"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore = "requires AETHER_TEST_DATABASE_URL; uses only a temporary table"]
|
||||
async fn live_api_key_routing_patch_preserves_concurrent_feature_edits() {
|
||||
use aether_data_contracts::repository::auth::{
|
||||
AuthApiKeyWriteRepository, UpdateApiKeyRoutingGroupSelection,
|
||||
UpdateUserApiKeyBasicRecord,
|
||||
};
|
||||
use serde_json::json;
|
||||
|
||||
let pool = sqlx::postgres::PgPoolOptions::new()
|
||||
.max_connections(1)
|
||||
.connect(&std::env::var("AETHER_TEST_DATABASE_URL").unwrap())
|
||||
.await
|
||||
.unwrap();
|
||||
// The repository's complete production UPDATE runs against a session-local table.
|
||||
sqlx::raw_sql(
|
||||
r#"
|
||||
CREATE TEMP TABLE api_keys (
|
||||
id text PRIMARY KEY, user_id text, key_hash text, key_encrypted text, name text,
|
||||
allowed_providers json, allowed_api_formats json, allowed_models json,
|
||||
ip_rules jsonb, rate_limit integer, concurrent_limit integer,
|
||||
force_capabilities json, feature_settings jsonb, is_active boolean DEFAULT true,
|
||||
is_locked boolean DEFAULT false, is_standalone boolean DEFAULT false,
|
||||
expires_at timestamptz, auto_delete_on_expiry boolean DEFAULT false,
|
||||
total_requests bigint DEFAULT 0, total_tokens bigint DEFAULT 0,
|
||||
total_cost_usd numeric DEFAULT 0, last_used_at timestamptz,
|
||||
created_at timestamptz DEFAULT NOW(), updated_at timestamptz DEFAULT NOW()
|
||||
);
|
||||
INSERT INTO api_keys (id,user_id,key_hash,name,feature_settings)
|
||||
VALUES ('key-1','user-1','hash-1','key','{"routing_group_id":"a","pii":false}');
|
||||
"#,
|
||||
)
|
||||
.execute(&pool)
|
||||
.await
|
||||
.unwrap();
|
||||
let repository = SqlxAuthApiKeySnapshotReadRepository::new(pool.clone());
|
||||
let patch = |features, group_id| UpdateUserApiKeyBasicRecord {
|
||||
user_id: "user-1".into(),
|
||||
api_key_id: "key-1".into(),
|
||||
key_encrypted: None,
|
||||
key_encrypted_present: false,
|
||||
name: None,
|
||||
name_present: false,
|
||||
rate_limit: None,
|
||||
rate_limit_present: false,
|
||||
concurrent_limit: None,
|
||||
concurrent_limit_present: false,
|
||||
ip_rules: None,
|
||||
feature_settings: features,
|
||||
routing_group_selection: Some(UpdateApiKeyRoutingGroupSelection { group_id }),
|
||||
};
|
||||
// Prepared before the group change: stale or injected group fields must not win.
|
||||
let stale_feature_edit =
|
||||
patch(Some(Some(json!({"routing_group_id":"a","pii":true}))), None);
|
||||
repository
|
||||
.update_user_api_key_basic_if_unlocked(patch(None, Some(Some("b".into()))))
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
let edited = repository
|
||||
.update_user_api_key_basic_if_unlocked(stale_feature_edit)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
edited.feature_settings,
|
||||
Some(json!({"routing_group_id":"b","pii":true}))
|
||||
);
|
||||
let changed = repository
|
||||
.update_user_api_key_basic_if_unlocked(patch(None, Some(Some("c".into()))))
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
changed.feature_settings,
|
||||
Some(json!({"routing_group_id":"c","pii":true}))
|
||||
);
|
||||
let cleared_features = repository
|
||||
.update_user_api_key_basic_if_unlocked(patch(Some(None), None))
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
cleared_features.feature_settings,
|
||||
Some(json!({"routing_group_id":"c"}))
|
||||
);
|
||||
let cleared_group = repository
|
||||
.update_user_api_key_basic_if_unlocked(patch(None, Some(None)))
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
assert_eq!(cleared_group.feature_settings, None);
|
||||
let mut admin = patch(Some(Some(json!({"admin":true}))), None);
|
||||
admin.routing_group_selection = None;
|
||||
assert_eq!(
|
||||
repository
|
||||
.update_user_api_key_basic(admin)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.feature_settings,
|
||||
Some(json!({"admin":true}))
|
||||
);
|
||||
sqlx::query("UPDATE api_keys SET is_locked=true")
|
||||
.execute(&pool)
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(repository
|
||||
.update_user_api_key_basic_if_unlocked(patch(None, Some(Some("d".into()))))
|
||||
.await
|
||||
.unwrap()
|
||||
.is_none());
|
||||
pool.close().await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn repository_constructs_from_lazy_pool() {
|
||||
let factory = PostgresPoolFactory::new(PostgresPoolConfig {
|
||||
|
||||
@@ -1509,6 +1509,85 @@ mod tests {
|
||||
(pool, schema)
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore = "requires AETHER_TEST_DATABASE_URL and PostgreSQL migrations"]
|
||||
async fn live_composite_billing_settlement_preserves_provider_cost_and_is_idempotent() {
|
||||
use super::*;
|
||||
|
||||
let (pool, schema) = isolated_settlement_test_pool().await;
|
||||
let result = AssertUnwindSafe(async {
|
||||
for table in ["wallets", "usage", "usage_settlement_snapshots", "usage_counter_deltas"] {
|
||||
sqlx::query(&format!("CREATE TABLE {table} (LIKE public.{table} INCLUDING ALL)"))
|
||||
.execute(&pool).await.expect("settlement fixture table should be created");
|
||||
}
|
||||
let repository = SqlxSettlementRepository::new(pool.clone());
|
||||
for (scenario, charge, quota_covered) in [
|
||||
("wallet", 20.0, 0.0),
|
||||
("quota_and_wallet", 20.0, 7.0),
|
||||
("zero_charge", 0.0, 0.0),
|
||||
] {
|
||||
sqlx::query("INSERT INTO users (id, username, email_verified) VALUES ($1, $1, false)")
|
||||
.bind(scenario).execute(&pool).await.expect("user should insert");
|
||||
sqlx::query("INSERT INTO wallets (id, user_id, balance, gift_balance, total_consumed, limit_mode, created_at, updated_at) VALUES ($1, $1, 100, 0, 0, 'finite', NOW(), NOW())")
|
||||
.bind(scenario).execute(&pool).await.expect("wallet should insert");
|
||||
// A zero-charge request must leave an active quota untouched too.
|
||||
if scenario != "wallet" {
|
||||
let grant = serde_json::json!([{
|
||||
"type": "daily_quota", "daily_quota_usd": 7.0,
|
||||
"reset_timezone": "UTC", "allow_wallet_overage": true,
|
||||
}]);
|
||||
sqlx::query("INSERT INTO billing_plans (id, title, price_amount, duration_unit, duration_value, entitlements_json, created_at, updated_at) VALUES ($1, $1, 10, 'month', 1, $2, NOW(), NOW())")
|
||||
.bind(scenario).bind(&grant).execute(&pool).await.expect("plan should insert");
|
||||
sqlx::query("INSERT INTO user_plan_entitlements (id, user_id, plan_id, payment_order_id, starts_at, expires_at, entitlements_snapshot, created_at, updated_at) VALUES ($1, $1, $1, $1, NOW() - INTERVAL '1 hour', NOW() + INTERVAL '1 day', $2, NOW(), NOW())")
|
||||
.bind(scenario).bind(&grant).execute(&pool).await.expect("entitlement should insert");
|
||||
}
|
||||
let multiplier = charge / 10.0;
|
||||
let metadata = serde_json::json!({"billing_multiplier_snapshot": {
|
||||
"version": 1, "factors": {"routing_group": multiplier}, "multiplier": multiplier,
|
||||
}});
|
||||
sqlx::query("INSERT INTO usage (id, request_id, user_id, provider_id, provider_name, model, status, billing_status, total_cost_usd, actual_total_cost_usd, request_metadata) VALUES ($1, $1, $1, 'provider', 'Provider', 'model', 'completed', 'pending', 10, 5, $2)")
|
||||
.bind(scenario).bind(metadata).execute(&pool).await.expect("usage should insert");
|
||||
let input = UsageSettlementInput {
|
||||
request_id: scenario.to_string(), user_id: Some(scenario.to_string()),
|
||||
api_key_id: None, api_key_is_standalone: false,
|
||||
provider_id: Some("provider".to_string()),
|
||||
status: "completed".to_string(), billing_status: "pending".to_string(),
|
||||
total_cost_usd: 10.0, actual_total_cost_usd: 5.0,
|
||||
billing_cost_usd: Some(charge), finalized_at_unix_secs: None,
|
||||
};
|
||||
let settled = repository.settle_usage(input.clone()).await.unwrap().unwrap();
|
||||
assert_eq!(settled.billing_status, "settled", "{scenario}");
|
||||
assert_eq!(settled.wallet_balance_before, Some(100.0));
|
||||
assert_eq!(settled.wallet_balance_after, Some(100.0 - (charge - quota_covered)));
|
||||
assert_eq!(repository.settle_usage(input).await.unwrap(), Some(settled), "replayed {scenario}");
|
||||
|
||||
let wallet: (f64, f64) = sqlx::query_as("SELECT (balance + gift_balance)::double precision, total_consumed::double precision FROM wallets WHERE id = $1")
|
||||
.bind(scenario).fetch_one(&pool).await.unwrap();
|
||||
assert_eq!(wallet, (100.0 - (charge - quota_covered), charge - quota_covered), "{scenario}");
|
||||
let quota: (i64, f64) = sqlx::query_as("SELECT COUNT(*), COALESCE(SUM(amount_usd), 0)::double precision FROM entitlement_usage_ledgers WHERE request_id = $1")
|
||||
.bind(scenario).fetch_one(&pool).await.unwrap();
|
||||
assert_eq!(quota, (if quota_covered > 0.0 { 1 } else { 0 }, quota_covered), "{scenario}");
|
||||
let allocation: (f64, f64, f64, String) = sqlx::query_as("SELECT quota_covered_amount_usd::double precision, wallet_consumed_amount_usd::double precision, wallet_debit_amount_usd::double precision, allocation_status FROM usage_settlement_snapshots WHERE request_id = $1")
|
||||
.bind(scenario).fetch_one(&pool).await.unwrap();
|
||||
assert_eq!(allocation, (quota_covered, charge - quota_covered, charge - quota_covered, "complete".to_string()), "{scenario}");
|
||||
let costs: (f64, f64) = sqlx::query_as("SELECT total_cost_usd::double precision, actual_total_cost_usd::double precision FROM usage WHERE request_id = $1")
|
||||
.bind(scenario).fetch_one(&pool).await.unwrap();
|
||||
assert_eq!(costs, (10.0, 5.0), "base and upstream cost must remain unchanged");
|
||||
let provider_cost: (i64, f64) = sqlx::query_as("SELECT COUNT(*), COALESCE(SUM(total_cost_usd_delta), 0)::double precision FROM usage_counter_deltas WHERE request_id = $1 AND kind = 'provider_monthly' AND target_id = 'provider'")
|
||||
.bind(scenario).fetch_one(&pool).await.unwrap();
|
||||
assert_eq!(provider_cost, (1, 5.0), "upstream cost must be recorded once even for a zero-charge request");
|
||||
}
|
||||
}).catch_unwind().await;
|
||||
sqlx::query(&format!("DROP SCHEMA {schema} CASCADE"))
|
||||
.execute(&pool)
|
||||
.await
|
||||
.expect("isolated settlement schema should be removed");
|
||||
pool.close().await;
|
||||
if let Err(panic) = result {
|
||||
std::panic::resume_unwind(panic);
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore = "requires AETHER_TEST_DATABASE_URL and PostgreSQL migrations"]
|
||||
async fn live_usage_policy_window_aggregates_preserve_exact_admission_and_idempotency() {
|
||||
|
||||
@@ -682,6 +682,7 @@ async fn live_overview_settlement_allocations_preserve_unlimited_and_finite_wall
|
||||
billing_status: "pending".into(),
|
||||
total_cost_usd: cost,
|
||||
actual_total_cost_usd: cost,
|
||||
billing_cost_usd: None,
|
||||
finalized_at_unix_secs: None,
|
||||
};
|
||||
assert_eq!(
|
||||
@@ -1266,6 +1267,19 @@ async fn live_overview_dashboard_total_matches_canonical_settlement_and_legacy_t
|
||||
serde_json::json!({}),
|
||||
1002,
|
||||
),
|
||||
(
|
||||
"billing-snapshot",
|
||||
"openai:chat",
|
||||
120,
|
||||
serde_json::json!({
|
||||
"billing_multiplier_snapshot": {
|
||||
"version": 1,
|
||||
"factors": {"routing_group": 2.0, "user_group": 0.75},
|
||||
"multiplier": 1.5
|
||||
}
|
||||
}),
|
||||
120,
|
||||
),
|
||||
(
|
||||
"unavailable",
|
||||
"openai:chat",
|
||||
@@ -1340,7 +1354,114 @@ async fn live_overview_dashboard_total_matches_canonical_settlement_and_legacy_t
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(total.total_tokens, expected_tokens, "{case}");
|
||||
if case == "billing-snapshot" {
|
||||
assert_eq!(total.billable_amount.as_deref(), Some("0.37500000"));
|
||||
}
|
||||
assert_dashboard_total_matches_canonical(&total, &canonical);
|
||||
}
|
||||
tx.rollback().await.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore = "requires migrated isolated AETHER_TEST_DATABASE_URL"]
|
||||
async fn live_customer_billing_amount_matches_canonical_and_dashboard_facts() {
|
||||
let pool = sqlx::PgPool::connect(&std::env::var("AETHER_TEST_DATABASE_URL").unwrap())
|
||||
.await
|
||||
.unwrap();
|
||||
let mut tx = pool.begin().await.unwrap();
|
||||
let start = Utc.with_ymd_and_hms(2024, 1, 1, 0, 0, 0).unwrap();
|
||||
let composite = serde_json::json!({
|
||||
"billing_multiplier_snapshot": {
|
||||
"version": 1, "factors": {"routing_group": 2.0, "user_group": 0.75}, "multiplier": 1.5
|
||||
},
|
||||
"routing_group_billing_multiplier": 99.0,
|
||||
"rate_multiplier": 0.25
|
||||
});
|
||||
for (case, metadata, expected) in [
|
||||
("legacy", serde_json::json!({}), Some("0.50000000")),
|
||||
("composite", composite.clone(), Some("3.00000000")),
|
||||
("settlement-base", composite, Some("6.00000000")),
|
||||
(
|
||||
"free",
|
||||
serde_json::json!({"routing_group_billing_multiplier": 0}),
|
||||
Some("0.00000000"),
|
||||
),
|
||||
(
|
||||
"null",
|
||||
serde_json::json!({"billing_multiplier_snapshot": null, "routing_group_billing_multiplier": 1}),
|
||||
None,
|
||||
),
|
||||
(
|
||||
"negative-factor",
|
||||
serde_json::json!({"billing_multiplier_snapshot": {"version": 1, "factors": {"routing_group": -1}, "multiplier": 1}}),
|
||||
None,
|
||||
),
|
||||
(
|
||||
"mismatch",
|
||||
serde_json::json!({"billing_multiplier_snapshot": {"version": 1, "factors": {"routing_group": 2}, "multiplier": 1}}),
|
||||
None,
|
||||
),
|
||||
(
|
||||
"negative-legacy",
|
||||
serde_json::json!({"routing_group_billing_multiplier": -1}),
|
||||
None,
|
||||
),
|
||||
(
|
||||
"string-legacy",
|
||||
serde_json::json!({"routing_group_billing_multiplier": "1"}),
|
||||
None,
|
||||
),
|
||||
(
|
||||
"zero-before-overflow",
|
||||
serde_json::json!({"billing_multiplier_snapshot": {"version": 1, "factors": {"a": 1e308, "b": 1e308, "z": 0}, "multiplier": 0}}),
|
||||
Some("0.00000000"),
|
||||
),
|
||||
(
|
||||
"overflow",
|
||||
serde_json::json!({"billing_multiplier_snapshot": {"version": 1, "factors": {"a": 1e308, "b": 1e308}, "multiplier": 1}}),
|
||||
None,
|
||||
),
|
||||
(
|
||||
"bad-key",
|
||||
serde_json::json!({"billing_multiplier_snapshot": {"version": 1, "factors": {"routing-group": 2}, "multiplier": 2}}),
|
||||
None,
|
||||
),
|
||||
] {
|
||||
let request = uuid::Uuid::new_v4().to_string();
|
||||
sqlx::query("INSERT INTO usage(id,request_id,model,provider_name,status,billing_status,total_cost_usd,actual_total_cost_usd,created_at,request_metadata) VALUES($1,$1,$1,'billing-test','completed','settled',2,0.5,$2,$3)")
|
||||
.bind(&request).bind(start).bind(metadata).execute(&mut *tx).await.unwrap();
|
||||
if case == "settlement-base" {
|
||||
sqlx::query("INSERT INTO usage_settlement_snapshots(request_id,billing_status,billing_total_cost_usd,billing_actual_total_cost_usd) VALUES($1,'settled',4,0.25)")
|
||||
.bind(&request).execute(&mut *tx).await.unwrap();
|
||||
}
|
||||
let amount: Option<String> = sqlx::query_scalar(
|
||||
"SELECT billable_amount::text FROM usage_analytics_facts_v1 WHERE request_id=$1",
|
||||
)
|
||||
.bind(&request)
|
||||
.fetch_one(&mut *tx)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(amount.as_deref(), expected, "{case}");
|
||||
let query = UsageAnalyticsQuery {
|
||||
from_unix_ms: start.timestamp_millis() as u64,
|
||||
to_unix_ms: (start + chrono::Duration::hours(1)).timestamp_millis() as u64,
|
||||
model: Some(request),
|
||||
..Default::default()
|
||||
};
|
||||
let canonical = super::analytics::read_analytics_metrics(&mut tx, &query, false)
|
||||
.await
|
||||
.unwrap();
|
||||
let inline = super::dashboard::read_dashboard_total_metrics(&mut tx, &query, false)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_dashboard_total_matches_canonical(&inline, &canonical);
|
||||
if let Some(expected) = expected {
|
||||
assert_eq!(
|
||||
canonical.billable_amount.as_deref(),
|
||||
Some(expected),
|
||||
"{case}"
|
||||
);
|
||||
}
|
||||
}
|
||||
tx.rollback().await.unwrap();
|
||||
}
|
||||
|
||||
@@ -17,10 +17,10 @@ SELECT u.created_at, u.api_key_id, u.model, u.provider_id, u.api_format, u.endpo
|
||||
u.request_type, u.status, u.is_stream, u.has_format_conversion, u.failure_origin,
|
||||
'request'::text AS record_kind,
|
||||
COALESCE(s.billing_status, u.billing_status) AS settlement_status,
|
||||
COALESCE(availability.usage_available, 'true'::jsonb) <> 'false'::jsonb AS usage_available,
|
||||
COALESCE(availability.usage_pricing_available, 'true'::jsonb) <> 'false'::jsonb
|
||||
COALESCE(metadata.value->'usage_available', 'true'::jsonb) <> 'false'::jsonb AS usage_available,
|
||||
COALESCE(metadata.value->'usage_pricing_available', 'true'::jsonb) <> 'false'::jsonb
|
||||
AND (s.billing_total_cost_usd IS NOT NULL OR COALESCE(s.billing_status, u.billing_status) = 'settled') AS pricing_available,
|
||||
CASE WHEN COALESCE(availability.usage_available, 'true'::jsonb) <> 'false'::jsonb THEN
|
||||
CASE WHEN COALESCE(metadata.value->'usage_available', 'true'::jsonb) <> 'false'::jsonb THEN
|
||||
GREATEST(
|
||||
COALESCE(
|
||||
CASE
|
||||
@@ -89,15 +89,15 @@ SELECT u.created_at, u.api_key_id, u.model, u.provider_id, u.api_format, u.endpo
|
||||
),
|
||||
0
|
||||
)::bigint END AS total_tokens,
|
||||
CASE WHEN COALESCE(availability.usage_pricing_available, 'true'::jsonb) <> 'false'::jsonb
|
||||
CASE WHEN COALESCE(metadata.value->'usage_pricing_available', 'true'::jsonb) <> 'false'::jsonb
|
||||
AND (s.billing_actual_total_cost_usd IS NOT NULL OR COALESCE(s.billing_status, u.billing_status) = 'settled')
|
||||
THEN round(COALESCE(s.billing_actual_total_cost_usd::numeric, u.actual_total_cost_usd::numeric), 8) END AS billable_amount,
|
||||
THEN public.usage_customer_billable_amount(metadata.value,
|
||||
COALESCE(s.billing_total_cost_usd::numeric, u.total_cost_usd::numeric),
|
||||
COALESCE(s.billing_actual_total_cost_usd::numeric, u.actual_total_cost_usd::numeric)) END AS billable_amount,
|
||||
s.allocation_status
|
||||
FROM public.usage u
|
||||
LEFT JOIN public.usage_settlement_snapshots s USING (request_id)
|
||||
CROSS JOIN LATERAL json_to_record(
|
||||
CASE WHEN json_typeof(u.request_metadata)='object' THEN u.request_metadata ELSE '{}'::json END
|
||||
) AS availability(usage_available jsonb, usage_pricing_available jsonb)
|
||||
CROSS JOIN LATERAL (SELECT u.request_metadata::jsonb AS value OFFSET 0) metadata
|
||||
WHERE NOT EXISTS (SELECT 1 FROM public.usage_attribution_snapshots a
|
||||
WHERE a.request_id=u.request_id AND a.record_kind='session')
|
||||
) AS usage_analytics_facts_v1"#;
|
||||
|
||||
@@ -134,6 +134,29 @@ async fn live_dashboard_restores_legacy_history_without_replaying_or_double_coun
|
||||
assert_eq!(advanced.activity_days, restored.activity_days);
|
||||
assert_eq!(advanced.active_days, restored.active_days);
|
||||
|
||||
// New daily rollups retain customer charges independently after detail
|
||||
// expires; older NULL daily charges retain their original legacy cost.
|
||||
sqlx::query("UPDATE stats_daily SET billing_cost=1.5 WHERE id='recent'")
|
||||
.execute(&pool).await.unwrap();
|
||||
sqlx::query("DELETE FROM usage WHERE request_id='overlap'")
|
||||
.execute(&pool).await.unwrap();
|
||||
let billed_history = repo.query_dashboard_summary(&query).await.unwrap();
|
||||
assert_eq!(billed_history.total.billable_amount.as_deref(), Some("123456791.59691357"));
|
||||
assert_eq!(billed_history.total.request_count, restored.total.request_count);
|
||||
assert_eq!(billed_history.today, restored.today);
|
||||
let provider_cost: String = sqlx::query_scalar("SELECT actual_total_cost::text FROM stats_daily WHERE id='recent'")
|
||||
.fetch_one(&pool).await.unwrap();
|
||||
assert_eq!(provider_cost, "0.30000003");
|
||||
|
||||
// The pre-activation live prefix applies the same composite snapshot
|
||||
// to its finalized base amount, independently of procurement cost.
|
||||
sqlx::query("UPDATE usage SET request_metadata=$1 WHERE request_id='before-shared'")
|
||||
.bind(serde_json::json!({"billing_multiplier_snapshot": {"version": 1, "factors": {"routing_group": 2, "user_group": 0.75}, "multiplier": 1.5}}))
|
||||
.execute(&pool).await.unwrap();
|
||||
let billed_prefix = repo.query_dashboard_summary(&query).await.unwrap();
|
||||
assert_eq!(billed_prefix.total.billable_amount.as_deref(), Some("123456791.65864196"));
|
||||
assert_eq!(billed_prefix.today.billable_amount.as_deref(), Some("0.93518517"));
|
||||
|
||||
// A summary cutoff without legacy daily history must leave the normal
|
||||
// future-only projection and its requested calendar unchanged.
|
||||
sqlx::query("DELETE FROM stats_daily").execute(&pool).await.unwrap();
|
||||
|
||||
@@ -42,18 +42,21 @@ use crate::{
|
||||
PostgresTransactionRunner,
|
||||
};
|
||||
use aether_data_contracts::repository::usage::{
|
||||
api_key_usage_contribution, model_usage_contribution, provider_api_key_usage_contribution,
|
||||
sanitize_usage_capture_controls_for_persistence, sanitize_usage_for_persistence,
|
||||
sanitize_usage_request_metadata, usage_can_recover_terminal_failure,
|
||||
usage_error_category_for_status_code, usage_lifecycle_update_allowed, ApiKeyUsageDelta,
|
||||
ModelUsageDelta, PendingUsageCleanupSummary, ProviderApiKeyUsageContribution,
|
||||
ProviderApiKeyUsageDelta, ProviderApiKeyWindowUsageRequest, StoredProviderApiKeyUsageSummary,
|
||||
StoredProviderApiKeyWindowUsageSummary, StoredProviderUsageSummary, StoredRequestUsageAudit,
|
||||
StoredUsageDailySummary, UpsertUsageRecord, UsageAuditListQuery, UsageCounterFlushSummary,
|
||||
UsageCounterHealthSnapshot, UsageCounterPendingHealthSnapshot, UsageDailyHeatmapQuery,
|
||||
UsageReadRepository, UsageWriteRepository, PROVIDER_CACHE_TTL_MINUTES_METADATA_KEY,
|
||||
PROVIDER_REASONING_EFFORT_METADATA_KEY, PROVIDER_SERVICE_TIER_METADATA_KEY,
|
||||
REQUESTED_REASONING_EFFORT_METADATA_KEY,
|
||||
api_key_usage_contribution, model_usage_contribution, preserve_usage_routing_group_snapshot,
|
||||
provider_api_key_usage_contribution, sanitize_usage_capture_controls_for_persistence,
|
||||
sanitize_usage_for_persistence, sanitize_usage_request_metadata,
|
||||
usage_can_recover_terminal_failure, usage_error_category_for_status_code,
|
||||
usage_lifecycle_update_allowed, ApiKeyUsageDelta, ModelUsageDelta, PendingUsageCleanupSummary,
|
||||
ProviderApiKeyUsageContribution, ProviderApiKeyUsageDelta, ProviderApiKeyWindowUsageRequest,
|
||||
StoredProviderApiKeyUsageSummary, StoredProviderApiKeyWindowUsageSummary,
|
||||
StoredProviderUsageSummary, StoredRequestUsageAudit, StoredUsageDailySummary,
|
||||
UpsertUsageRecord, UsageAuditListQuery, UsageCounterFlushSummary, UsageCounterHealthSnapshot,
|
||||
UsageCounterPendingHealthSnapshot, UsageDailyHeatmapQuery, UsageReadRepository,
|
||||
UsageWriteRepository, BILLING_MULTIPLIER_SNAPSHOT_METADATA_KEY,
|
||||
PROVIDER_CACHE_TTL_MINUTES_METADATA_KEY, PROVIDER_REASONING_EFFORT_METADATA_KEY,
|
||||
PROVIDER_SERVICE_TIER_METADATA_KEY, REQUESTED_REASONING_EFFORT_METADATA_KEY,
|
||||
ROUTING_GROUP_BILLING_MULTIPLIER_METADATA_KEY, ROUTING_GROUP_ID_METADATA_KEY,
|
||||
ROUTING_GROUP_NAME_METADATA_KEY,
|
||||
};
|
||||
use aether_data_contracts::DataLayerError;
|
||||
|
||||
@@ -8762,6 +8765,15 @@ ORDER BY "usage".user_id ASC
|
||||
);
|
||||
request_metadata_json = json_bind_text(request_metadata_value.as_ref())?;
|
||||
}
|
||||
if capture_update_allowed {
|
||||
request_metadata_value = preserve_usage_routing_group_snapshot(
|
||||
request_metadata_value,
|
||||
previous_usage
|
||||
.as_ref()
|
||||
.and_then(|stored| stored.request_metadata.as_ref()),
|
||||
);
|
||||
request_metadata_json = json_bind_text(request_metadata_value.as_ref())?;
|
||||
}
|
||||
let _row = sqlx::query(UPSERT_SQL)
|
||||
.bind(Uuid::new_v4().to_string())
|
||||
.bind(&usage.request_id)
|
||||
@@ -12554,6 +12566,10 @@ fn retain_previous_request_audit_metadata(
|
||||
"request_path",
|
||||
"request_query_string",
|
||||
"request_path_and_query",
|
||||
ROUTING_GROUP_BILLING_MULTIPLIER_METADATA_KEY,
|
||||
BILLING_MULTIPLIER_SNAPSHOT_METADATA_KEY,
|
||||
ROUTING_GROUP_ID_METADATA_KEY,
|
||||
ROUTING_GROUP_NAME_METADATA_KEY,
|
||||
] {
|
||||
if let Some(value) = previous_metadata.get(key) {
|
||||
retained.insert(key.to_string(), value.clone());
|
||||
|
||||
@@ -11,7 +11,7 @@ WITH daily AS (
|
||||
CASE WHEN effective_input_tokens=0 AND input_tokens>0 THEN input_tokens
|
||||
ELSE effective_input_tokens END + cache_creation_tokens + cache_read_tokens
|
||||
ELSE total_input_context END AS cache_input_tokens,
|
||||
actual_total_cost::numeric AS billable_amount
|
||||
COALESCE(billing_cost,actual_total_cost::numeric) AS billable_amount
|
||||
FROM stats_daily
|
||||
), facts AS MATERIALIZED (
|
||||
SELECT (day AT TIME ZONE 'UTC')::date AS day, request_count,
|
||||
@@ -22,7 +22,11 @@ WITH daily AS (
|
||||
SELECT (b.created_at AT TIME ZONE 'UTC')::date, 1::bigint,
|
||||
b.input_tokens, b.output_tokens, b.total_tokens, b.cache_creation_input_tokens,
|
||||
b.cache_read_input_tokens, b.total_input_context,
|
||||
COALESCE(s.billing_actual_total_cost_usd::numeric,u.actual_total_cost_usd::numeric)
|
||||
CASE WHEN COALESCE(u.request_metadata::jsonb->'usage_pricing_available','true'::jsonb)<>'false'::jsonb
|
||||
AND (s.billing_actual_total_cost_usd IS NOT NULL OR COALESCE(s.billing_status,u.billing_status)='settled')
|
||||
THEN public.usage_customer_billable_amount(u.request_metadata::jsonb,
|
||||
COALESCE(s.billing_total_cost_usd::numeric,u.total_cost_usd::numeric),
|
||||
COALESCE(s.billing_actual_total_cost_usd::numeric,u.actual_total_cost_usd::numeric)) END
|
||||
FROM usage_billing_facts b
|
||||
JOIN usage u USING (request_id)
|
||||
LEFT JOIN usage_settlement_snapshots s USING (request_id)
|
||||
|
||||
+20
-2
@@ -176,6 +176,10 @@ SELECT
|
||||
NULL::bytea AS client_response_body_compressed,
|
||||
CASE
|
||||
WHEN NULLIF(BTRIM("usage".request_metadata->>'client_ip'), '') IS NOT NULL
|
||||
OR NULLIF(BTRIM("usage".request_metadata->>'routing_group_id'), '') IS NOT NULL
|
||||
OR NULLIF(BTRIM("usage".request_metadata->>'routing_group_name'), '') IS NOT NULL
|
||||
OR json_typeof("usage".request_metadata->'routing_group_billing_multiplier') = 'number'
|
||||
OR "usage".request_metadata->'billing_multiplier_snapshot' IS NOT NULL
|
||||
OR NULLIF(BTRIM("usage".request_metadata->>'user_agent'), '') IS NOT NULL
|
||||
OR NULLIF(BTRIM("usage".request_metadata->>'request_path'), '') IS NOT NULL
|
||||
OR NULLIF(BTRIM("usage".request_metadata->>'request_path_and_query'), '') IS NOT NULL
|
||||
@@ -192,7 +196,17 @@ SELECT
|
||||
OR ("usage".request_metadata->>'usage_pricing_available') IN ('true', 'false')
|
||||
OR json_typeof("usage".request_metadata->'live_session') = 'object'
|
||||
OR json_typeof("usage".request_metadata->'realtime_session') = 'object'
|
||||
THEN jsonb_strip_nulls(jsonb_build_object(
|
||||
THEN (jsonb_strip_nulls(jsonb_build_object(
|
||||
'routing_group_id',
|
||||
NULLIF(BTRIM("usage".request_metadata->>'routing_group_id'), ''),
|
||||
'routing_group_name',
|
||||
NULLIF(BTRIM("usage".request_metadata->>'routing_group_name'), ''),
|
||||
'routing_group_billing_multiplier',
|
||||
CASE
|
||||
WHEN json_typeof("usage".request_metadata->'routing_group_billing_multiplier') = 'number'
|
||||
THEN "usage".request_metadata->'routing_group_billing_multiplier'
|
||||
ELSE NULL
|
||||
END,
|
||||
'client_ip',
|
||||
NULLIF(BTRIM("usage".request_metadata->>'client_ip'), ''),
|
||||
'user_agent',
|
||||
@@ -255,7 +269,11 @@ SELECT
|
||||
THEN "usage".request_metadata->'realtime_session'
|
||||
ELSE NULL
|
||||
END
|
||||
))::json
|
||||
)) || CASE
|
||||
WHEN "usage".request_metadata->'billing_multiplier_snapshot' IS NOT NULL
|
||||
THEN jsonb_build_object('billing_multiplier_snapshot', "usage".request_metadata->'billing_multiplier_snapshot')
|
||||
ELSE '{}'::jsonb
|
||||
END)::json
|
||||
ELSE NULL::json
|
||||
END AS request_metadata,
|
||||
NULL::varchar AS http_request_body_ref,
|
||||
|
||||
+20
-2
@@ -176,6 +176,10 @@ SELECT
|
||||
NULL::bytea AS client_response_body_compressed,
|
||||
CASE
|
||||
WHEN NULLIF(BTRIM("usage".request_metadata->>'client_ip'), '') IS NOT NULL
|
||||
OR NULLIF(BTRIM("usage".request_metadata->>'routing_group_id'), '') IS NOT NULL
|
||||
OR NULLIF(BTRIM("usage".request_metadata->>'routing_group_name'), '') IS NOT NULL
|
||||
OR json_typeof("usage".request_metadata->'routing_group_billing_multiplier') = 'number'
|
||||
OR "usage".request_metadata->'billing_multiplier_snapshot' IS NOT NULL
|
||||
OR NULLIF(BTRIM("usage".request_metadata->>'user_agent'), '') IS NOT NULL
|
||||
OR NULLIF(BTRIM("usage".request_metadata->>'request_path'), '') IS NOT NULL
|
||||
OR NULLIF(BTRIM("usage".request_metadata->>'request_path_and_query'), '') IS NOT NULL
|
||||
@@ -192,7 +196,17 @@ SELECT
|
||||
OR ("usage".request_metadata->>'usage_pricing_available') IN ('true', 'false')
|
||||
OR json_typeof("usage".request_metadata->'live_session') = 'object'
|
||||
OR json_typeof("usage".request_metadata->'realtime_session') = 'object'
|
||||
THEN jsonb_strip_nulls(jsonb_build_object(
|
||||
THEN (jsonb_strip_nulls(jsonb_build_object(
|
||||
'routing_group_id',
|
||||
NULLIF(BTRIM("usage".request_metadata->>'routing_group_id'), ''),
|
||||
'routing_group_name',
|
||||
NULLIF(BTRIM("usage".request_metadata->>'routing_group_name'), ''),
|
||||
'routing_group_billing_multiplier',
|
||||
CASE
|
||||
WHEN json_typeof("usage".request_metadata->'routing_group_billing_multiplier') = 'number'
|
||||
THEN "usage".request_metadata->'routing_group_billing_multiplier'
|
||||
ELSE NULL
|
||||
END,
|
||||
'client_ip',
|
||||
NULLIF(BTRIM("usage".request_metadata->>'client_ip'), ''),
|
||||
'user_agent',
|
||||
@@ -255,7 +269,11 @@ SELECT
|
||||
THEN "usage".request_metadata->'realtime_session'
|
||||
ELSE NULL
|
||||
END
|
||||
))::json
|
||||
)) || CASE
|
||||
WHEN "usage".request_metadata->'billing_multiplier_snapshot' IS NOT NULL
|
||||
THEN jsonb_build_object('billing_multiplier_snapshot', "usage".request_metadata->'billing_multiplier_snapshot')
|
||||
ELSE '{}'::jsonb
|
||||
END)::json
|
||||
ELSE NULL::json
|
||||
END AS request_metadata,
|
||||
NULL::varchar AS http_request_body_ref,
|
||||
|
||||
Reference in New Issue
Block a user