@hasna/economy 0.3.6 → 0.3.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +31 -0
- package/README.md +73 -60
- package/dashboard/README.md +17 -64
- package/dist/cli/commands/extras.d.ts.map +1 -1
- package/dist/cli/commands/todos.d.ts +3 -0
- package/dist/cli/commands/todos.d.ts.map +1 -1
- package/dist/cli/index.js +914 -199
- package/dist/db/cloud.d.ts +3 -4
- package/dist/db/cloud.d.ts.map +1 -1
- package/dist/db/database.d.ts +10 -3
- package/dist/db/database.d.ts.map +1 -1
- package/dist/db/dialect.d.ts +13 -0
- package/dist/db/dialect.d.ts.map +1 -0
- package/dist/db/pg-migrate.d.ts +30 -0
- package/dist/db/pg-migrate.d.ts.map +1 -0
- package/dist/db/pg-migrations.d.ts.map +1 -1
- package/dist/db/sqlite-adapter.d.ts +36 -0
- package/dist/db/sqlite-adapter.d.ts.map +1 -0
- package/dist/db/sync-pg.d.ts +1 -1
- package/dist/db/sync-pg.d.ts.map +1 -1
- package/dist/index.js +309 -59
- package/dist/ingest/billing.d.ts +1 -1
- package/dist/ingest/billing.d.ts.map +1 -1
- package/dist/ingest/claude-quota.d.ts +1 -1
- package/dist/ingest/claude-quota.d.ts.map +1 -1
- package/dist/ingest/claude.d.ts +1 -1
- package/dist/ingest/claude.d.ts.map +1 -1
- package/dist/ingest/codex-quota.d.ts +1 -1
- package/dist/ingest/codex-quota.d.ts.map +1 -1
- package/dist/ingest/codex.d.ts +1 -1
- package/dist/ingest/codex.d.ts.map +1 -1
- package/dist/ingest/cursor.d.ts +1 -1
- package/dist/ingest/cursor.d.ts.map +1 -1
- package/dist/ingest/gemini.d.ts +1 -1
- package/dist/ingest/gemini.d.ts.map +1 -1
- package/dist/ingest/hermes.d.ts +1 -1
- package/dist/ingest/hermes.d.ts.map +1 -1
- package/dist/ingest/loops.d.ts +6 -0
- package/dist/ingest/loops.d.ts.map +1 -0
- package/dist/ingest/opencode.d.ts +1 -1
- package/dist/ingest/opencode.d.ts.map +1 -1
- package/dist/ingest/otel.d.ts +16 -3
- package/dist/ingest/otel.d.ts.map +1 -1
- package/dist/ingest/pi.d.ts +1 -1
- package/dist/ingest/pi.d.ts.map +1 -1
- package/dist/ingest/plugin.d.ts +1 -1
- package/dist/ingest/plugin.d.ts.map +1 -1
- package/dist/lib/accounts.d.ts.map +1 -1
- package/dist/lib/analytics.d.ts +1 -1
- package/dist/lib/analytics.d.ts.map +1 -1
- package/dist/lib/billing-diff.d.ts +1 -1
- package/dist/lib/billing-diff.d.ts.map +1 -1
- package/dist/lib/brief.d.ts +1 -1
- package/dist/lib/brief.d.ts.map +1 -1
- package/dist/lib/open-projects.d.ts +10 -2
- package/dist/lib/open-projects.d.ts.map +1 -1
- package/dist/lib/pricing.d.ts +1 -1
- package/dist/lib/pricing.d.ts.map +1 -1
- package/dist/lib/savings.d.ts +1 -1
- package/dist/lib/savings.d.ts.map +1 -1
- package/dist/lib/spikes.d.ts +1 -1
- package/dist/lib/spikes.d.ts.map +1 -1
- package/dist/lib/store/index.d.ts +9 -1
- package/dist/lib/store/index.d.ts.map +1 -1
- package/dist/lib/sync-all.d.ts +3 -1
- package/dist/lib/sync-all.d.ts.map +1 -1
- package/dist/lib/webhooks.d.ts +1 -1
- package/dist/lib/webhooks.d.ts.map +1 -1
- package/dist/mcp/index.js +757 -166
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/openapi.d.ts.map +1 -1
- package/dist/otel/index.js +262 -27
- package/dist/server/index.js +683 -135
- package/dist/server/serve.d.ts +1 -1
- package/dist/server/serve.d.ts.map +1 -1
- package/dist/types/index.d.ts +37 -4
- package/dist/types/index.d.ts.map +1 -1
- package/docs/README.md +12 -0
- package/docs/cli.md +88 -0
- package/docs/configuration.md +86 -0
- package/docs/ingestion.md +47 -0
- package/docs/mcp.md +65 -0
- package/docs/otel.md +53 -0
- package/docs/rest-api.md +89 -0
- package/package.json +4 -5
- package/dist/lib/cloud-sync.d.ts +0 -35
- package/dist/lib/cloud-sync.d.ts.map +0 -1
- package/dist/lib/fleet-sync.d.ts +0 -61
- package/dist/lib/fleet-sync.d.ts.map +0 -1
- package/dist/lib/peer-sync.d.ts +0 -21
- package/dist/lib/peer-sync.d.ts.map +0 -1
- package/dist/storage.js +0 -2746
package/dist/mcp/server.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/mcp/server.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/mcp/server.ts"],"names":[],"mappings":"AAYA,eAAO,MAAM,QAAQ,YAAY,CAAA;AACjC,eAAO,MAAM,qBAAqB,OAAO,CAAA;AAEzC,wBAAgB,WAAW,IAAI,GAAG,CAm0BjC"}
|
package/dist/openapi.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"openapi.d.ts","sourceRoot":"","sources":["../src/openapi.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,
|
|
1
|
+
{"version":3,"file":"openapi.d.ts","sourceRoot":"","sources":["../src/openapi.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAikD/C,CAAA;AACD,eAAe,WAAW,CAAA"}
|
package/dist/otel/index.js
CHANGED
|
@@ -16,6 +16,62 @@ var __export = (target, all) => {
|
|
|
16
16
|
};
|
|
17
17
|
var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
|
18
18
|
|
|
19
|
+
// src/db/sqlite-adapter.ts
|
|
20
|
+
import { Database } from "bun:sqlite";
|
|
21
|
+
|
|
22
|
+
class SqliteAdapter {
|
|
23
|
+
db;
|
|
24
|
+
constructor(path) {
|
|
25
|
+
this.db = new Database(path, { create: true });
|
|
26
|
+
this.db.exec("PRAGMA journal_mode=WAL");
|
|
27
|
+
this.db.exec("PRAGMA foreign_keys=ON");
|
|
28
|
+
}
|
|
29
|
+
run(sql, ...params) {
|
|
30
|
+
const result = this.db.prepare(sql).run(...params);
|
|
31
|
+
return { changes: result.changes, lastInsertRowid: result.lastInsertRowid };
|
|
32
|
+
}
|
|
33
|
+
get(sql, ...params) {
|
|
34
|
+
return this.db.prepare(sql).get(...params);
|
|
35
|
+
}
|
|
36
|
+
all(sql, ...params) {
|
|
37
|
+
return this.db.prepare(sql).all(...params);
|
|
38
|
+
}
|
|
39
|
+
exec(sql) {
|
|
40
|
+
this.db.exec(sql);
|
|
41
|
+
}
|
|
42
|
+
query(sql) {
|
|
43
|
+
return this.db.query(sql);
|
|
44
|
+
}
|
|
45
|
+
prepare(sql) {
|
|
46
|
+
const stmt = this.db.prepare(sql);
|
|
47
|
+
return {
|
|
48
|
+
run(...params) {
|
|
49
|
+
const r = stmt.run(...params);
|
|
50
|
+
return { changes: r.changes, lastInsertRowid: r.lastInsertRowid };
|
|
51
|
+
},
|
|
52
|
+
get(...params) {
|
|
53
|
+
return stmt.get(...params);
|
|
54
|
+
},
|
|
55
|
+
all(...params) {
|
|
56
|
+
return stmt.all(...params);
|
|
57
|
+
},
|
|
58
|
+
finalize() {
|
|
59
|
+
stmt.finalize();
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
close() {
|
|
64
|
+
this.db.close();
|
|
65
|
+
}
|
|
66
|
+
transaction(fn) {
|
|
67
|
+
return this.db.transaction(fn)();
|
|
68
|
+
}
|
|
69
|
+
get raw() {
|
|
70
|
+
return this.db;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
var init_sqlite_adapter = () => {};
|
|
74
|
+
|
|
19
75
|
// src/lib/pricing.ts
|
|
20
76
|
var exports_pricing = {};
|
|
21
77
|
__export(exports_pricing, {
|
|
@@ -534,7 +590,6 @@ var init_pricing = __esm(() => {
|
|
|
534
590
|
});
|
|
535
591
|
|
|
536
592
|
// src/db/database.ts
|
|
537
|
-
import { SqliteAdapter as Database } from "@hasna/cloud";
|
|
538
593
|
import { execFileSync } from "child_process";
|
|
539
594
|
import { copyFileSync, existsSync, mkdirSync, readdirSync, statSync } from "fs";
|
|
540
595
|
import { hostname, platform } from "os";
|
|
@@ -627,7 +682,7 @@ function openDatabase(dbPath, skipSeed = false) {
|
|
|
627
682
|
mkdirSync(dir, { recursive: true });
|
|
628
683
|
}
|
|
629
684
|
const db = withSqliteBusyRetry(() => {
|
|
630
|
-
const opened = new
|
|
685
|
+
const opened = new SqliteAdapter(path);
|
|
631
686
|
try {
|
|
632
687
|
opened.exec("PRAGMA busy_timeout = 10000");
|
|
633
688
|
opened.exec("PRAGMA journal_mode = WAL");
|
|
@@ -668,6 +723,16 @@ function addColumnIfMissing(db, table, column, definition) {
|
|
|
668
723
|
}
|
|
669
724
|
function initSchema(db) {
|
|
670
725
|
db.exec(`
|
|
726
|
+
CREATE TABLE IF NOT EXISTS cost_centers (
|
|
727
|
+
id TEXT PRIMARY KEY,
|
|
728
|
+
kind TEXT NOT NULL,
|
|
729
|
+
name TEXT NOT NULL,
|
|
730
|
+
repo_path TEXT,
|
|
731
|
+
labels_json TEXT DEFAULT '{}',
|
|
732
|
+
created_at TEXT NOT NULL,
|
|
733
|
+
updated_at TEXT NOT NULL
|
|
734
|
+
);
|
|
735
|
+
|
|
671
736
|
CREATE TABLE IF NOT EXISTS requests (
|
|
672
737
|
id TEXT PRIMARY KEY,
|
|
673
738
|
agent TEXT NOT NULL,
|
|
@@ -684,6 +749,7 @@ function initSchema(db) {
|
|
|
684
749
|
timestamp TEXT NOT NULL,
|
|
685
750
|
source_request_id TEXT,
|
|
686
751
|
machine_id TEXT DEFAULT '',
|
|
752
|
+
cost_center_id TEXT,
|
|
687
753
|
account_key TEXT DEFAULT '',
|
|
688
754
|
account_tool TEXT DEFAULT '',
|
|
689
755
|
account_name TEXT DEFAULT '',
|
|
@@ -702,6 +768,7 @@ function initSchema(db) {
|
|
|
702
768
|
total_tokens INTEGER DEFAULT 0,
|
|
703
769
|
request_count INTEGER DEFAULT 0,
|
|
704
770
|
machine_id TEXT DEFAULT '',
|
|
771
|
+
cost_center_id TEXT,
|
|
705
772
|
account_key TEXT DEFAULT '',
|
|
706
773
|
account_tool TEXT DEFAULT '',
|
|
707
774
|
account_name TEXT DEFAULT '',
|
|
@@ -722,6 +789,7 @@ function initSchema(db) {
|
|
|
722
789
|
id TEXT PRIMARY KEY,
|
|
723
790
|
project_path TEXT,
|
|
724
791
|
agent TEXT,
|
|
792
|
+
cost_center_id TEXT,
|
|
725
793
|
period TEXT NOT NULL,
|
|
726
794
|
limit_usd REAL NOT NULL,
|
|
727
795
|
alert_at_percent INTEGER DEFAULT 80,
|
|
@@ -752,6 +820,7 @@ function initSchema(db) {
|
|
|
752
820
|
CREATE INDEX IF NOT EXISTS idx_sessions_agent ON sessions(agent);
|
|
753
821
|
CREATE INDEX IF NOT EXISTS idx_sessions_project ON sessions(project_path);
|
|
754
822
|
CREATE INDEX IF NOT EXISTS idx_sessions_started ON sessions(started_at);
|
|
823
|
+
CREATE INDEX IF NOT EXISTS idx_cost_centers_kind ON cost_centers(kind);
|
|
755
824
|
|
|
756
825
|
CREATE TABLE IF NOT EXISTS model_pricing (
|
|
757
826
|
model TEXT PRIMARY KEY,
|
|
@@ -838,6 +907,12 @@ function initSchema(db) {
|
|
|
838
907
|
`);
|
|
839
908
|
addColumnIfMissing(db, "requests", "machine_id", `TEXT DEFAULT ''`);
|
|
840
909
|
addColumnIfMissing(db, "sessions", "machine_id", `TEXT DEFAULT ''`);
|
|
910
|
+
if (addColumnIfMissing(db, "cost_centers", "updated_at", `TEXT DEFAULT ''`)) {
|
|
911
|
+
db.exec(`UPDATE cost_centers SET updated_at = created_at WHERE updated_at = '' OR updated_at IS NULL`);
|
|
912
|
+
}
|
|
913
|
+
addColumnIfMissing(db, "requests", "cost_center_id", `TEXT`);
|
|
914
|
+
addColumnIfMissing(db, "sessions", "cost_center_id", `TEXT`);
|
|
915
|
+
addColumnIfMissing(db, "budgets", "cost_center_id", `TEXT`);
|
|
841
916
|
if (addColumnIfMissing(db, "requests", "cache_create_5m_tokens", "INTEGER DEFAULT 0")) {
|
|
842
917
|
db.exec(`UPDATE requests SET cache_create_5m_tokens = cache_create_tokens WHERE cache_create_5m_tokens = 0`);
|
|
843
918
|
}
|
|
@@ -866,8 +941,20 @@ function initSchema(db) {
|
|
|
866
941
|
CREATE INDEX IF NOT EXISTS idx_sessions_machine ON sessions(machine_id);
|
|
867
942
|
CREATE INDEX IF NOT EXISTS idx_requests_account ON requests(account_key);
|
|
868
943
|
CREATE INDEX IF NOT EXISTS idx_sessions_account ON sessions(account_key);
|
|
944
|
+
CREATE INDEX IF NOT EXISTS idx_requests_cost_center ON requests(cost_center_id);
|
|
945
|
+
CREATE INDEX IF NOT EXISTS idx_sessions_cost_center ON sessions(cost_center_id);
|
|
946
|
+
CREATE INDEX IF NOT EXISTS idx_budgets_cost_center ON budgets(cost_center_id);
|
|
947
|
+
CREATE INDEX IF NOT EXISTS idx_cost_centers_kind ON cost_centers(kind);
|
|
869
948
|
`);
|
|
870
949
|
}
|
|
950
|
+
function upsertCostCenter(db, center) {
|
|
951
|
+
const updatedAt = center.updated_at ?? center.created_at;
|
|
952
|
+
db.prepare(`
|
|
953
|
+
INSERT OR REPLACE INTO cost_centers
|
|
954
|
+
(id, kind, name, repo_path, labels_json, created_at, updated_at)
|
|
955
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
956
|
+
`).run(center.id, center.kind, center.name, center.repo_path ?? null, center.labels_json ?? "{}", center.created_at, updatedAt);
|
|
957
|
+
}
|
|
871
958
|
function upsertRequest(db, req) {
|
|
872
959
|
const now = req.updated_at ?? new Date().toISOString();
|
|
873
960
|
db.prepare(`
|
|
@@ -875,10 +962,10 @@ function upsertRequest(db, req) {
|
|
|
875
962
|
(id, agent, session_id, model, input_tokens, output_tokens,
|
|
876
963
|
cache_read_tokens, cache_create_tokens, cache_create_5m_tokens,
|
|
877
964
|
cache_create_1h_tokens, cost_usd, cost_basis, duration_ms, timestamp,
|
|
878
|
-
source_request_id, machine_id, attribution_tag, account_key, account_tool,
|
|
965
|
+
source_request_id, machine_id, cost_center_id, attribution_tag, account_key, account_tool,
|
|
879
966
|
account_name, account_email, account_source, updated_at, synced_at)
|
|
880
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
881
|
-
`).run(req.id, req.agent, req.session_id, req.model, req.input_tokens, req.output_tokens, req.cache_read_tokens, req.cache_create_tokens, req.cache_create_5m_tokens ?? req.cache_create_tokens, req.cache_create_1h_tokens ?? 0, req.cost_usd, req.cost_basis ?? "estimated", req.duration_ms, req.timestamp, req.source_request_id, req.machine_id ?? "", req.attribution_tag ?? process.env["ECONOMY_TAG"] ?? "", req.account_key ?? "", req.account_tool ?? "", req.account_name ?? "", req.account_email ?? "", req.account_source ?? "", now, req.synced_at ?? "");
|
|
967
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
968
|
+
`).run(req.id, req.agent, req.session_id, req.model, req.input_tokens, req.output_tokens, req.cache_read_tokens, req.cache_create_tokens, req.cache_create_5m_tokens ?? req.cache_create_tokens, req.cache_create_1h_tokens ?? 0, req.cost_usd, req.cost_basis ?? "estimated", req.duration_ms, req.timestamp, req.source_request_id, req.machine_id ?? "", req.cost_center_id ?? null, req.attribution_tag ?? process.env["ECONOMY_TAG"] ?? "", req.account_key ?? "", req.account_tool ?? "", req.account_name ?? "", req.account_email ?? "", req.account_source ?? "", now, req.synced_at ?? "");
|
|
882
969
|
}
|
|
883
970
|
function upsertSession(db, session) {
|
|
884
971
|
const now = session.updated_at ?? new Date().toISOString();
|
|
@@ -886,9 +973,9 @@ function upsertSession(db, session) {
|
|
|
886
973
|
INSERT OR REPLACE INTO sessions
|
|
887
974
|
(id, agent, project_path, project_name, started_at, ended_at,
|
|
888
975
|
total_cost_usd, total_tokens, request_count, machine_id, attribution_tag,
|
|
889
|
-
account_key, account_tool, account_name, account_email, account_source, updated_at, synced_at)
|
|
890
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
891
|
-
`).run(session.id, session.agent, session.project_path, session.project_name, session.started_at, session.ended_at ?? null, session.total_cost_usd, session.total_tokens, session.request_count, session.machine_id ?? "", session.attribution_tag ?? process.env["ECONOMY_TAG"] ?? "", session.account_key ?? "", session.account_tool ?? "", session.account_name ?? "", session.account_email ?? "", session.account_source ?? "", now, session.synced_at ?? "");
|
|
976
|
+
cost_center_id, account_key, account_tool, account_name, account_email, account_source, updated_at, synced_at)
|
|
977
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
978
|
+
`).run(session.id, session.agent, session.project_path, session.project_name, session.started_at, session.ended_at ?? null, session.total_cost_usd, session.total_tokens, session.request_count, session.machine_id ?? "", session.attribution_tag ?? process.env["ECONOMY_TAG"] ?? "", session.cost_center_id ?? null, session.account_key ?? "", session.account_tool ?? "", session.account_name ?? "", session.account_email ?? "", session.account_source ?? "", now, session.synced_at ?? "");
|
|
892
979
|
}
|
|
893
980
|
function rollupSession(db, sessionId) {
|
|
894
981
|
db.prepare(`
|
|
@@ -914,9 +1001,12 @@ function rollupSession(db, sessionId) {
|
|
|
914
1001
|
ELSE account_email END,
|
|
915
1002
|
account_source = CASE WHEN account_source = '' OR account_source IS NULL
|
|
916
1003
|
THEN COALESCE((SELECT account_source FROM requests WHERE session_id = ? AND account_source != '' ORDER BY timestamp DESC LIMIT 1), '')
|
|
917
|
-
ELSE account_source END
|
|
1004
|
+
ELSE account_source END,
|
|
1005
|
+
cost_center_id = CASE WHEN cost_center_id = '' OR cost_center_id IS NULL
|
|
1006
|
+
THEN COALESCE((SELECT cost_center_id FROM requests WHERE session_id = ? AND cost_center_id IS NOT NULL AND cost_center_id != '' ORDER BY timestamp DESC LIMIT 1), NULL)
|
|
1007
|
+
ELSE cost_center_id END
|
|
918
1008
|
WHERE id = ?
|
|
919
|
-
`).run(sessionId, sessionId, sessionId, sessionId, sessionId, sessionId, sessionId, sessionId, sessionId, sessionId, sessionId);
|
|
1009
|
+
`).run(sessionId, sessionId, sessionId, sessionId, sessionId, sessionId, sessionId, sessionId, sessionId, sessionId, sessionId, sessionId);
|
|
920
1010
|
}
|
|
921
1011
|
function upsertModelPricing(db, p) {
|
|
922
1012
|
db.prepare(`
|
|
@@ -949,7 +1039,9 @@ function seedModelPricing(db, defaults) {
|
|
|
949
1039
|
});
|
|
950
1040
|
}
|
|
951
1041
|
}
|
|
952
|
-
var init_database = () => {
|
|
1042
|
+
var init_database = __esm(() => {
|
|
1043
|
+
init_sqlite_adapter();
|
|
1044
|
+
});
|
|
953
1045
|
|
|
954
1046
|
// src/otel/index.ts
|
|
955
1047
|
init_database();
|
|
@@ -995,6 +1087,97 @@ function attrsMap(attributes) {
|
|
|
995
1087
|
}
|
|
996
1088
|
return out;
|
|
997
1089
|
}
|
|
1090
|
+
var COST_CENTER_KINDS = ["loop", "app", "repo", "service", "team"];
|
|
1091
|
+
var PSEUDO_AGENTS = ["app", "service", "repo", "loop"];
|
|
1092
|
+
function isCostCenterKind(value) {
|
|
1093
|
+
return COST_CENTER_KINDS.includes(value);
|
|
1094
|
+
}
|
|
1095
|
+
function asString(value) {
|
|
1096
|
+
if (value == null)
|
|
1097
|
+
return;
|
|
1098
|
+
const text = String(value).trim();
|
|
1099
|
+
return text ? text : undefined;
|
|
1100
|
+
}
|
|
1101
|
+
function firstString(source, keys) {
|
|
1102
|
+
for (const key of keys) {
|
|
1103
|
+
const value = asString(source[key]);
|
|
1104
|
+
if (value)
|
|
1105
|
+
return value;
|
|
1106
|
+
}
|
|
1107
|
+
return;
|
|
1108
|
+
}
|
|
1109
|
+
function normalizeCostCenterKind(raw) {
|
|
1110
|
+
if (!raw)
|
|
1111
|
+
return;
|
|
1112
|
+
const value = raw.toLowerCase();
|
|
1113
|
+
return isCostCenterKind(value) ? value : undefined;
|
|
1114
|
+
}
|
|
1115
|
+
function normalizeAgent(raw, kind) {
|
|
1116
|
+
if (raw && isAgent(raw))
|
|
1117
|
+
return raw;
|
|
1118
|
+
if (raw && PSEUDO_AGENTS.includes(raw))
|
|
1119
|
+
return raw;
|
|
1120
|
+
if (kind && PSEUDO_AGENTS.includes(kind))
|
|
1121
|
+
return kind;
|
|
1122
|
+
return "service";
|
|
1123
|
+
}
|
|
1124
|
+
function slug(value) {
|
|
1125
|
+
return value.trim().toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "") || "unknown";
|
|
1126
|
+
}
|
|
1127
|
+
function costCenterId(kind, name, explicitId) {
|
|
1128
|
+
if (explicitId)
|
|
1129
|
+
return explicitId;
|
|
1130
|
+
if (!kind || !name)
|
|
1131
|
+
return;
|
|
1132
|
+
return `${kind}:${slug(name)}`;
|
|
1133
|
+
}
|
|
1134
|
+
function projectNameFromPath(projectPath) {
|
|
1135
|
+
if (!projectPath)
|
|
1136
|
+
return;
|
|
1137
|
+
return projectPath.split("/").filter(Boolean).at(-1);
|
|
1138
|
+
}
|
|
1139
|
+
function extractAttribution(source) {
|
|
1140
|
+
const sourceKind = firstString(source, ["source", "source_kind", "ai.source"]);
|
|
1141
|
+
const rawKind = firstString(source, ["cost_center_kind", "cost_center.kind", "economy.cost_center.kind"]) ?? sourceKind;
|
|
1142
|
+
const costCenterKind = normalizeCostCenterKind(rawKind);
|
|
1143
|
+
const costCenter = firstString(source, [
|
|
1144
|
+
"cost_center",
|
|
1145
|
+
"cost_center_name",
|
|
1146
|
+
"cost_center.name",
|
|
1147
|
+
"economy.cost_center",
|
|
1148
|
+
"service.name",
|
|
1149
|
+
"app",
|
|
1150
|
+
"app.name",
|
|
1151
|
+
"repo",
|
|
1152
|
+
"repository"
|
|
1153
|
+
]);
|
|
1154
|
+
const projectPath = firstString(source, ["project_path", "project.path", "repo_path", "repository.path"]);
|
|
1155
|
+
const explicitId = firstString(source, ["cost_center_id", "cost_center.id", "economy.cost_center.id"]);
|
|
1156
|
+
const costBasisRaw = firstString(source, ["cost_basis", "economy.cost_basis"]);
|
|
1157
|
+
const costBasis = costBasisRaw && ["metered_api", "subscription_included", "estimated", "unknown"].includes(costBasisRaw) ? costBasisRaw : undefined;
|
|
1158
|
+
return {
|
|
1159
|
+
agent: normalizeAgent(firstString(source, ["agent", "ai.agent"]) ?? sourceKind, costCenterKind),
|
|
1160
|
+
cost_center: costCenter,
|
|
1161
|
+
cost_center_kind: costCenterKind,
|
|
1162
|
+
cost_center_id: costCenterId(costCenterKind, costCenter, explicitId),
|
|
1163
|
+
attribution_tag: firstString(source, ["attribution_tag", "attribution.tag", "economy.attribution_tag"]),
|
|
1164
|
+
project_path: projectPath,
|
|
1165
|
+
project_name: firstString(source, ["project_name", "project.name"]) ?? projectNameFromPath(projectPath) ?? costCenter,
|
|
1166
|
+
repo: firstString(source, ["repo", "repository", "repository.name"]),
|
|
1167
|
+
account_key: firstString(source, ["account_key", "account.key", "economy.account_key"]),
|
|
1168
|
+
account_tool: firstString(source, ["account_tool", "account.tool", "economy.account_tool"]),
|
|
1169
|
+
account_name: firstString(source, ["account_name", "account.name", "economy.account_name"]),
|
|
1170
|
+
account_email: firstString(source, ["account_email", "account.email", "economy.account_email"]),
|
|
1171
|
+
account_source: firstString(source, ["account_source", "account.source", "economy.account_source"]),
|
|
1172
|
+
cost_basis: costBasis
|
|
1173
|
+
};
|
|
1174
|
+
}
|
|
1175
|
+
function mergeAttribution(row, attribution) {
|
|
1176
|
+
return {
|
|
1177
|
+
...row,
|
|
1178
|
+
...Object.fromEntries(Object.entries(attribution).filter(([, value]) => value !== undefined && value !== ""))
|
|
1179
|
+
};
|
|
1180
|
+
}
|
|
998
1181
|
function metricKind(body) {
|
|
999
1182
|
const name = String(body["name"] ?? "").toLowerCase();
|
|
1000
1183
|
if (name.includes("cost") || name.includes(".usd"))
|
|
@@ -1043,13 +1226,13 @@ function parseOtlpMetrics(body) {
|
|
|
1043
1226
|
continue;
|
|
1044
1227
|
const pointAttrs = attrsMap(dp.attributes);
|
|
1045
1228
|
const merged = { ...resourceAttrs, ...pointAttrs };
|
|
1046
|
-
const
|
|
1047
|
-
const agent =
|
|
1229
|
+
const attribution = extractAttribution(merged);
|
|
1230
|
+
const agent = attribution.agent;
|
|
1048
1231
|
const sessionId = String(merged["session_id"] ?? merged["session.id"] ?? "otel-session");
|
|
1049
1232
|
const model = String(merged["model"] ?? merged["ai.model"] ?? "unknown");
|
|
1050
1233
|
const sourceId = String(merged["request_id"] ?? merged["event.id"] ?? `${sessionId}-${kind}`);
|
|
1051
1234
|
const key = `${agent}:${sourceId}`;
|
|
1052
|
-
const row = partial.get(key) ?? {
|
|
1235
|
+
const row = mergeAttribution(partial.get(key) ?? {
|
|
1053
1236
|
key,
|
|
1054
1237
|
agent,
|
|
1055
1238
|
session_id: sessionId,
|
|
@@ -1059,7 +1242,7 @@ function parseOtlpMetrics(body) {
|
|
|
1059
1242
|
output_tokens: 0,
|
|
1060
1243
|
timestamp: new Date().toISOString(),
|
|
1061
1244
|
source_request_id: sourceId
|
|
1062
|
-
};
|
|
1245
|
+
}, attribution);
|
|
1063
1246
|
const asDouble = dp.asDouble;
|
|
1064
1247
|
const asInt = dp.asInt;
|
|
1065
1248
|
const value = asDouble ?? (asInt != null ? Number(asInt) : 0);
|
|
@@ -1086,16 +1269,27 @@ function parseOtlpMetrics(body) {
|
|
|
1086
1269
|
input_tokens: r.input_tokens ?? 0,
|
|
1087
1270
|
output_tokens: r.output_tokens ?? 0,
|
|
1088
1271
|
timestamp: r.timestamp,
|
|
1089
|
-
source_request_id: r.source_request_id
|
|
1272
|
+
source_request_id: r.source_request_id,
|
|
1273
|
+
cost_basis: r.cost_basis,
|
|
1274
|
+
cost_center: r.cost_center,
|
|
1275
|
+
cost_center_kind: r.cost_center_kind,
|
|
1276
|
+
cost_center_id: r.cost_center_id,
|
|
1277
|
+
attribution_tag: r.attribution_tag,
|
|
1278
|
+
project_path: r.project_path,
|
|
1279
|
+
project_name: r.project_name,
|
|
1280
|
+
repo: r.repo,
|
|
1281
|
+
account_key: r.account_key,
|
|
1282
|
+
account_tool: r.account_tool,
|
|
1283
|
+
account_name: r.account_name,
|
|
1284
|
+
account_email: r.account_email,
|
|
1285
|
+
account_source: r.account_source
|
|
1090
1286
|
}));
|
|
1091
1287
|
}
|
|
1092
1288
|
function parseSimpleIngest(body) {
|
|
1093
1289
|
if (!body || typeof body !== "object")
|
|
1094
1290
|
return null;
|
|
1095
1291
|
const b = body;
|
|
1096
|
-
const
|
|
1097
|
-
if (!isAgent(agentRaw))
|
|
1098
|
-
return null;
|
|
1292
|
+
const attribution = extractAttribution(b);
|
|
1099
1293
|
const cost = Number(b["cost_usd"] ?? 0);
|
|
1100
1294
|
const input = Number(b["input_tokens"] ?? 0);
|
|
1101
1295
|
const output = Number(b["output_tokens"] ?? 0);
|
|
@@ -1104,7 +1298,8 @@ function parseSimpleIngest(body) {
|
|
|
1104
1298
|
const sessionId = String(b["session_id"] ?? "otel-session");
|
|
1105
1299
|
const sourceId = String(b["request_id"] ?? b["source_request_id"] ?? `${sessionId}-${Date.now()}`);
|
|
1106
1300
|
return {
|
|
1107
|
-
|
|
1301
|
+
...attribution,
|
|
1302
|
+
agent: attribution.agent,
|
|
1108
1303
|
session_id: sessionId,
|
|
1109
1304
|
model: String(b["model"] ?? "unknown"),
|
|
1110
1305
|
cost_usd: cost,
|
|
@@ -1114,12 +1309,31 @@ function parseSimpleIngest(body) {
|
|
|
1114
1309
|
source_request_id: sourceId
|
|
1115
1310
|
};
|
|
1116
1311
|
}
|
|
1312
|
+
function ensureCostCenter(db, row) {
|
|
1313
|
+
const id = row.cost_center_id ?? costCenterId(row.cost_center_kind, row.cost_center);
|
|
1314
|
+
if (!id || !row.cost_center_kind || !row.cost_center)
|
|
1315
|
+
return id;
|
|
1316
|
+
const labels = { source: "otel" };
|
|
1317
|
+
if (row.repo)
|
|
1318
|
+
labels["repo"] = row.repo;
|
|
1319
|
+
upsertCostCenter(db, {
|
|
1320
|
+
id,
|
|
1321
|
+
kind: row.cost_center_kind,
|
|
1322
|
+
name: row.cost_center,
|
|
1323
|
+
repo_path: row.project_path ?? null,
|
|
1324
|
+
labels_json: JSON.stringify(labels),
|
|
1325
|
+
created_at: row.timestamp
|
|
1326
|
+
});
|
|
1327
|
+
return id;
|
|
1328
|
+
}
|
|
1117
1329
|
async function ingestOtelRows(db, rows) {
|
|
1118
1330
|
const machineId = getMachineId();
|
|
1119
1331
|
const sessions = new Set;
|
|
1120
1332
|
let requests = 0;
|
|
1121
1333
|
for (const row of rows) {
|
|
1122
|
-
const
|
|
1334
|
+
const costCenterId2 = ensureCostCenter(db, row);
|
|
1335
|
+
const requestScope = slug(costCenterId2 ?? row.cost_center ?? row.project_path ?? row.session_id);
|
|
1336
|
+
const reqId = `otel-${row.agent}-${requestScope}-${row.source_request_id}`;
|
|
1123
1337
|
upsertRequest(db, {
|
|
1124
1338
|
id: reqId,
|
|
1125
1339
|
agent: row.agent,
|
|
@@ -1130,25 +1344,38 @@ async function ingestOtelRows(db, rows) {
|
|
|
1130
1344
|
cache_read_tokens: 0,
|
|
1131
1345
|
cache_create_tokens: 0,
|
|
1132
1346
|
cost_usd: row.cost_usd,
|
|
1133
|
-
cost_basis: "estimated",
|
|
1347
|
+
cost_basis: row.cost_basis ?? "estimated",
|
|
1134
1348
|
duration_ms: 0,
|
|
1135
1349
|
timestamp: row.timestamp,
|
|
1136
1350
|
source_request_id: row.source_request_id,
|
|
1137
1351
|
machine_id: machineId,
|
|
1138
|
-
|
|
1352
|
+
cost_center_id: costCenterId2 ?? null,
|
|
1353
|
+
attribution_tag: row.attribution_tag ?? "otel",
|
|
1354
|
+
account_key: row.account_key ?? "",
|
|
1355
|
+
account_tool: row.account_tool ?? "",
|
|
1356
|
+
account_name: row.account_name ?? "",
|
|
1357
|
+
account_email: row.account_email ?? "",
|
|
1358
|
+
account_source: row.account_source ?? ""
|
|
1139
1359
|
});
|
|
1140
1360
|
if (!sessions.has(row.session_id)) {
|
|
1141
1361
|
upsertSession(db, {
|
|
1142
1362
|
id: row.session_id,
|
|
1143
1363
|
agent: row.agent,
|
|
1144
|
-
project_path: "",
|
|
1145
|
-
project_name: "",
|
|
1364
|
+
project_path: row.project_path ?? "",
|
|
1365
|
+
project_name: row.project_name ?? row.repo ?? row.cost_center ?? "",
|
|
1146
1366
|
started_at: row.timestamp,
|
|
1147
1367
|
ended_at: null,
|
|
1148
1368
|
total_cost_usd: 0,
|
|
1149
1369
|
total_tokens: 0,
|
|
1150
1370
|
request_count: 0,
|
|
1151
|
-
machine_id: machineId
|
|
1371
|
+
machine_id: machineId,
|
|
1372
|
+
cost_center_id: costCenterId2 ?? null,
|
|
1373
|
+
attribution_tag: row.attribution_tag ?? "otel",
|
|
1374
|
+
account_key: row.account_key ?? "",
|
|
1375
|
+
account_tool: row.account_tool ?? "",
|
|
1376
|
+
account_name: row.account_name ?? "",
|
|
1377
|
+
account_email: row.account_email ?? "",
|
|
1378
|
+
account_source: row.account_source ?? ""
|
|
1152
1379
|
});
|
|
1153
1380
|
sessions.add(row.session_id);
|
|
1154
1381
|
}
|
|
@@ -1186,10 +1413,18 @@ function resolvePort(argv) {
|
|
|
1186
1413
|
}
|
|
1187
1414
|
var args = process.argv.slice(2);
|
|
1188
1415
|
if (args.includes("--help") || args.includes("-h")) {
|
|
1189
|
-
console.log(`Usage: economy-otel [
|
|
1416
|
+
console.log(`Usage: economy-otel [options]
|
|
1190
1417
|
|
|
1191
1418
|
OTLP/HTTP metrics sidecar \u2014 ingests *.cost.* / *.token.* metrics into economy.db
|
|
1192
1419
|
|
|
1420
|
+
Options:
|
|
1421
|
+
-p, --port <port> Port to bind (default: ECONOMY_OTEL_PORT or 4318)
|
|
1422
|
+
-h, --help display help for command
|
|
1423
|
+
|
|
1424
|
+
Environment:
|
|
1425
|
+
ECONOMY_OTEL_PORT Override the default port (4318)
|
|
1426
|
+
ECONOMY_OTEL_BIND Bind address (default: 127.0.0.1)
|
|
1427
|
+
|
|
1193
1428
|
Endpoints:
|
|
1194
1429
|
POST /v1/metrics OTLP JSON metrics
|
|
1195
1430
|
POST /ingest Simplified single-event JSON
|