mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-03 05:45:04 +03:00
* chore(release): open v3.8.22 development cycle * refactor(dashboard): extract ProviderDetailPageClient — #3501 Phase 0 (#3633) #3501 Phase 0: extract ProviderDetailPageClient + smoke test. Co-authored-by: oyi77 <oyi77@users.noreply.github.com> * refactor(dashboard): extract auth-import modals — #3501 Phase 1a (#3634) #3501 Phase 1a: extract 3 auth-import modal clusters. Co-authored-by: oyi77 <oyi77@users.noreply.github.com> * fix(db): reclassify localDb unexported modules as intentionally-internal (#3499) (#3635) Closes #3499 — reclassify localDb unexported modules as intentionally-internal (audit + honest gate framing). * refactor(db): move call_logs aggregations into callLogStats db module (#3500) (#3636) #3500 slice 1: call_logs aggregations → src/lib/db/callLogStats.ts (Rule #5). Byte-identical queries; TDD 6/6. * refactor(dashboard): extract EditCompatibleNodeModal — #3501 Phase 1b (#3638) #3501 Phase 1b: extract EditCompatibleNodeModal (cycle-safe via leaf constants module). Co-authored-by: oyi77 <oyi77@users.noreply.github.com> * refactor(db): move community_servers SQL into gamification db module (#3500 slice 3) (#3639) #3500 slice 3: community_servers SQL → gamification db module. * refactor(db): move usage_history SQL into usageAnalytics module (#3500 slice 2) (#3644) #3500 slice 2: usage_history/daily_usage_summary SQL → usageAnalytics db module. * refactor(db): move skills UPDATE + db-backups SQL into db modules (#3500 slice 5) (#3647) #3500 slice 5: skills UPDATE (allowlist) + db-backups SQL → db modules. * refactor(db): move usage_logs/semantic_cache/proxy_logs SQL into db modules (#3500 slice 4) (#3648) #3500 slice 4: usage_logs/semantic_cache/proxy_logs SQL → db modules. All internal routes done (2 external by-design remain). * chore(db-gate): reclassify external-DB reads, fully close #3500 (#3649) Closes #3500: reclassify external-DB reads; all internal raw-SQL migrated to db/ modules. * refactor(dashboard): extract pure helpers to providerPageHelpers — #3501 Phase 2 (#3653) #3501 Phase 2: extract pure helpers to providerPageHelpers (leaf, cycle-safe). Co-authored-by: oyi77 <oyi77@users.noreply.github.com> * refactor(dashboard): extract remaining shared helpers to providerPageHelpers — #3501 Phase 2b (#3658) #3501 Phase 2b: extract remaining shared helpers to providerPageHelpers (leaf, cycle-safe). Heavy modals unblocked. Co-authored-by: oyi77 <oyi77@users.noreply.github.com> * fix(reasoning): replay reasoning_content on plain DeepSeek turns (#1682) (#3632) Integrated into release/v3.8.22 * fix(kiro): route enterprise IAM Identity Center accounts to their regional endpoint (#3631) Integrated into release/v3.8.22 * refactor: small code cleanup (#3523) Integrated into release/v3.8.22 * fix(combo): skip same-provider targets on 408/500/502/503/504/524 errors (#3637) Integrated into release/v3.8.22 — circuit-breaker guard added in review (#1731v2) * feat(providers): add MiMoCode free-tier provider with bootstrap JWT auth (#3659) Integrated into release/v3.8.22 — page.tsx conflict resolved + NoAuthAccountCard re-applied to ProviderDetailPageClient in review. MiMoCode endpoint validated live. * Log Responses WebSocket calls in history (#3616) Integrated into release/v3.8.22 — Codex Responses WebSocket call history logging. * Add Claude Code routing preference for unprefixed Claude models (#3540) Integrated into release/v3.8.22 — page.tsx conflict resolved (re-applied toggle to ProviderDetailPageClient) + disable-test updated for catalog drift in review. * docs(changelog): credit #3632/#3631/#3637/#3659/#3540/#3616/#3523 (v3.8.22 targeted review round) * fix(mimocode): add required authHeader:"none" to registry entry (#3659 follow-up) The mimocode RegistryEntry omitted the required authHeader field, which broke typecheck:core (TS2741). Match the no-auth convention (authType:"none" + authHeader:"none") used by veoaifree-web and other free providers. Follow-up to #3659 (@pizzav-xyz). * fix(responses): detect stream readiness for tool-call-only and object-less chunks (#3612) (#3661) Closes #3612 * fix(mitm): remove duplicated 'Command failed:' error prefix (#3641) (#3662) Closes #3641 * fix(cli): honor HERMES_HOME for Hermes Agent config path (#3628) (#3663) Closes #3628 * fix(api): fetch live OpenCode model catalog for no-auth model picker (#3611) (#3664) Closes #3611 * fix(api): flag provider topology error state by current status, not stale history (#3619) (#3666) Closes #3619 * fix(electron): launch peer-stamping server-ws.mjs entrypoint to avoid 403 LOCAL_ONLY (#3386) (#3665) Closes #3386 * fix(dashboard): restore home topology live in-flight pulse (#3507) (#3667) Closes #3507 * fix(oauth): name Kiro/AWS auto-imported accounts and dedupe by profileArn (#3615) (#3671) Closes #3615 * fix(resilience): clear stale transient connection cooldowns on startup (#3625) (#3672) Closes #3625 * fix(i18n): use logical CSS direction utilities for sidebar and key overlays (RTL #3541) (#3670) Closes #3541 * fix(dashboard): honor auto-hide and switch to visible filter on passthrough Test-all (#3610) (#3669) Closes #3610 * refactor(dashboard): extract AddApiKeyModal + EditConnectionModal — #3501 Phase 1c (#3674) #3501 Phase 1c: extract AddApiKeyModal, EditConnectionModal, WebSessionCredentialGuide into components/; god-component 10,166->8,092 LOC. Reconciles the v3.8.22 file-size drift for this file. Co-authored-by: oyi77 <oyi77@users.noreply.github.com> * docs(changelog): reconcile v3.8.22 — credit #3621/#3622 + MiMoCode follow-up roll-up * refactor(dashboard): extract ConnectionRow + ModelCompatPopover + SiliconFlowEndpointModal — #3501 Phase 1d (#3676) #3501 Phase 1d: god-component 8,092->6,838 LOC. Co-authored-by: oyi77 <oyi77@users.noreply.github.com> * feat(obsidian): add WebDAV config route + encrypt creds at rest (#3485 part 1) (#3677) Part 1 of #3485. Adds /api/settings/obsidian/webdav (GET/POST/DELETE) wiring the ready obsidianSync lib, encrypts webdav password + obsidian token at rest, removes the duplicate UI block, drops the KNOWN_MISSING entry. WebDAV file server is part 2. * feat(obsidian): add /api/v1/webdav file server for Obsidian vault sync (#3485 part 2) (#3678) Part 2 of #3485. WebDAV server (PROPFIND/GET/PUT/DELETE/MKCOL/MOVE/OPTIONS) handled in the custom server layer (standalone-server-ws.mjs) since the App Router cannot export WebDAV methods. Basic-Auth (constant-time), path-traversal hardened, password decrypt ported from encryption.ts (parity-tested), DATA_DIR resolution parity-tested against dataPaths.ts. End-to-end Obsidian-over-Tailscale validation is a live VPS step (Rule #18). * fix(combo): stop premature context compaction — real auto-combo windows + per-target compression limit (#3680) Integrated into release/v3.8.22 * feat(dashboard): deactivate/activate accounts from the quota overview (#3675) Integrated into release/v3.8.22 * fix(dashboard): close review gaps in bulk provider connection actions (#3271 follow-up) (#3673) Integrated into release/v3.8.22 — page.tsx conflict (god-component split #3501) resolved by re-applying the bulk-action deltas to ProviderDetailPageClient.tsx * refactor(dashboard): extract useModelCompatState hook + model sections — #3501 Phase 1e (#3683) #3501 Phase 1e: extract useModelCompatState hook (unblocks the model sections) + ModelRow/PassthroughModelsSection/PassthroughModelRow/CustomModelsSection/CompatibleModelsSection. god-component 6,838->4,921 LOC. Co-authored-by: oyi77 <oyi77@users.noreply.github.com> * refactor(dashboard): extract useProviderConnections/Settings/Models hooks — #3501 Phase 1f (#3684) #3501 Phase 1f: god-component 4,948->4,062 LOC. Connection state+handlers, settings, and model metadata moved into hooks/. Co-authored-by: oyi77 <oyi77@users.noreply.github.com> * chore(release): v3.8.22 CHANGELOG + env-doc sync - Set release date in CHANGELOG [3.8.22] to 2026-06-11 - Add HERMES_HOME to .env.example (from #3628/#3663) - Add HERMES_HOME + OMNIROUTE_PREFER_CLAUDE_CODE_FOR_UNPREFIXED_CLAUDE_MODELS to ENVIRONMENT.md (#3628/#3540) * docs(changelog): credit #3673 + #3675 — leninejunior bulk-actions + quota-toggle --------- Co-authored-by: oyi77 <oyi77@users.noreply.github.com> Co-authored-by: Abhishek Divekar <adivekar@utexas.edu> Co-authored-by: NOXX - Commiter <artur1992123@mail.ru> Co-authored-by: Nicolas Lorin <androw95220@gmail.com> Co-authored-by: Hernan Javier Ardila Sanchez <hjasgr@gmail.com> Co-authored-by: PizzaV <103120356+pizzav-xyz@users.noreply.github.com> Co-authored-by: kkkayye <98376609+kkkayye@users.noreply.github.com> Co-authored-by: Witroch4 <witalo_rocha@hotmail.com> Co-authored-by: Lenine Júnior <lenine@engrene.com.br>
423 lines
14 KiB
TypeScript
423 lines
14 KiB
TypeScript
import { getDbInstance } from "./core";
|
|
|
|
export interface CompressionAnalyticsRow {
|
|
id?: number;
|
|
timestamp: string;
|
|
combo_id?: string | null;
|
|
compression_combo_id?: string | null;
|
|
engine?: string | null;
|
|
provider?: string | null;
|
|
mode: string;
|
|
original_tokens: number;
|
|
compressed_tokens: number;
|
|
tokens_saved: number;
|
|
duration_ms?: number | null;
|
|
request_id?: string | null;
|
|
actual_prompt_tokens?: number | null;
|
|
actual_completion_tokens?: number | null;
|
|
actual_total_tokens?: number | null;
|
|
actual_cache_read_tokens?: number | null;
|
|
actual_cache_write_tokens?: number | null;
|
|
estimated_usd_saved?: number | null;
|
|
mcp_description_tokens_saved?: number | null;
|
|
multimodal_skip_count?: number | null;
|
|
receipt_source?: string | null;
|
|
validation_fallback?: boolean | number | null;
|
|
output_mode?: string | null;
|
|
rtk_raw_output_pointer?: string | null;
|
|
rtk_raw_output_bytes?: number | null;
|
|
rtk_raw_output_pointers?: string | null;
|
|
rtk_raw_output_total_bytes?: number | null;
|
|
}
|
|
|
|
export interface CompressionAnalyticsSummary {
|
|
totalRequests: number;
|
|
totalTokensSaved: number;
|
|
avgSavingsPct: number;
|
|
avgDurationMs: number;
|
|
byMode: Record<string, { count: number; tokensSaved: number; avgSavingsPct: number }>;
|
|
byEngine: Record<string, { count: number; tokensSaved: number; avgSavingsPct: number }>;
|
|
byCompressionCombo: Record<string, { count: number; tokensSaved: number }>;
|
|
byProvider: Record<string, { count: number; tokensSaved: number }>;
|
|
last24h: Array<{ hour: string; count: number; tokensSaved: number }>;
|
|
validationFallbacks: number;
|
|
realUsage: {
|
|
requestsWithReceipts: number;
|
|
promptTokens: number;
|
|
completionTokens: number;
|
|
totalTokens: number;
|
|
cacheReadTokens: number;
|
|
cacheWriteTokens: number;
|
|
estimatedUsdSaved: number;
|
|
bySource: Record<string, number>;
|
|
};
|
|
mcpDescriptionCompression: {
|
|
snapshots: number;
|
|
estimatedTokensSaved: number;
|
|
};
|
|
}
|
|
|
|
let columnsEnsuredForDb: unknown = null;
|
|
|
|
const COMPRESSION_ANALYTICS_COLUMNS = [
|
|
["actual_prompt_tokens", "INTEGER"],
|
|
["actual_completion_tokens", "INTEGER"],
|
|
["actual_total_tokens", "INTEGER"],
|
|
["actual_cache_read_tokens", "INTEGER"],
|
|
["actual_cache_write_tokens", "INTEGER"],
|
|
["estimated_usd_saved", "REAL"],
|
|
["mcp_description_tokens_saved", "INTEGER DEFAULT 0"],
|
|
["multimodal_skip_count", "INTEGER DEFAULT 0"],
|
|
["receipt_source", "TEXT"],
|
|
["validation_fallback", "INTEGER DEFAULT 0"],
|
|
["output_mode", "TEXT"],
|
|
["compression_combo_id", "TEXT"],
|
|
["engine", "TEXT"],
|
|
["rtk_raw_output_pointer", "TEXT"],
|
|
["rtk_raw_output_bytes", "INTEGER"],
|
|
["rtk_raw_output_pointers", "TEXT"],
|
|
["rtk_raw_output_total_bytes", "INTEGER"],
|
|
] as const;
|
|
|
|
function ensureCompressionAnalyticsColumns(): void {
|
|
const db = getDbInstance();
|
|
if (columnsEnsuredForDb === db) return;
|
|
const rows = db.prepare("PRAGMA table_info(compression_analytics)").all() as Array<{
|
|
name: string;
|
|
}>;
|
|
const existing = new Set(rows.map((row) => row.name));
|
|
for (const [name, type] of COMPRESSION_ANALYTICS_COLUMNS) {
|
|
if (!existing.has(name)) {
|
|
db.exec(`ALTER TABLE compression_analytics ADD COLUMN ${name} ${type}`);
|
|
}
|
|
}
|
|
columnsEnsuredForDb = db;
|
|
}
|
|
|
|
export function insertCompressionAnalyticsRow(row: CompressionAnalyticsRow): void {
|
|
const db = getDbInstance();
|
|
ensureCompressionAnalyticsColumns();
|
|
db.prepare(
|
|
`
|
|
INSERT INTO compression_analytics (
|
|
timestamp, combo_id, compression_combo_id, engine, provider, mode, original_tokens, compressed_tokens, tokens_saved,
|
|
duration_ms, request_id, actual_prompt_tokens, actual_completion_tokens,
|
|
actual_total_tokens, actual_cache_read_tokens, actual_cache_write_tokens,
|
|
estimated_usd_saved, mcp_description_tokens_saved, multimodal_skip_count,
|
|
receipt_source, validation_fallback, output_mode, rtk_raw_output_pointer, rtk_raw_output_bytes,
|
|
rtk_raw_output_pointers, rtk_raw_output_total_bytes
|
|
)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
`
|
|
).run(
|
|
row.timestamp,
|
|
row.combo_id ?? null,
|
|
row.compression_combo_id ?? null,
|
|
row.engine ?? row.mode,
|
|
row.provider ?? null,
|
|
row.mode,
|
|
row.original_tokens,
|
|
row.compressed_tokens,
|
|
row.tokens_saved,
|
|
row.duration_ms ?? null,
|
|
row.request_id ?? null,
|
|
row.actual_prompt_tokens ?? null,
|
|
row.actual_completion_tokens ?? null,
|
|
row.actual_total_tokens ?? null,
|
|
row.actual_cache_read_tokens ?? null,
|
|
row.actual_cache_write_tokens ?? null,
|
|
row.estimated_usd_saved ?? null,
|
|
row.mcp_description_tokens_saved ?? 0,
|
|
row.multimodal_skip_count ?? 0,
|
|
row.receipt_source ?? null,
|
|
row.validation_fallback ? 1 : 0,
|
|
row.output_mode ?? null,
|
|
row.rtk_raw_output_pointer ?? null,
|
|
row.rtk_raw_output_bytes ?? null,
|
|
row.rtk_raw_output_pointers ?? null,
|
|
row.rtk_raw_output_total_bytes ?? null
|
|
);
|
|
}
|
|
|
|
export function attachCompressionUsageReceipt(
|
|
requestId: string | null | undefined,
|
|
usage: Record<string, unknown> | null | undefined,
|
|
source: "provider" | "estimated" | "stream" = "provider"
|
|
): void {
|
|
if (!requestId || !usage || typeof usage !== "object") return;
|
|
const promptTokens = toFiniteInt(usage.prompt_tokens);
|
|
const completionTokens = toFiniteInt(usage.completion_tokens);
|
|
const totalTokens =
|
|
toFiniteInt(usage.total_tokens) ?? (promptTokens ?? 0) + (completionTokens ?? 0);
|
|
const promptDetails =
|
|
usage.prompt_tokens_details && typeof usage.prompt_tokens_details === "object"
|
|
? (usage.prompt_tokens_details as Record<string, unknown>)
|
|
: {};
|
|
const cacheReadTokens = toFiniteInt(
|
|
usage.cache_read_input_tokens ?? usage.cached_tokens ?? promptDetails.cached_tokens
|
|
);
|
|
const cacheWriteTokens = toFiniteInt(
|
|
usage.cache_creation_input_tokens ?? promptDetails.cache_creation_tokens
|
|
);
|
|
if (promptTokens === null && completionTokens === null && totalTokens <= 0) return;
|
|
|
|
const db = getDbInstance();
|
|
ensureCompressionAnalyticsColumns();
|
|
db.prepare(
|
|
`
|
|
UPDATE compression_analytics
|
|
SET actual_prompt_tokens = ?,
|
|
actual_completion_tokens = ?,
|
|
actual_total_tokens = ?,
|
|
actual_cache_read_tokens = ?,
|
|
actual_cache_write_tokens = ?,
|
|
receipt_source = ?
|
|
WHERE request_id = ?
|
|
AND id = (
|
|
SELECT id FROM compression_analytics
|
|
WHERE request_id = ?
|
|
ORDER BY id DESC
|
|
LIMIT 1
|
|
)
|
|
`
|
|
).run(
|
|
promptTokens,
|
|
completionTokens,
|
|
totalTokens,
|
|
cacheReadTokens,
|
|
cacheWriteTokens,
|
|
source,
|
|
requestId,
|
|
requestId
|
|
);
|
|
}
|
|
|
|
function toFiniteInt(value: unknown): number | null {
|
|
if (typeof value === "number" && Number.isFinite(value)) return Math.max(0, Math.floor(value));
|
|
if (typeof value === "string" && value.trim()) {
|
|
const parsed = Number(value);
|
|
if (Number.isFinite(parsed)) return Math.max(0, Math.floor(parsed));
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function appendCondition(whereClause: string, condition: string): string {
|
|
return whereClause ? `${whereClause} AND ${condition}` : `WHERE ${condition}`;
|
|
}
|
|
|
|
export function getCompressionAnalyticsSummary(since?: string): CompressionAnalyticsSummary {
|
|
const db = getDbInstance();
|
|
ensureCompressionAnalyticsColumns();
|
|
|
|
let cutoff: string | null = null;
|
|
if (since === "24h") {
|
|
cutoff = new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString();
|
|
} else if (since === "7d") {
|
|
cutoff = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString();
|
|
} else if (since === "30d") {
|
|
cutoff = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString();
|
|
}
|
|
|
|
const whereClause = cutoff ? "WHERE timestamp >= ?" : "";
|
|
const params = cutoff ? [cutoff] : [];
|
|
|
|
type ScalarRow = { total: number; totalSaved: number; avgPct: number; avgDur: number };
|
|
const scalar = db
|
|
.prepare(
|
|
`
|
|
SELECT
|
|
COUNT(*) as total,
|
|
COALESCE(SUM(tokens_saved), 0) as totalSaved,
|
|
COALESCE(AVG(CASE WHEN original_tokens > 0 THEN CAST(tokens_saved AS REAL) / original_tokens * 100 ELSE 0 END), 0) as avgPct,
|
|
COALESCE(AVG(duration_ms), 0) as avgDur
|
|
FROM compression_analytics ${whereClause}
|
|
`
|
|
)
|
|
.get(...params) as ScalarRow | undefined;
|
|
|
|
const modeRows = db
|
|
.prepare(
|
|
`
|
|
SELECT mode, COUNT(*) as cnt, COALESCE(SUM(tokens_saved), 0) as saved,
|
|
COALESCE(AVG(CASE WHEN original_tokens > 0 THEN CAST(tokens_saved AS REAL) / original_tokens * 100 ELSE 0 END), 0) as avgPct
|
|
FROM compression_analytics ${whereClause}
|
|
GROUP BY mode
|
|
`
|
|
)
|
|
.all(...params) as Array<{ mode: string; cnt: number; saved: number; avgPct: number }>;
|
|
|
|
const byMode: Record<string, { count: number; tokensSaved: number; avgSavingsPct: number }> = {};
|
|
for (const r of modeRows) {
|
|
byMode[r.mode] = { count: r.cnt, tokensSaved: r.saved, avgSavingsPct: Math.round(r.avgPct) };
|
|
}
|
|
|
|
const engineRows = db
|
|
.prepare(
|
|
`
|
|
SELECT COALESCE(engine, mode) as engine, COUNT(*) as cnt, COALESCE(SUM(tokens_saved), 0) as saved,
|
|
COALESCE(AVG(CASE WHEN original_tokens > 0 THEN CAST(tokens_saved AS REAL) / original_tokens * 100 ELSE 0 END), 0) as avgPct
|
|
FROM compression_analytics ${whereClause}
|
|
GROUP BY COALESCE(engine, mode)
|
|
`
|
|
)
|
|
.all(...params) as Array<{ engine: string; cnt: number; saved: number; avgPct: number }>;
|
|
|
|
const byEngine: Record<string, { count: number; tokensSaved: number; avgSavingsPct: number }> =
|
|
{};
|
|
for (const r of engineRows) {
|
|
byEngine[r.engine] = {
|
|
count: r.cnt,
|
|
tokensSaved: r.saved,
|
|
avgSavingsPct: Math.round(r.avgPct),
|
|
};
|
|
}
|
|
|
|
const compressionComboRows = db
|
|
.prepare(
|
|
`
|
|
SELECT compression_combo_id as compressionComboId, COUNT(*) as cnt,
|
|
COALESCE(SUM(tokens_saved), 0) as saved
|
|
FROM compression_analytics ${appendCondition(whereClause, "compression_combo_id IS NOT NULL")}
|
|
GROUP BY compression_combo_id ORDER BY cnt DESC
|
|
`
|
|
)
|
|
.all(...params) as Array<{ compressionComboId: string | null; cnt: number; saved: number }>;
|
|
|
|
const byCompressionCombo: Record<string, { count: number; tokensSaved: number }> = {};
|
|
for (const r of compressionComboRows) {
|
|
const key = r.compressionComboId ?? "unknown";
|
|
byCompressionCombo[key] = { count: r.cnt, tokensSaved: r.saved };
|
|
}
|
|
|
|
const provRows = db
|
|
.prepare(
|
|
`
|
|
SELECT provider, COUNT(*) as cnt, COALESCE(SUM(tokens_saved), 0) as saved
|
|
FROM compression_analytics ${whereClause}
|
|
GROUP BY provider ORDER BY cnt DESC
|
|
`
|
|
)
|
|
.all(...params) as Array<{ provider: string | null; cnt: number; saved: number }>;
|
|
|
|
const byProvider: Record<string, { count: number; tokensSaved: number }> = {};
|
|
for (const r of provRows) {
|
|
const key = r.provider ?? "unknown";
|
|
byProvider[key] = { count: r.cnt, tokensSaved: r.saved };
|
|
}
|
|
|
|
const last24hMap = new Map<string, { hour: string; count: number; tokensSaved: number }>();
|
|
const now = new Date();
|
|
for (let i = 23; i >= 0; i--) {
|
|
const d = new Date(now.getTime() - i * 60 * 60 * 1000);
|
|
const hourStr = d.toISOString().substring(0, 14) + "00:00Z";
|
|
last24hMap.set(hourStr, { hour: hourStr, count: 0, tokensSaved: 0 });
|
|
}
|
|
|
|
const hourRows = db
|
|
.prepare(
|
|
`
|
|
SELECT strftime('%Y-%m-%dT%H:00:00Z', timestamp) as hour,
|
|
COUNT(*) as cnt, COALESCE(SUM(tokens_saved), 0) as saved
|
|
FROM compression_analytics
|
|
WHERE timestamp >= ?
|
|
GROUP BY hour ORDER BY hour ASC
|
|
`
|
|
)
|
|
.all(new Date(now.getTime() - 24 * 60 * 60 * 1000).toISOString()) as Array<{
|
|
hour: string;
|
|
cnt: number;
|
|
saved: number;
|
|
}>;
|
|
|
|
for (const r of hourRows) {
|
|
if (last24hMap.has(r.hour)) {
|
|
last24hMap.set(r.hour, { hour: r.hour, count: r.cnt, tokensSaved: r.saved });
|
|
}
|
|
}
|
|
|
|
const last24h = Array.from(last24hMap.values());
|
|
|
|
const receiptRows = db
|
|
.prepare(
|
|
`
|
|
SELECT receipt_source as source, COUNT(*) as cnt,
|
|
COALESCE(SUM(actual_prompt_tokens), 0) as prompt,
|
|
COALESCE(SUM(actual_completion_tokens), 0) as completion,
|
|
COALESCE(SUM(actual_total_tokens), 0) as total,
|
|
COALESCE(SUM(actual_cache_read_tokens), 0) as cacheRead,
|
|
COALESCE(SUM(actual_cache_write_tokens), 0) as cacheWrite,
|
|
COALESCE(SUM(estimated_usd_saved), 0) as usdSaved
|
|
FROM compression_analytics ${appendCondition(whereClause, "receipt_source IS NOT NULL")}
|
|
GROUP BY receipt_source
|
|
`
|
|
)
|
|
.all(...params) as Array<{
|
|
source: string | null;
|
|
cnt: number;
|
|
prompt: number;
|
|
completion: number;
|
|
total: number;
|
|
cacheRead: number;
|
|
cacheWrite: number;
|
|
usdSaved: number;
|
|
}>;
|
|
|
|
const realUsage = {
|
|
requestsWithReceipts: 0,
|
|
promptTokens: 0,
|
|
completionTokens: 0,
|
|
totalTokens: 0,
|
|
cacheReadTokens: 0,
|
|
cacheWriteTokens: 0,
|
|
estimatedUsdSaved: 0,
|
|
bySource: {} as Record<string, number>,
|
|
};
|
|
for (const row of receiptRows) {
|
|
const source = row.source ?? "unknown";
|
|
realUsage.requestsWithReceipts += row.cnt;
|
|
realUsage.promptTokens += row.prompt;
|
|
realUsage.completionTokens += row.completion;
|
|
realUsage.totalTokens += row.total;
|
|
realUsage.cacheReadTokens += row.cacheRead;
|
|
realUsage.cacheWriteTokens += row.cacheWrite;
|
|
realUsage.estimatedUsdSaved += row.usdSaved;
|
|
realUsage.bySource[source] = row.cnt;
|
|
}
|
|
|
|
const fallbackRow = db
|
|
.prepare(
|
|
`
|
|
SELECT COUNT(*) as cnt
|
|
FROM compression_analytics ${appendCondition(whereClause, "validation_fallback = 1")}
|
|
`
|
|
)
|
|
.get(...params) as { cnt: number } | undefined;
|
|
|
|
const mcpDescriptionRow = db
|
|
.prepare(
|
|
`
|
|
SELECT COUNT(*) as cnt, COALESCE(SUM(mcp_description_tokens_saved), 0) as saved
|
|
FROM compression_analytics ${appendCondition(whereClause, "mcp_description_tokens_saved > 0")}
|
|
`
|
|
)
|
|
.get(...params) as { cnt: number; saved: number } | undefined;
|
|
|
|
return {
|
|
totalRequests: scalar?.total ?? 0,
|
|
totalTokensSaved: scalar?.totalSaved ?? 0,
|
|
avgSavingsPct: Math.round(scalar?.avgPct ?? 0),
|
|
avgDurationMs: Math.round(scalar?.avgDur ?? 0),
|
|
byMode,
|
|
byEngine,
|
|
byCompressionCombo,
|
|
byProvider,
|
|
last24h,
|
|
validationFallbacks: fallbackRow?.cnt ?? 0,
|
|
realUsage,
|
|
mcpDescriptionCompression: {
|
|
snapshots: mcpDescriptionRow?.cnt ?? 0,
|
|
estimatedTokensSaved: mcpDescriptionRow?.saved ?? 0,
|
|
},
|
|
};
|
|
}
|