Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
99 changes: 99 additions & 0 deletions app/Sources/GunkApp/Models/SpendModel.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import Foundation

struct SpendModel: Equatable, Sendable {
struct Row: Equatable, Identifiable, Sendable {
var id: String { "\(provider)::\(model)" }

let provider: String
let model: String
let inputTokens: Int64
let outputTokens: Int64
let runCount: Int
let hasUnknownTokens: Bool
let costEstimate: CostEstimate
}

let rows: [Row]
let totalInputTokens: Int64
let totalOutputTokens: Int64
let totalRunCount: Int
let totalUSD: Double
let unknownPriceRowCount: Int
let priceTableVersion: String
let effectiveDate: String

init(
aggregates: [LLMRunAggregate],
priceTable: PriceTable = .current,
limit: Int = 50
) {
let allRows = aggregates.map { aggregate in
Row(
provider: aggregate.provider,
model: aggregate.model,
inputTokens: aggregate.inputTokens,
outputTokens: aggregate.outputTokens,
runCount: aggregate.runCount,
hasUnknownTokens: aggregate.hasUnknownTokens,
costEstimate: estimate(
inputTokens: Self.intTokenCount(aggregate.inputTokens),
outputTokens: Self.intTokenCount(aggregate.outputTokens),
provider: aggregate.provider,
model: aggregate.model,
priceTable: priceTable
)
)
}

totalInputTokens = allRows.reduce(0) { $0 + $1.inputTokens }
totalOutputTokens = allRows.reduce(0) { $0 + $1.outputTokens }
totalRunCount = allRows.reduce(0) { $0 + $1.runCount }
totalUSD = allRows.reduce(0) { total, row in
total + (row.costEstimate.usd ?? 0)
}
unknownPriceRowCount = allRows.filter(\.costEstimate.unknownPrice).count
priceTableVersion = priceTable.priceTableVersion
effectiveDate = priceTable.effectiveDate

rows = Array(
allRows
.sorted(by: Self.rowSort)
.prefix(max(0, limit))
)
}

static func load(
store: Store,
priceTable: PriceTable = .current,
limit: Int = 50
) throws -> SpendModel {
SpendModel(
aggregates: try store.llmRunAggregatesByModel(),
priceTable: priceTable,
limit: limit
)
}

private static func intTokenCount(_ value: Int64) -> Int {
Int(min(max(0, value), Int64(Int.max)))
}

private static func rowSort(_ lhs: Row, _ rhs: Row) -> Bool {
switch (lhs.costEstimate.usd, rhs.costEstimate.usd) {
case let (left?, right?) where left != right:
return left > right
case (_?, nil):
return true
case (nil, _?):
return false
default:
if lhs.inputTokens + lhs.outputTokens != rhs.inputTokens + rhs.outputTokens {
return lhs.inputTokens + lhs.outputTokens > rhs.inputTokens + rhs.outputTokens
}
if lhs.provider.localizedCaseInsensitiveCompare(rhs.provider) != .orderedSame {
return lhs.provider.localizedCaseInsensitiveCompare(rhs.provider) == .orderedAscending
}
return lhs.model.localizedCaseInsensitiveCompare(rhs.model) == .orderedAscending
}
}
}
9 changes: 9 additions & 0 deletions app/Sources/GunkApp/Store/Models.swift
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,15 @@ struct LLMRun: Equatable, Identifiable, Sendable {
let finishedAt: Int64?
}

struct LLMRunAggregate: Equatable, Sendable {
let provider: String
let model: String
let inputTokens: Int64
let outputTokens: Int64
let runCount: Int
let hasUnknownTokens: Bool
}

struct GunkEmbedding: Equatable, Identifiable, Sendable {
var id: Int64 { gunkId }

Expand Down
125 changes: 121 additions & 4 deletions app/Sources/GunkApp/Store/Store.swift
Original file line number Diff line number Diff line change
Expand Up @@ -489,19 +489,17 @@ final class Store {
model,
input_tokens,
output_tokens,
cost_usd,
started_at,
finished_at
)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
VALUES (?, ?, ?, ?, ?, ?, ?)
""",
arguments: [
sourceId,
provider,
model,
inputTokens,
outputTokens,
costUsd,
startedAt,
finishedAt
]
Expand All @@ -514,13 +512,73 @@ final class Store {
model: model,
inputTokens: inputTokens,
outputTokens: outputTokens,
costUsd: costUsd,
costUsd: nil,
startedAt: startedAt,
finishedAt: finishedAt
)
}
}

func listLLMRuns() throws -> [LLMRun] {
try databaseQueue.read { db in
let rows = try Row.fetchAll(
db,
sql: """
SELECT
id,
source_id,
provider,
model,
input_tokens,
output_tokens,
started_at,
finished_at
FROM llm_runs
ORDER BY started_at ASC, id ASC
"""
)

return rows.map(Store.llmRun(from:))
}
}

func llmRunsForSource(_ sourceId: Int64) throws -> [LLMRun] {
try databaseQueue.read { db in
let rows = try Row.fetchAll(
db,
sql: """
SELECT
id,
source_id,
provider,
model,
input_tokens,
output_tokens,
started_at,
finished_at
FROM llm_runs
WHERE source_id = ?
ORDER BY started_at ASC, id ASC
""",
arguments: [sourceId]
)

return rows.map(Store.llmRun(from:))
}
}

func llmRunAggregatesByModel() throws -> [LLMRunAggregate] {
try databaseQueue.read { db in
try Store.llmRunAggregatesByModel(db)
}
}

func llmRunAggregatesByModel(sourceId: Int64) throws -> [LLMRunAggregate] {
try databaseQueue.read { db in
try Store.llmRunAggregatesByModel(db, sourceId: sourceId)
}
}

@discardableResult
func upsertGunkEmbedding(gunkId: Int64, vector: [Double], model: String) throws -> GunkEmbedding {
let dim = vector.count
Expand Down Expand Up @@ -1062,6 +1120,65 @@ final class Store {
)
}

private static func llmRun(from row: Row) -> LLMRun {
LLMRun(
id: row["id"],
sourceId: row["source_id"],
provider: row["provider"],
model: row["model"],
inputTokens: row["input_tokens"],
outputTokens: row["output_tokens"],
costUsd: nil,
startedAt: row["started_at"],
finishedAt: row["finished_at"]
)
}

private static func llmRunAggregate(from row: Row) -> LLMRunAggregate {
LLMRunAggregate(
provider: row["provider"],
model: row["model"],
inputTokens: row["input_tokens"],
outputTokens: row["output_tokens"],
runCount: row["run_count"],
hasUnknownTokens: row["has_unknown_tokens"]
)
}

private static func llmRunAggregatesByModel(
_ db: Database,
sourceId: Int64? = nil
) throws -> [LLMRunAggregate] {
let whereClause = sourceId == nil ? "" : "WHERE source_id = ?"
let rows = try Row.fetchAll(
db,
sql: """
SELECT
provider,
model,
SUM(COALESCE(input_tokens, 0)) AS input_tokens,
SUM(COALESCE(output_tokens, 0)) AS output_tokens,
COUNT(id) AS run_count,
CASE
WHEN SUM(
CASE
WHEN input_tokens IS NULL OR output_tokens IS NULL THEN 1
ELSE 0
END
) > 0 THEN 1
ELSE 0
END AS has_unknown_tokens
FROM llm_runs
\(whereClause)
GROUP BY provider, model
ORDER BY provider ASC, model ASC
""",
arguments: sourceId.map { [$0] } ?? []
)

return rows.map(Store.llmRunAggregate(from:))
}

private static func smokeRun(from row: Row) -> SmokeRunRecord {
SmokeRunRecord(
id: row["id"],
Expand Down
Loading
Loading