mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-01 17:00:21 +08:00
Merge remote-tracking branch 'origin/pr/377' into codex/pr-376-377-383-384-combined
# Conflicts: # apps/aether-gateway/src/tests/frontdoor/public_support/dashboard.rs # crates/aether-data/src/lifecycle/backfill.rs # crates/aether-data/src/repository/usage/postgres/mod.rs
This commit is contained in:
@@ -877,6 +877,7 @@ async fn gateway_handles_admin_stats_provider_performance_locally_with_trusted_a
|
|||||||
);
|
);
|
||||||
row.response_time_ms = Some((index * 100) as u64);
|
row.response_time_ms = Some((index * 100) as u64);
|
||||||
row.first_byte_time_ms = Some((index * 10) as u64);
|
row.first_byte_time_ms = Some((index * 10) as u64);
|
||||||
|
row.request_metadata = Some(json!({ "upstream_is_stream": true }));
|
||||||
row
|
row
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
@@ -956,7 +957,7 @@ async fn gateway_handles_admin_stats_provider_performance_locally_with_trusted_a
|
|||||||
let payload: serde_json::Value = response.json().await.expect("json body should parse");
|
let payload: serde_json::Value = response.json().await.expect("json body should parse");
|
||||||
assert_eq!(payload["summary"]["request_count"], 12);
|
assert_eq!(payload["summary"]["request_count"], 12);
|
||||||
assert_eq!(payload["summary"]["success_rate"], 91.67);
|
assert_eq!(payload["summary"]["success_rate"], 91.67);
|
||||||
assert_eq!(payload["summary"]["avg_output_tps"], 18.46);
|
assert_eq!(payload["summary"]["avg_output_tps"], 20.17);
|
||||||
assert_eq!(payload["summary"]["avg_first_byte_time_ms"], 55.0);
|
assert_eq!(payload["summary"]["avg_first_byte_time_ms"], 55.0);
|
||||||
assert_eq!(payload["summary"]["avg_response_time_ms"], 590.91);
|
assert_eq!(payload["summary"]["avg_response_time_ms"], 590.91);
|
||||||
|
|
||||||
@@ -968,7 +969,7 @@ async fn gateway_handles_admin_stats_provider_performance_locally_with_trusted_a
|
|||||||
assert_eq!(payload["providers"][0]["error_count"], 1);
|
assert_eq!(payload["providers"][0]["error_count"], 1);
|
||||||
assert_eq!(payload["providers"][0]["success_rate"], 90.91);
|
assert_eq!(payload["providers"][0]["success_rate"], 90.91);
|
||||||
assert_eq!(payload["providers"][0]["output_tokens"], 199);
|
assert_eq!(payload["providers"][0]["output_tokens"], 199);
|
||||||
assert_eq!(payload["providers"][0]["avg_output_tps"], 18.18);
|
assert_eq!(payload["providers"][0]["avg_output_tps"], 20.2);
|
||||||
assert_eq!(payload["providers"][0]["avg_first_byte_time_ms"], 55.0);
|
assert_eq!(payload["providers"][0]["avg_first_byte_time_ms"], 55.0);
|
||||||
assert_eq!(payload["providers"][0]["avg_response_time_ms"], 550.0);
|
assert_eq!(payload["providers"][0]["avg_response_time_ms"], 550.0);
|
||||||
assert_eq!(payload["providers"][0]["p90_response_time_ms"], 910);
|
assert_eq!(payload["providers"][0]["p90_response_time_ms"], 910);
|
||||||
@@ -991,7 +992,7 @@ async fn gateway_handles_admin_stats_provider_performance_locally_with_trusted_a
|
|||||||
assert_eq!(payload["timeline"].as_array().map(Vec::len), Some(2));
|
assert_eq!(payload["timeline"].as_array().map(Vec::len), Some(2));
|
||||||
assert_eq!(payload["timeline"][0]["date"], "2024-03-21T05:00:00+00:00");
|
assert_eq!(payload["timeline"][0]["date"], "2024-03-21T05:00:00+00:00");
|
||||||
assert_eq!(payload["timeline"][0]["provider_id"], "provider-1");
|
assert_eq!(payload["timeline"][0]["provider_id"], "provider-1");
|
||||||
assert_eq!(payload["timeline"][0]["avg_output_tps"], 18.18);
|
assert_eq!(payload["timeline"][0]["avg_output_tps"], 20.2);
|
||||||
assert_eq!(payload["timeline"][0]["success_rate"], 90.91);
|
assert_eq!(payload["timeline"][0]["success_rate"], 90.91);
|
||||||
assert_eq!(payload["timeline"][1]["provider_id"], "provider-2");
|
assert_eq!(payload["timeline"][1]["provider_id"], "provider-2");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ async fn gateway_executes_gemini_files_download_via_local_decision_gate_with_loc
|
|||||||
auth_header_value: String,
|
auth_header_value: String,
|
||||||
endpoint_tag: String,
|
endpoint_tag: String,
|
||||||
proxy_node_id: String,
|
proxy_node_id: String,
|
||||||
tls_profile: String,
|
transport_profile_id: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
let decision_hits = Arc::new(Mutex::new(0usize));
|
let decision_hits = Arc::new(Mutex::new(0usize));
|
||||||
@@ -126,8 +126,9 @@ async fn gateway_executes_gemini_files_download_via_local_decision_gate_with_loc
|
|||||||
.and_then(|value| value.as_str())
|
.and_then(|value| value.as_str())
|
||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
.to_string(),
|
.to_string(),
|
||||||
tls_profile: payload
|
transport_profile_id: payload
|
||||||
.get("tls_profile")
|
.get("transport_profile")
|
||||||
|
.and_then(|value| value.get("profile_id"))
|
||||||
.and_then(|value| value.as_str())
|
.and_then(|value| value.as_str())
|
||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
.to_string(),
|
.to_string(),
|
||||||
@@ -258,7 +259,10 @@ async fn gateway_executes_gemini_files_download_via_local_decision_gate_with_loc
|
|||||||
seen_execution_runtime_request.proxy_node_id,
|
seen_execution_runtime_request.proxy_node_id,
|
||||||
"proxy-node-gemini-files-download-local"
|
"proxy-node-gemini-files-download-local"
|
||||||
);
|
);
|
||||||
assert_eq!(seen_execution_runtime_request.tls_profile, "chrome_136");
|
assert_eq!(
|
||||||
|
seen_execution_runtime_request.transport_profile_id,
|
||||||
|
"chrome_136"
|
||||||
|
);
|
||||||
|
|
||||||
let stored_candidates = request_candidate_repository
|
let stored_candidates = request_candidate_repository
|
||||||
.list_by_request_id("trace-gemini-files-download-local-123")
|
.list_by_request_id("trace-gemini-files-download-local-123")
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ async fn gateway_executes_gemini_files_upload_via_local_decision_gate_with_local
|
|||||||
body_bytes_b64: String,
|
body_bytes_b64: String,
|
||||||
endpoint_tag: String,
|
endpoint_tag: String,
|
||||||
proxy_node_id: String,
|
proxy_node_id: String,
|
||||||
tls_profile: String,
|
transport_profile_id: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
let decision_hits = Arc::new(Mutex::new(0usize));
|
let decision_hits = Arc::new(Mutex::new(0usize));
|
||||||
@@ -154,8 +154,9 @@ async fn gateway_executes_gemini_files_upload_via_local_decision_gate_with_local
|
|||||||
.and_then(|value| value.as_str())
|
.and_then(|value| value.as_str())
|
||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
.to_string(),
|
.to_string(),
|
||||||
tls_profile: payload
|
transport_profile_id: payload
|
||||||
.get("tls_profile")
|
.get("transport_profile")
|
||||||
|
.and_then(|value| value.get("profile_id"))
|
||||||
.and_then(|value| value.as_str())
|
.and_then(|value| value.as_str())
|
||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
.to_string(),
|
.to_string(),
|
||||||
@@ -277,7 +278,10 @@ async fn gateway_executes_gemini_files_upload_via_local_decision_gate_with_local
|
|||||||
seen_execution_runtime_request.proxy_node_id,
|
seen_execution_runtime_request.proxy_node_id,
|
||||||
"proxy-node-gemini-files-upload-local"
|
"proxy-node-gemini-files-upload-local"
|
||||||
);
|
);
|
||||||
assert_eq!(seen_execution_runtime_request.tls_profile, "chrome_136");
|
assert_eq!(
|
||||||
|
seen_execution_runtime_request.transport_profile_id,
|
||||||
|
"chrome_136"
|
||||||
|
);
|
||||||
|
|
||||||
let stored_candidates = request_candidate_repository
|
let stored_candidates = request_candidate_repository
|
||||||
.list_by_request_id("trace-gemini-files-upload-local-123")
|
.list_by_request_id("trace-gemini-files-upload-local-123")
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ async fn gateway_executes_gemini_video_create_via_local_decision_gate_with_local
|
|||||||
metadata_source: String,
|
metadata_source: String,
|
||||||
store_present: bool,
|
store_present: bool,
|
||||||
proxy_node_id: String,
|
proxy_node_id: String,
|
||||||
tls_profile: String,
|
transport_profile_id: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn hash_api_key(value: &str) -> String {
|
fn hash_api_key(value: &str) -> String {
|
||||||
@@ -350,8 +350,9 @@ async fn gateway_executes_gemini_video_create_via_local_decision_gate_with_local
|
|||||||
.and_then(|value| value.as_str())
|
.and_then(|value| value.as_str())
|
||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
.to_string(),
|
.to_string(),
|
||||||
tls_profile: payload
|
transport_profile_id: payload
|
||||||
.get("tls_profile")
|
.get("transport_profile")
|
||||||
|
.and_then(|value| value.get("profile_id"))
|
||||||
.and_then(|value| value.as_str())
|
.and_then(|value| value.as_str())
|
||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
.to_string(),
|
.to_string(),
|
||||||
@@ -465,7 +466,10 @@ async fn gateway_executes_gemini_video_create_via_local_decision_gate_with_local
|
|||||||
seen_execution_runtime_request.proxy_node_id,
|
seen_execution_runtime_request.proxy_node_id,
|
||||||
"proxy-node-gemini-video-local"
|
"proxy-node-gemini-video-local"
|
||||||
);
|
);
|
||||||
assert_eq!(seen_execution_runtime_request.tls_profile, "chrome_136");
|
assert_eq!(
|
||||||
|
seen_execution_runtime_request.transport_profile_id,
|
||||||
|
"chrome_136"
|
||||||
|
);
|
||||||
|
|
||||||
let stored_candidates = request_candidate_repository
|
let stored_candidates = request_candidate_repository
|
||||||
.list_by_request_id("trace-gemini-video-local-123")
|
.list_by_request_id("trace-gemini-video-local-123")
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ async fn gateway_executes_openai_video_create_via_local_decision_gate_with_local
|
|||||||
metadata_source: String,
|
metadata_source: String,
|
||||||
store_present: bool,
|
store_present: bool,
|
||||||
proxy_node_id: String,
|
proxy_node_id: String,
|
||||||
tls_profile: String,
|
transport_profile_id: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn hash_api_key(value: &str) -> String {
|
fn hash_api_key(value: &str) -> String {
|
||||||
@@ -362,8 +362,9 @@ async fn gateway_executes_openai_video_create_via_local_decision_gate_with_local
|
|||||||
.and_then(|value| value.as_str())
|
.and_then(|value| value.as_str())
|
||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
.to_string(),
|
.to_string(),
|
||||||
tls_profile: payload
|
transport_profile_id: payload
|
||||||
.get("tls_profile")
|
.get("transport_profile")
|
||||||
|
.and_then(|value| value.get("profile_id"))
|
||||||
.and_then(|value| value.as_str())
|
.and_then(|value| value.as_str())
|
||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
.to_string(),
|
.to_string(),
|
||||||
@@ -479,7 +480,10 @@ async fn gateway_executes_openai_video_create_via_local_decision_gate_with_local
|
|||||||
seen_execution_runtime_request.proxy_node_id,
|
seen_execution_runtime_request.proxy_node_id,
|
||||||
"proxy-node-openai-video-local"
|
"proxy-node-openai-video-local"
|
||||||
);
|
);
|
||||||
assert_eq!(seen_execution_runtime_request.tls_profile, "chrome_136");
|
assert_eq!(
|
||||||
|
seen_execution_runtime_request.transport_profile_id,
|
||||||
|
"chrome_136"
|
||||||
|
);
|
||||||
|
|
||||||
let stored_candidates = request_candidate_repository
|
let stored_candidates = request_candidate_repository
|
||||||
.list_by_request_id("trace-openai-video-local-123")
|
.list_by_request_id("trace-openai-video-local-123")
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
WITH aggregated AS (
|
||||||
|
SELECT
|
||||||
|
usage.model,
|
||||||
|
COUNT(*)::BIGINT AS usage_count
|
||||||
|
FROM usage_billing_facts AS usage
|
||||||
|
WHERE usage.model IS NOT NULL
|
||||||
|
AND BTRIM(usage.model) <> ''
|
||||||
|
AND usage.status NOT IN ('pending', 'streaming')
|
||||||
|
GROUP BY usage.model
|
||||||
|
),
|
||||||
|
refreshed AS (
|
||||||
|
SELECT
|
||||||
|
gm.id,
|
||||||
|
COALESCE(aggregated.usage_count, 0)::BIGINT AS usage_count
|
||||||
|
FROM global_models AS gm
|
||||||
|
LEFT JOIN aggregated
|
||||||
|
ON aggregated.model = gm.name
|
||||||
|
)
|
||||||
|
UPDATE global_models AS gm
|
||||||
|
SET
|
||||||
|
usage_count = refreshed.usage_count,
|
||||||
|
updated_at = NOW()
|
||||||
|
FROM refreshed
|
||||||
|
WHERE gm.id = refreshed.id
|
||||||
|
AND COALESCE(gm.usage_count, 0) <> refreshed.usage_count;
|
||||||
@@ -118,7 +118,6 @@ CREATE TABLE IF NOT EXISTS management_tokens (
|
|||||||
UNIQUE KEY uq_management_tokens_user_name (user_id, name),
|
UNIQUE KEY uq_management_tokens_user_name (user_id, name),
|
||||||
KEY management_tokens_user_id_idx (user_id)
|
KEY management_tokens_user_id_idx (user_id)
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS billing_rules (
|
CREATE TABLE IF NOT EXISTS billing_rules (
|
||||||
id VARCHAR(64) PRIMARY KEY,
|
id VARCHAR(64) PRIMARY KEY,
|
||||||
global_model_id VARCHAR(64),
|
global_model_id VARCHAR(64),
|
||||||
@@ -414,7 +413,6 @@ CREATE TABLE IF NOT EXISTS global_models (
|
|||||||
updated_at BIGINT NOT NULL,
|
updated_at BIGINT NOT NULL,
|
||||||
UNIQUE KEY global_models_name_key (name)
|
UNIQUE KEY global_models_name_key (name)
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS system_configs (
|
CREATE TABLE IF NOT EXISTS system_configs (
|
||||||
id VARCHAR(64) PRIMARY KEY,
|
id VARCHAR(64) PRIMARY KEY,
|
||||||
`key` VARCHAR(255) NOT NULL,
|
`key` VARCHAR(255) NOT NULL,
|
||||||
@@ -484,7 +482,6 @@ CREATE TABLE IF NOT EXISTS user_oauth_links (
|
|||||||
KEY user_oauth_links_provider_type_idx (provider_type),
|
KEY user_oauth_links_provider_type_idx (provider_type),
|
||||||
KEY user_oauth_links_user_id_idx (user_id)
|
KEY user_oauth_links_user_id_idx (user_id)
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS proxy_nodes (
|
CREATE TABLE IF NOT EXISTS proxy_nodes (
|
||||||
id VARCHAR(64) PRIMARY KEY,
|
id VARCHAR(64) PRIMARY KEY,
|
||||||
name VARCHAR(255) NOT NULL,
|
name VARCHAR(255) NOT NULL,
|
||||||
@@ -524,7 +521,6 @@ CREATE TABLE IF NOT EXISTS proxy_node_events (
|
|||||||
detail VARCHAR(500),
|
detail VARCHAR(500),
|
||||||
created_at BIGINT NOT NULL
|
created_at BIGINT NOT NULL
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS wallets (
|
CREATE TABLE IF NOT EXISTS wallets (
|
||||||
id VARCHAR(64) PRIMARY KEY,
|
id VARCHAR(64) PRIMARY KEY,
|
||||||
user_id VARCHAR(64),
|
user_id VARCHAR(64),
|
||||||
@@ -708,7 +704,6 @@ CREATE TABLE IF NOT EXISTS redeem_codes (
|
|||||||
KEY idx_redeem_codes_redeemed_user (redeemed_by_user_id, redeemed_at),
|
KEY idx_redeem_codes_redeemed_user (redeemed_by_user_id, redeemed_at),
|
||||||
KEY idx_redeem_codes_redeemed_order (redeemed_payment_order_id)
|
KEY idx_redeem_codes_redeemed_order (redeemed_payment_order_id)
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS `usage` (
|
CREATE TABLE IF NOT EXISTS `usage` (
|
||||||
request_id VARCHAR(128) PRIMARY KEY,
|
request_id VARCHAR(128) PRIMARY KEY,
|
||||||
id VARCHAR(128),
|
id VARCHAR(128),
|
||||||
|
|||||||
@@ -4620,7 +4620,6 @@ EXCEPTION
|
|||||||
WHEN duplicate_table THEN NULL;
|
WHEN duplicate_table THEN NULL;
|
||||||
WHEN invalid_table_definition THEN NULL;
|
WHEN invalid_table_definition THEN NULL;
|
||||||
END $mig$;
|
END $mig$;
|
||||||
|
|
||||||
-- Restore a normal lookup path before sqlx records this migration in the
|
-- Restore a normal lookup path before sqlx records this migration in the
|
||||||
-- same transaction. sqlx inserts into `_sqlx_migrations` unqualified.
|
-- same transaction. sqlx inserts into `_sqlx_migrations` unqualified.
|
||||||
SELECT pg_catalog.set_config('search_path', 'public', true);
|
SELECT pg_catalog.set_config('search_path', 'public', true);
|
||||||
|
|||||||
@@ -0,0 +1,233 @@
|
|||||||
|
ALTER TABLE IF EXISTS public.usage
|
||||||
|
ADD COLUMN IF NOT EXISTS upstream_is_stream boolean;
|
||||||
|
|
||||||
|
UPDATE public.usage
|
||||||
|
SET upstream_is_stream = COALESCE(
|
||||||
|
CASE
|
||||||
|
WHEN (request_metadata->>'upstream_is_stream') IN ('true', 'false')
|
||||||
|
THEN (request_metadata->>'upstream_is_stream')::boolean
|
||||||
|
ELSE NULL
|
||||||
|
END,
|
||||||
|
COALESCE(is_stream, FALSE)
|
||||||
|
)
|
||||||
|
WHERE upstream_is_stream IS NULL;
|
||||||
|
|
||||||
|
ANALYZE public.usage;
|
||||||
|
|
||||||
|
COMMENT ON COLUMN public.usage.upstream_is_stream IS
|
||||||
|
'Resolved upstream stream mode from request_metadata.upstream_is_stream, falling back to is_stream for legacy rows.';
|
||||||
|
|
||||||
|
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,
|
||||||
|
COALESCE(usage_rows.upstream_is_stream, COALESCE(usage_rows.is_stream, FALSE)) AS upstream_is_stream
|
||||||
|
FROM public."usage" AS usage_rows
|
||||||
|
LEFT JOIN public.usage_settlement_snapshots AS settlement
|
||||||
|
ON settlement.request_id = usage_rows.request_id;
|
||||||
|
|
||||||
|
COMMENT ON COLUMN public.usage_billing_facts.upstream_is_stream IS
|
||||||
|
'Resolved upstream stream mode from public.usage.upstream_is_stream, falling back to usage.is_stream for legacy rows.';
|
||||||
@@ -114,7 +114,6 @@ CREATE TABLE IF NOT EXISTS management_tokens (
|
|||||||
UNIQUE (user_id, name)
|
UNIQUE (user_id, name)
|
||||||
);
|
);
|
||||||
CREATE INDEX IF NOT EXISTS management_tokens_user_id_idx ON management_tokens (user_id);
|
CREATE INDEX IF NOT EXISTS management_tokens_user_id_idx ON management_tokens (user_id);
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS billing_rules (
|
CREATE TABLE IF NOT EXISTS billing_rules (
|
||||||
id TEXT PRIMARY KEY,
|
id TEXT PRIMARY KEY,
|
||||||
global_model_id TEXT,
|
global_model_id TEXT,
|
||||||
@@ -414,7 +413,6 @@ CREATE TABLE IF NOT EXISTS global_models (
|
|||||||
created_at INTEGER NOT NULL,
|
created_at INTEGER NOT NULL,
|
||||||
updated_at INTEGER NOT NULL
|
updated_at INTEGER NOT NULL
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS system_configs (
|
CREATE TABLE IF NOT EXISTS system_configs (
|
||||||
id TEXT PRIMARY KEY,
|
id TEXT PRIMARY KEY,
|
||||||
key TEXT NOT NULL UNIQUE,
|
key TEXT NOT NULL UNIQUE,
|
||||||
@@ -482,7 +480,6 @@ CREATE TABLE IF NOT EXISTS user_oauth_links (
|
|||||||
);
|
);
|
||||||
CREATE INDEX IF NOT EXISTS user_oauth_links_provider_type_idx ON user_oauth_links (provider_type);
|
CREATE INDEX IF NOT EXISTS user_oauth_links_provider_type_idx ON user_oauth_links (provider_type);
|
||||||
CREATE INDEX IF NOT EXISTS user_oauth_links_user_id_idx ON user_oauth_links (user_id);
|
CREATE INDEX IF NOT EXISTS user_oauth_links_user_id_idx ON user_oauth_links (user_id);
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS proxy_nodes (
|
CREATE TABLE IF NOT EXISTS proxy_nodes (
|
||||||
id TEXT PRIMARY KEY,
|
id TEXT PRIMARY KEY,
|
||||||
name TEXT NOT NULL,
|
name TEXT NOT NULL,
|
||||||
@@ -522,7 +519,6 @@ CREATE TABLE IF NOT EXISTS proxy_node_events (
|
|||||||
detail TEXT,
|
detail TEXT,
|
||||||
created_at INTEGER NOT NULL
|
created_at INTEGER NOT NULL
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS wallets (
|
CREATE TABLE IF NOT EXISTS wallets (
|
||||||
id TEXT PRIMARY KEY,
|
id TEXT PRIMARY KEY,
|
||||||
user_id TEXT UNIQUE,
|
user_id TEXT UNIQUE,
|
||||||
@@ -725,7 +721,6 @@ CREATE INDEX IF NOT EXISTS idx_redeem_codes_redeemed_user
|
|||||||
ON redeem_codes (redeemed_by_user_id, redeemed_at);
|
ON redeem_codes (redeemed_by_user_id, redeemed_at);
|
||||||
CREATE INDEX IF NOT EXISTS idx_redeem_codes_redeemed_order
|
CREATE INDEX IF NOT EXISTS idx_redeem_codes_redeemed_order
|
||||||
ON redeem_codes (redeemed_payment_order_id);
|
ON redeem_codes (redeemed_payment_order_id);
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS "usage" (
|
CREATE TABLE IF NOT EXISTS "usage" (
|
||||||
request_id TEXT PRIMARY KEY,
|
request_id TEXT PRIMARY KEY,
|
||||||
id TEXT,
|
id TEXT,
|
||||||
|
|||||||
@@ -1083,6 +1083,7 @@ CREATE TABLE IF NOT EXISTS public.usage (
|
|||||||
request_type character varying(50),
|
request_type character varying(50),
|
||||||
api_format character varying(50),
|
api_format character varying(50),
|
||||||
is_stream boolean DEFAULT false,
|
is_stream boolean DEFAULT false,
|
||||||
|
upstream_is_stream boolean,
|
||||||
status_code integer,
|
status_code integer,
|
||||||
error_message text,
|
error_message text,
|
||||||
response_time_ms integer,
|
response_time_ms integer,
|
||||||
|
|||||||
@@ -849,4 +849,3 @@ EXCEPTION
|
|||||||
WHEN duplicate_table THEN NULL;
|
WHEN duplicate_table THEN NULL;
|
||||||
WHEN invalid_table_definition THEN NULL;
|
WHEN invalid_table_definition THEN NULL;
|
||||||
END $mig$;
|
END $mig$;
|
||||||
|
|
||||||
|
|||||||
@@ -7,4 +7,3 @@ SELECT pg_catalog.set_config('search_path', 'public', true);
|
|||||||
--
|
--
|
||||||
-- PostgreSQL database dump complete
|
-- PostgreSQL database dump complete
|
||||||
--
|
--
|
||||||
|
|
||||||
|
|||||||
@@ -351,7 +351,8 @@ SELECT
|
|||||||
) AS price_per_request,
|
) AS price_per_request,
|
||||||
settlement.billing_pricing_source,
|
settlement.billing_pricing_source,
|
||||||
settlement.billing_rule_id,
|
settlement.billing_rule_id,
|
||||||
settlement.billing_rule_version
|
settlement.billing_rule_version,
|
||||||
|
COALESCE(usage_rows.upstream_is_stream, COALESCE(usage_rows.is_stream, FALSE)) AS upstream_is_stream
|
||||||
FROM public."usage" AS usage_rows
|
FROM public."usage" AS usage_rows
|
||||||
LEFT JOIN public.usage_settlement_snapshots AS settlement
|
LEFT JOIN public.usage_settlement_snapshots AS settlement
|
||||||
ON settlement.request_id = usage_rows.request_id;
|
ON settlement.request_id = usage_rows.request_id;
|
||||||
@@ -359,6 +360,9 @@ LEFT JOIN public.usage_settlement_snapshots AS settlement
|
|||||||
COMMENT ON VIEW public.usage_billing_facts IS
|
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.';
|
'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_billing_facts.upstream_is_stream IS
|
||||||
|
'Resolved upstream stream mode from public.usage.upstream_is_stream, falling back to usage.is_stream for legacy rows.';
|
||||||
|
|
||||||
COMMENT ON COLUMN public.usage.input_tokens IS
|
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.';
|
'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
|
COMMENT ON COLUMN public.usage.output_tokens IS
|
||||||
@@ -440,4 +444,3 @@ COMMENT ON COLUMN public.usage.client_response_body IS
|
|||||||
'DEPRECATED: HTTP body owner moved to public.usage_body_blobs plus public.usage_http_audits.client_response_body_ref. Legacy compatibility only; do not write new values.';
|
'DEPRECATED: HTTP body owner moved to public.usage_body_blobs plus public.usage_http_audits.client_response_body_ref. Legacy compatibility only; do not write new values.';
|
||||||
COMMENT ON COLUMN public.usage.client_response_body_compressed IS
|
COMMENT ON COLUMN public.usage.client_response_body_compressed IS
|
||||||
'DEPRECATED: HTTP body owner moved to public.usage_body_blobs plus public.usage_http_audits.client_response_body_ref. Legacy compatibility only; do not write new values.';
|
'DEPRECATED: HTTP body owner moved to public.usage_body_blobs plus public.usage_http_audits.client_response_body_ref. Legacy compatibility only; do not write new values.';
|
||||||
|
|
||||||
|
|||||||
@@ -130,4 +130,3 @@ ALTER TABLE public.stats_hourly_user
|
|||||||
ADD COLUMN IF NOT EXISTS actual_total_cost numeric(20,8) DEFAULT '0'::double precision NOT NULL,
|
ADD COLUMN IF NOT EXISTS actual_total_cost numeric(20,8) DEFAULT '0'::double precision NOT NULL,
|
||||||
ADD COLUMN IF NOT EXISTS response_time_sum_ms double precision DEFAULT '0'::double precision NOT NULL,
|
ADD COLUMN IF NOT EXISTS response_time_sum_ms double precision DEFAULT '0'::double precision NOT NULL,
|
||||||
ADD COLUMN IF NOT EXISTS response_time_samples bigint DEFAULT 0 NOT NULL;
|
ADD COLUMN IF NOT EXISTS response_time_samples bigint DEFAULT 0 NOT NULL;
|
||||||
|
|
||||||
|
|||||||
@@ -322,4 +322,3 @@ ALTER TABLE public.stats_hourly_user
|
|||||||
ADD COLUMN IF NOT EXISTS settled_cache_read_tokens bigint DEFAULT 0 NOT NULL,
|
ADD COLUMN IF NOT EXISTS settled_cache_read_tokens bigint DEFAULT 0 NOT NULL,
|
||||||
ADD COLUMN IF NOT EXISTS settled_first_finalized_at_unix_secs bigint,
|
ADD COLUMN IF NOT EXISTS settled_first_finalized_at_unix_secs bigint,
|
||||||
ADD COLUMN IF NOT EXISTS settled_last_finalized_at_unix_secs bigint;
|
ADD COLUMN IF NOT EXISTS settled_last_finalized_at_unix_secs bigint;
|
||||||
|
|
||||||
|
|||||||
@@ -118,4 +118,3 @@ CREATE TABLE IF NOT EXISTS management_tokens (
|
|||||||
UNIQUE KEY uq_management_tokens_user_name (user_id, name),
|
UNIQUE KEY uq_management_tokens_user_name (user_id, name),
|
||||||
KEY management_tokens_user_id_idx (user_id)
|
KEY management_tokens_user_id_idx (user_id)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -293,4 +293,3 @@ CREATE TABLE IF NOT EXISTS global_models (
|
|||||||
updated_at BIGINT NOT NULL,
|
updated_at BIGINT NOT NULL,
|
||||||
UNIQUE KEY global_models_name_key (name)
|
UNIQUE KEY global_models_name_key (name)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -67,4 +67,3 @@ CREATE TABLE IF NOT EXISTS user_oauth_links (
|
|||||||
KEY user_oauth_links_provider_type_idx (provider_type),
|
KEY user_oauth_links_provider_type_idx (provider_type),
|
||||||
KEY user_oauth_links_user_id_idx (user_id)
|
KEY user_oauth_links_user_id_idx (user_id)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -37,4 +37,3 @@ CREATE TABLE IF NOT EXISTS proxy_node_events (
|
|||||||
detail VARCHAR(500),
|
detail VARCHAR(500),
|
||||||
created_at BIGINT NOT NULL
|
created_at BIGINT NOT NULL
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -181,4 +181,3 @@ CREATE TABLE IF NOT EXISTS redeem_codes (
|
|||||||
KEY idx_redeem_codes_redeemed_user (redeemed_by_user_id, redeemed_at),
|
KEY idx_redeem_codes_redeemed_user (redeemed_by_user_id, redeemed_at),
|
||||||
KEY idx_redeem_codes_redeemed_order (redeemed_payment_order_id)
|
KEY idx_redeem_codes_redeemed_order (redeemed_payment_order_id)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -849,4 +849,3 @@ EXCEPTION
|
|||||||
WHEN duplicate_table THEN NULL;
|
WHEN duplicate_table THEN NULL;
|
||||||
WHEN invalid_table_definition THEN NULL;
|
WHEN invalid_table_definition THEN NULL;
|
||||||
END $mig$;
|
END $mig$;
|
||||||
|
|
||||||
|
|||||||
@@ -114,4 +114,3 @@ CREATE TABLE IF NOT EXISTS management_tokens (
|
|||||||
UNIQUE (user_id, name)
|
UNIQUE (user_id, name)
|
||||||
);
|
);
|
||||||
CREATE INDEX IF NOT EXISTS management_tokens_user_id_idx ON management_tokens (user_id);
|
CREATE INDEX IF NOT EXISTS management_tokens_user_id_idx ON management_tokens (user_id);
|
||||||
|
|
||||||
|
|||||||
@@ -297,4 +297,3 @@ CREATE TABLE IF NOT EXISTS global_models (
|
|||||||
created_at INTEGER NOT NULL,
|
created_at INTEGER NOT NULL,
|
||||||
updated_at INTEGER NOT NULL
|
updated_at INTEGER NOT NULL
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -65,4 +65,3 @@ CREATE TABLE IF NOT EXISTS user_oauth_links (
|
|||||||
);
|
);
|
||||||
CREATE INDEX IF NOT EXISTS user_oauth_links_provider_type_idx ON user_oauth_links (provider_type);
|
CREATE INDEX IF NOT EXISTS user_oauth_links_provider_type_idx ON user_oauth_links (provider_type);
|
||||||
CREATE INDEX IF NOT EXISTS user_oauth_links_user_id_idx ON user_oauth_links (user_id);
|
CREATE INDEX IF NOT EXISTS user_oauth_links_user_id_idx ON user_oauth_links (user_id);
|
||||||
|
|
||||||
|
|||||||
@@ -37,4 +37,3 @@ CREATE TABLE IF NOT EXISTS proxy_node_events (
|
|||||||
detail TEXT,
|
detail TEXT,
|
||||||
created_at INTEGER NOT NULL
|
created_at INTEGER NOT NULL
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -200,4 +200,3 @@ CREATE INDEX IF NOT EXISTS idx_redeem_codes_redeemed_user
|
|||||||
ON redeem_codes (redeemed_by_user_id, redeemed_at);
|
ON redeem_codes (redeemed_by_user_id, redeemed_at);
|
||||||
CREATE INDEX IF NOT EXISTS idx_redeem_codes_redeemed_order
|
CREATE INDEX IF NOT EXISTS idx_redeem_codes_redeemed_order
|
||||||
ON redeem_codes (redeemed_payment_order_id);
|
ON redeem_codes (redeemed_payment_order_id);
|
||||||
|
|
||||||
|
|||||||
@@ -308,7 +308,12 @@ mod tests {
|
|||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
versions,
|
versions,
|
||||||
vec![20260422110000, 20260422120000, 20260504120000]
|
vec![
|
||||||
|
20260422110000,
|
||||||
|
20260422120000,
|
||||||
|
20260504120000,
|
||||||
|
20260505120000
|
||||||
|
]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -321,7 +326,10 @@ mod tests {
|
|||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|item| item.version)
|
.map(|item| item.version)
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
assert_eq!(versions, vec![20260422120000, 20260504120000]);
|
assert_eq!(
|
||||||
|
versions,
|
||||||
|
vec![20260422120000, 20260504120000, 20260505120000]
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
@@ -604,6 +612,25 @@ mod tests {
|
|||||||
.await
|
.await
|
||||||
.expect("api key fixture should insert");
|
.expect("api key fixture should insert");
|
||||||
|
|
||||||
|
query(
|
||||||
|
r#"
|
||||||
|
INSERT INTO public.global_models (
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
display_name,
|
||||||
|
usage_count
|
||||||
|
) VALUES (
|
||||||
|
'global-model-backfill-1',
|
||||||
|
'gpt-4o-mini',
|
||||||
|
'GPT-4o Mini',
|
||||||
|
77
|
||||||
|
)
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.execute(&pool)
|
||||||
|
.await
|
||||||
|
.expect("global model fixture should insert");
|
||||||
|
|
||||||
query(
|
query(
|
||||||
r#"
|
r#"
|
||||||
INSERT INTO public.usage (
|
INSERT INTO public.usage (
|
||||||
@@ -670,10 +697,11 @@ mod tests {
|
|||||||
let pending_before = pending_backfills(&pool)
|
let pending_before = pending_backfills(&pool)
|
||||||
.await
|
.await
|
||||||
.expect("pending backfills should load");
|
.expect("pending backfills should load");
|
||||||
assert_eq!(pending_before.len(), 3);
|
assert_eq!(pending_before.len(), 4);
|
||||||
assert_eq!(pending_before[0].version, 20260422110000);
|
assert_eq!(pending_before[0].version, 20260422110000);
|
||||||
assert_eq!(pending_before[1].version, 20260422120000);
|
assert_eq!(pending_before[1].version, 20260422120000);
|
||||||
assert_eq!(pending_before[2].version, 20260504120000);
|
assert_eq!(pending_before[2].version, 20260504120000);
|
||||||
|
assert_eq!(pending_before[3].version, 20260505120000);
|
||||||
|
|
||||||
run_backfills(&pool)
|
run_backfills(&pool)
|
||||||
.await
|
.await
|
||||||
@@ -691,7 +719,12 @@ mod tests {
|
|||||||
.expect("applied backfill versions should load");
|
.expect("applied backfill versions should load");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
applied_versions,
|
applied_versions,
|
||||||
vec![20260422110000, 20260422120000, 20260504120000]
|
vec![
|
||||||
|
20260422110000,
|
||||||
|
20260422120000,
|
||||||
|
20260504120000,
|
||||||
|
20260505120000
|
||||||
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
let api_key_total_requests: i64 = query_scalar(
|
let api_key_total_requests: i64 = query_scalar(
|
||||||
@@ -702,6 +735,14 @@ mod tests {
|
|||||||
.expect("api key total requests should load");
|
.expect("api key total requests should load");
|
||||||
assert_eq!(api_key_total_requests, 1);
|
assert_eq!(api_key_total_requests, 1);
|
||||||
|
|
||||||
|
let global_model_usage_count: i64 = query_scalar(
|
||||||
|
"SELECT COALESCE(usage_count, 0)::BIGINT FROM public.global_models WHERE name = 'gpt-4o-mini'",
|
||||||
|
)
|
||||||
|
.fetch_one(&pool)
|
||||||
|
.await
|
||||||
|
.expect("global model usage count should load");
|
||||||
|
assert_eq!(global_model_usage_count, 1);
|
||||||
|
|
||||||
let api_key_total_tokens: i64 = query_scalar(
|
let api_key_total_tokens: i64 = query_scalar(
|
||||||
"SELECT COALESCE(total_tokens, 0)::BIGINT FROM public.api_keys WHERE id = 'api-key-backfill-1'",
|
"SELECT COALESCE(total_tokens, 0)::BIGINT FROM public.api_keys WHERE id = 'api-key-backfill-1'",
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ use tracing::info;
|
|||||||
// Generated by build.rs from schema/bootstrap/postgres.
|
// Generated by build.rs from schema/bootstrap/postgres.
|
||||||
pub(crate) static EMPTY_DATABASE_SNAPSHOT_SQL: &str =
|
pub(crate) static EMPTY_DATABASE_SNAPSHOT_SQL: &str =
|
||||||
include_str!(concat!(env!("OUT_DIR"), "/empty_database_snapshot.sql"));
|
include_str!(concat!(env!("OUT_DIR"), "/empty_database_snapshot.sql"));
|
||||||
pub(crate) const EMPTY_DATABASE_SNAPSHOT_CUTOFF_VERSION: i64 = 20260505000000;
|
pub(crate) const EMPTY_DATABASE_SNAPSHOT_CUTOFF_VERSION: i64 = 20260505130000;
|
||||||
|
|
||||||
const PUBLIC_BASE_TABLE_COUNT_SQL: &str = r#"
|
const PUBLIC_BASE_TABLE_COUNT_SQL: &str = r#"
|
||||||
SELECT COUNT(*)::BIGINT
|
SELECT COUNT(*)::BIGINT
|
||||||
|
|||||||
@@ -290,6 +290,7 @@ fn empty_database_snapshot_covers_current_cutoff_versions() {
|
|||||||
20260428000000,
|
20260428000000,
|
||||||
20260502000000,
|
20260502000000,
|
||||||
20260505000000,
|
20260505000000,
|
||||||
|
20260505130000,
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -989,6 +990,7 @@ fn pending_migrations_from_applied_skips_versions_already_applied() {
|
|||||||
20260428000000,
|
20260428000000,
|
||||||
20260502000000,
|
20260502000000,
|
||||||
20260505000000,
|
20260505000000,
|
||||||
|
20260505130000,
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1205,7 +1205,10 @@ fn map_provider_active_global_model_row(
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::{SqlxGlobalModelReadRepository, LIST_ADMIN_PROVIDER_MODELS_PREFIX};
|
use super::{
|
||||||
|
SqlxGlobalModelReadRepository, LIST_ADMIN_GLOBAL_MODELS_PREFIX,
|
||||||
|
LIST_ADMIN_PROVIDER_MODELS_PREFIX,
|
||||||
|
};
|
||||||
use crate::driver::postgres::{PostgresPoolConfig, PostgresPoolFactory};
|
use crate::driver::postgres::{PostgresPoolConfig, PostgresPoolFactory};
|
||||||
|
|
||||||
const ADMIN_PROVIDER_MODEL_REQUIRED_COLUMNS: &[&str] = &[
|
const ADMIN_PROVIDER_MODEL_REQUIRED_COLUMNS: &[&str] = &[
|
||||||
@@ -1241,6 +1244,66 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn admin_global_model_sql_projects_usage_count_from_billing_facts() {
|
||||||
|
let source = include_str!("postgres.rs");
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
!LIST_ADMIN_GLOBAL_MODELS_PREFIX.contains("usage_billing_facts"),
|
||||||
|
"admin global model list should read the maintained usage_count field"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
source.contains("WHERE usage.model = gm.name"),
|
||||||
|
"admin global model detail lookups should count usage facts for the selected model"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
source.contains("AND usage.status NOT IN ('pending', 'streaming')"),
|
||||||
|
"admin global model detail usage_count should use the maintained read-model status scope"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
source.contains("COALESCE(usage_stats.usage_count, gm.usage_count, 0)::bigint AS usage_count"),
|
||||||
|
"admin global model usage_count should prefer actual usage facts with stored-count fallback"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn admin_global_model_list_sql_counts_usage_without_full_fact_aggregate() {
|
||||||
|
assert!(
|
||||||
|
LIST_ADMIN_GLOBAL_MODELS_PREFIX.contains("COALESCE(gm.usage_count, 0)::bigint AS usage_count"),
|
||||||
|
"admin global model list should read maintained usage_count without per-request fact scans"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
!LIST_ADMIN_GLOBAL_MODELS_PREFIX.contains("GROUP BY usage.model"),
|
||||||
|
"admin global model list must not aggregate the full usage fact table before pagination"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn global_model_usage_count_read_model_has_backfill_and_delta_maintenance() {
|
||||||
|
let backfill_sql = include_str!(
|
||||||
|
"../../../backfills/postgres/20260505120000_rebuild_global_model_usage_count.sql"
|
||||||
|
);
|
||||||
|
let delta_sql =
|
||||||
|
include_str!("../usage/postgres/queries/apply_global_model_usage_delta_sql.sql");
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
backfill_sql.contains("UPDATE global_models AS gm"),
|
||||||
|
"global model usage_count backfill should refresh the read model"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
backfill_sql.contains("FROM usage_billing_facts AS usage"),
|
||||||
|
"global model usage_count backfill should rebuild from canonical usage facts"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
delta_sql.contains("UPDATE global_models"),
|
||||||
|
"usage writes should maintain the global model usage_count read model"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
delta_sql.contains("WHERE name = $1"),
|
||||||
|
"global model usage_count delta should target models by canonical model name"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn repository_constructs_from_lazy_pool() {
|
async fn repository_constructs_from_lazy_pool() {
|
||||||
let factory = PostgresPoolFactory::new(PostgresPoolConfig {
|
let factory = PostgresPoolFactory::new(PostgresPoolConfig {
|
||||||
|
|||||||
@@ -902,6 +902,32 @@ fn usage_is_success(item: &StoredRequestUsageAudit) -> bool {
|
|||||||
) && item.status_code.is_none_or(|code| code < 400)
|
) && item.status_code.is_none_or(|code| code < 400)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn usage_output_tps_uses_generation_time(item: &StoredRequestUsageAudit) -> bool {
|
||||||
|
item.request_metadata
|
||||||
|
.as_ref()
|
||||||
|
.and_then(Value::as_object)
|
||||||
|
.and_then(|metadata| metadata.get("upstream_is_stream"))
|
||||||
|
.and_then(Value::as_bool)
|
||||||
|
.unwrap_or(item.is_stream)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn usage_output_tps_duration_ms(item: &StoredRequestUsageAudit) -> Option<u64> {
|
||||||
|
let response_time_ms = item.response_time_ms?;
|
||||||
|
if response_time_ms == 0 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
if !usage_output_tps_uses_generation_time(item) {
|
||||||
|
return Some(response_time_ms);
|
||||||
|
}
|
||||||
|
|
||||||
|
let first_byte_time_ms = item.first_byte_time_ms?;
|
||||||
|
if first_byte_time_ms >= response_time_ms {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
Some(response_time_ms - first_byte_time_ms)
|
||||||
|
}
|
||||||
|
|
||||||
fn usage_provider_display_name(item: &StoredRequestUsageAudit) -> Option<String> {
|
fn usage_provider_display_name(item: &StoredRequestUsageAudit) -> Option<String> {
|
||||||
let provider_name = item.provider_name.trim();
|
let provider_name = item.provider_name.trim();
|
||||||
if provider_name.is_empty() || matches!(provider_name, "unknown" | "pending") {
|
if provider_name.is_empty() || matches!(provider_name, "unknown" | "pending") {
|
||||||
@@ -1720,13 +1746,15 @@ impl UsageReadRepository for InMemoryUsageReadRepository {
|
|||||||
self.response_time_sample_count =
|
self.response_time_sample_count =
|
||||||
self.response_time_sample_count.saturating_add(1);
|
self.response_time_sample_count.saturating_add(1);
|
||||||
self.response_times.push(response_time_ms);
|
self.response_times.push(response_time_ms);
|
||||||
if response_time_ms > 0 && item.output_tokens > 0 {
|
if let Some(output_tps_duration_ms) = usage_output_tps_duration_ms(item) {
|
||||||
self.tps_output_tokens =
|
if item.output_tokens > 0 {
|
||||||
self.tps_output_tokens.saturating_add(item.output_tokens);
|
self.tps_output_tokens =
|
||||||
self.tps_response_time_ms_sum = self
|
self.tps_output_tokens.saturating_add(item.output_tokens);
|
||||||
.tps_response_time_ms_sum
|
self.tps_response_time_ms_sum = self
|
||||||
.saturating_add(response_time_ms);
|
.tps_response_time_ms_sum
|
||||||
self.tps_sample_count = self.tps_sample_count.saturating_add(1);
|
.saturating_add(output_tps_duration_ms);
|
||||||
|
self.tps_sample_count = self.tps_sample_count.saturating_add(1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if let Some(first_byte_time_ms) = item.first_byte_time_ms {
|
if let Some(first_byte_time_ms) = item.first_byte_time_ms {
|
||||||
@@ -4822,6 +4850,7 @@ mod tests {
|
|||||||
second.output_tokens = 40;
|
second.output_tokens = 40;
|
||||||
second.response_time_ms = Some(1000);
|
second.response_time_ms = Some(1000);
|
||||||
second.first_byte_time_ms = Some(200);
|
second.first_byte_time_ms = Some(200);
|
||||||
|
second.request_metadata = Some(json!({ "upstream_is_stream": true }));
|
||||||
|
|
||||||
let mut failed = sample_usage("req-provider-perf-failed", 1_711_000_400);
|
let mut failed = sample_usage("req-provider-perf-failed", 1_711_000_400);
|
||||||
failed.output_tokens = 999;
|
failed.output_tokens = 999;
|
||||||
@@ -4852,7 +4881,7 @@ mod tests {
|
|||||||
|
|
||||||
assert_eq!(summary.summary.request_count, 4);
|
assert_eq!(summary.summary.request_count, 4);
|
||||||
assert_eq!(summary.summary.success_count, 3);
|
assert_eq!(summary.summary.success_count, 3);
|
||||||
assert!((summary.summary.avg_output_tps.expect("summary tps") - 18.571_428).abs() < 0.001);
|
assert!((summary.summary.avg_output_tps.expect("summary tps") - 19.117_647).abs() < 0.001);
|
||||||
assert_eq!(summary.summary.avg_first_byte_time_ms, Some(150.0));
|
assert_eq!(summary.summary.avg_first_byte_time_ms, Some(150.0));
|
||||||
assert!(
|
assert!(
|
||||||
(summary
|
(summary
|
||||||
@@ -4870,7 +4899,7 @@ mod tests {
|
|||||||
assert_eq!(provider.request_count, 3);
|
assert_eq!(provider.request_count, 3);
|
||||||
assert_eq!(provider.success_count, 2);
|
assert_eq!(provider.success_count, 2);
|
||||||
assert_eq!(provider.output_tokens, 1099);
|
assert_eq!(provider.output_tokens, 1099);
|
||||||
assert_eq!(provider.avg_output_tps, Some(25.0));
|
assert!((provider.avg_output_tps.expect("provider tps") - 26.315_789).abs() < 0.001);
|
||||||
assert_eq!(provider.avg_first_byte_time_ms, Some(150.0));
|
assert_eq!(provider.avg_first_byte_time_ms, Some(150.0));
|
||||||
assert_eq!(provider.avg_response_time_ms, Some(2000.0));
|
assert_eq!(provider.avg_response_time_ms, Some(2000.0));
|
||||||
assert_eq!(provider.p90_response_time_ms, None);
|
assert_eq!(provider.p90_response_time_ms, None);
|
||||||
@@ -4880,6 +4909,8 @@ mod tests {
|
|||||||
assert_eq!(summary.timeline.len(), 1);
|
assert_eq!(summary.timeline.len(), 1);
|
||||||
assert_eq!(summary.timeline[0].date, "2024-03-21T05:00:00+00:00");
|
assert_eq!(summary.timeline[0].date, "2024-03-21T05:00:00+00:00");
|
||||||
assert_eq!(summary.timeline[0].provider_id, "provider-1");
|
assert_eq!(summary.timeline[0].provider_id, "provider-1");
|
||||||
assert_eq!(summary.timeline[0].avg_output_tps, Some(25.0));
|
assert!(
|
||||||
|
(summary.timeline[0].avg_output_tps.expect("timeline tps") - 26.315_789).abs() < 0.001
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -444,6 +444,41 @@ impl ApiKeyUsageDelta {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Default)]
|
||||||
|
pub(crate) struct ModelUsageContribution {
|
||||||
|
pub model: String,
|
||||||
|
pub request_count: i64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Default)]
|
||||||
|
pub(crate) struct ModelUsageDelta {
|
||||||
|
pub request_count: i64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ModelUsageDelta {
|
||||||
|
pub(crate) fn between(before: &ModelUsageContribution, after: &ModelUsageContribution) -> Self {
|
||||||
|
Self {
|
||||||
|
request_count: after.request_count - before.request_count,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn addition(after: &ModelUsageContribution) -> Self {
|
||||||
|
Self {
|
||||||
|
request_count: after.request_count,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn removal(before: &ModelUsageContribution) -> Self {
|
||||||
|
Self {
|
||||||
|
request_count: -before.request_count,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn is_noop(&self) -> bool {
|
||||||
|
self.request_count == 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Default)]
|
#[derive(Debug, Clone, PartialEq, Default)]
|
||||||
pub(crate) struct ProviderApiKeyUsageContribution {
|
pub(crate) struct ProviderApiKeyUsageContribution {
|
||||||
pub key_id: String,
|
pub key_id: String,
|
||||||
@@ -620,6 +655,23 @@ pub(crate) fn provider_api_key_usage_contribution(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn model_usage_contribution(
|
||||||
|
usage: &StoredRequestUsageAudit,
|
||||||
|
) -> Option<ModelUsageContribution> {
|
||||||
|
if matches!(usage.status.as_str(), "pending" | "streaming") {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let model = usage.model.trim();
|
||||||
|
if model.is_empty() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(ModelUsageContribution {
|
||||||
|
model: model.to_string(),
|
||||||
|
request_count: 1,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn api_key_usage_contribution(
|
pub(crate) fn api_key_usage_contribution(
|
||||||
usage: &StoredRequestUsageAudit,
|
usage: &StoredRequestUsageAudit,
|
||||||
) -> Option<ApiKeyUsageContribution> {
|
) -> Option<ApiKeyUsageContribution> {
|
||||||
@@ -647,9 +699,10 @@ pub(crate) fn api_key_usage_contribution(
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::{
|
use super::{
|
||||||
api_key_usage_contribution, incoming_usage_can_recover_terminal_failure,
|
api_key_usage_contribution, incoming_usage_can_recover_terminal_failure,
|
||||||
provider_api_key_usage_contribution, provider_api_key_usage_is_error,
|
model_usage_contribution, provider_api_key_usage_contribution,
|
||||||
provider_api_key_usage_is_success, strip_deprecated_usage_display_fields,
|
provider_api_key_usage_is_error, provider_api_key_usage_is_success,
|
||||||
usage_can_recover_terminal_failure, StoredRequestUsageAudit, UpsertUsageRecord,
|
strip_deprecated_usage_display_fields, usage_can_recover_terminal_failure, ModelUsageDelta,
|
||||||
|
StoredRequestUsageAudit, UpsertUsageRecord,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -925,4 +978,75 @@ mod tests {
|
|||||||
assert_eq!(contribution.total_cost_usd, 0.25);
|
assert_eq!(contribution.total_cost_usd, 0.25);
|
||||||
assert_eq!(contribution.last_used_at_unix_secs, Some(123));
|
assert_eq!(contribution.last_used_at_unix_secs, Some(123));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn model_usage_contribution_tracks_terminal_requests_only() {
|
||||||
|
let completed = StoredRequestUsageAudit::new(
|
||||||
|
"usage-1".to_string(),
|
||||||
|
"request-1".to_string(),
|
||||||
|
Some("user-1".to_string()),
|
||||||
|
Some("api-key-1".to_string()),
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
"OpenAI".to_string(),
|
||||||
|
" gpt-5.5 ".to_string(),
|
||||||
|
None,
|
||||||
|
Some("provider-1".to_string()),
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
12,
|
||||||
|
8,
|
||||||
|
20,
|
||||||
|
0.25,
|
||||||
|
0.25,
|
||||||
|
Some(200),
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
Some(120),
|
||||||
|
None,
|
||||||
|
"completed".to_string(),
|
||||||
|
"settled".to_string(),
|
||||||
|
123,
|
||||||
|
124,
|
||||||
|
Some(125),
|
||||||
|
)
|
||||||
|
.expect("usage should build");
|
||||||
|
let contribution =
|
||||||
|
model_usage_contribution(&completed).expect("completed usage should count");
|
||||||
|
assert_eq!(contribution.model, "gpt-5.5");
|
||||||
|
assert_eq!(contribution.request_count, 1);
|
||||||
|
|
||||||
|
let mut streaming = completed.clone();
|
||||||
|
streaming.status = "streaming".to_string();
|
||||||
|
assert!(model_usage_contribution(&streaming).is_none());
|
||||||
|
|
||||||
|
let mut pending = completed;
|
||||||
|
pending.status = "pending".to_string();
|
||||||
|
assert!(model_usage_contribution(&pending).is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn model_usage_delta_handles_model_changes() {
|
||||||
|
let before = super::ModelUsageContribution {
|
||||||
|
model: "gpt-5.4".to_string(),
|
||||||
|
request_count: 1,
|
||||||
|
};
|
||||||
|
let after = super::ModelUsageContribution {
|
||||||
|
model: "gpt-5.5".to_string(),
|
||||||
|
request_count: 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(ModelUsageDelta::removal(&before).request_count, -1);
|
||||||
|
assert_eq!(ModelUsageDelta::addition(&after).request_count, 1);
|
||||||
|
assert!(ModelUsageDelta::between(&before, &before).is_noop());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,7 +36,8 @@ use uuid::Uuid;
|
|||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
api_key_usage_contribution, incoming_usage_can_recover_terminal_failure,
|
api_key_usage_contribution, incoming_usage_can_recover_terminal_failure,
|
||||||
provider_api_key_usage_contribution, strip_deprecated_usage_display_fields, ApiKeyUsageDelta,
|
model_usage_contribution, provider_api_key_usage_contribution,
|
||||||
|
strip_deprecated_usage_display_fields, ApiKeyUsageDelta, ModelUsageDelta,
|
||||||
PendingUsageCleanupSummary, ProviderApiKeyUsageDelta, StoredProviderApiKeyUsageSummary,
|
PendingUsageCleanupSummary, ProviderApiKeyUsageDelta, StoredProviderApiKeyUsageSummary,
|
||||||
StoredProviderUsageSummary, StoredRequestUsageAudit, StoredUsageDailySummary,
|
StoredProviderUsageSummary, StoredRequestUsageAudit, StoredUsageDailySummary,
|
||||||
UpsertUsageRecord, UsageAuditListQuery, UsageDailyHeatmapQuery, UsageReadRepository,
|
UpsertUsageRecord, UsageAuditListQuery, UsageDailyHeatmapQuery, UsageReadRepository,
|
||||||
@@ -885,6 +886,67 @@ fn decode_usage_audit_summary_row(row: &PgRow) -> Result<StoredUsageAuditSummary
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn decode_usage_audit_aggregation_row(
|
||||||
|
row: &PgRow,
|
||||||
|
) -> Result<StoredUsageAuditAggregation, DataLayerError> {
|
||||||
|
Ok(StoredUsageAuditAggregation {
|
||||||
|
group_key: row.try_get::<String, _>("group_key").map_postgres_err()?,
|
||||||
|
display_name: row
|
||||||
|
.try_get::<Option<String>, _>("display_name")
|
||||||
|
.map_postgres_err()?,
|
||||||
|
secondary_name: row
|
||||||
|
.try_get::<Option<String>, _>("secondary_name")
|
||||||
|
.map_postgres_err()?,
|
||||||
|
request_count: row
|
||||||
|
.try_get::<i64, _>("request_count")
|
||||||
|
.map_postgres_err()?
|
||||||
|
.max(0) as u64,
|
||||||
|
total_tokens: row
|
||||||
|
.try_get::<i64, _>("total_tokens")
|
||||||
|
.map_postgres_err()?
|
||||||
|
.max(0) as u64,
|
||||||
|
output_tokens: row
|
||||||
|
.try_get::<i64, _>("output_tokens")
|
||||||
|
.map_postgres_err()?
|
||||||
|
.max(0) as u64,
|
||||||
|
effective_input_tokens: row
|
||||||
|
.try_get::<i64, _>("effective_input_tokens")
|
||||||
|
.map_postgres_err()?
|
||||||
|
.max(0) as u64,
|
||||||
|
total_input_context: row
|
||||||
|
.try_get::<i64, _>("total_input_context")
|
||||||
|
.map_postgres_err()?
|
||||||
|
.max(0) as u64,
|
||||||
|
cache_creation_tokens: row
|
||||||
|
.try_get::<i64, _>("cache_creation_tokens")
|
||||||
|
.map_postgres_err()?
|
||||||
|
.max(0) as u64,
|
||||||
|
cache_creation_ephemeral_5m_tokens: row
|
||||||
|
.try_get::<i64, _>("cache_creation_ephemeral_5m_tokens")
|
||||||
|
.map_postgres_err()?
|
||||||
|
.max(0) as u64,
|
||||||
|
cache_creation_ephemeral_1h_tokens: row
|
||||||
|
.try_get::<i64, _>("cache_creation_ephemeral_1h_tokens")
|
||||||
|
.map_postgres_err()?
|
||||||
|
.max(0) as u64,
|
||||||
|
cache_read_tokens: row
|
||||||
|
.try_get::<i64, _>("cache_read_tokens")
|
||||||
|
.map_postgres_err()?
|
||||||
|
.max(0) as u64,
|
||||||
|
total_cost_usd: row.try_get::<f64, _>("total_cost_usd").map_postgres_err()?,
|
||||||
|
actual_total_cost_usd: row
|
||||||
|
.try_get::<f64, _>("actual_total_cost_usd")
|
||||||
|
.map_postgres_err()?,
|
||||||
|
avg_response_time_ms: row
|
||||||
|
.try_get::<Option<f64>, _>("avg_response_time_ms")
|
||||||
|
.map_postgres_err()?,
|
||||||
|
success_count: row
|
||||||
|
.try_get::<Option<i64>, _>("success_count")
|
||||||
|
.map_postgres_err()?
|
||||||
|
.map(|value| value.max(0) as u64),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
fn decode_usage_error_distribution_row(
|
fn decode_usage_error_distribution_row(
|
||||||
row: &PgRow,
|
row: &PgRow,
|
||||||
) -> Result<StoredUsageErrorDistributionRow, DataLayerError> {
|
) -> Result<StoredUsageErrorDistributionRow, DataLayerError> {
|
||||||
@@ -1213,6 +1275,9 @@ const SUMMARIZE_USAGE_BY_PROVIDER_API_KEY_IDS_SQL: &str =
|
|||||||
const APPLY_API_KEY_USAGE_DELTA_SQL: &str =
|
const APPLY_API_KEY_USAGE_DELTA_SQL: &str =
|
||||||
include_str!("queries/apply_api_key_usage_delta_sql.sql");
|
include_str!("queries/apply_api_key_usage_delta_sql.sql");
|
||||||
|
|
||||||
|
const APPLY_GLOBAL_MODEL_USAGE_DELTA_SQL: &str =
|
||||||
|
include_str!("queries/apply_global_model_usage_delta_sql.sql");
|
||||||
|
|
||||||
const RESET_API_KEY_USAGE_STATS_SQL: &str =
|
const RESET_API_KEY_USAGE_STATS_SQL: &str =
|
||||||
include_str!("queries/reset_api_key_usage_stats_sql.sql");
|
include_str!("queries/reset_api_key_usage_stats_sql.sql");
|
||||||
|
|
||||||
@@ -4789,6 +4854,17 @@ WITH filtered_usage AS (
|
|||||||
GREATEST(COALESCE("usage".first_byte_time_ms, 0), 0) AS first_byte_time_ms,
|
GREATEST(COALESCE("usage".first_byte_time_ms, 0), 0) AS first_byte_time_ms,
|
||||||
"usage".response_time_ms IS NOT NULL AS has_response_time,
|
"usage".response_time_ms IS NOT NULL AS has_response_time,
|
||||||
"usage".first_byte_time_ms IS NOT NULL AS has_first_byte_time,
|
"usage".first_byte_time_ms IS NOT NULL AS has_first_byte_time,
|
||||||
|
CASE
|
||||||
|
WHEN COALESCE("usage".upstream_is_stream, "usage".is_stream, false)
|
||||||
|
THEN CASE
|
||||||
|
WHEN "usage".response_time_ms IS NOT NULL
|
||||||
|
AND "usage".first_byte_time_ms IS NOT NULL
|
||||||
|
AND GREATEST(COALESCE("usage".response_time_ms, 0), 0) > GREATEST(COALESCE("usage".first_byte_time_ms, 0), 0)
|
||||||
|
THEN GREATEST(COALESCE("usage".response_time_ms, 0), 0) - GREATEST(COALESCE("usage".first_byte_time_ms, 0), 0)
|
||||||
|
ELSE 0
|
||||||
|
END
|
||||||
|
ELSE GREATEST(COALESCE("usage".response_time_ms, 0), 0)
|
||||||
|
END AS output_tps_duration_ms,
|
||||||
CASE
|
CASE
|
||||||
WHEN lower(COALESCE("usage".status, '')) IN ('completed', 'success', 'ok', 'billed', 'settled')
|
WHEN lower(COALESCE("usage".status, '')) IN ('completed', 'success', 'ok', 'billed', 'settled')
|
||||||
AND ("usage".status_code IS NULL OR "usage".status_code < 400)
|
AND ("usage".status_code IS NULL OR "usage".status_code < 400)
|
||||||
@@ -4862,6 +4938,17 @@ WITH filtered_usage AS (
|
|||||||
GREATEST(COALESCE("usage".first_byte_time_ms, 0), 0) AS first_byte_time_ms,
|
GREATEST(COALESCE("usage".first_byte_time_ms, 0), 0) AS first_byte_time_ms,
|
||||||
"usage".response_time_ms IS NOT NULL AS has_response_time,
|
"usage".response_time_ms IS NOT NULL AS has_response_time,
|
||||||
"usage".first_byte_time_ms IS NOT NULL AS has_first_byte_time,
|
"usage".first_byte_time_ms IS NOT NULL AS has_first_byte_time,
|
||||||
|
CASE
|
||||||
|
WHEN COALESCE("usage".upstream_is_stream, "usage".is_stream, false)
|
||||||
|
THEN CASE
|
||||||
|
WHEN "usage".response_time_ms IS NOT NULL
|
||||||
|
AND "usage".first_byte_time_ms IS NOT NULL
|
||||||
|
AND GREATEST(COALESCE("usage".response_time_ms, 0), 0) > GREATEST(COALESCE("usage".first_byte_time_ms, 0), 0)
|
||||||
|
THEN GREATEST(COALESCE("usage".response_time_ms, 0), 0) - GREATEST(COALESCE("usage".first_byte_time_ms, 0), 0)
|
||||||
|
ELSE 0
|
||||||
|
END
|
||||||
|
ELSE GREATEST(COALESCE("usage".response_time_ms, 0), 0)
|
||||||
|
END AS output_tps_duration_ms,
|
||||||
CASE
|
CASE
|
||||||
WHEN lower(COALESCE("usage".status, '')) IN ('completed', 'success', 'ok', 'billed', 'settled')
|
WHEN lower(COALESCE("usage".status, '')) IN ('completed', 'success', 'ok', 'billed', 'settled')
|
||||||
AND ("usage".status_code IS NULL OR "usage".status_code < 400)
|
AND ("usage".status_code IS NULL OR "usage".status_code < 400)
|
||||||
@@ -4977,6 +5064,17 @@ ORDER BY request_count DESC, provider_id ASC
|
|||||||
GREATEST(COALESCE("usage".first_byte_time_ms, 0), 0) AS first_byte_time_ms,
|
GREATEST(COALESCE("usage".first_byte_time_ms, 0), 0) AS first_byte_time_ms,
|
||||||
"usage".response_time_ms IS NOT NULL AS has_response_time,
|
"usage".response_time_ms IS NOT NULL AS has_response_time,
|
||||||
"usage".first_byte_time_ms IS NOT NULL AS has_first_byte_time,
|
"usage".first_byte_time_ms IS NOT NULL AS has_first_byte_time,
|
||||||
|
CASE
|
||||||
|
WHEN COALESCE("usage".upstream_is_stream, "usage".is_stream, false)
|
||||||
|
THEN CASE
|
||||||
|
WHEN "usage".response_time_ms IS NOT NULL
|
||||||
|
AND "usage".first_byte_time_ms IS NOT NULL
|
||||||
|
AND GREATEST(COALESCE("usage".response_time_ms, 0), 0) > GREATEST(COALESCE("usage".first_byte_time_ms, 0), 0)
|
||||||
|
THEN GREATEST(COALESCE("usage".response_time_ms, 0), 0) - GREATEST(COALESCE("usage".first_byte_time_ms, 0), 0)
|
||||||
|
ELSE 0
|
||||||
|
END
|
||||||
|
ELSE GREATEST(COALESCE("usage".response_time_ms, 0), 0)
|
||||||
|
END AS output_tps_duration_ms,
|
||||||
CASE
|
CASE
|
||||||
WHEN lower(COALESCE("usage".status, '')) IN ('completed', 'success', 'ok', 'billed', 'settled')
|
WHEN lower(COALESCE("usage".status, '')) IN ('completed', 'success', 'ok', 'billed', 'settled')
|
||||||
AND ("usage".status_code IS NULL OR "usage".status_code < 400)
|
AND ("usage".status_code IS NULL OR "usage".status_code < 400)
|
||||||
@@ -6216,6 +6314,11 @@ WHERE date >= $1
|
|||||||
GROUP BY {group_column}
|
GROUP BY {group_column}
|
||||||
ORDER BY request_count DESC, group_key ASC
|
ORDER BY request_count DESC, group_key ASC
|
||||||
"#,
|
"#,
|
||||||
|
group_column = group_column,
|
||||||
|
display_name_expr = display_name_expr,
|
||||||
|
avg_response_time_expr = avg_response_time_expr,
|
||||||
|
success_count_expr = success_count_expr,
|
||||||
|
table_name = table_name,
|
||||||
);
|
);
|
||||||
|
|
||||||
let mut rows = sqlx::query(&sql)
|
let mut rows = sqlx::query(&sql)
|
||||||
@@ -7326,6 +7429,40 @@ ORDER BY "usage".user_id ASC
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let before_model_contribution =
|
||||||
|
previous_usage.as_ref().and_then(model_usage_contribution);
|
||||||
|
let after_model_contribution = model_usage_contribution(&stored);
|
||||||
|
match (
|
||||||
|
before_model_contribution.as_ref(),
|
||||||
|
after_model_contribution.as_ref(),
|
||||||
|
) {
|
||||||
|
(Some(before), Some(after)) if before.model == after.model => {
|
||||||
|
let delta = ModelUsageDelta::between(before, after);
|
||||||
|
apply_global_model_usage_delta_in_tx(tx, before.model.as_str(), &delta)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
if let Some(before) = before_model_contribution.as_ref() {
|
||||||
|
let delta = ModelUsageDelta::removal(before);
|
||||||
|
apply_global_model_usage_delta_in_tx(
|
||||||
|
tx,
|
||||||
|
before.model.as_str(),
|
||||||
|
&delta,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
if let Some(after) = after_model_contribution.as_ref() {
|
||||||
|
let delta = ModelUsageDelta::addition(after);
|
||||||
|
apply_global_model_usage_delta_in_tx(
|
||||||
|
tx,
|
||||||
|
after.model.as_str(),
|
||||||
|
&delta,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let before_provider_contribution = previous_usage
|
let before_provider_contribution = previous_usage
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.and_then(provider_api_key_usage_contribution);
|
.and_then(provider_api_key_usage_contribution);
|
||||||
@@ -7884,6 +8021,32 @@ async fn apply_api_key_usage_delta_in_tx(
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn apply_global_model_usage_delta_in_tx(
|
||||||
|
tx: &mut sqlx::Transaction<'_, Postgres>,
|
||||||
|
model: &str,
|
||||||
|
delta: &ModelUsageDelta,
|
||||||
|
) -> Result<(), DataLayerError> {
|
||||||
|
if model.trim().is_empty() {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
if delta.is_noop() {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
sqlx::query(APPLY_GLOBAL_MODEL_USAGE_DELTA_SQL)
|
||||||
|
.bind(model)
|
||||||
|
.bind(i32::try_from(delta.request_count).map_err(|_| {
|
||||||
|
DataLayerError::UnexpectedValue(format!(
|
||||||
|
"global_models.usage_count delta exceeds i32: {}",
|
||||||
|
delta.request_count
|
||||||
|
))
|
||||||
|
})?)
|
||||||
|
.execute(&mut **tx)
|
||||||
|
.await
|
||||||
|
.map_postgres_err()?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
async fn apply_provider_api_key_usage_delta_in_tx(
|
async fn apply_provider_api_key_usage_delta_in_tx(
|
||||||
tx: &mut sqlx::Transaction<'_, Postgres>,
|
tx: &mut sqlx::Transaction<'_, Postgres>,
|
||||||
key_id: &str,
|
key_id: &str,
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
UPDATE global_models
|
||||||
|
SET
|
||||||
|
usage_count = GREATEST(COALESCE(usage_count, 0) + $2, 0),
|
||||||
|
updated_at = NOW()
|
||||||
|
WHERE name = $1
|
||||||
@@ -20,6 +20,7 @@ INSERT INTO "usage" (
|
|||||||
provider_endpoint_kind,
|
provider_endpoint_kind,
|
||||||
has_format_conversion,
|
has_format_conversion,
|
||||||
is_stream,
|
is_stream,
|
||||||
|
upstream_is_stream,
|
||||||
input_tokens,
|
input_tokens,
|
||||||
output_tokens,
|
output_tokens,
|
||||||
total_tokens,
|
total_tokens,
|
||||||
@@ -76,6 +77,14 @@ INSERT INTO "usage" (
|
|||||||
$19,
|
$19,
|
||||||
COALESCE($20, FALSE),
|
COALESCE($20, FALSE),
|
||||||
COALESCE($21, FALSE),
|
COALESCE($21, FALSE),
|
||||||
|
COALESCE(
|
||||||
|
CASE
|
||||||
|
WHEN ($53::json->>'upstream_is_stream') IN ('true', 'false')
|
||||||
|
THEN ($53::json->>'upstream_is_stream')::boolean
|
||||||
|
ELSE NULL
|
||||||
|
END,
|
||||||
|
COALESCE($21, FALSE)
|
||||||
|
),
|
||||||
COALESCE($22, 0),
|
COALESCE($22, 0),
|
||||||
COALESCE($23, 0),
|
COALESCE($23, 0),
|
||||||
COALESCE($24, COALESCE($22, 0) + COALESCE($23, 0)),
|
COALESCE($24, COALESCE($22, 0) + COALESCE($23, 0)),
|
||||||
@@ -135,6 +144,7 @@ DO UPDATE SET
|
|||||||
provider_endpoint_kind = CASE WHEN "usage".billing_status = 'pending' THEN COALESCE(EXCLUDED.provider_endpoint_kind, "usage".provider_endpoint_kind) ELSE "usage".provider_endpoint_kind END,
|
provider_endpoint_kind = CASE WHEN "usage".billing_status = 'pending' THEN COALESCE(EXCLUDED.provider_endpoint_kind, "usage".provider_endpoint_kind) ELSE "usage".provider_endpoint_kind END,
|
||||||
has_format_conversion = CASE WHEN "usage".billing_status = 'pending' THEN COALESCE(EXCLUDED.has_format_conversion, "usage".has_format_conversion) ELSE "usage".has_format_conversion END,
|
has_format_conversion = CASE WHEN "usage".billing_status = 'pending' THEN COALESCE(EXCLUDED.has_format_conversion, "usage".has_format_conversion) ELSE "usage".has_format_conversion END,
|
||||||
is_stream = CASE WHEN "usage".billing_status = 'pending' THEN COALESCE(EXCLUDED.is_stream, "usage".is_stream) ELSE "usage".is_stream END,
|
is_stream = CASE WHEN "usage".billing_status = 'pending' THEN COALESCE(EXCLUDED.is_stream, "usage".is_stream) ELSE "usage".is_stream END,
|
||||||
|
upstream_is_stream = CASE WHEN "usage".billing_status = 'pending' THEN COALESCE(EXCLUDED.upstream_is_stream, "usage".upstream_is_stream, "usage".is_stream, false) ELSE "usage".upstream_is_stream END,
|
||||||
input_tokens = CASE WHEN "usage".billing_status = 'pending' AND EXCLUDED.status IN ('completed', 'failed', 'cancelled') THEN GREATEST("usage".input_tokens, EXCLUDED.input_tokens) ELSE "usage".input_tokens END,
|
input_tokens = CASE WHEN "usage".billing_status = 'pending' AND EXCLUDED.status IN ('completed', 'failed', 'cancelled') THEN GREATEST("usage".input_tokens, EXCLUDED.input_tokens) ELSE "usage".input_tokens END,
|
||||||
output_tokens = CASE WHEN "usage".billing_status = 'pending' AND EXCLUDED.status IN ('completed', 'failed', 'cancelled') THEN GREATEST("usage".output_tokens, EXCLUDED.output_tokens) ELSE "usage".output_tokens END,
|
output_tokens = CASE WHEN "usage".billing_status = 'pending' AND EXCLUDED.status IN ('completed', 'failed', 'cancelled') THEN GREATEST("usage".output_tokens, EXCLUDED.output_tokens) ELSE "usage".output_tokens END,
|
||||||
total_tokens = CASE WHEN "usage".billing_status = 'pending' AND EXCLUDED.status IN ('completed', 'failed', 'cancelled') THEN GREATEST("usage".total_tokens, EXCLUDED.total_tokens) ELSE "usage".total_tokens END,
|
total_tokens = CASE WHEN "usage".billing_status = 'pending' AND EXCLUDED.status IN ('completed', 'failed', 'cancelled') THEN GREATEST("usage".total_tokens, EXCLUDED.total_tokens) ELSE "usage".total_tokens END,
|
||||||
|
|||||||
@@ -443,6 +443,33 @@ fn usage_sql_raw_aggregates_use_canonical_billing_facts() {
|
|||||||
.contains("FROM usage_billing_facts AS \"usage\""));
|
.contains("FROM usage_billing_facts AS \"usage\""));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn usage_sql_provider_performance_reads_upstream_stream_from_billing_facts() {
|
||||||
|
let source = include_str!("mod.rs");
|
||||||
|
assert!(source.contains("\"usage\".upstream_is_stream"));
|
||||||
|
assert!(
|
||||||
|
!source.contains("usage_base.request_metadata->>'upstream_is_stream'"),
|
||||||
|
"provider performance queries should not rejoin public.usage to resolve upstream stream mode"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
!source.contains("LEFT JOIN public.usage AS usage_base"),
|
||||||
|
"provider performance queries should stay on usage_billing_facts to avoid an extra usage scan"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn usage_billing_facts_projects_upstream_stream_mode() {
|
||||||
|
let migration = include_str!(
|
||||||
|
"../../../../migrations/postgres/20260505130000_project_upstream_stream_in_usage_billing_facts.sql"
|
||||||
|
);
|
||||||
|
|
||||||
|
assert!(migration.contains("AS upstream_is_stream"));
|
||||||
|
assert!(migration.contains("COALESCE(usage_rows.upstream_is_stream"));
|
||||||
|
assert!(migration.contains("COALESCE(usage_rows.is_stream, FALSE)"));
|
||||||
|
assert!(migration.contains("ADD COLUMN IF NOT EXISTS upstream_is_stream boolean"));
|
||||||
|
assert!(migration.contains("request_metadata->>'upstream_is_stream'"));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn usage_sql_reads_http_audits_for_single_record_fetches() {
|
fn usage_sql_reads_http_audits_for_single_record_fetches() {
|
||||||
assert!(super::FIND_BY_REQUEST_ID_SQL.contains("LEFT JOIN usage_http_audits"));
|
assert!(super::FIND_BY_REQUEST_ID_SQL.contains("LEFT JOIN usage_http_audits"));
|
||||||
@@ -577,6 +604,14 @@ fn usage_sql_insert_values_aligns_request_metadata_and_timestamps() {
|
|||||||
assert!(super::UPSERT_SQL.contains("TO_TIMESTAMP($55::double precision)"));
|
assert!(super::UPSERT_SQL.contains("TO_TIMESTAMP($55::double precision)"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn usage_sql_upsert_materializes_upstream_stream_mode() {
|
||||||
|
assert!(super::UPSERT_SQL.contains("upstream_is_stream,"));
|
||||||
|
assert!(super::UPSERT_SQL.contains("$53::json->>'upstream_is_stream'"));
|
||||||
|
assert!(super::UPSERT_SQL.contains("COALESCE($21, FALSE)"));
|
||||||
|
assert!(super::UPSERT_SQL.contains("upstream_is_stream = CASE"));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn usage_sql_upsert_returning_includes_routing_placeholders() {
|
fn usage_sql_upsert_returning_includes_routing_placeholders() {
|
||||||
assert!(super::UPSERT_SQL.contains("NULL::varchar AS http_request_body_state"));
|
assert!(super::UPSERT_SQL.contains("NULL::varchar AS http_request_body_state"));
|
||||||
|
|||||||
@@ -207,6 +207,7 @@ export interface RequestDetail {
|
|||||||
errors?: RequestErrorDomains | null
|
errors?: RequestErrorDomains | null
|
||||||
error_flow?: RequestErrorFlow | null
|
error_flow?: RequestErrorFlow | null
|
||||||
response_time_ms: number
|
response_time_ms: number
|
||||||
|
first_byte_time_ms?: number | null
|
||||||
created_at: string
|
created_at: string
|
||||||
request_headers?: Record<string, unknown>
|
request_headers?: Record<string, unknown>
|
||||||
request_body?: Record<string, unknown>
|
request_body?: Record<string, unknown>
|
||||||
|
|||||||
101
frontend/src/features/usage/__tests__/performance.spec.ts
Normal file
101
frontend/src/features/usage/__tests__/performance.spec.ts
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
import { describe, expect, it } from 'vitest'
|
||||||
|
|
||||||
|
import {
|
||||||
|
calculateOutputRate,
|
||||||
|
formatDurationMs,
|
||||||
|
formatOutputRate,
|
||||||
|
formatOutputRateValue,
|
||||||
|
getDisplayOutputRate,
|
||||||
|
getGenerationTimeMs,
|
||||||
|
getOutputRateDurationMs,
|
||||||
|
shouldHideOutputRate,
|
||||||
|
} from '../performance'
|
||||||
|
|
||||||
|
describe('usage performance metrics', () => {
|
||||||
|
it('calculates generation time after first byte', () => {
|
||||||
|
expect(getGenerationTimeMs({
|
||||||
|
response_time_ms: 1000,
|
||||||
|
first_byte_time_ms: 250,
|
||||||
|
})).toBe(750)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('calculates stream output tokens per generated second after first byte', () => {
|
||||||
|
expect(calculateOutputRate({
|
||||||
|
output_tokens: 50,
|
||||||
|
response_time_ms: 1000,
|
||||||
|
first_byte_time_ms: 500,
|
||||||
|
upstream_is_stream: true,
|
||||||
|
})).toBe(100)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('calculates standard output tokens per total response second', () => {
|
||||||
|
const timing = {
|
||||||
|
output_tokens: 50,
|
||||||
|
response_time_ms: 1000,
|
||||||
|
first_byte_time_ms: 500,
|
||||||
|
upstream_is_stream: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(getOutputRateDurationMs(timing)).toBe(1000)
|
||||||
|
expect(calculateOutputRate(timing)).toBe(50)
|
||||||
|
expect(getDisplayOutputRate(timing)).toBe(50)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('does not calculate output rate without output tokens', () => {
|
||||||
|
expect(calculateOutputRate({
|
||||||
|
output_tokens: 0,
|
||||||
|
response_time_ms: 1000,
|
||||||
|
first_byte_time_ms: 500,
|
||||||
|
})).toBeNull()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('does not calculate output rate when first byte is not before completion', () => {
|
||||||
|
expect(calculateOutputRate({
|
||||||
|
output_tokens: 50,
|
||||||
|
response_time_ms: 1000,
|
||||||
|
first_byte_time_ms: 1000,
|
||||||
|
upstream_is_stream: true,
|
||||||
|
})).toBeNull()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('hides implausible rates from very short generation tails', () => {
|
||||||
|
const timing = {
|
||||||
|
output_tokens: 300,
|
||||||
|
response_time_ms: 1000,
|
||||||
|
first_byte_time_ms: 950,
|
||||||
|
upstream_is_stream: true,
|
||||||
|
}
|
||||||
|
const rate = calculateOutputRate(timing)
|
||||||
|
|
||||||
|
expect(rate).toBe(6000)
|
||||||
|
expect(shouldHideOutputRate(rate, timing)).toBe(true)
|
||||||
|
expect(getDisplayOutputRate(timing)).toBeNull()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('keeps normal output rates visible', () => {
|
||||||
|
const timing = {
|
||||||
|
output_tokens: 50,
|
||||||
|
response_time_ms: 1000,
|
||||||
|
first_byte_time_ms: 500,
|
||||||
|
upstream_is_stream: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(getDisplayOutputRate(timing)).toBe(100)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('calculates standard output rate without first byte timing', () => {
|
||||||
|
expect(getDisplayOutputRate({
|
||||||
|
output_tokens: 50,
|
||||||
|
response_time_ms: 1000,
|
||||||
|
upstream_is_stream: false,
|
||||||
|
})).toBe(50)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('formats durations and output rates for compact UI display', () => {
|
||||||
|
expect(formatDurationMs(456)).toBe('456ms')
|
||||||
|
expect(formatDurationMs(1234)).toBe('1.23s')
|
||||||
|
expect(formatOutputRateValue(87.65)).toBe('87.7')
|
||||||
|
expect(formatOutputRate(87.65)).toBe('87.7 tps')
|
||||||
|
expect(formatOutputRate(1200)).toBe('1,200 tps')
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -179,6 +179,47 @@
|
|||||||
<span class="text-lg font-bold">{{ detail.response_time_ms ? formatResponseTime(detail.response_time_ms).value : 'N/A' }}</span>
|
<span class="text-lg font-bold">{{ detail.response_time_ms ? formatResponseTime(detail.response_time_ms).value : 'N/A' }}</span>
|
||||||
<span class="text-sm text-muted-foreground ml-1">{{ detail.response_time_ms ? formatResponseTime(detail.response_time_ms).unit : '' }}</span>
|
<span class="text-sm text-muted-foreground ml-1">{{ detail.response_time_ms ? formatResponseTime(detail.response_time_ms).unit : '' }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
<template v-if="detailOutputRate != null">
|
||||||
|
<Separator
|
||||||
|
orientation="vertical"
|
||||||
|
class="h-6 mx-6"
|
||||||
|
/>
|
||||||
|
<div class="flex items-center">
|
||||||
|
<span class="text-xs text-muted-foreground w-[56px]">输出速度</span>
|
||||||
|
<span class="text-lg font-bold text-primary">{{ formatOutputRateValue(detailOutputRate) }}</span>
|
||||||
|
<span class="text-sm text-muted-foreground ml-1">tps</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="hasDetailPerformanceBreakdown"
|
||||||
|
class="grid grid-cols-1 sm:grid-cols-3 gap-2 mb-4 text-xs"
|
||||||
|
>
|
||||||
|
<div class="rounded-md border border-border/50 bg-muted/20 px-3 py-2">
|
||||||
|
<div class="text-muted-foreground mb-1">
|
||||||
|
首字时间
|
||||||
|
</div>
|
||||||
|
<div class="font-mono text-foreground">
|
||||||
|
{{ formatDurationMs(detail.first_byte_time_ms) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="rounded-md border border-border/50 bg-muted/20 px-3 py-2">
|
||||||
|
<div class="text-muted-foreground mb-1">
|
||||||
|
生成耗时
|
||||||
|
</div>
|
||||||
|
<div class="font-mono text-foreground">
|
||||||
|
{{ formatDurationMs(detailGenerationTimeMs) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="rounded-md border border-border/50 bg-muted/20 px-3 py-2">
|
||||||
|
<div class="text-muted-foreground mb-1">
|
||||||
|
输出 Tokens
|
||||||
|
</div>
|
||||||
|
<div class="font-mono text-foreground">
|
||||||
|
{{ formatNumber(detailOutputTokens) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 分隔线 -->
|
<!-- 分隔线 -->
|
||||||
@@ -714,6 +755,13 @@ import { formatApiFormat } from '@/api/endpoints/types/api-format'
|
|||||||
import { formatShortRequestId } from '@/utils/format'
|
import { formatShortRequestId } from '@/utils/format'
|
||||||
import { log } from '@/utils/logger'
|
import { log } from '@/utils/logger'
|
||||||
import { getEffectiveInputTokens } from '../token-normalization'
|
import { getEffectiveInputTokens } from '../token-normalization'
|
||||||
|
import {
|
||||||
|
formatDurationMs,
|
||||||
|
formatOutputRate,
|
||||||
|
formatOutputRateValue,
|
||||||
|
getDisplayOutputRate,
|
||||||
|
getGenerationTimeMs,
|
||||||
|
} from '../performance'
|
||||||
import {
|
import {
|
||||||
formatUsageStreamLabel,
|
formatUsageStreamLabel,
|
||||||
isUsageUpstreamStream,
|
isUsageUpstreamStream,
|
||||||
@@ -892,6 +940,37 @@ const displayInputTokens = computed(() => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const detailOutputTokens = computed(() => {
|
||||||
|
if (!detail.value) return 0
|
||||||
|
return detail.value.tokens?.output ?? detail.value.output_tokens ?? 0
|
||||||
|
})
|
||||||
|
|
||||||
|
const detailGenerationTimeMs = computed(() => {
|
||||||
|
if (!detail.value) return null
|
||||||
|
return getGenerationTimeMs({
|
||||||
|
response_time_ms: detail.value.response_time_ms,
|
||||||
|
first_byte_time_ms: detail.value.first_byte_time_ms,
|
||||||
|
is_stream: detail.value.is_stream,
|
||||||
|
upstream_is_stream: detail.value.upstream_is_stream,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
const detailOutputRate = computed(() => {
|
||||||
|
if (!detail.value) return null
|
||||||
|
return getDisplayOutputRate({
|
||||||
|
output_tokens: detailOutputTokens.value,
|
||||||
|
response_time_ms: detail.value.response_time_ms,
|
||||||
|
first_byte_time_ms: detail.value.first_byte_time_ms,
|
||||||
|
is_stream: detail.value.is_stream,
|
||||||
|
upstream_is_stream: detail.value.upstream_is_stream,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
const hasDetailPerformanceBreakdown = computed(() => {
|
||||||
|
if (!detail.value) return false
|
||||||
|
return detail.value.first_byte_time_ms != null || detailGenerationTimeMs.value != null || detailOutputTokens.value > 0
|
||||||
|
})
|
||||||
|
|
||||||
// 监听标签页切换
|
// 监听标签页切换
|
||||||
watch(activeTab, (newTab) => {
|
watch(activeTab, (newTab) => {
|
||||||
if (!['request-headers', 'response-headers'].includes(newTab) && viewMode.value === 'compare') {
|
if (!['request-headers', 'response-headers'].includes(newTab) && viewMode.value === 'compare') {
|
||||||
|
|||||||
85
frontend/src/features/usage/performance.ts
Normal file
85
frontend/src/features/usage/performance.ts
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
export interface UsagePerformanceTiming {
|
||||||
|
output_tokens?: number | null
|
||||||
|
response_time_ms?: number | null
|
||||||
|
first_byte_time_ms?: number | null
|
||||||
|
is_stream?: boolean | null
|
||||||
|
upstream_is_stream?: boolean | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getGenerationTimeMs(timing: UsagePerformanceTiming): number | null {
|
||||||
|
const responseTimeMs = timing.response_time_ms
|
||||||
|
const firstByteTimeMs = timing.first_byte_time_ms
|
||||||
|
|
||||||
|
if (responseTimeMs == null || firstByteTimeMs == null) return null
|
||||||
|
if (!Number.isFinite(responseTimeMs) || !Number.isFinite(firstByteTimeMs)) return null
|
||||||
|
if (responseTimeMs <= 0 || firstByteTimeMs < 0 || firstByteTimeMs >= responseTimeMs) return null
|
||||||
|
|
||||||
|
return responseTimeMs - firstByteTimeMs
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isOutputRateUsingGenerationTime(timing: UsagePerformanceTiming): boolean {
|
||||||
|
return timing.upstream_is_stream ?? timing.is_stream ?? false
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getOutputRateDurationMs(timing: UsagePerformanceTiming): number | null {
|
||||||
|
if (isOutputRateUsingGenerationTime(timing)) {
|
||||||
|
return getGenerationTimeMs(timing)
|
||||||
|
}
|
||||||
|
|
||||||
|
const responseTimeMs = timing.response_time_ms
|
||||||
|
if (responseTimeMs == null || !Number.isFinite(responseTimeMs) || responseTimeMs <= 0) return null
|
||||||
|
return responseTimeMs
|
||||||
|
}
|
||||||
|
|
||||||
|
export function calculateOutputRate(timing: UsagePerformanceTiming): number | null {
|
||||||
|
const outputTokens = timing.output_tokens ?? 0
|
||||||
|
const outputRateDurationMs = getOutputRateDurationMs(timing)
|
||||||
|
|
||||||
|
if (!Number.isFinite(outputTokens) || outputTokens <= 0 || outputRateDurationMs == null) return null
|
||||||
|
|
||||||
|
const outputRateSeconds = outputRateDurationMs / 1000
|
||||||
|
if (outputRateSeconds <= 0) return null
|
||||||
|
|
||||||
|
return outputTokens / outputRateSeconds
|
||||||
|
}
|
||||||
|
|
||||||
|
export function shouldHideOutputRate(
|
||||||
|
outputRate: number | null,
|
||||||
|
timing: UsagePerformanceTiming,
|
||||||
|
): boolean {
|
||||||
|
if (!isOutputRateUsingGenerationTime(timing)) return false
|
||||||
|
|
||||||
|
const responseTimeMs = timing.response_time_ms
|
||||||
|
const generationTimeMs = getGenerationTimeMs(timing)
|
||||||
|
|
||||||
|
if (outputRate == null || responseTimeMs == null || generationTimeMs == null) return false
|
||||||
|
if (!Number.isFinite(outputRate) || !Number.isFinite(responseTimeMs) || responseTimeMs <= 0) return true
|
||||||
|
|
||||||
|
return generationTimeMs / responseTimeMs < 0.1 && outputRate > 5000
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getDisplayOutputRate(timing: UsagePerformanceTiming): number | null {
|
||||||
|
const outputRate = calculateOutputRate(timing)
|
||||||
|
if (shouldHideOutputRate(outputRate, timing)) return null
|
||||||
|
return outputRate
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatDurationMs(ms: number | null | undefined, fractionDigits = 2): string {
|
||||||
|
if (ms == null || !Number.isFinite(ms)) return '-'
|
||||||
|
if (ms >= 1000) return `${(ms / 1000).toFixed(fractionDigits)}s`
|
||||||
|
return `${Math.round(ms)}ms`
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatOutputRateValue(outputRate: number | null | undefined): string {
|
||||||
|
if (outputRate == null || !Number.isFinite(outputRate)) return '-'
|
||||||
|
if (outputRate >= 1000) return Math.round(outputRate).toLocaleString()
|
||||||
|
if (outputRate >= 100) return `${Math.round(outputRate)}`
|
||||||
|
if (outputRate >= 10) return outputRate.toFixed(1)
|
||||||
|
return outputRate.toFixed(2)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatOutputRate(outputRate: number | null | undefined): string {
|
||||||
|
const value = formatOutputRateValue(outputRate)
|
||||||
|
if (value === '-') return value
|
||||||
|
return `${value} tps`
|
||||||
|
}
|
||||||
@@ -594,7 +594,7 @@
|
|||||||
{{ formatProviderPerformanceMetric(provider.success_rate, '%') }}
|
{{ formatProviderPerformanceMetric(provider.success_rate, '%') }}
|
||||||
</td>
|
</td>
|
||||||
<td class="px-3 py-2 text-right">
|
<td class="px-3 py-2 text-right">
|
||||||
{{ formatProviderPerformanceMetric(provider.avg_output_tps, '/s') }}
|
{{ formatProviderPerformanceMetric(provider.avg_output_tps, ' tps') }}
|
||||||
</td>
|
</td>
|
||||||
<td class="px-3 py-2 text-right">
|
<td class="px-3 py-2 text-right">
|
||||||
{{ formatProviderPerformanceMetric(provider.avg_first_byte_time_ms, 'ms') }}
|
{{ formatProviderPerformanceMetric(provider.avg_first_byte_time_ms, 'ms') }}
|
||||||
@@ -1093,7 +1093,7 @@ const providerPerformanceSummaryCards = computed(() => {
|
|||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
title: '输出 TPS',
|
title: '输出 TPS',
|
||||||
value: formatProviderPerformanceMetric(summary?.avg_output_tps, '/s'),
|
value: formatProviderPerformanceMetric(summary?.avg_output_tps, ' tps'),
|
||||||
hint: `请求 ${formatMetricNumber(summary?.request_count)}`,
|
hint: `请求 ${formatMetricNumber(summary?.request_count)}`,
|
||||||
icon: Zap,
|
icon: Zap,
|
||||||
iconClass: 'text-amber-500',
|
iconClass: 'text-amber-500',
|
||||||
@@ -1142,7 +1142,7 @@ const providerTpsChartOptions = computed(() => ({
|
|||||||
scales: {
|
scales: {
|
||||||
y: {
|
y: {
|
||||||
ticks: {
|
ticks: {
|
||||||
callback: (value: string | number) => `${value}/s`,
|
callback: (value: string | number) => `${value} tps`,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user