mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-01 17:00:21 +08:00
feat(billing): 引入 billing v3 settlement snapshot 及 usage_billing_facts 视图
- 新增迁移 20260424000000:为 usage_settlement_snapshots 添加 settlement_snapshot、 billing_dimensions、billing_input/output/cache tokens、billing_total_cost_usd 等列, 并创建 usage_billing_facts 视图(从 settlement snapshot 覆盖原始 usage token/cost 字段) - event_enrichment:构建结构化 settlement_snapshot(含 pricing_snapshot、billing_plan_snapshot、 resolved_dimensions、cost_breakdown 等),写入 request_metadata - pricing:新增 pricing_source() 方法区分 provider_override / global_default / unpriced - sql.rs:settlement snapshot 写入新列;用量汇总查询改用 usage_billing_facts 视图 - maintenance/runtime:所有统计查询的 FROM usage 替换为 FROM usage_billing_facts - admin observability:在 settlement 响应中暴露 settlement_snapshot / billing_dimensions, 并从 metadata 中剥离对应字段 - request_metadata:允许 settlement_snapshot / billing_dimensions 字段传播 - stream execution:在首个数据帧到达时记录 TTFB,补全流式请求的 streaming 事件
This commit is contained in:
@@ -4711,7 +4711,268 @@ ALTER TABLE IF EXISTS public.usage_settlement_snapshots
|
||||
ADD COLUMN IF NOT EXISTS output_price_per_1m numeric(20,8),
|
||||
ADD COLUMN IF NOT EXISTS cache_creation_price_per_1m numeric(20,8),
|
||||
ADD COLUMN IF NOT EXISTS cache_read_price_per_1m numeric(20,8),
|
||||
ADD COLUMN IF NOT EXISTS price_per_request numeric(20,8);
|
||||
ADD COLUMN IF NOT EXISTS price_per_request numeric(20,8),
|
||||
ADD COLUMN IF NOT EXISTS settlement_snapshot_schema_version character varying(20),
|
||||
ADD COLUMN IF NOT EXISTS settlement_snapshot jsonb,
|
||||
ADD COLUMN IF NOT EXISTS billing_dimensions jsonb,
|
||||
ADD COLUMN IF NOT EXISTS billing_input_tokens bigint,
|
||||
ADD COLUMN IF NOT EXISTS billing_effective_input_tokens bigint,
|
||||
ADD COLUMN IF NOT EXISTS billing_output_tokens bigint,
|
||||
ADD COLUMN IF NOT EXISTS billing_cache_creation_tokens bigint,
|
||||
ADD COLUMN IF NOT EXISTS billing_cache_creation_5m_tokens bigint,
|
||||
ADD COLUMN IF NOT EXISTS billing_cache_creation_1h_tokens bigint,
|
||||
ADD COLUMN IF NOT EXISTS billing_cache_read_tokens bigint,
|
||||
ADD COLUMN IF NOT EXISTS billing_total_input_context bigint,
|
||||
ADD COLUMN IF NOT EXISTS billing_cache_creation_cost_usd numeric(20,8),
|
||||
ADD COLUMN IF NOT EXISTS billing_cache_read_cost_usd numeric(20,8),
|
||||
ADD COLUMN IF NOT EXISTS billing_total_cost_usd numeric(20,8),
|
||||
ADD COLUMN IF NOT EXISTS billing_actual_total_cost_usd numeric(20,8),
|
||||
ADD COLUMN IF NOT EXISTS billing_pricing_source character varying(50),
|
||||
ADD COLUMN IF NOT EXISTS billing_rule_id character varying(100),
|
||||
ADD COLUMN IF NOT EXISTS billing_rule_version character varying(50);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS ix_usage_settlement_snapshots_schema_version
|
||||
ON public.usage_settlement_snapshots USING btree (settlement_snapshot_schema_version);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS ix_usage_settlement_snapshots_pricing_source
|
||||
ON public.usage_settlement_snapshots USING btree (billing_pricing_source);
|
||||
|
||||
CREATE OR REPLACE VIEW public.usage_billing_facts AS
|
||||
SELECT
|
||||
usage_rows.id,
|
||||
usage_rows.request_id,
|
||||
usage_rows.user_id,
|
||||
usage_rows.api_key_id,
|
||||
usage_rows.username,
|
||||
usage_rows.api_key_name,
|
||||
usage_rows.provider_name,
|
||||
usage_rows.model,
|
||||
usage_rows.target_model,
|
||||
usage_rows.provider_id,
|
||||
usage_rows.provider_endpoint_id,
|
||||
usage_rows.provider_api_key_id,
|
||||
usage_rows.request_type,
|
||||
usage_rows.api_format,
|
||||
usage_rows.api_family,
|
||||
usage_rows.endpoint_kind,
|
||||
usage_rows.endpoint_api_format,
|
||||
usage_rows.provider_api_family,
|
||||
usage_rows.provider_endpoint_kind,
|
||||
COALESCE(usage_rows.has_format_conversion, FALSE) AS has_format_conversion,
|
||||
COALESCE(usage_rows.is_stream, FALSE) AS is_stream,
|
||||
usage_rows.status_code,
|
||||
usage_rows.error_message,
|
||||
usage_rows.error_category,
|
||||
usage_rows.response_time_ms,
|
||||
usage_rows.first_byte_time_ms,
|
||||
usage_rows.status,
|
||||
COALESCE(settlement.billing_status, usage_rows.billing_status) AS billing_status,
|
||||
usage_rows.created_at,
|
||||
COALESCE(settlement.finalized_at, usage_rows.finalized_at) AS finalized_at,
|
||||
GREATEST(COALESCE(settlement.billing_input_tokens, usage_rows.input_tokens, 0), 0)::bigint
|
||||
AS input_tokens,
|
||||
GREATEST(
|
||||
COALESCE(
|
||||
settlement.billing_effective_input_tokens,
|
||||
CASE
|
||||
WHEN GREATEST(COALESCE(usage_rows.input_tokens, 0), 0) <= 0 THEN 0
|
||||
WHEN GREATEST(COALESCE(usage_rows.cache_read_input_tokens, 0), 0) <= 0
|
||||
THEN GREATEST(COALESCE(usage_rows.input_tokens, 0), 0)
|
||||
WHEN split_part(lower(COALESCE(COALESCE(usage_rows.endpoint_api_format, usage_rows.api_format), '')), ':', 1)
|
||||
IN ('openai', 'gemini', 'google')
|
||||
THEN GREATEST(
|
||||
GREATEST(COALESCE(usage_rows.input_tokens, 0), 0)
|
||||
- GREATEST(COALESCE(usage_rows.cache_read_input_tokens, 0), 0),
|
||||
0
|
||||
)
|
||||
ELSE GREATEST(COALESCE(usage_rows.input_tokens, 0), 0)
|
||||
END
|
||||
),
|
||||
0
|
||||
)::bigint AS effective_input_tokens,
|
||||
GREATEST(COALESCE(settlement.billing_output_tokens, usage_rows.output_tokens, 0), 0)::bigint
|
||||
AS output_tokens,
|
||||
GREATEST(
|
||||
COALESCE(
|
||||
settlement.billing_cache_creation_tokens,
|
||||
CASE
|
||||
WHEN COALESCE(usage_rows.cache_creation_input_tokens, 0) = 0
|
||||
AND (
|
||||
COALESCE(usage_rows.cache_creation_input_tokens_5m, 0)
|
||||
+ COALESCE(usage_rows.cache_creation_input_tokens_1h, 0)
|
||||
) > 0
|
||||
THEN COALESCE(usage_rows.cache_creation_input_tokens_5m, 0)
|
||||
+ COALESCE(usage_rows.cache_creation_input_tokens_1h, 0)
|
||||
ELSE COALESCE(usage_rows.cache_creation_input_tokens, 0)
|
||||
END,
|
||||
0
|
||||
),
|
||||
0
|
||||
)::bigint AS cache_creation_input_tokens,
|
||||
GREATEST(
|
||||
COALESCE(
|
||||
settlement.billing_cache_creation_5m_tokens,
|
||||
usage_rows.cache_creation_input_tokens_5m,
|
||||
0
|
||||
),
|
||||
0
|
||||
)::bigint AS cache_creation_input_tokens_5m,
|
||||
GREATEST(
|
||||
COALESCE(
|
||||
settlement.billing_cache_creation_1h_tokens,
|
||||
usage_rows.cache_creation_input_tokens_1h,
|
||||
0
|
||||
),
|
||||
0
|
||||
)::bigint AS cache_creation_input_tokens_1h,
|
||||
GREATEST(COALESCE(settlement.billing_cache_read_tokens, usage_rows.cache_read_input_tokens, 0), 0)::bigint
|
||||
AS cache_read_input_tokens,
|
||||
GREATEST(
|
||||
COALESCE(
|
||||
CASE
|
||||
WHEN settlement.billing_input_tokens IS NOT NULL
|
||||
OR settlement.billing_output_tokens IS NOT NULL
|
||||
OR settlement.billing_cache_creation_tokens IS NOT NULL
|
||||
OR settlement.billing_cache_creation_5m_tokens IS NOT NULL
|
||||
OR settlement.billing_cache_creation_1h_tokens IS NOT NULL
|
||||
OR settlement.billing_cache_read_tokens IS NOT NULL
|
||||
THEN COALESCE(settlement.billing_input_tokens, 0)
|
||||
+ COALESCE(settlement.billing_output_tokens, 0)
|
||||
+ COALESCE(
|
||||
settlement.billing_cache_creation_tokens,
|
||||
COALESCE(settlement.billing_cache_creation_5m_tokens, 0)
|
||||
+ COALESCE(settlement.billing_cache_creation_1h_tokens, 0),
|
||||
0
|
||||
)
|
||||
+ COALESCE(settlement.billing_cache_read_tokens, 0)
|
||||
END,
|
||||
usage_rows.total_tokens,
|
||||
0
|
||||
),
|
||||
0
|
||||
)::bigint AS total_tokens,
|
||||
GREATEST(
|
||||
COALESCE(
|
||||
settlement.billing_total_input_context,
|
||||
CASE
|
||||
WHEN split_part(lower(COALESCE(COALESCE(usage_rows.endpoint_api_format, usage_rows.api_format), '')), ':', 1)
|
||||
IN ('claude', 'anthropic')
|
||||
THEN GREATEST(COALESCE(usage_rows.input_tokens, 0), 0)
|
||||
+ CASE
|
||||
WHEN COALESCE(usage_rows.cache_creation_input_tokens, 0) = 0
|
||||
AND (
|
||||
COALESCE(usage_rows.cache_creation_input_tokens_5m, 0)
|
||||
+ COALESCE(usage_rows.cache_creation_input_tokens_1h, 0)
|
||||
) > 0
|
||||
THEN COALESCE(usage_rows.cache_creation_input_tokens_5m, 0)
|
||||
+ COALESCE(usage_rows.cache_creation_input_tokens_1h, 0)
|
||||
ELSE COALESCE(usage_rows.cache_creation_input_tokens, 0)
|
||||
END
|
||||
+ GREATEST(COALESCE(usage_rows.cache_read_input_tokens, 0), 0)
|
||||
WHEN split_part(lower(COALESCE(COALESCE(usage_rows.endpoint_api_format, usage_rows.api_format), '')), ':', 1)
|
||||
IN ('openai', 'gemini', 'google')
|
||||
THEN CASE
|
||||
WHEN GREATEST(COALESCE(usage_rows.input_tokens, 0), 0) <= 0 THEN 0
|
||||
WHEN GREATEST(COALESCE(usage_rows.cache_read_input_tokens, 0), 0) <= 0
|
||||
THEN GREATEST(COALESCE(usage_rows.input_tokens, 0), 0)
|
||||
ELSE GREATEST(
|
||||
GREATEST(COALESCE(usage_rows.input_tokens, 0), 0)
|
||||
- GREATEST(COALESCE(usage_rows.cache_read_input_tokens, 0), 0),
|
||||
0
|
||||
)
|
||||
END
|
||||
+ GREATEST(COALESCE(usage_rows.cache_read_input_tokens, 0), 0)
|
||||
ELSE GREATEST(COALESCE(usage_rows.input_tokens, 0), 0)
|
||||
+ CASE
|
||||
WHEN COALESCE(usage_rows.cache_creation_input_tokens, 0) = 0
|
||||
AND (
|
||||
COALESCE(usage_rows.cache_creation_input_tokens_5m, 0)
|
||||
+ COALESCE(usage_rows.cache_creation_input_tokens_1h, 0)
|
||||
) > 0
|
||||
THEN COALESCE(usage_rows.cache_creation_input_tokens_5m, 0)
|
||||
+ COALESCE(usage_rows.cache_creation_input_tokens_1h, 0)
|
||||
ELSE COALESCE(usage_rows.cache_creation_input_tokens, 0)
|
||||
END
|
||||
+ GREATEST(COALESCE(usage_rows.cache_read_input_tokens, 0), 0)
|
||||
END,
|
||||
0
|
||||
),
|
||||
0
|
||||
)::bigint AS total_input_context,
|
||||
COALESCE(CAST(usage_rows.input_cost_usd AS DOUBLE PRECISION), 0) AS input_cost_usd,
|
||||
COALESCE(CAST(usage_rows.output_cost_usd AS DOUBLE PRECISION), 0) AS output_cost_usd,
|
||||
COALESCE(
|
||||
CAST(settlement.billing_cache_creation_cost_usd AS DOUBLE PRECISION),
|
||||
CAST(usage_rows.cache_creation_cost_usd AS DOUBLE PRECISION),
|
||||
0
|
||||
) AS cache_creation_cost_usd,
|
||||
COALESCE(
|
||||
CAST(settlement.billing_cache_read_cost_usd AS DOUBLE PRECISION),
|
||||
CAST(usage_rows.cache_read_cost_usd AS DOUBLE PRECISION),
|
||||
0
|
||||
) AS cache_read_cost_usd,
|
||||
COALESCE(
|
||||
CAST(settlement.billing_total_cost_usd AS DOUBLE PRECISION),
|
||||
CAST(usage_rows.total_cost_usd AS DOUBLE PRECISION),
|
||||
0
|
||||
) AS total_cost_usd,
|
||||
COALESCE(
|
||||
CAST(settlement.billing_actual_total_cost_usd AS DOUBLE PRECISION),
|
||||
CAST(usage_rows.actual_total_cost_usd AS DOUBLE PRECISION),
|
||||
0
|
||||
) AS actual_total_cost_usd,
|
||||
COALESCE(
|
||||
CAST(settlement.output_price_per_1m AS DOUBLE PRECISION),
|
||||
CAST(usage_rows.output_price_per_1m AS DOUBLE PRECISION)
|
||||
) AS output_price_per_1m,
|
||||
COALESCE(
|
||||
CAST(settlement.input_price_per_1m AS DOUBLE PRECISION),
|
||||
CAST(usage_rows.input_price_per_1m AS DOUBLE PRECISION)
|
||||
) AS input_price_per_1m,
|
||||
COALESCE(
|
||||
CAST(settlement.cache_creation_price_per_1m AS DOUBLE PRECISION),
|
||||
CAST(usage_rows.cache_creation_price_per_1m AS DOUBLE PRECISION)
|
||||
) AS cache_creation_price_per_1m,
|
||||
COALESCE(
|
||||
CAST(settlement.cache_read_price_per_1m AS DOUBLE PRECISION),
|
||||
CAST(usage_rows.cache_read_price_per_1m AS DOUBLE PRECISION)
|
||||
) AS cache_read_price_per_1m,
|
||||
COALESCE(
|
||||
CAST(settlement.price_per_request AS DOUBLE PRECISION),
|
||||
CAST(usage_rows.price_per_request AS DOUBLE PRECISION)
|
||||
) AS price_per_request,
|
||||
settlement.billing_pricing_source,
|
||||
settlement.billing_rule_id,
|
||||
settlement.billing_rule_version
|
||||
FROM public."usage" AS usage_rows
|
||||
LEFT JOIN public.usage_settlement_snapshots AS settlement
|
||||
ON settlement.request_id = usage_rows.request_id;
|
||||
|
||||
COMMENT ON VIEW public.usage_billing_facts IS
|
||||
'Canonical billing read model. Token/cost fields prefer usage_settlement_snapshots.billing_* and fall back to deprecated usage mirrors for legacy rows.';
|
||||
|
||||
COMMENT ON COLUMN public.usage.input_tokens IS
|
||||
'DEPRECATED: billing dimension mirror. Use public.usage_settlement_snapshots.billing_input_tokens or public.usage_billing_facts.input_tokens.';
|
||||
COMMENT ON COLUMN public.usage.output_tokens IS
|
||||
'DEPRECATED: billing dimension mirror. Use public.usage_settlement_snapshots.billing_output_tokens or public.usage_billing_facts.output_tokens.';
|
||||
COMMENT ON COLUMN public.usage.total_tokens IS
|
||||
'DEPRECATED: billing dimension mirror. Use public.usage_billing_facts.total_tokens.';
|
||||
COMMENT ON COLUMN public.usage.cache_creation_input_tokens IS
|
||||
'DEPRECATED: billing dimension mirror. Use public.usage_settlement_snapshots.billing_cache_creation_tokens or public.usage_billing_facts.cache_creation_input_tokens.';
|
||||
COMMENT ON COLUMN public.usage.cache_creation_input_tokens_5m IS
|
||||
'DEPRECATED: billing dimension mirror. Use public.usage_settlement_snapshots.billing_cache_creation_5m_tokens or public.usage_billing_facts.cache_creation_input_tokens_5m.';
|
||||
COMMENT ON COLUMN public.usage.cache_creation_input_tokens_1h IS
|
||||
'DEPRECATED: billing dimension mirror. Use public.usage_settlement_snapshots.billing_cache_creation_1h_tokens or public.usage_billing_facts.cache_creation_input_tokens_1h.';
|
||||
COMMENT ON COLUMN public.usage.cache_read_input_tokens IS
|
||||
'DEPRECATED: billing dimension mirror. Use public.usage_settlement_snapshots.billing_cache_read_tokens or public.usage_billing_facts.cache_read_input_tokens.';
|
||||
COMMENT ON COLUMN public.usage.cache_creation_cost_usd IS
|
||||
'DEPRECATED: billing cost mirror. Use public.usage_settlement_snapshots.billing_cache_creation_cost_usd or public.usage_billing_facts.cache_creation_cost_usd.';
|
||||
COMMENT ON COLUMN public.usage.cache_read_cost_usd IS
|
||||
'DEPRECATED: billing cost mirror. Use public.usage_settlement_snapshots.billing_cache_read_cost_usd or public.usage_billing_facts.cache_read_cost_usd.';
|
||||
COMMENT ON COLUMN public.usage.total_cost_usd IS
|
||||
'DEPRECATED: billing cost mirror. Use public.usage_settlement_snapshots.billing_total_cost_usd or public.usage_billing_facts.total_cost_usd.';
|
||||
COMMENT ON COLUMN public.usage.actual_total_cost_usd IS
|
||||
'DEPRECATED: billing cost mirror. Use public.usage_settlement_snapshots.billing_actual_total_cost_usd or public.usage_billing_facts.actual_total_cost_usd.';
|
||||
|
||||
COMMENT ON COLUMN public.usage.wallet_id IS
|
||||
'DEPRECATED: settlement owner moved to public.usage_settlement_snapshots.wallet_id. Legacy compatibility only; do not write new values.';
|
||||
|
||||
@@ -0,0 +1,262 @@
|
||||
ALTER TABLE IF EXISTS public.usage_settlement_snapshots
|
||||
ADD COLUMN IF NOT EXISTS settlement_snapshot_schema_version character varying(20),
|
||||
ADD COLUMN IF NOT EXISTS settlement_snapshot jsonb,
|
||||
ADD COLUMN IF NOT EXISTS billing_dimensions jsonb,
|
||||
ADD COLUMN IF NOT EXISTS billing_input_tokens bigint,
|
||||
ADD COLUMN IF NOT EXISTS billing_effective_input_tokens bigint,
|
||||
ADD COLUMN IF NOT EXISTS billing_output_tokens bigint,
|
||||
ADD COLUMN IF NOT EXISTS billing_cache_creation_tokens bigint,
|
||||
ADD COLUMN IF NOT EXISTS billing_cache_creation_5m_tokens bigint,
|
||||
ADD COLUMN IF NOT EXISTS billing_cache_creation_1h_tokens bigint,
|
||||
ADD COLUMN IF NOT EXISTS billing_cache_read_tokens bigint,
|
||||
ADD COLUMN IF NOT EXISTS billing_total_input_context bigint,
|
||||
ADD COLUMN IF NOT EXISTS billing_cache_creation_cost_usd numeric(20,8),
|
||||
ADD COLUMN IF NOT EXISTS billing_cache_read_cost_usd numeric(20,8),
|
||||
ADD COLUMN IF NOT EXISTS billing_total_cost_usd numeric(20,8),
|
||||
ADD COLUMN IF NOT EXISTS billing_actual_total_cost_usd numeric(20,8),
|
||||
ADD COLUMN IF NOT EXISTS billing_pricing_source character varying(50),
|
||||
ADD COLUMN IF NOT EXISTS billing_rule_id character varying(100),
|
||||
ADD COLUMN IF NOT EXISTS billing_rule_version character varying(50);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS ix_usage_settlement_snapshots_schema_version
|
||||
ON public.usage_settlement_snapshots USING btree (settlement_snapshot_schema_version);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS ix_usage_settlement_snapshots_pricing_source
|
||||
ON public.usage_settlement_snapshots USING btree (billing_pricing_source);
|
||||
|
||||
CREATE OR REPLACE VIEW public.usage_billing_facts AS
|
||||
SELECT
|
||||
usage_rows.id,
|
||||
usage_rows.request_id,
|
||||
usage_rows.user_id,
|
||||
usage_rows.api_key_id,
|
||||
usage_rows.username,
|
||||
usage_rows.api_key_name,
|
||||
usage_rows.provider_name,
|
||||
usage_rows.model,
|
||||
usage_rows.target_model,
|
||||
usage_rows.provider_id,
|
||||
usage_rows.provider_endpoint_id,
|
||||
usage_rows.provider_api_key_id,
|
||||
usage_rows.request_type,
|
||||
usage_rows.api_format,
|
||||
usage_rows.api_family,
|
||||
usage_rows.endpoint_kind,
|
||||
usage_rows.endpoint_api_format,
|
||||
usage_rows.provider_api_family,
|
||||
usage_rows.provider_endpoint_kind,
|
||||
COALESCE(usage_rows.has_format_conversion, FALSE) AS has_format_conversion,
|
||||
COALESCE(usage_rows.is_stream, FALSE) AS is_stream,
|
||||
usage_rows.status_code,
|
||||
usage_rows.error_message,
|
||||
usage_rows.error_category,
|
||||
usage_rows.response_time_ms,
|
||||
usage_rows.first_byte_time_ms,
|
||||
usage_rows.status,
|
||||
COALESCE(settlement.billing_status, usage_rows.billing_status) AS billing_status,
|
||||
usage_rows.created_at,
|
||||
COALESCE(settlement.finalized_at, usage_rows.finalized_at) AS finalized_at,
|
||||
GREATEST(COALESCE(settlement.billing_input_tokens, usage_rows.input_tokens, 0), 0)::bigint
|
||||
AS input_tokens,
|
||||
GREATEST(
|
||||
COALESCE(
|
||||
settlement.billing_effective_input_tokens,
|
||||
CASE
|
||||
WHEN GREATEST(COALESCE(usage_rows.input_tokens, 0), 0) <= 0 THEN 0
|
||||
WHEN GREATEST(COALESCE(usage_rows.cache_read_input_tokens, 0), 0) <= 0
|
||||
THEN GREATEST(COALESCE(usage_rows.input_tokens, 0), 0)
|
||||
WHEN split_part(lower(COALESCE(COALESCE(usage_rows.endpoint_api_format, usage_rows.api_format), '')), ':', 1)
|
||||
IN ('openai', 'gemini', 'google')
|
||||
THEN GREATEST(
|
||||
GREATEST(COALESCE(usage_rows.input_tokens, 0), 0)
|
||||
- GREATEST(COALESCE(usage_rows.cache_read_input_tokens, 0), 0),
|
||||
0
|
||||
)
|
||||
ELSE GREATEST(COALESCE(usage_rows.input_tokens, 0), 0)
|
||||
END
|
||||
),
|
||||
0
|
||||
)::bigint AS effective_input_tokens,
|
||||
GREATEST(COALESCE(settlement.billing_output_tokens, usage_rows.output_tokens, 0), 0)::bigint
|
||||
AS output_tokens,
|
||||
GREATEST(
|
||||
COALESCE(
|
||||
settlement.billing_cache_creation_tokens,
|
||||
CASE
|
||||
WHEN COALESCE(usage_rows.cache_creation_input_tokens, 0) = 0
|
||||
AND (
|
||||
COALESCE(usage_rows.cache_creation_input_tokens_5m, 0)
|
||||
+ COALESCE(usage_rows.cache_creation_input_tokens_1h, 0)
|
||||
) > 0
|
||||
THEN COALESCE(usage_rows.cache_creation_input_tokens_5m, 0)
|
||||
+ COALESCE(usage_rows.cache_creation_input_tokens_1h, 0)
|
||||
ELSE COALESCE(usage_rows.cache_creation_input_tokens, 0)
|
||||
END,
|
||||
0
|
||||
),
|
||||
0
|
||||
)::bigint AS cache_creation_input_tokens,
|
||||
GREATEST(
|
||||
COALESCE(
|
||||
settlement.billing_cache_creation_5m_tokens,
|
||||
usage_rows.cache_creation_input_tokens_5m,
|
||||
0
|
||||
),
|
||||
0
|
||||
)::bigint AS cache_creation_input_tokens_5m,
|
||||
GREATEST(
|
||||
COALESCE(
|
||||
settlement.billing_cache_creation_1h_tokens,
|
||||
usage_rows.cache_creation_input_tokens_1h,
|
||||
0
|
||||
),
|
||||
0
|
||||
)::bigint AS cache_creation_input_tokens_1h,
|
||||
GREATEST(COALESCE(settlement.billing_cache_read_tokens, usage_rows.cache_read_input_tokens, 0), 0)::bigint
|
||||
AS cache_read_input_tokens,
|
||||
GREATEST(
|
||||
COALESCE(
|
||||
CASE
|
||||
WHEN settlement.billing_input_tokens IS NOT NULL
|
||||
OR settlement.billing_output_tokens IS NOT NULL
|
||||
OR settlement.billing_cache_creation_tokens IS NOT NULL
|
||||
OR settlement.billing_cache_creation_5m_tokens IS NOT NULL
|
||||
OR settlement.billing_cache_creation_1h_tokens IS NOT NULL
|
||||
OR settlement.billing_cache_read_tokens IS NOT NULL
|
||||
THEN COALESCE(settlement.billing_input_tokens, 0)
|
||||
+ COALESCE(settlement.billing_output_tokens, 0)
|
||||
+ COALESCE(
|
||||
settlement.billing_cache_creation_tokens,
|
||||
COALESCE(settlement.billing_cache_creation_5m_tokens, 0)
|
||||
+ COALESCE(settlement.billing_cache_creation_1h_tokens, 0),
|
||||
0
|
||||
)
|
||||
+ COALESCE(settlement.billing_cache_read_tokens, 0)
|
||||
END,
|
||||
usage_rows.total_tokens,
|
||||
0
|
||||
),
|
||||
0
|
||||
)::bigint AS total_tokens,
|
||||
GREATEST(
|
||||
COALESCE(
|
||||
settlement.billing_total_input_context,
|
||||
CASE
|
||||
WHEN split_part(lower(COALESCE(COALESCE(usage_rows.endpoint_api_format, usage_rows.api_format), '')), ':', 1)
|
||||
IN ('claude', 'anthropic')
|
||||
THEN GREATEST(COALESCE(usage_rows.input_tokens, 0), 0)
|
||||
+ CASE
|
||||
WHEN COALESCE(usage_rows.cache_creation_input_tokens, 0) = 0
|
||||
AND (
|
||||
COALESCE(usage_rows.cache_creation_input_tokens_5m, 0)
|
||||
+ COALESCE(usage_rows.cache_creation_input_tokens_1h, 0)
|
||||
) > 0
|
||||
THEN COALESCE(usage_rows.cache_creation_input_tokens_5m, 0)
|
||||
+ COALESCE(usage_rows.cache_creation_input_tokens_1h, 0)
|
||||
ELSE COALESCE(usage_rows.cache_creation_input_tokens, 0)
|
||||
END
|
||||
+ GREATEST(COALESCE(usage_rows.cache_read_input_tokens, 0), 0)
|
||||
WHEN split_part(lower(COALESCE(COALESCE(usage_rows.endpoint_api_format, usage_rows.api_format), '')), ':', 1)
|
||||
IN ('openai', 'gemini', 'google')
|
||||
THEN CASE
|
||||
WHEN GREATEST(COALESCE(usage_rows.input_tokens, 0), 0) <= 0 THEN 0
|
||||
WHEN GREATEST(COALESCE(usage_rows.cache_read_input_tokens, 0), 0) <= 0
|
||||
THEN GREATEST(COALESCE(usage_rows.input_tokens, 0), 0)
|
||||
ELSE GREATEST(
|
||||
GREATEST(COALESCE(usage_rows.input_tokens, 0), 0)
|
||||
- GREATEST(COALESCE(usage_rows.cache_read_input_tokens, 0), 0),
|
||||
0
|
||||
)
|
||||
END
|
||||
+ GREATEST(COALESCE(usage_rows.cache_read_input_tokens, 0), 0)
|
||||
ELSE GREATEST(COALESCE(usage_rows.input_tokens, 0), 0)
|
||||
+ CASE
|
||||
WHEN COALESCE(usage_rows.cache_creation_input_tokens, 0) = 0
|
||||
AND (
|
||||
COALESCE(usage_rows.cache_creation_input_tokens_5m, 0)
|
||||
+ COALESCE(usage_rows.cache_creation_input_tokens_1h, 0)
|
||||
) > 0
|
||||
THEN COALESCE(usage_rows.cache_creation_input_tokens_5m, 0)
|
||||
+ COALESCE(usage_rows.cache_creation_input_tokens_1h, 0)
|
||||
ELSE COALESCE(usage_rows.cache_creation_input_tokens, 0)
|
||||
END
|
||||
+ GREATEST(COALESCE(usage_rows.cache_read_input_tokens, 0), 0)
|
||||
END,
|
||||
0
|
||||
),
|
||||
0
|
||||
)::bigint AS total_input_context,
|
||||
COALESCE(CAST(usage_rows.input_cost_usd AS DOUBLE PRECISION), 0) AS input_cost_usd,
|
||||
COALESCE(CAST(usage_rows.output_cost_usd AS DOUBLE PRECISION), 0) AS output_cost_usd,
|
||||
COALESCE(
|
||||
CAST(settlement.billing_cache_creation_cost_usd AS DOUBLE PRECISION),
|
||||
CAST(usage_rows.cache_creation_cost_usd AS DOUBLE PRECISION),
|
||||
0
|
||||
) AS cache_creation_cost_usd,
|
||||
COALESCE(
|
||||
CAST(settlement.billing_cache_read_cost_usd AS DOUBLE PRECISION),
|
||||
CAST(usage_rows.cache_read_cost_usd AS DOUBLE PRECISION),
|
||||
0
|
||||
) AS cache_read_cost_usd,
|
||||
COALESCE(
|
||||
CAST(settlement.billing_total_cost_usd AS DOUBLE PRECISION),
|
||||
CAST(usage_rows.total_cost_usd AS DOUBLE PRECISION),
|
||||
0
|
||||
) AS total_cost_usd,
|
||||
COALESCE(
|
||||
CAST(settlement.billing_actual_total_cost_usd AS DOUBLE PRECISION),
|
||||
CAST(usage_rows.actual_total_cost_usd AS DOUBLE PRECISION),
|
||||
0
|
||||
) AS actual_total_cost_usd,
|
||||
COALESCE(
|
||||
CAST(settlement.output_price_per_1m AS DOUBLE PRECISION),
|
||||
CAST(usage_rows.output_price_per_1m AS DOUBLE PRECISION)
|
||||
) AS output_price_per_1m,
|
||||
COALESCE(
|
||||
CAST(settlement.input_price_per_1m AS DOUBLE PRECISION),
|
||||
CAST(usage_rows.input_price_per_1m AS DOUBLE PRECISION)
|
||||
) AS input_price_per_1m,
|
||||
COALESCE(
|
||||
CAST(settlement.cache_creation_price_per_1m AS DOUBLE PRECISION),
|
||||
CAST(usage_rows.cache_creation_price_per_1m AS DOUBLE PRECISION)
|
||||
) AS cache_creation_price_per_1m,
|
||||
COALESCE(
|
||||
CAST(settlement.cache_read_price_per_1m AS DOUBLE PRECISION),
|
||||
CAST(usage_rows.cache_read_price_per_1m AS DOUBLE PRECISION)
|
||||
) AS cache_read_price_per_1m,
|
||||
COALESCE(
|
||||
CAST(settlement.price_per_request AS DOUBLE PRECISION),
|
||||
CAST(usage_rows.price_per_request AS DOUBLE PRECISION)
|
||||
) AS price_per_request,
|
||||
settlement.billing_pricing_source,
|
||||
settlement.billing_rule_id,
|
||||
settlement.billing_rule_version
|
||||
FROM public."usage" AS usage_rows
|
||||
LEFT JOIN public.usage_settlement_snapshots AS settlement
|
||||
ON settlement.request_id = usage_rows.request_id;
|
||||
|
||||
COMMENT ON VIEW public.usage_billing_facts IS
|
||||
'Canonical billing read model. Token/cost fields prefer usage_settlement_snapshots.billing_* and fall back to deprecated usage mirrors for legacy rows.';
|
||||
|
||||
COMMENT ON COLUMN public.usage.input_tokens IS
|
||||
'DEPRECATED: billing dimension mirror. Use public.usage_settlement_snapshots.billing_input_tokens or public.usage_billing_facts.input_tokens.';
|
||||
COMMENT ON COLUMN public.usage.output_tokens IS
|
||||
'DEPRECATED: billing dimension mirror. Use public.usage_settlement_snapshots.billing_output_tokens or public.usage_billing_facts.output_tokens.';
|
||||
COMMENT ON COLUMN public.usage.total_tokens IS
|
||||
'DEPRECATED: billing dimension mirror. Use public.usage_billing_facts.total_tokens.';
|
||||
COMMENT ON COLUMN public.usage.cache_creation_input_tokens IS
|
||||
'DEPRECATED: billing dimension mirror. Use public.usage_settlement_snapshots.billing_cache_creation_tokens or public.usage_billing_facts.cache_creation_input_tokens.';
|
||||
COMMENT ON COLUMN public.usage.cache_creation_input_tokens_5m IS
|
||||
'DEPRECATED: billing dimension mirror. Use public.usage_settlement_snapshots.billing_cache_creation_5m_tokens or public.usage_billing_facts.cache_creation_input_tokens_5m.';
|
||||
COMMENT ON COLUMN public.usage.cache_creation_input_tokens_1h IS
|
||||
'DEPRECATED: billing dimension mirror. Use public.usage_settlement_snapshots.billing_cache_creation_1h_tokens or public.usage_billing_facts.cache_creation_input_tokens_1h.';
|
||||
COMMENT ON COLUMN public.usage.cache_read_input_tokens IS
|
||||
'DEPRECATED: billing dimension mirror. Use public.usage_settlement_snapshots.billing_cache_read_tokens or public.usage_billing_facts.cache_read_input_tokens.';
|
||||
COMMENT ON COLUMN public.usage.cache_creation_cost_usd IS
|
||||
'DEPRECATED: billing cost mirror. Use public.usage_settlement_snapshots.billing_cache_creation_cost_usd or public.usage_billing_facts.cache_creation_cost_usd.';
|
||||
COMMENT ON COLUMN public.usage.cache_read_cost_usd IS
|
||||
'DEPRECATED: billing cost mirror. Use public.usage_settlement_snapshots.billing_cache_read_cost_usd or public.usage_billing_facts.cache_read_cost_usd.';
|
||||
COMMENT ON COLUMN public.usage.total_cost_usd IS
|
||||
'DEPRECATED: billing cost mirror. Use public.usage_settlement_snapshots.billing_total_cost_usd or public.usage_billing_facts.total_cost_usd.';
|
||||
COMMENT ON COLUMN public.usage.actual_total_cost_usd IS
|
||||
'DEPRECATED: billing cost mirror. Use public.usage_settlement_snapshots.billing_actual_total_cost_usd or public.usage_billing_facts.actual_total_cost_usd.';
|
||||
@@ -8,7 +8,7 @@ use tracing::{error, info, warn};
|
||||
|
||||
static MIGRATOR: Migrator = sqlx::migrate!("./migrations");
|
||||
static BASELINE_V2_SQL: &str = include_str!("../bootstrap/20260413020000_baseline_v2.sql");
|
||||
const BASELINE_V2_CUTOFF_VERSION: i64 = 20260423000000;
|
||||
const BASELINE_V2_CUTOFF_VERSION: i64 = 20260424000000;
|
||||
const MIGRATIONS_TABLE_EXISTS_SQL: &str =
|
||||
"SELECT to_regclass('public._sqlx_migrations') IS NOT NULL";
|
||||
const PUBLIC_BASE_TABLE_COUNT_SQL: &str = r#"
|
||||
@@ -663,6 +663,7 @@ SELECT EXISTS (
|
||||
20260422110000,
|
||||
20260422120000,
|
||||
20260423000000,
|
||||
20260424000000,
|
||||
]
|
||||
);
|
||||
}
|
||||
@@ -683,6 +684,10 @@ SELECT EXISTS (
|
||||
.contains("CREATE TABLE IF NOT EXISTS public.usage_settlement_snapshots"));
|
||||
assert!(BASELINE_V2_SQL.contains("billing_snapshot_schema_version"));
|
||||
assert!(BASELINE_V2_SQL.contains("price_per_request"));
|
||||
assert!(BASELINE_V2_SQL.contains("settlement_snapshot_schema_version"));
|
||||
assert!(BASELINE_V2_SQL.contains("billing_effective_input_tokens"));
|
||||
assert!(BASELINE_V2_SQL.contains("CREATE OR REPLACE VIEW public.usage_billing_facts"));
|
||||
assert!(BASELINE_V2_SQL.contains("usage_settlement_snapshots.billing_total_cost_usd"));
|
||||
assert!(BASELINE_V2_SQL.contains("candidate_index integer"));
|
||||
assert!(BASELINE_V2_SQL.contains("CREATE TABLE IF NOT EXISTS public.stats_user_summary"));
|
||||
assert!(
|
||||
@@ -810,6 +815,7 @@ SELECT EXISTS (
|
||||
20260422110000,
|
||||
20260422120000,
|
||||
20260423000000,
|
||||
20260424000000,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user