Files
OmniRoute/src/lib/db/usageAnalytics.ts
小妍儿 ✨ afb91a83bd fix(analytics): expose flat-rate estimates on cost dashboards (#11460)
Claude Code (claude / cc) is correctly classified as a flat-rate subscription, so the analytics API reports $0 — accurate as billed cost, and useless as a view of what the subscription actually consumed. Neither the Costs nor the Analytics dashboard had a token-price-equivalent view.

The fix keeps both meanings rather than picking one: ordinary analytics callers keep billed-cost semantics ($0 for flat-rate), /dashboard/costs and /dashboard/analytics opt in explicitly via includeFlatRateEstimates=true, the response reports whether estimates were included so a caller cannot mistake them for vendor billing records, and the figures on /dashboard/costs are labelled as flat-rate estimates rather than presented as spend. Omitted, false and unknown values all retain the existing behaviour.

Scope note carried from the description: this is a checkpoint on #11459, not its full closure — the issue stays open.

Verified in a combined worktree with three sibling PRs of this batch: typecheck:core clean, 134/134 focused tests (4 skipped), and i18n UI coverage PASS across all 42 locales for the 43-file locale pass.

One cross-PR interaction worth recording, since it is invisible from either side: this grows CostOverviewTab.tsx from 1282 to 1318 lines, which is fine against the tip's current 2002 cap but exceeds the 1283 that #12411 (file-size ratchet re-tightening) would freeze. Neither PR fails alone. Merged first on purpose so #12411's mechanical --update recomputes against the real post-merge LOC — the cap still only goes down.

Thanks @xiaoyaner0201 — the opt-in contract plus the "were estimates included" flag is the right shape for this.
2026-09-02 01:18:47 -03:00

857 lines
28 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* db/usageAnalytics.ts — Read-only aggregation queries over `usage_history`
* and `daily_usage_summary` extracted from route handlers.
*
* Hard Rule #5: routes must not embed raw SQL — these queries live here so the
* /api/usage/analytics and /api/settings/export-json routes can delegate.
* Read-only aggregation; no writes.
*
* Sliced out of #3500 (usage_history / daily_usage_summary cluster).
*/
import { getDbInstance } from "./core";
import type { AnalyticsParams } from "./usageAnalytics/sources";
export { buildUnifiedSource, buildPresetUnifiedSource } from "./usageAnalytics/sources";
export type {
AnalyticsParams,
BuildUnifiedSourceOptions,
UnifiedSourceResult,
} from "./usageAnalytics/sources";
// ---------------------------------------------------------------------------
// Analytics summary — /api/usage/analytics
// ---------------------------------------------------------------------------
export interface UsageSummaryRow {
totalRequests: number;
promptTokens: number;
completionTokens: number;
totalTokens: number;
uniqueModels: number;
uniqueAccounts: number;
uniqueApiKeys: number;
successfulRequests: number;
avgLatencyMs: number;
firstRequest: string;
lastRequest: string;
}
/**
* Scalar summary over the unified source CTE.
*
* @param unifiedSource - Pre-built subquery string (UNION of raw + aggregated rows).
* @param params - Named params referenced inside `unifiedSource`.
*/
export function getUsageSummary(unifiedSource: string, params: AnalyticsParams): UsageSummaryRow {
const db = getDbInstance();
const row = db
.prepare(
`
SELECT
COALESCE(SUM(requests), 0) as totalRequests,
COALESCE(SUM(tokens_input), 0) as promptTokens,
COALESCE(SUM(tokens_output), 0) as completionTokens,
COALESCE(SUM(tokens_input + tokens_output), 0) as totalTokens,
COUNT(DISTINCT model) as uniqueModels,
COUNT(DISTINCT COALESCE(NULLIF(account_key, ''), NULLIF(connection_id, ''))) as uniqueAccounts,
COUNT(DISTINCT COALESCE(NULLIF(api_key_id, ''), NULLIF(api_key_name, ''))) as uniqueApiKeys,
COALESCE(SUM(CASE WHEN success = 1 THEN requests ELSE 0 END), 0) as successfulRequests,
COALESCE(AVG(latency_ms), 0) as avgLatencyMs,
COALESCE(MIN(timestamp), '') as firstRequest,
COALESCE(MAX(timestamp), '') as lastRequest
FROM ${unifiedSource} AS _u
`
)
.get(params) as UsageSummaryRow | undefined;
return (
row ?? {
totalRequests: 0,
promptTokens: 0,
completionTokens: 0,
totalTokens: 0,
uniqueModels: 0,
uniqueAccounts: 0,
uniqueApiKeys: 0,
successfulRequests: 0,
avgLatencyMs: 0,
firstRequest: "",
lastRequest: "",
}
);
}
// ---------------------------------------------------------------------------
export interface DailyUsageRow {
date: string;
requests: number;
promptTokens: number;
completionTokens: number;
totalTokens: number;
}
/**
* Daily request + token counts aggregated from the unified source CTE.
*/
export function getDailyUsage(unifiedSource: string, params: AnalyticsParams): DailyUsageRow[] {
const db = getDbInstance();
return db
.prepare(
`
SELECT
DATE(timestamp) as date,
COALESCE(SUM(requests), 0) as requests,
COALESCE(SUM(tokens_input), 0) as promptTokens,
COALESCE(SUM(tokens_output), 0) as completionTokens,
COALESCE(SUM(tokens_input + tokens_output), 0) as totalTokens
FROM ${unifiedSource} AS _u
GROUP BY DATE(timestamp)
ORDER BY date ASC
`
)
.all(params) as DailyUsageRow[];
}
// ---------------------------------------------------------------------------
export interface DailyCostRow {
date: string;
provider: string;
model: string;
serviceTier: string;
promptTokens: number;
completionTokens: number;
cacheReadTokens: number;
cacheCreationTokens: number;
reasoningTokens: number;
storedCost: number;
isAggregated: number;
}
/**
* Per-day, per-provider, per-model token breakdown for cost calculation.
*/
export function getDailyCostRows(unifiedSource: string, params: AnalyticsParams): DailyCostRow[] {
const db = getDbInstance();
return db
.prepare(
`
SELECT
DATE(timestamp) as date,
LOWER(provider) as provider,
LOWER(model) as model,
COALESCE(NULLIF(service_tier, ''), 'standard') as serviceTier,
COALESCE(SUM(tokens_input), 0) as promptTokens,
COALESCE(SUM(tokens_output), 0) as completionTokens,
COALESCE(SUM(tokens_cache_read), 0) as cacheReadTokens,
COALESCE(SUM(tokens_cache_creation), 0) as cacheCreationTokens,
COALESCE(SUM(tokens_reasoning), 0) as reasoningTokens,
COALESCE(SUM(stored_cost), 0.0) as storedCost,
MAX(is_aggregated) as isAggregated
FROM ${unifiedSource} AS _u
GROUP BY DATE(timestamp), LOWER(provider), LOWER(model), serviceTier
ORDER BY date ASC
`
)
.all(params) as DailyCostRow[];
}
// ---------------------------------------------------------------------------
export interface HeatmapRow {
date: string;
totalTokens: number;
}
/**
* Per-day token totals for the activity heatmap.
* Uses `usage_history` directly (not the unified CTE) since the heatmap has its
* own independent time window and api_key filter.
*
* @param heatmapConditions - Array of SQL condition strings (combined with AND).
* @param params - Named params referenced inside the conditions.
*/
export function getHeatmapRows(heatmapConditions: string[], params: AnalyticsParams): HeatmapRow[] {
const db = getDbInstance();
return db
.prepare(
`
SELECT
DATE(timestamp) as date,
COALESCE(SUM(tokens_input + tokens_output), 0) as totalTokens
FROM usage_history
WHERE ${heatmapConditions.join(" AND ")}
GROUP BY DATE(timestamp)
ORDER BY date ASC
`
)
.all(params) as HeatmapRow[];
}
// ---------------------------------------------------------------------------
export interface ModelUsageRow {
model: string;
provider: string;
serviceTier: string;
requests: number;
promptTokens: number;
completionTokens: number;
cacheReadTokens: number;
cacheCreationTokens: number;
reasoningTokens: number;
totalTokens: number;
avgLatencyMs: number;
successfulRequests: number;
lastUsed: string;
storedCost: number;
isAggregated: number;
}
/**
* Per-model usage aggregates from the unified source CTE.
*/
export function getModelUsageRows(unifiedSource: string, params: AnalyticsParams): ModelUsageRow[] {
const db = getDbInstance();
return db
.prepare(
`
SELECT
LOWER(model) as model,
LOWER(provider) as provider,
COALESCE(NULLIF(service_tier, ''), 'standard') as serviceTier,
COALESCE(SUM(requests), 0) as requests,
COALESCE(SUM(tokens_input), 0) as promptTokens,
COALESCE(SUM(tokens_output), 0) as completionTokens,
COALESCE(SUM(tokens_cache_read), 0) as cacheReadTokens,
COALESCE(SUM(tokens_cache_creation), 0) as cacheCreationTokens,
COALESCE(SUM(tokens_reasoning), 0) as reasoningTokens,
COALESCE(SUM(tokens_input + tokens_output), 0) as totalTokens,
COALESCE(AVG(latency_ms), 0) as avgLatencyMs,
COALESCE(SUM(CASE WHEN success = 1 THEN requests ELSE 0 END), 0) as successfulRequests,
COALESCE(MAX(timestamp), '') as lastUsed,
COALESCE(SUM(stored_cost), 0.0) as storedCost,
MAX(is_aggregated) as isAggregated
FROM ${unifiedSource} AS _u
-- Keep cost inputs separated by day. Historical provider rows do not
-- always use one cache-token convention, and computeCostFromPricing's
-- non-cached-input clamp is intentionally non-linear across those rows.
GROUP BY DATE(timestamp), LOWER(model), LOWER(provider), serviceTier
ORDER BY requests DESC
`
)
.all(params) as ModelUsageRow[];
}
// ---------------------------------------------------------------------------
export interface ProviderCostRow {
provider: string;
model: string;
serviceTier: string;
promptTokens: number;
completionTokens: number;
cacheReadTokens: number;
cacheCreationTokens: number;
reasoningTokens: number;
storedCost: number;
isAggregated: number;
}
/**
* Per-provider, per-model token breakdown for provider cost calculation.
*/
export function getProviderCostRows(
unifiedSource: string,
params: AnalyticsParams
): ProviderCostRow[] {
const db = getDbInstance();
return db
.prepare(
`
SELECT
LOWER(provider) as provider,
LOWER(model) as model,
COALESCE(NULLIF(service_tier, ''), 'standard') as serviceTier,
COALESCE(SUM(tokens_input), 0) as promptTokens,
COALESCE(SUM(tokens_output), 0) as completionTokens,
COALESCE(SUM(tokens_cache_read), 0) as cacheReadTokens,
COALESCE(SUM(tokens_cache_creation), 0) as cacheCreationTokens,
COALESCE(SUM(tokens_reasoning), 0) as reasoningTokens,
COALESCE(SUM(stored_cost), 0.0) as storedCost,
MAX(is_aggregated) as isAggregated
FROM ${unifiedSource} AS _u
GROUP BY DATE(timestamp), LOWER(provider), LOWER(model), serviceTier
`
)
.all(params) as ProviderCostRow[];
}
// ---------------------------------------------------------------------------
export interface ProviderUsageRow {
provider: string;
requests: number;
promptTokens: number;
completionTokens: number;
totalTokens: number;
avgLatencyMs: number;
successfulRequests: number;
}
/**
* Per-provider usage aggregates from the unified source CTE.
*/
export function getProviderUsageRows(
unifiedSource: string,
params: AnalyticsParams
): ProviderUsageRow[] {
const db = getDbInstance();
return db
.prepare(
`
SELECT
LOWER(provider) as provider,
COALESCE(SUM(requests), 0) as requests,
COALESCE(SUM(tokens_input), 0) as promptTokens,
COALESCE(SUM(tokens_output), 0) as completionTokens,
COALESCE(SUM(tokens_input + tokens_output), 0) as totalTokens,
COALESCE(AVG(latency_ms), 0) as avgLatencyMs,
COALESCE(SUM(CASE WHEN success = 1 THEN requests ELSE 0 END), 0) as successfulRequests
FROM ${unifiedSource} AS _u
GROUP BY LOWER(provider)
ORDER BY requests DESC
`
)
.all(params) as ProviderUsageRow[];
}
// ---------------------------------------------------------------------------
export interface AccountCostRow {
accountKey: string;
provider: string;
model: string;
serviceTier: string;
promptTokens: number;
completionTokens: number;
cacheReadTokens: number;
cacheCreationTokens: number;
reasoningTokens: number;
}
/**
* Per-account cost breakdown grouped by the identity snapshot stored on each usage event.
*
* @param whereClause - SQL WHERE clause (may be empty string); column refs already
* prefixed with `usage_history.` by the caller.
* @param params - Named params referenced inside `whereClause`.
*/
export function getAccountCostRows(whereClause: string, params: AnalyticsParams): AccountCostRow[] {
const db = getDbInstance();
return db
.prepare(
`
WITH account_events AS (
SELECT
COALESCE(
NULLIF(usage_history.account_key, ''),
'connection:' || COALESCE(LOWER(usage_history.provider), 'unknown') || ':' || COALESCE(NULLIF(TRIM(usage_history.connection_id), ''), 'unknown')
) as resolved_account_key,
usage_history.provider,
usage_history.model,
usage_history.service_tier,
usage_history.timestamp,
usage_history.tokens_input,
usage_history.tokens_output,
usage_history.tokens_cache_read,
usage_history.tokens_cache_creation,
usage_history.tokens_reasoning
FROM usage_history
${whereClause}
)
SELECT
account_events.resolved_account_key as accountKey,
LOWER(account_events.provider) as provider,
LOWER(account_events.model) as model,
COALESCE(NULLIF(account_events.service_tier, ''), 'standard') as serviceTier,
COALESCE(SUM(account_events.tokens_input), 0) as promptTokens,
COALESCE(SUM(account_events.tokens_output), 0) as completionTokens,
COALESCE(SUM(account_events.tokens_cache_read), 0) as cacheReadTokens,
COALESCE(SUM(account_events.tokens_cache_creation), 0) as cacheCreationTokens,
COALESCE(SUM(account_events.tokens_reasoning), 0) as reasoningTokens
FROM account_events
GROUP BY DATE(account_events.timestamp), accountKey, LOWER(account_events.provider), LOWER(account_events.model), serviceTier
`
)
.all(params) as AccountCostRow[];
}
// ---------------------------------------------------------------------------
export interface AccountUsageRow {
accountKey: string;
account: string;
requests: number;
promptTokens: number;
completionTokens: number;
totalTokens: number;
avgLatencyMs: number;
lastUsed: string;
}
/**
* Per-account usage aggregates grouped by the identity snapshot stored on each usage event.
*
* @param whereClause - SQL WHERE clause (may be empty string); column refs already
* prefixed with `usage_history.` by the caller.
* @param params - Named params referenced inside `whereClause`.
*/
export function getAccountUsageRows(
whereClause: string,
params: AnalyticsParams
): AccountUsageRow[] {
const db = getDbInstance();
return db
.prepare(
`
WITH account_events AS (
SELECT
usage_history.*,
COALESCE(NULLIF(usage_history.account_key, ''), 'connection:' || COALESCE(LOWER(usage_history.provider), 'unknown') || ':' || COALESCE(NULLIF(TRIM(usage_history.connection_id), ''), 'unknown')) as resolved_account_key
FROM usage_history
${whereClause}
),
stable_account_keys AS (
SELECT DISTINCT account_key
FROM account_events
WHERE account_key > ''
),
stable_labels AS (
SELECT
stable_account_keys.account_key,
(
SELECT TRIM(usage_history.account_label)
FROM usage_history
WHERE usage_history.account_key = stable_account_keys.account_key
AND NULLIF(TRIM(usage_history.account_label), '') IS NOT NULL
ORDER BY COALESCE(usage_history.account_label_priority, 0) DESC,
usage_history.timestamp DESC,
usage_history.id DESC
LIMIT 1
) as account_label
FROM stable_account_keys
),
legacy_labels AS (
SELECT account_key, account_label
FROM (
SELECT
account_events.resolved_account_key as account_key,
TRIM(account_events.account_label) as account_label,
ROW_NUMBER() OVER (
PARTITION BY account_events.resolved_account_key
ORDER BY COALESCE(account_events.account_label_priority, 0) DESC,
account_events.timestamp DESC,
account_events.id DESC
) as label_rank
FROM account_events
WHERE (account_events.account_key IS NULL OR account_events.account_key = '')
AND NULLIF(TRIM(account_events.account_label), '') IS NOT NULL
)
WHERE label_rank = 1
),
selected_labels AS (
SELECT account_key, account_label FROM stable_labels
UNION ALL
SELECT account_key, account_label FROM legacy_labels
)
SELECT
account_events.resolved_account_key as accountKey,
COALESCE(NULLIF(TRIM(selected_labels.account_label), ''), NULLIF(TRIM(account_events.connection_id), ''), 'unknown') as account,
COUNT(account_events.id) as requests,
COALESCE(SUM(account_events.tokens_input), 0) as promptTokens,
COALESCE(SUM(account_events.tokens_output), 0) as completionTokens,
COALESCE(SUM(account_events.tokens_input + account_events.tokens_output), 0) as totalTokens,
COALESCE(AVG(account_events.latency_ms), 0) as avgLatencyMs,
COALESCE(MAX(account_events.timestamp), '') as lastUsed
FROM account_events
LEFT JOIN selected_labels
ON selected_labels.account_key = account_events.resolved_account_key
GROUP BY accountKey
ORDER BY requests DESC
LIMIT 50
`
)
.all(params) as AccountUsageRow[];
}
// ---------------------------------------------------------------------------
export interface ApiKeyUsageRow {
apiKeyId: string | null;
apiKeyGroupKey: string;
provider: string;
model: string;
serviceTier: string;
requests: number;
promptTokens: number;
completionTokens: number;
cacheReadTokens: number;
cacheCreationTokens: number;
reasoningTokens: number;
totalTokens: number;
}
/**
* Per-API-key usage aggregates from usage_history.
*
* @param apiKeyWhereClause - Full WHERE clause including api_key presence guard.
* @param params - Named params referenced inside `apiKeyWhereClause`.
*/
export function getApiKeyUsageRows(
apiKeyWhereClause: string,
params: AnalyticsParams
): ApiKeyUsageRow[] {
const db = getDbInstance();
return db
.prepare(
`
SELECT
NULLIF(api_key_id, '') as apiKeyId,
COALESCE(NULLIF(api_key_id, ''), NULLIF(api_key_name, ''), 'unknown') as apiKeyGroupKey,
LOWER(provider) as provider,
LOWER(model) as model,
COALESCE(NULLIF(service_tier, ''), 'standard') as serviceTier,
COUNT(*) as requests,
COALESCE(SUM(tokens_input), 0) as promptTokens,
COALESCE(SUM(tokens_output), 0) as completionTokens,
COALESCE(SUM(tokens_cache_read), 0) as cacheReadTokens,
COALESCE(SUM(tokens_cache_creation), 0) as cacheCreationTokens,
COALESCE(SUM(tokens_reasoning), 0) as reasoningTokens,
COALESCE(SUM(tokens_input + tokens_output), 0) as totalTokens
FROM usage_history
${apiKeyWhereClause}
GROUP BY DATE(timestamp), COALESCE(NULLIF(api_key_id, ''), NULLIF(api_key_name, ''), 'unknown'), NULLIF(api_key_id, ''), LOWER(provider), LOWER(model), serviceTier
`
)
.all(params) as ApiKeyUsageRow[];
}
// ---------------------------------------------------------------------------
export interface ServiceTierUsageRow {
serviceTier: string;
provider: string;
model: string;
requests: number;
promptTokens: number;
completionTokens: number;
cacheReadTokens: number;
cacheCreationTokens: number;
reasoningTokens: number;
totalTokens: number;
storedCost: number;
isAggregated: number;
}
/**
* Per-service-tier, per-provider, per-model usage aggregates.
*/
export function getServiceTierUsageRows(
unifiedSource: string,
params: AnalyticsParams
): ServiceTierUsageRow[] {
const db = getDbInstance();
return db
.prepare(
`
SELECT
COALESCE(NULLIF(service_tier, ''), 'standard') as serviceTier,
LOWER(provider) as provider,
LOWER(model) as model,
COALESCE(NULLIF(service_tier, ''), 'standard') as serviceTier,
COALESCE(SUM(requests), 0) as requests,
COALESCE(SUM(tokens_input), 0) as promptTokens,
COALESCE(SUM(tokens_output), 0) as completionTokens,
COALESCE(SUM(tokens_cache_read), 0) as cacheReadTokens,
COALESCE(SUM(tokens_cache_creation), 0) as cacheCreationTokens,
COALESCE(SUM(tokens_reasoning), 0) as reasoningTokens,
COALESCE(SUM(tokens_input + tokens_output), 0) as totalTokens,
COALESCE(SUM(stored_cost), 0.0) as storedCost,
MAX(is_aggregated) as isAggregated
FROM ${unifiedSource} AS _u
GROUP BY DATE(timestamp), serviceTier, LOWER(provider), LOWER(model)
`
)
.all(params) as ServiceTierUsageRow[];
}
// ---------------------------------------------------------------------------
export interface ApiKeyMetadataRow {
apiKeyId: string | null;
apiKeyName: string | null;
apiKeyGroupKey: string;
lastUsed: string;
}
/**
* Latest API key name + group key from usage_history for display metadata.
*
* @param apiKeyWhereClause - Full WHERE clause including api_key presence guard.
* @param params - Named params referenced inside `apiKeyWhereClause`.
*/
export function getApiKeyMetadataRows(
apiKeyWhereClause: string,
params: AnalyticsParams
): ApiKeyMetadataRow[] {
const db = getDbInstance();
return db
.prepare(
`
SELECT
NULLIF(api_key_id, '') as apiKeyId,
NULLIF(api_key_name, '') as apiKeyName,
COALESCE(NULLIF(api_key_id, ''), NULLIF(api_key_name, ''), 'unknown') as apiKeyGroupKey,
MAX(timestamp) as lastUsed
FROM usage_history
${apiKeyWhereClause}
GROUP BY NULLIF(api_key_id, ''), NULLIF(api_key_name, '')
ORDER BY lastUsed DESC
`
)
.all(params) as ApiKeyMetadataRow[];
}
// ---------------------------------------------------------------------------
export interface WeeklyPatternRow {
dayOfWeek: string;
days: number;
requests: number;
totalTokens: number;
}
/**
* Day-of-week aggregates for the weekly activity pattern chart.
*/
export function getWeeklyPatternRows(
unifiedSource: string,
params: AnalyticsParams
): WeeklyPatternRow[] {
const db = getDbInstance();
return db
.prepare(
`
SELECT
dayOfWeek,
COUNT(*) as days,
COALESCE(SUM(requests), 0) as requests,
COALESCE(SUM(totalTokens), 0) as totalTokens
FROM (
SELECT
DATE(timestamp) as date,
strftime('%w', timestamp) as dayOfWeek,
COALESCE(SUM(requests), 0) as requests,
COALESCE(SUM(tokens_input + tokens_output), 0) as totalTokens
FROM ${unifiedSource} AS _u
GROUP BY DATE(timestamp), strftime('%w', timestamp)
)
GROUP BY dayOfWeek
ORDER BY dayOfWeek ASC
`
)
.all(params) as WeeklyPatternRow[];
}
// ---------------------------------------------------------------------------
export interface PresetCostModelRow {
model: string;
provider: string;
serviceTier: string;
promptTokens: number;
completionTokens: number;
cacheReadTokens: number;
cacheCreationTokens: number;
reasoningTokens: number;
storedCost: number;
isAggregated: number;
}
/**
* Per-model token breakdown for preset range cost calculation.
* Uses a preset-specific unified source (may differ from the main query window).
*/
export function getPresetCostModelRows(
presetUnifiedSource: string,
params: AnalyticsParams
): PresetCostModelRow[] {
const db = getDbInstance();
return db
.prepare(
`
SELECT
LOWER(model) as model,
LOWER(provider) as provider,
COALESCE(NULLIF(service_tier, ''), 'standard') as serviceTier,
COALESCE(SUM(tokens_input), 0) as promptTokens,
COALESCE(SUM(tokens_output), 0) as completionTokens,
COALESCE(SUM(tokens_cache_read), 0) as cacheReadTokens,
COALESCE(SUM(tokens_cache_creation), 0) as cacheCreationTokens,
COALESCE(SUM(tokens_reasoning), 0) as reasoningTokens,
COALESCE(SUM(stored_cost), 0.0) as storedCost,
MAX(is_aggregated) as isAggregated
FROM ${presetUnifiedSource} AS _pu
GROUP BY DATE(timestamp), LOWER(model), LOWER(provider), serviceTier
`
)
.all(params) as PresetCostModelRow[];
}
// ---------------------------------------------------------------------------
// Endpoint dimension — ported from decolua/9router#152 (thanks @toanalien).
// Reads directly from usage_history (raw rows) so the unified CTE stays
// untouched; matches the pattern used by getAutoRoutingVariantBreakdown.
// ---------------------------------------------------------------------------
export interface EndpointUsageRow {
endpoint: string;
provider: string;
model: string;
requests: number;
promptTokens: number;
completionTokens: number;
cacheReadTokens: number;
cacheCreationTokens: number;
reasoningTokens: number;
totalTokens: number;
avgLatencyMs: number;
successfulRequests: number;
lastUsed: string;
}
export interface EndpointUsageParams {
sinceIso?: string | null;
untilIso?: string | null;
}
/**
* Per-endpoint × provider × model usage aggregates from `usage_history`.
* NULL endpoints fold into the 'unknown' bucket so legacy rows stay visible.
*
* Inspired by decolua/9router#152 (byEndpoint aggregation), reshaped for the
* OmniRoute SQLite schema + analytics conventions.
*/
export function getEndpointUsageRows(params: EndpointUsageParams = {}): EndpointUsageRow[] {
const db = getDbInstance();
const conditions: string[] = [];
const bind: Record<string, unknown> = {};
if (params.sinceIso) {
conditions.push("timestamp >= @since");
bind.since = params.sinceIso;
}
if (params.untilIso) {
conditions.push("timestamp <= @until");
bind.until = params.untilIso;
}
const whereSql = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
return db
.prepare(
`
SELECT
COALESCE(NULLIF(endpoint, ''), 'unknown') as endpoint,
LOWER(COALESCE(provider, 'unknown')) as provider,
LOWER(COALESCE(model, 'unknown')) as model,
COUNT(*) as requests,
COALESCE(SUM(tokens_input), 0) as promptTokens,
COALESCE(SUM(tokens_output), 0) as completionTokens,
COALESCE(SUM(tokens_cache_read), 0) as cacheReadTokens,
COALESCE(SUM(tokens_cache_creation), 0) as cacheCreationTokens,
COALESCE(SUM(tokens_reasoning), 0) as reasoningTokens,
COALESCE(SUM(tokens_input + tokens_output), 0) as totalTokens,
COALESCE(AVG(latency_ms), 0) as avgLatencyMs,
COALESCE(SUM(CASE WHEN success = 1 THEN 1 ELSE 0 END), 0) as successfulRequests,
COALESCE(MAX(timestamp), '') as lastUsed
FROM usage_history
${whereSql}
GROUP BY endpoint, LOWER(COALESCE(provider, 'unknown')), LOWER(COALESCE(model, 'unknown'))
ORDER BY requests DESC
`
)
.all(bind) as EndpointUsageRow[];
}
// ---------------------------------------------------------------------------
// Request count per provider, per date — #4009
// ---------------------------------------------------------------------------
export interface ProviderDailyUsageRow {
date: string;
provider: string;
requests: number;
promptTokens: number;
completionTokens: number;
totalTokens: number;
}
/**
* Per-day, per-provider request counts + token totals from the unified source CTE.
* Answers "how many requests did provider X get on date Y" (#4009) — providers that
* bill per-request rather than per-token need this breakdown, not just the
* per-provider aggregate (`getProviderUsageRows`) or the per-day aggregate
* (`getDailyUsage`).
*/
export function getProviderDailyUsageRows(
unifiedSource: string,
params: AnalyticsParams
): ProviderDailyUsageRow[] {
const db = getDbInstance();
return db
.prepare(
`
SELECT
DATE(timestamp) as date,
LOWER(provider) as provider,
COUNT(*) as requests,
COALESCE(SUM(tokens_input), 0) as promptTokens,
COALESCE(SUM(tokens_output), 0) as completionTokens,
COALESCE(SUM(tokens_input + tokens_output), 0) as totalTokens
FROM ${unifiedSource} AS _u
GROUP BY DATE(timestamp), LOWER(provider)
ORDER BY date DESC, requests DESC
`
)
.all(params) as ProviderDailyUsageRow[];
}
// ---------------------------------------------------------------------------
// Export-JSON backup — /api/settings/export-json
// ---------------------------------------------------------------------------
/**
* Returns all rows from `usage_history` for backup export.
* Only called when `?includeHistory=true` is explicitly requested.
*/
export function getAllUsageHistory(): Record<string, unknown>[] {
const db = getDbInstance();
return db.prepare("SELECT * FROM usage_history").all() as Record<string, unknown>[];
}
/**
* Returns all rows from `domain_cost_history` for backup export.
*/
export function getAllDomainCostHistory(): Record<string, unknown>[] {
const db = getDbInstance();
return db.prepare("SELECT * FROM domain_cost_history").all() as Record<string, unknown>[];
}
/**
* Returns all rows from `domain_budgets` for backup export.
*/
export function getAllDomainBudgets(): Record<string, unknown>[] {
const db = getDbInstance();
return db.prepare("SELECT * FROM domain_budgets").all() as Record<string, unknown>[];
}