File size: 4,152 Bytes
cd8bd0a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
import { getDbInstance } from "./core";

export type ComboForecastUsageRow = {
  comboName: string;
  executionKey: string | null;
  stepId: string | null;
  provider: string;
  model: string;
  requestedModel: string | null;
  connectionId: string | null;
  requests: number;
  successCount: number;
  inputTokens: number;
  outputTokens: number;
  cacheReadTokens: number;
  cacheCreationTokens: number;
  reasoningTokens: number;
  totalTokens: number;
  avgLatencyMs: number;
  lastUsedAt: string | null;
};

type ComboForecastUsageSqlRow = {
  comboName: string | null;
  executionKey: string | null;
  stepId: string | null;
  provider: string | null;
  model: string | null;
  requestedModel: string | null;
  connectionId: string | null;
  requests: number | null;
  successCount: number | null;
  inputTokens: number | null;
  outputTokens: number | null;
  cacheReadTokens: number | null;
  cacheCreationTokens: number | null;
  reasoningTokens: number | null;
  totalTokens: number | null;
  avgLatencyMs: number | null;
  lastUsedAt: string | null;
};

function toNumber(value: unknown): number {
  return typeof value === "number" && Number.isFinite(value) ? value : 0;
}

function toString(value: unknown, fallback = "unknown"): string {
  return typeof value === "string" && value.trim().length > 0 ? value.trim() : fallback;
}

export function getComboForecastUsageRows(opts: {
  since: string;
  until?: string;
  comboName?: string;
}): ComboForecastUsageRow[] {
  const db = getDbInstance();
  const conditions = ["combo_name IS NOT NULL", "combo_name != ''", "timestamp >= @since"];
  const params: Record<string, unknown> = { since: opts.since };

  if (opts.until) {
    conditions.push("timestamp <= @until");
    params.until = opts.until;
  }

  if (opts.comboName) {
    conditions.push("combo_name = @comboName");
    params.comboName = opts.comboName;
  }

  const rows = db
    .prepare(
      `SELECT
         combo_name as comboName,
         COALESCE(NULLIF(combo_execution_key, ''), NULLIF(combo_step_id, '')) as executionKey,
         combo_step_id as stepId,
         provider,
         model,
         requested_model as requestedModel,
         connection_id as connectionId,
         COUNT(*) as requests,
         SUM(CASE WHEN status >= 200 AND status < 400 THEN 1 ELSE 0 END) as successCount,
         COALESCE(SUM(tokens_in), 0) as inputTokens,
         COALESCE(SUM(tokens_out), 0) as outputTokens,
         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_in), 0) + COALESCE(SUM(tokens_out), 0) as totalTokens,
         COALESCE(AVG(duration), 0) as avgLatencyMs,
         MAX(timestamp) as lastUsedAt
       FROM call_logs
       WHERE ${conditions.join(" AND ")}
       GROUP BY combo_name, executionKey, combo_step_id, provider, model, requested_model, connection_id
       ORDER BY combo_name ASC, requests DESC`
    )
    .all(params) as ComboForecastUsageSqlRow[];

  return rows.map((row) => ({
    comboName: toString(row.comboName),
    executionKey:
      typeof row.executionKey === "string" && row.executionKey ? row.executionKey : null,
    stepId: typeof row.stepId === "string" && row.stepId ? row.stepId : null,
    provider: toString(row.provider),
    model: toString(row.model),
    requestedModel:
      typeof row.requestedModel === "string" && row.requestedModel ? row.requestedModel : null,
    connectionId:
      typeof row.connectionId === "string" && row.connectionId ? row.connectionId : null,
    requests: toNumber(row.requests),
    successCount: toNumber(row.successCount),
    inputTokens: toNumber(row.inputTokens),
    outputTokens: toNumber(row.outputTokens),
    cacheReadTokens: toNumber(row.cacheReadTokens),
    cacheCreationTokens: toNumber(row.cacheCreationTokens),
    reasoningTokens: toNumber(row.reasoningTokens),
    totalTokens: toNumber(row.totalTokens),
    avgLatencyMs: toNumber(row.avgLatencyMs),
    lastUsedAt: typeof row.lastUsedAt === "string" ? row.lastUsedAt : null,
  }));
}