Add the SQLite store for leases and accounting
Implemented-By: OpenCode session (model recorded in docs/implementer-log.md)
This commit is contained in:
@@ -0,0 +1,156 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
// schema is the durable layout: the lease table, the lease-event log, the
|
||||
// per-request accounting rows, the host-health poll log, and the daily
|
||||
// rollup that Prune writes into. Times are Unix milliseconds.
|
||||
const schema = `
|
||||
CREATE TABLE IF NOT EXISTS leases (
|
||||
route TEXT NOT NULL,
|
||||
fp TEXT NOT NULL,
|
||||
model TEXT NOT NULL,
|
||||
host TEXT NOT NULL,
|
||||
state TEXT NOT NULL,
|
||||
created INTEGER NOT NULL,
|
||||
last_used INTEGER NOT NULL,
|
||||
PRIMARY KEY (route, fp, model)
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS lease_events (
|
||||
ts INTEGER NOT NULL,
|
||||
route TEXT NOT NULL,
|
||||
model TEXT NOT NULL,
|
||||
from_host TEXT NOT NULL,
|
||||
to_host TEXT NOT NULL,
|
||||
reason TEXT NOT NULL
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS requests (
|
||||
id INTEGER PRIMARY KEY,
|
||||
route TEXT NOT NULL,
|
||||
fp TEXT NOT NULL,
|
||||
model TEXT NOT NULL,
|
||||
host TEXT NOT NULL,
|
||||
started INTEGER NOT NULL,
|
||||
queued_ms INTEGER NOT NULL,
|
||||
ttfb_ms INTEGER NOT NULL,
|
||||
total_ms INTEGER NOT NULL,
|
||||
status INTEGER NOT NULL,
|
||||
streamed INTEGER NOT NULL,
|
||||
prompt_tokens INTEGER NOT NULL,
|
||||
cached_tokens INTEGER NOT NULL,
|
||||
completion_tokens INTEGER NOT NULL,
|
||||
err TEXT NOT NULL
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS host_health (
|
||||
ts INTEGER NOT NULL,
|
||||
host TEXT NOT NULL,
|
||||
healthy INTEGER NOT NULL,
|
||||
loaded_models TEXT NOT NULL
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS requests_daily (
|
||||
day INTEGER NOT NULL,
|
||||
route TEXT NOT NULL,
|
||||
model TEXT NOT NULL,
|
||||
host TEXT NOT NULL,
|
||||
requests INTEGER NOT NULL,
|
||||
errors INTEGER NOT NULL,
|
||||
busy_ms INTEGER NOT NULL,
|
||||
queued_ms INTEGER NOT NULL,
|
||||
prompt_tokens INTEGER NOT NULL,
|
||||
cached_tokens INTEGER NOT NULL,
|
||||
completion_tokens INTEGER NOT NULL,
|
||||
PRIMARY KEY (day, route, model, host)
|
||||
);
|
||||
`
|
||||
|
||||
// byColumn maps a grouping to its table column, rejecting anything else.
|
||||
func byColumn(b By) (string, error) {
|
||||
switch b {
|
||||
case ByRoute:
|
||||
return "route", nil
|
||||
case ByModel:
|
||||
return "model", nil
|
||||
case ByHost:
|
||||
return "host", nil
|
||||
default:
|
||||
return "", fmt.Errorf("store: unknown grouping %q", b)
|
||||
}
|
||||
}
|
||||
|
||||
// midnight returns UTC midnight of the day holding ms.
|
||||
func midnight(ms int64) int64 {
|
||||
return time.UnixMilli(ms).Truncate(24 * time.Hour).UnixMilli()
|
||||
}
|
||||
|
||||
// btoi converts a bool to 0/1 for storage.
|
||||
func btoi(b bool) int64 {
|
||||
if b {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// encodeLoaded serialises a Loaded slice as JSON, writing nil as [].
|
||||
func encodeLoaded(loaded []string) ([]byte, error) {
|
||||
if loaded == nil {
|
||||
loaded = []string{}
|
||||
}
|
||||
return json.Marshal(loaded)
|
||||
}
|
||||
|
||||
// wrap prefixes a driver error with the package name.
|
||||
func wrap(err error) error {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("store: %w", err)
|
||||
}
|
||||
|
||||
func scanLeases(rows *sql.Rows) ([]Lease, error) {
|
||||
var out []Lease
|
||||
for rows.Next() {
|
||||
var (
|
||||
l Lease
|
||||
created, last int64
|
||||
)
|
||||
if err := rows.Scan(&l.Route, &l.FP, &l.Model, &l.Host, (*string)(&l.State), &created, &last); err != nil {
|
||||
return nil, wrap(err)
|
||||
}
|
||||
l.Created = time.UnixMilli(created).UTC()
|
||||
l.LastUsed = time.UnixMilli(last).UTC()
|
||||
out = append(out, l)
|
||||
}
|
||||
return out, rows.Err()
|
||||
}
|
||||
|
||||
func scanUsage(rows *sql.Rows) ([]UsageRow, error) {
|
||||
var out []UsageRow
|
||||
for rows.Next() {
|
||||
var u UsageRow
|
||||
if err := rows.Scan(&u.Key, &u.Requests, &u.Errors, &u.BusyMs, &u.QueuedMs,
|
||||
&u.PromptTokens, &u.CachedTokens, &u.CompletionTokens); err != nil {
|
||||
return nil, wrap(err)
|
||||
}
|
||||
out = append(out, u)
|
||||
}
|
||||
return out, rows.Err()
|
||||
}
|
||||
|
||||
func scanEvents(rows *sql.Rows) ([]LeaseEvent, error) {
|
||||
var out []LeaseEvent
|
||||
for rows.Next() {
|
||||
var e LeaseEvent
|
||||
var ts int64
|
||||
if err := rows.Scan(&ts, &e.Route, &e.Model, &e.FromHost, &e.ToHost, (*string)(&e.Reason)); err != nil {
|
||||
return nil, wrap(err)
|
||||
}
|
||||
e.TS = time.UnixMilli(ts).UTC()
|
||||
out = append(out, e)
|
||||
}
|
||||
return out, rows.Err()
|
||||
}
|
||||
Reference in New Issue
Block a user