@hasna/economy 0.3.28 → 0.5.0
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 +119 -0
- package/CONTRIBUTING.md +1 -1
- package/README.md +18 -22
- package/SECURITY.md +1 -1
- package/dist/chunks/agent-registry-store-tpnhx3wj.js +39 -0
- package/dist/chunks/billing-bkpxy4kv.js +13 -0
- package/dist/chunks/config-j9mv3ehk.js +15 -0
- package/dist/chunks/index-2hwzmfgv.js +3407 -0
- package/dist/chunks/index-axh32d30.js +1313 -0
- package/dist/chunks/index-emvs5ph2.js +2085 -0
- package/dist/chunks/index-ez9thsr7.js +3440 -0
- package/dist/chunks/index-hesxq90g.js +62 -0
- package/dist/chunks/index-k540hfww.js +258 -0
- package/dist/chunks/index-kgy9b6bf.js +451 -0
- package/dist/chunks/index-kn5572kd.js +124 -0
- package/dist/chunks/index-p35cyhc4.js +4 -0
- package/dist/chunks/index-w3bvv0ns.js +26 -0
- package/dist/chunks/index-zpvea08j.js +3372 -0
- package/dist/chunks/menubar-6xtkt17h.js +296 -0
- package/dist/chunks/open-projects-6z4pq6mx.js +39 -0
- package/dist/chunks/pricing-f80p81gr.js +29 -0
- package/dist/chunks/pricing-vf2pycmh.js +30 -0
- package/dist/chunks/pricing-y21mcjsa.js +30 -0
- package/dist/chunks/serve-0k69e4h9.js +4655 -0
- package/dist/chunks/sqlite-store-501f32ts.js +258 -0
- package/dist/chunks/sqlite-store-fq4natnf.js +256 -0
- package/dist/chunks/sqlite-store-st99garx.js +258 -0
- package/dist/chunks/third-party-sqlite-5e85fxa2.js +11 -0
- package/dist/chunks/tui-7f8f951w.js +22 -0
- package/dist/chunks/watch-mm1j39vy.js +196 -0
- package/dist/chunks/webhooks-k4fh2t9c.js +109 -0
- package/dist/cli/commands/brief.d.ts.map +1 -1
- package/dist/cli/commands/extras.d.ts.map +1 -1
- package/dist/cli/commands/journal.d.ts +3 -0
- package/dist/cli/commands/journal.d.ts.map +1 -0
- package/dist/cli/commands/menubar.d.ts +97 -4
- package/dist/cli/commands/menubar.d.ts.map +1 -1
- package/dist/cli/commands/tui.d.ts +1 -1
- package/dist/cli/commands/tui.d.ts.map +1 -1
- package/dist/cli/commands/watch.d.ts.map +1 -1
- package/dist/cli/index.js +1524 -9998
- package/dist/db/cloud.d.ts +18 -12
- package/dist/db/cloud.d.ts.map +1 -1
- package/dist/db/database.d.ts +33 -12
- package/dist/db/database.d.ts.map +1 -1
- package/dist/db/ingest-concurrency-worker.d.ts +2 -0
- package/dist/db/ingest-concurrency-worker.d.ts.map +1 -0
- package/dist/db/ingest-errors.d.ts +9 -0
- package/dist/db/ingest-errors.d.ts.map +1 -0
- package/dist/db/ingest-schema.d.ts +3 -0
- package/dist/db/ingest-schema.d.ts.map +1 -0
- package/dist/db/ingest-specs.d.ts +8 -0
- package/dist/db/ingest-specs.d.ts.map +1 -0
- package/dist/db/ingest-test-contract.d.ts +3 -0
- package/dist/db/ingest-test-contract.d.ts.map +1 -0
- package/dist/db/ingest-validation.d.ts +12 -0
- package/dist/db/ingest-validation.d.ts.map +1 -0
- package/dist/db/ingest.d.ts +27 -0
- package/dist/db/ingest.d.ts.map +1 -0
- package/dist/db/legacy-schema.d.ts +7 -0
- package/dist/db/legacy-schema.d.ts.map +1 -0
- package/dist/db/observation-contract.d.ts +3 -0
- package/dist/db/observation-contract.d.ts.map +1 -0
- package/dist/db/observation-provenance.d.ts +23 -0
- package/dist/db/observation-provenance.d.ts.map +1 -0
- package/dist/db/observation-schema.d.ts +24 -0
- package/dist/db/observation-schema.d.ts.map +1 -0
- package/dist/db/pg-connection.d.ts +10 -0
- package/dist/db/pg-connection.d.ts.map +1 -0
- package/dist/db/pg-migrate.d.ts +4 -9
- package/dist/db/pg-migrate.d.ts.map +1 -1
- package/dist/db/pg-migrations.d.ts.map +1 -1
- package/dist/db/pg-protocol.d.ts +28 -0
- package/dist/db/pg-protocol.d.ts.map +1 -0
- package/dist/db/sqlite-store.d.ts +4 -0
- package/dist/db/sqlite-store.d.ts.map +1 -0
- package/dist/db/sync-pg.d.ts +17 -21
- package/dist/db/sync-pg.d.ts.map +1 -1
- package/dist/db/third-party-sqlite.d.ts +10 -0
- package/dist/db/third-party-sqlite.d.ts.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1281 -2405
- package/dist/ingest/claude-quota.d.ts.map +1 -1
- package/dist/ingest/claude.d.ts.map +1 -1
- package/dist/ingest/codex-quota.d.ts.map +1 -1
- package/dist/ingest/codex.d.ts +25 -0
- package/dist/ingest/codex.d.ts.map +1 -1
- package/dist/ingest/collection-outcome.d.ts +17 -0
- package/dist/ingest/collection-outcome.d.ts.map +1 -0
- package/dist/ingest/cursor.d.ts.map +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 +9 -0
- package/dist/ingest/loops.d.ts.map +1 -1
- package/dist/ingest/opencode.d.ts +1 -1
- package/dist/ingest/opencode.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/quota-observation.d.ts +16 -0
- package/dist/ingest/quota-observation.d.ts.map +1 -0
- package/dist/ingest/source-time.d.ts +4 -0
- package/dist/ingest/source-time.d.ts.map +1 -0
- package/dist/lib/accounts-store.d.ts +109 -0
- package/dist/lib/accounts-store.d.ts.map +1 -0
- package/dist/lib/accounts.d.ts +2 -1
- package/dist/lib/accounts.d.ts.map +1 -1
- package/dist/lib/analytics.d.ts.map +1 -1
- package/dist/lib/api-display-url.d.ts +19 -0
- package/dist/lib/api-display-url.d.ts.map +1 -0
- package/dist/lib/autosync-gate.d.ts +22 -0
- package/dist/lib/autosync-gate.d.ts.map +1 -0
- package/dist/lib/billing-diff.d.ts +18 -40
- package/dist/lib/billing-diff.d.ts.map +1 -1
- package/dist/lib/brief.d.ts +2 -0
- package/dist/lib/brief.d.ts.map +1 -1
- package/dist/lib/budget-decimal.d.ts +14 -0
- package/dist/lib/budget-decimal.d.ts.map +1 -0
- package/dist/lib/cloud-ingest.d.ts +30 -1
- package/dist/lib/cloud-ingest.d.ts.map +1 -1
- package/dist/lib/cloud-storage.d.ts +166 -10
- package/dist/lib/cloud-storage.d.ts.map +1 -1
- package/dist/lib/gatherer.d.ts.map +1 -1
- package/dist/lib/ingest-batches.d.ts +21 -0
- package/dist/lib/ingest-batches.d.ts.map +1 -0
- package/dist/lib/ingest-journal-client.d.ts +37 -0
- package/dist/lib/ingest-journal-client.d.ts.map +1 -0
- package/dist/lib/ingest-journal-lock.d.ts +5 -0
- package/dist/lib/ingest-journal-lock.d.ts.map +1 -0
- package/dist/lib/ingest-journal-maintenance.d.ts +92 -0
- package/dist/lib/ingest-journal-maintenance.d.ts.map +1 -0
- package/dist/lib/ingest-journal-operation.d.ts +55 -0
- package/dist/lib/ingest-journal-operation.d.ts.map +1 -0
- package/dist/lib/ingest-journal.d.ts +73 -0
- package/dist/lib/ingest-journal.d.ts.map +1 -0
- package/dist/lib/money-display.d.ts +13 -0
- package/dist/lib/money-display.d.ts.map +1 -0
- package/dist/lib/periods.d.ts +12 -0
- package/dist/lib/periods.d.ts.map +1 -1
- package/dist/lib/pricing.d.ts +14 -0
- package/dist/lib/pricing.d.ts.map +1 -1
- package/dist/lib/savings.d.ts +64 -7
- package/dist/lib/savings.d.ts.map +1 -1
- package/dist/lib/serve-auth.d.ts.map +1 -1
- package/dist/lib/store/index.d.ts +29 -22
- package/dist/lib/store/index.d.ts.map +1 -1
- package/dist/lib/summary-contract.d.ts +32 -0
- package/dist/lib/summary-contract.d.ts.map +1 -0
- package/dist/lib/summary-test-contract.d.ts +3 -0
- package/dist/lib/summary-test-contract.d.ts.map +1 -0
- package/dist/lib/sync-all.d.ts +4 -0
- package/dist/lib/sync-all.d.ts.map +1 -1
- package/dist/lib/sync-maintenance.d.ts.map +1 -1
- package/dist/lib/sync-report.d.ts +4 -0
- package/dist/lib/sync-report.d.ts.map +1 -0
- package/dist/lib/test-keychain-fixture.d.ts +9 -0
- package/dist/lib/test-keychain-fixture.d.ts.map +1 -0
- package/dist/mcp/agent-registry-store.d.ts +30 -0
- package/dist/mcp/agent-registry-store.d.ts.map +1 -0
- package/dist/mcp/agent-registry.d.ts +78 -0
- package/dist/mcp/agent-registry.d.ts.map +1 -0
- package/dist/mcp/harness.d.ts +26 -0
- package/dist/mcp/harness.d.ts.map +1 -0
- package/dist/mcp/index.js +3219 -3357
- package/dist/mcp/ingest-journal-tool.d.ts +2 -0
- package/dist/mcp/ingest-journal-tool.d.ts.map +1 -0
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/openapi.d.ts.map +1 -1
- package/dist/otel/index.js +2197 -121
- package/dist/server/human-auth.d.ts +20 -0
- package/dist/server/human-auth.d.ts.map +1 -0
- package/dist/server/index.js +10477 -6378
- package/dist/server/legacy-inventory.d.ts +52 -0
- package/dist/server/legacy-inventory.d.ts.map +1 -0
- package/dist/server/legacy-operations.d.ts +64 -0
- package/dist/server/legacy-operations.d.ts.map +1 -0
- package/dist/server/legacy-plan.d.ts +55 -0
- package/dist/server/legacy-plan.d.ts.map +1 -0
- package/dist/server/legacy-preservation.d.ts +58 -0
- package/dist/server/legacy-preservation.d.ts.map +1 -0
- package/dist/server/pg-sync-worker.js +195 -21
- package/dist/server/serve.d.ts +20 -18
- package/dist/server/serve.d.ts.map +1 -1
- package/dist/server/service-admission.d.ts +10 -0
- package/dist/server/service-admission.d.ts.map +1 -0
- package/dist/server/service-dispatcher.d.ts +45 -0
- package/dist/server/service-dispatcher.d.ts.map +1 -0
- package/dist/server/service-operations.d.ts +5 -0
- package/dist/server/service-operations.d.ts.map +1 -0
- package/dist/server/service-protocol.d.ts +50 -0
- package/dist/server/service-protocol.d.ts.map +1 -0
- package/dist/server/service-worker.d.ts +2 -0
- package/dist/server/service-worker.d.ts.map +1 -0
- package/dist/server/service-worker.js +8029 -0
- package/dist/types/index.d.ts +66 -0
- package/dist/types/index.d.ts.map +1 -1
- package/docs/README.md +1 -1
- package/docs/cli.md +2 -3
- package/docs/configuration.md +25 -12
- package/docs/cost-reporting.md +49 -0
- package/docs/historical-data-reconciliation.md +203 -0
- package/docs/ingest-journal.md +108 -0
- package/docs/ingestion.md +61 -5
- package/docs/mcp.md +5 -3
- package/docs/native-budget-save.md +84 -0
- package/docs/native-human-auth.md +102 -0
- package/docs/otel.md +8 -2
- package/docs/rest-api.md +4 -4
- package/package.json +17 -20
- package/postinstall.js +52 -1
- package/dashboard/README.md +0 -26
- package/dist/cli/brains.d.ts +0 -3
- package/dist/cli/brains.d.ts.map +0 -1
- package/dist/lib/test-hermetic-accounts.d.ts +0 -6
- package/dist/lib/test-hermetic-accounts.d.ts.map +0 -1
|
@@ -0,0 +1,1313 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
import {
|
|
3
|
+
calendarPeriodBounds,
|
|
4
|
+
calendarPeriodWhere,
|
|
5
|
+
formatBillingValue,
|
|
6
|
+
formatReportedUsd,
|
|
7
|
+
hasKnownUsagePrice,
|
|
8
|
+
listMachineRegistry,
|
|
9
|
+
listMachines,
|
|
10
|
+
listModelPricing,
|
|
11
|
+
queryAccountBreakdown,
|
|
12
|
+
queryAgentBreakdown,
|
|
13
|
+
queryBillingSummary,
|
|
14
|
+
querySummary,
|
|
15
|
+
requireCurrentBillingSummary,
|
|
16
|
+
requireCurrentCostSummary,
|
|
17
|
+
sessionOverlapWhere
|
|
18
|
+
} from "./index-ez9thsr7.js";
|
|
19
|
+
// package.json
|
|
20
|
+
var package_default = {
|
|
21
|
+
name: "@hasna/economy",
|
|
22
|
+
version: "0.5.0",
|
|
23
|
+
description: "AI coding cost tracker \u2014 CLI + MCP server + REST API for Claude Code, Codex, OpenCode, Cursor, Pi, and Hermes, with legacy Gemini CLI session and Gemini API billing ingestion",
|
|
24
|
+
repository: {
|
|
25
|
+
type: "git",
|
|
26
|
+
url: "https://github.com/hasna/apps.git",
|
|
27
|
+
directory: "apps/economy"
|
|
28
|
+
},
|
|
29
|
+
bugs: {
|
|
30
|
+
url: "https://github.com/hasna/apps/issues"
|
|
31
|
+
},
|
|
32
|
+
homepage: "https://github.com/hasna/apps/tree/main/apps/economy#readme",
|
|
33
|
+
type: "module",
|
|
34
|
+
main: "dist/index.js",
|
|
35
|
+
types: "dist/index.d.ts",
|
|
36
|
+
bin: {
|
|
37
|
+
economy: "dist/cli/index.js",
|
|
38
|
+
"economy-mcp": "dist/mcp/index.js",
|
|
39
|
+
"economy-serve": "dist/server/index.js",
|
|
40
|
+
"economy-otel": "dist/otel/index.js"
|
|
41
|
+
},
|
|
42
|
+
exports: {
|
|
43
|
+
".": {
|
|
44
|
+
types: "./dist/index.d.ts",
|
|
45
|
+
import: "./dist/index.js"
|
|
46
|
+
},
|
|
47
|
+
"./sdk": {
|
|
48
|
+
types: "./dist/index.d.ts",
|
|
49
|
+
import: "./dist/index.js"
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
files: [
|
|
53
|
+
"dist",
|
|
54
|
+
"docs",
|
|
55
|
+
"postinstall.js",
|
|
56
|
+
"LICENSE",
|
|
57
|
+
"README.md",
|
|
58
|
+
"CHANGELOG.md",
|
|
59
|
+
"SECURITY.md",
|
|
60
|
+
"CONTRIBUTING.md",
|
|
61
|
+
"CODE_OF_CONDUCT.md",
|
|
62
|
+
"!dist/lib/test-hermetic-*"
|
|
63
|
+
],
|
|
64
|
+
scripts: {
|
|
65
|
+
build: 'bun build src/cli/index.ts --outdir dist --root src --target bun --packages external --splitting --chunk-naming "chunks/[name]-[hash].[ext]" && bun build src/mcp/index.ts --outdir dist --root src --target bun --packages external --splitting --chunk-naming "chunks/[name]-[hash].[ext]" && bun build src/server/index.ts --outdir dist/server --target bun --packages external && bun build src/db/pg-sync-worker.ts --outdir dist/server --target bun --packages external && bun build src/server/service-worker.ts --outdir dist/server --target bun --packages external && bun build src/otel/index.ts --outdir dist/otel --target bun --packages external && bun build src/index.ts --outdir dist --root src --target bun --packages external --splitting --chunk-naming "chunks/[name]-[hash].[ext]" && tsc --emitDeclarationOnly --outDir dist',
|
|
66
|
+
"build:cli": 'bun build src/cli/index.ts --outdir dist --root src --target bun --packages external --splitting --chunk-naming "chunks/[name]-[hash].[ext]"',
|
|
67
|
+
"build:mcp": 'bun build src/mcp/index.ts --outdir dist --root src --target bun --packages external --splitting --chunk-naming "chunks/[name]-[hash].[ext]"',
|
|
68
|
+
"build:server": "bun build src/server/index.ts --outdir dist/server --target bun --packages external && bun build src/db/pg-sync-worker.ts --outdir dist/server --target bun --packages external && bun build src/server/service-worker.ts --outdir dist/server --target bun --packages external",
|
|
69
|
+
"build:lib": 'bun build src/index.ts --outdir dist --root src --target bun --packages external --splitting --chunk-naming "chunks/[name]-[hash].[ext]"',
|
|
70
|
+
"artifact-scan": "bun scripts/contracts-cli.mjs artifact-scan dist",
|
|
71
|
+
prepack: "bun run build && bun run artifact-scan",
|
|
72
|
+
typecheck: "tsc --noEmit",
|
|
73
|
+
test: "bun test",
|
|
74
|
+
"dev:cli": "bun run src/cli/index.ts",
|
|
75
|
+
"dev:mcp": "bun run src/mcp/index.ts",
|
|
76
|
+
"dev:serve": "bun run src/server/index.ts",
|
|
77
|
+
"dev:otel": "bun run src/otel/index.ts",
|
|
78
|
+
postinstall: "node postinstall.js"
|
|
79
|
+
},
|
|
80
|
+
keywords: [
|
|
81
|
+
"economy",
|
|
82
|
+
"cost",
|
|
83
|
+
"ai",
|
|
84
|
+
"claude",
|
|
85
|
+
"codex",
|
|
86
|
+
"gemini",
|
|
87
|
+
"mcp",
|
|
88
|
+
"cli",
|
|
89
|
+
"budget",
|
|
90
|
+
"tracking"
|
|
91
|
+
],
|
|
92
|
+
author: "hasna",
|
|
93
|
+
license: "Apache-2.0",
|
|
94
|
+
publishConfig: {
|
|
95
|
+
registry: "https://registry.npmjs.org",
|
|
96
|
+
access: "public"
|
|
97
|
+
},
|
|
98
|
+
dependencies: {
|
|
99
|
+
"@hasna/contracts": "1.0.2",
|
|
100
|
+
"@hasna/events": "^0.1.16",
|
|
101
|
+
"@hasna/projects": "1.0.0",
|
|
102
|
+
"@modelcontextprotocol/sdk": "^1.12.1",
|
|
103
|
+
chalk: "^5.4.1",
|
|
104
|
+
commander: "^13.1.0",
|
|
105
|
+
pg: "8.13.1",
|
|
106
|
+
zod: "^3.24.2",
|
|
107
|
+
"@hasna/trash": "0.2.3"
|
|
108
|
+
},
|
|
109
|
+
devDependencies: {
|
|
110
|
+
"@types/bun": "1.3.14",
|
|
111
|
+
"@types/pg": "^8.20.0",
|
|
112
|
+
"bun-types": "latest",
|
|
113
|
+
typescript: "^5.7.2"
|
|
114
|
+
},
|
|
115
|
+
trustedDependencies: [
|
|
116
|
+
"@hasna/projects"
|
|
117
|
+
]
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
// src/lib/package-metadata.ts
|
|
121
|
+
var cachedMetadata = null;
|
|
122
|
+
function getPackageMetadata() {
|
|
123
|
+
if (cachedMetadata)
|
|
124
|
+
return cachedMetadata;
|
|
125
|
+
cachedMetadata = {
|
|
126
|
+
name: package_default.name ?? "@hasna/economy",
|
|
127
|
+
version: package_default.version ?? "0.0.0"
|
|
128
|
+
};
|
|
129
|
+
return cachedMetadata;
|
|
130
|
+
}
|
|
131
|
+
var packageMetadata = getPackageMetadata();
|
|
132
|
+
|
|
133
|
+
// src/lib/cloud-storage.ts
|
|
134
|
+
import {
|
|
135
|
+
appConfigDiskValue,
|
|
136
|
+
ClientTransportConfigurationError,
|
|
137
|
+
clientTransportEnvKeys,
|
|
138
|
+
createClientTransport,
|
|
139
|
+
credentialDiskSources,
|
|
140
|
+
credentialOverrideEnvKey,
|
|
141
|
+
credentialPointerEnvKey,
|
|
142
|
+
CREDENTIAL_PROFILE_ENV_KEY,
|
|
143
|
+
keychainConfigValue
|
|
144
|
+
} from "@hasna/contracts/client";
|
|
145
|
+
import { createHasnaStorageClient } from "@hasna/contracts/client/storage";
|
|
146
|
+
var ECONOMY_APP = "economy";
|
|
147
|
+
var LOCAL_STORAGE_OPT_IN_KEYS = ["HASNA_ECONOMY_LOCAL", "ECONOMY_LOCAL"];
|
|
148
|
+
function envFlagSet(env, key) {
|
|
149
|
+
const raw = env[key];
|
|
150
|
+
if (raw === undefined)
|
|
151
|
+
return false;
|
|
152
|
+
const value = raw.trim().toLowerCase();
|
|
153
|
+
return value !== "" && value !== "0" && value !== "false" && value !== "no" && value !== "off";
|
|
154
|
+
}
|
|
155
|
+
function localStorageExplicitlyOptedIn(env) {
|
|
156
|
+
return LOCAL_STORAGE_OPT_IN_KEYS.some((key) => envFlagSet(env, key));
|
|
157
|
+
}
|
|
158
|
+
function economyAuthorityEnvKeys() {
|
|
159
|
+
const keys = clientTransportEnvKeys(ECONOMY_APP);
|
|
160
|
+
return [
|
|
161
|
+
...keys.apiUrlKeys,
|
|
162
|
+
...keys.apiKeyKeys,
|
|
163
|
+
credentialOverrideEnvKey(ECONOMY_APP),
|
|
164
|
+
credentialPointerEnvKey(ECONOMY_APP),
|
|
165
|
+
CREDENTIAL_PROFILE_ENV_KEY
|
|
166
|
+
];
|
|
167
|
+
}
|
|
168
|
+
function hasEconomyEnvAuthorityIntent(env) {
|
|
169
|
+
return economyAuthorityEnvKeys().some((key) => (env[key] ?? "").trim().length > 0);
|
|
170
|
+
}
|
|
171
|
+
function hostedAuthorityConfigured(env, options = {}) {
|
|
172
|
+
const keys = clientTransportEnvKeys(ECONOMY_APP);
|
|
173
|
+
if (keys.apiUrlKeys.some((key) => (env[key] ?? "").trim().length > 0))
|
|
174
|
+
return true;
|
|
175
|
+
if (keychainConfigValue(ECONOMY_APP, env, options.credentials?.keychain) !== null)
|
|
176
|
+
return true;
|
|
177
|
+
return appConfigDiskValue(ECONOMY_APP, env, keys.apiUrlKeys) !== null;
|
|
178
|
+
}
|
|
179
|
+
function economyResolverEnv(env) {
|
|
180
|
+
const blanks = economyAuthorityEnvKeys().filter((key) => (key in env) && (env[key] ?? "").trim() === "");
|
|
181
|
+
if (blanks.length === 0)
|
|
182
|
+
return env;
|
|
183
|
+
const next = { ...env };
|
|
184
|
+
for (const key of blanks)
|
|
185
|
+
delete next[key];
|
|
186
|
+
return next;
|
|
187
|
+
}
|
|
188
|
+
var CONTRACTS_AMBIENT_ENVIRONMENT = Symbol.for("hasna:contracts:ambientClientEnvironment");
|
|
189
|
+
function isAmbientEconomyEnv(env) {
|
|
190
|
+
if (typeof process !== "undefined" && env === process.env)
|
|
191
|
+
return true;
|
|
192
|
+
return env[CONTRACTS_AMBIENT_ENVIRONMENT] === true;
|
|
193
|
+
}
|
|
194
|
+
function economyResolverInputs(env, credentials = {}) {
|
|
195
|
+
const normalised = economyResolverEnv(env);
|
|
196
|
+
if (normalised === env)
|
|
197
|
+
return { env: normalised, credentials };
|
|
198
|
+
const keychain = { ...credentials.keychain };
|
|
199
|
+
if (keychain.enabled === undefined && keychain.run === undefined) {
|
|
200
|
+
keychain.enabled = isAmbientEconomyEnv(env);
|
|
201
|
+
}
|
|
202
|
+
return { env: normalised, credentials: { ...credentials, keychain } };
|
|
203
|
+
}
|
|
204
|
+
function resolveEconomyStorageClient(name, env, overrides = {}) {
|
|
205
|
+
const { env: resolverEnv, credentials } = economyResolverInputs(env, overrides.credentials);
|
|
206
|
+
const wired = createClientTransport(name, resolverEnv, {
|
|
207
|
+
credentials,
|
|
208
|
+
...overrides.fetchImpl ? { fetchImpl: overrides.fetchImpl } : {},
|
|
209
|
+
...overrides.headers ? { headers: overrides.headers } : {},
|
|
210
|
+
...overrides.timeoutMs ? { timeoutMs: overrides.timeoutMs } : {},
|
|
211
|
+
...overrides.retry !== undefined ? { retry: overrides.retry } : {},
|
|
212
|
+
...overrides.sleepImpl ? { sleepImpl: overrides.sleepImpl } : {}
|
|
213
|
+
});
|
|
214
|
+
return {
|
|
215
|
+
transport: "http",
|
|
216
|
+
client: createHasnaStorageClient(name, wired.client),
|
|
217
|
+
resolution: wired.resolution
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
function localEconomyNotice() {
|
|
221
|
+
return `economy: LOCAL mode (${LOCAL_STORAGE_OPT_IN_KEYS[0]}=1) \u2014 reading and writing the on-box ` + `SQLite store, not the hosted fleet. Data in the shared economy API is NOT included.`;
|
|
222
|
+
}
|
|
223
|
+
function credentialRequiredError(env, cause) {
|
|
224
|
+
const keys = clientTransportEnvKeys(ECONOMY_APP);
|
|
225
|
+
const clientEnv = env;
|
|
226
|
+
const detail = cause instanceof Error ? cause.message : `No API key could be resolved for '${ECONOMY_APP}'. Looked in the Keychain (macOS only), ` + `then for a credential file at ${credentialDiskSources(ECONOMY_APP, clientEnv).join(" or ") || "<no HOME set>"}, ` + `then for ${keys.apiKeyKeys[0]} in the environment.`;
|
|
227
|
+
const wayOut = hostedAuthorityConfigured(clientEnv) ? `${keys.apiUrlKeys[0]} (or the Keychain hasna.credentials.${ECONOMY_APP}.api-url item, or ` + `~/.hasna/${ECONOMY_APP}/config/credentials) selects a hosted service, so ${LOCAL_STORAGE_OPT_IN_KEYS[0]} ` + `does not apply; unset the authority to use the local store.` : `Or set ${LOCAL_STORAGE_OPT_IN_KEYS[0]}=1 to explicitly opt into the local store.`;
|
|
228
|
+
return new Error(`Economy fails closed instead of silently serving the local SQLite store: ${detail} ` + `Set ${keys.apiUrlKeys[0]} and ${keys.apiKeyKeys[0]} (or store the key in the Keychain item ` + `hasna.credentials.${ECONOMY_APP}.api-key, or in ~/.hasna/${ECONOMY_APP}/config/credentials) to use the ` + `hosted economy API at ${keys.apiUrlKeys[0]} or the default fleet gateway. ` + wayOut);
|
|
229
|
+
}
|
|
230
|
+
var cache;
|
|
231
|
+
var localNoticePrinted = false;
|
|
232
|
+
function resolveEconomyCloudStorage(env = process.env, overrides) {
|
|
233
|
+
const clientEnv = env;
|
|
234
|
+
if (localStorageExplicitlyOptedIn(env) && !hasEconomyEnvAuthorityIntent(clientEnv)) {
|
|
235
|
+
return { active: false, client: null };
|
|
236
|
+
}
|
|
237
|
+
let resolved;
|
|
238
|
+
try {
|
|
239
|
+
resolved = resolveEconomyStorageClient(ECONOMY_APP, clientEnv, overrides ?? {});
|
|
240
|
+
} catch (error) {
|
|
241
|
+
if (error instanceof ClientTransportConfigurationError) {
|
|
242
|
+
throw credentialRequiredError(env, error);
|
|
243
|
+
}
|
|
244
|
+
throw error;
|
|
245
|
+
}
|
|
246
|
+
return { active: true, client: resolved.client };
|
|
247
|
+
}
|
|
248
|
+
function economyCloudStorage(env = process.env) {
|
|
249
|
+
if (cache === undefined) {
|
|
250
|
+
cache = resolveEconomyCloudStorage(env);
|
|
251
|
+
if (!cache.active && !localNoticePrinted) {
|
|
252
|
+
localNoticePrinted = true;
|
|
253
|
+
if (typeof process !== "undefined")
|
|
254
|
+
process.stderr.write(`${localEconomyNotice()}
|
|
255
|
+
`);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
return cache;
|
|
259
|
+
}
|
|
260
|
+
function cleanQuery(query) {
|
|
261
|
+
if (!query)
|
|
262
|
+
return;
|
|
263
|
+
const out = {};
|
|
264
|
+
for (const [k, v] of Object.entries(query)) {
|
|
265
|
+
if (v !== undefined && v !== null && v !== "")
|
|
266
|
+
out[k] = v;
|
|
267
|
+
}
|
|
268
|
+
return Object.keys(out).length > 0 ? out : undefined;
|
|
269
|
+
}
|
|
270
|
+
async function cloudListItems(storage, resource, query) {
|
|
271
|
+
const cleaned = cleanQuery(query);
|
|
272
|
+
const res = await storage.client.list(resource, cleaned ? { query: cleaned } : {});
|
|
273
|
+
return res.items;
|
|
274
|
+
}
|
|
275
|
+
async function cloudObject(storage, path, query) {
|
|
276
|
+
const cleaned = cleanQuery(query);
|
|
277
|
+
const raw = await storage.client.transport.get(path, cleaned ? { query: cleaned } : {});
|
|
278
|
+
if (raw && typeof raw === "object" && "data" in raw) {
|
|
279
|
+
return raw.data;
|
|
280
|
+
}
|
|
281
|
+
return raw;
|
|
282
|
+
}
|
|
283
|
+
function economyTransportReport(env = process.env, overrides) {
|
|
284
|
+
const clientEnv = env;
|
|
285
|
+
if (localStorageExplicitlyOptedIn(env) && !hasEconomyEnvAuthorityIntent(clientEnv)) {
|
|
286
|
+
return {
|
|
287
|
+
ok: true,
|
|
288
|
+
selected: true,
|
|
289
|
+
transport: "sqlite",
|
|
290
|
+
source: "local-opt-in",
|
|
291
|
+
authority: null,
|
|
292
|
+
issues: []
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
try {
|
|
296
|
+
const resolved = resolveEconomyStorageClient(ECONOMY_APP, clientEnv, overrides ?? {});
|
|
297
|
+
return {
|
|
298
|
+
ok: true,
|
|
299
|
+
selected: true,
|
|
300
|
+
transport: "http",
|
|
301
|
+
source: resolved.resolution.transportSource,
|
|
302
|
+
authority: {
|
|
303
|
+
baseUrl: resolved.resolution.baseUrl,
|
|
304
|
+
apiUrlSource: resolved.resolution.apiUrlSource,
|
|
305
|
+
apiKeySource: resolved.resolution.apiKeySource,
|
|
306
|
+
apiKeyTier: resolved.resolution.apiKeyTier,
|
|
307
|
+
warning: resolved.resolution.warning
|
|
308
|
+
},
|
|
309
|
+
issues: []
|
|
310
|
+
};
|
|
311
|
+
} catch (error) {
|
|
312
|
+
if (error instanceof ClientTransportConfigurationError) {
|
|
313
|
+
return {
|
|
314
|
+
ok: false,
|
|
315
|
+
selected: true,
|
|
316
|
+
transport: "http",
|
|
317
|
+
source: null,
|
|
318
|
+
authority: null,
|
|
319
|
+
issues: [error.message]
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
throw error;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// src/lib/savings.ts
|
|
327
|
+
function requireCurrentSavingsReport(value) {
|
|
328
|
+
if (!value || typeof value !== "object")
|
|
329
|
+
throw new Error("SAVINGS_REPORT_UNAVAILABLE: Invalid savings response");
|
|
330
|
+
const report = value;
|
|
331
|
+
if (report.comparison_basis !== "included_usage_vs_current_plan_estimate" || !["estimated", "not_applicable", "unavailable"].includes(String(report.comparison_status))) {
|
|
332
|
+
throw new Error("SAVINGS_REPORT_UNAVAILABLE: The service must support the current subscription comparison contract");
|
|
333
|
+
}
|
|
334
|
+
for (const field of ["api_equivalent_usd", "subscription_fee_usd", "on_demand_usd", "saved_usd", "net_comparison_usd"]) {
|
|
335
|
+
const number = report[field];
|
|
336
|
+
if (number !== null && (typeof number !== "number" || !Number.isFinite(number))) {
|
|
337
|
+
throw new Error("SAVINGS_REPORT_UNAVAILABLE: Invalid monetary field " + field);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
if (report.comparison_status === "unavailable" && (report.saved_usd !== null || report.net_comparison_usd !== null)) {
|
|
341
|
+
throw new Error("SAVINGS_REPORT_UNAVAILABLE: Unavailable comparison carries a monetary claim");
|
|
342
|
+
}
|
|
343
|
+
return value;
|
|
344
|
+
}
|
|
345
|
+
function prorateMonthlyFee(monthlyFee, period, now = new Date) {
|
|
346
|
+
if (!Number.isFinite(monthlyFee) || monthlyFee < 0)
|
|
347
|
+
throw new RangeError("Monthly fee must be finite and nonnegative");
|
|
348
|
+
const bounds = calendarPeriodBounds(period, now);
|
|
349
|
+
if (bounds.start_date === null)
|
|
350
|
+
return null;
|
|
351
|
+
let cursor = new Date(bounds.start_date + "T00:00:00.000Z");
|
|
352
|
+
const end = new Date(bounds.end_date_exclusive + "T00:00:00.000Z");
|
|
353
|
+
let fee = 0;
|
|
354
|
+
while (cursor < end) {
|
|
355
|
+
const monthEnd = new Date(Date.UTC(cursor.getUTCFullYear(), cursor.getUTCMonth() + 1, 1));
|
|
356
|
+
const next = new Date(Math.min(end.getTime(), monthEnd.getTime()));
|
|
357
|
+
const days = (next.getTime() - cursor.getTime()) / 86400000;
|
|
358
|
+
const monthDays = new Date(Date.UTC(cursor.getUTCFullYear(), cursor.getUTCMonth() + 1, 0)).getUTCDate();
|
|
359
|
+
fee += monthlyFee / monthDays * days;
|
|
360
|
+
cursor = next;
|
|
361
|
+
}
|
|
362
|
+
return fee;
|
|
363
|
+
}
|
|
364
|
+
function feeFor(plans, period, now) {
|
|
365
|
+
let known = 0;
|
|
366
|
+
let complete = true;
|
|
367
|
+
for (const plan of plans) {
|
|
368
|
+
if (plan.fee_basis !== "observed" && plan.fee_basis !== "configured" || plan.monthly_fee_usd === null || !Number.isFinite(plan.monthly_fee_usd) || plan.monthly_fee_usd < 0) {
|
|
369
|
+
complete = false;
|
|
370
|
+
continue;
|
|
371
|
+
}
|
|
372
|
+
const amount = prorateMonthlyFee(plan.monthly_fee_usd, period, now);
|
|
373
|
+
if (amount === null || !Number.isFinite(amount) || !Number.isFinite(known + amount))
|
|
374
|
+
complete = false;
|
|
375
|
+
else
|
|
376
|
+
known += amount;
|
|
377
|
+
}
|
|
378
|
+
return { known, total: complete ? known : null };
|
|
379
|
+
}
|
|
380
|
+
function summarizeOnDemand(observations, period, now = new Date) {
|
|
381
|
+
const rows = observations.filter((row) => row.metric === "on_demand_usd");
|
|
382
|
+
if (!rows.length)
|
|
383
|
+
return { usd: null, status: "not_observed" };
|
|
384
|
+
const bounds = calendarPeriodBounds(period, now);
|
|
385
|
+
const start = bounds.start_date ? Date.parse(bounds.start_date + "T00:00:00Z") : -Infinity;
|
|
386
|
+
const end = bounds.end_date_exclusive ? Date.parse(bounds.end_date_exclusive + "T00:00:00Z") : Infinity;
|
|
387
|
+
const windows = new Map;
|
|
388
|
+
for (const row of rows) {
|
|
389
|
+
const from = Date.parse(row.window_start ?? "");
|
|
390
|
+
const until = Date.parse(row.window_end ?? "");
|
|
391
|
+
const at = Date.parse(row.source_timestamp ?? row.observed_at ?? "");
|
|
392
|
+
if (row.unit.toLowerCase() !== "usd" || !Number.isFinite(row.value) || row.value < 0 || !row.provider || !row.account_key || !Number.isFinite(from) || !Number.isFinite(until) || from >= until || !Number.isFinite(at) || row.value_kind !== "cumulative" && row.value_kind !== "delta") {
|
|
393
|
+
return { usd: null, status: "unavailable" };
|
|
394
|
+
}
|
|
395
|
+
if (until <= start || from >= end)
|
|
396
|
+
continue;
|
|
397
|
+
if (from < start || until > end)
|
|
398
|
+
return { usd: null, status: "unavailable" };
|
|
399
|
+
const key = JSON.stringify([row.provider, row.account_key, from, until]);
|
|
400
|
+
const previous = windows.get(key);
|
|
401
|
+
if (previous && previous.row.value_kind !== row.value_kind)
|
|
402
|
+
return { usd: null, status: "unavailable" };
|
|
403
|
+
if (previous && ((row.value_kind === "delta" || at === previous.at) && previous.row.value !== row.value)) {
|
|
404
|
+
return { usd: null, status: "unavailable" };
|
|
405
|
+
}
|
|
406
|
+
if (!previous || at > previous.at)
|
|
407
|
+
windows.set(key, { row, at, start: from, end: until });
|
|
408
|
+
}
|
|
409
|
+
const byAccount = new Map;
|
|
410
|
+
let total = 0;
|
|
411
|
+
for (const window of windows.values()) {
|
|
412
|
+
const key = JSON.stringify([window.row.provider, window.row.account_key]);
|
|
413
|
+
const intervals = byAccount.get(key) ?? [];
|
|
414
|
+
if (intervals.some((other) => window.start < other.end && window.end > other.start)) {
|
|
415
|
+
return { usd: null, status: "unavailable" };
|
|
416
|
+
}
|
|
417
|
+
intervals.push(window);
|
|
418
|
+
byAccount.set(key, intervals);
|
|
419
|
+
total += window.row.value;
|
|
420
|
+
}
|
|
421
|
+
return windows.size ? { usd: total, status: "observed" } : { usd: null, status: "not_observed" };
|
|
422
|
+
}
|
|
423
|
+
function buildSavingsSummary(period, usage, plans, observations, agent, now = new Date, prices = []) {
|
|
424
|
+
const selected = usage.filter((row) => !agent || row.agent === agent);
|
|
425
|
+
const activePlans = plans.filter((row) => row.active === 1);
|
|
426
|
+
const selectedPlans = activePlans.filter((row) => !agent || row.agent === agent);
|
|
427
|
+
const sharedPlans = activePlans.filter((row) => !row.agent || !row.account_key);
|
|
428
|
+
const fee = feeFor(selectedPlans, period, now);
|
|
429
|
+
const unallocated = feeFor(sharedPlans, period, now);
|
|
430
|
+
const reasons = new Set;
|
|
431
|
+
let priced = 0, metered = 0, estimated = 0, included = 0, unknownPrices = 0, unknownBilling = 0, estimatedSessions = 0;
|
|
432
|
+
for (const row of selected) {
|
|
433
|
+
if (row.session_count) {
|
|
434
|
+
estimatedSessions += row.session_count;
|
|
435
|
+
reasons.add("billing_route_unknown");
|
|
436
|
+
}
|
|
437
|
+
if (!hasKnownUsagePrice(row, prices)) {
|
|
438
|
+
unknownPrices += row.requests;
|
|
439
|
+
reasons.add("model_price_unknown");
|
|
440
|
+
continue;
|
|
441
|
+
}
|
|
442
|
+
priced += row.cost_usd;
|
|
443
|
+
if (row.cost_basis === "metered_api")
|
|
444
|
+
metered += row.cost_usd;
|
|
445
|
+
else if (row.cost_basis === "subscription_included") {
|
|
446
|
+
included += row.cost_usd;
|
|
447
|
+
const matches = selectedPlans.filter((plan) => !!row.account_key && plan.account_key === row.account_key);
|
|
448
|
+
if (matches.length === 0)
|
|
449
|
+
reasons.add("subscription_account_unmapped");
|
|
450
|
+
if (matches.length > 1)
|
|
451
|
+
reasons.add("subscription_account_ambiguous");
|
|
452
|
+
} else {
|
|
453
|
+
estimated += row.cost_usd;
|
|
454
|
+
unknownBilling += row.requests;
|
|
455
|
+
reasons.add("billing_route_unknown");
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
if (unknownPrices)
|
|
459
|
+
reasons.add("model_price_unknown");
|
|
460
|
+
if (unknownBilling)
|
|
461
|
+
reasons.add("billing_route_unknown");
|
|
462
|
+
if (fee.total === null)
|
|
463
|
+
reasons.add(period === "all" ? "historical_plan_fees_unavailable" : "plan_fee_unknown");
|
|
464
|
+
if (included > 0 && !agent && sharedPlans.some((plan) => !plan.account_key))
|
|
465
|
+
reasons.add("subscription_account_unmapped");
|
|
466
|
+
const demand = summarizeOnDemand(observations.filter((row) => !agent || row.agent === agent), period, now);
|
|
467
|
+
const applicable = selectedPlans.length > 0 || selected.some((row) => row.cost_basis === "subscription_included");
|
|
468
|
+
const net = reasons.size ? null : applicable ? included - fee.known : 0;
|
|
469
|
+
const byAgent = {};
|
|
470
|
+
if (!agent) {
|
|
471
|
+
const agents = new Set([...usage.map((row) => row.agent), ...activePlans.flatMap((row) => row.agent ? [row.agent] : [])]);
|
|
472
|
+
for (const name of [...agents].sort()) {
|
|
473
|
+
const result = buildSavingsSummary(period, usage, plans, observations, name, now, prices);
|
|
474
|
+
const { by_agent: _, ...scalars } = result;
|
|
475
|
+
byAgent[name] = scalars;
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
return {
|
|
479
|
+
period,
|
|
480
|
+
api_equivalent_usd: reasons.has("model_price_unknown") ? null : priced,
|
|
481
|
+
priced_usage_value_usd: priced,
|
|
482
|
+
recorded_metered_api_usd: metered,
|
|
483
|
+
estimated_usage_value_usd: estimated,
|
|
484
|
+
subscription_fee_usd: fee.total,
|
|
485
|
+
known_subscription_fee_usd: fee.known,
|
|
486
|
+
unallocated_subscription_fee_usd: unallocated.total,
|
|
487
|
+
included_consumed_usd: included,
|
|
488
|
+
on_demand_usd: demand.usd,
|
|
489
|
+
on_demand_status: demand.status,
|
|
490
|
+
saved_usd: net === null ? null : Math.max(0, net),
|
|
491
|
+
net_comparison_usd: net,
|
|
492
|
+
comparison_status: reasons.size ? "unavailable" : applicable ? "estimated" : "not_applicable",
|
|
493
|
+
comparison_basis: "included_usage_vs_current_plan_estimate",
|
|
494
|
+
unavailable_reasons: [...reasons].sort(),
|
|
495
|
+
unknown_price_requests: unknownPrices,
|
|
496
|
+
unknown_billing_requests: unknownBilling,
|
|
497
|
+
estimated_only_sessions: estimatedSessions,
|
|
498
|
+
by_agent: byAgent
|
|
499
|
+
};
|
|
500
|
+
}
|
|
501
|
+
function querySavingsSummary(db, period, agent) {
|
|
502
|
+
const now = new Date;
|
|
503
|
+
const usage = db.prepare(`
|
|
504
|
+
SELECT agent, account_key, cost_basis, model,
|
|
505
|
+
CASE WHEN cost_usd = 0 THEN 1 WHEN cost_usd < 0 THEN 2 ELSE 0 END AS zero_cost,
|
|
506
|
+
COALESCE(SUM(cost_usd), 0) AS cost_usd, COUNT(*) AS requests,
|
|
507
|
+
COALESCE(SUM(input_tokens),0) AS input_tokens, COALESCE(SUM(output_tokens),0) AS output_tokens,
|
|
508
|
+
COALESCE(SUM(cache_read_tokens),0) AS cache_read_tokens, COALESCE(SUM(cache_create_tokens),0) AS cache_create_tokens,
|
|
509
|
+
COALESCE(SUM(cache_create_1h_tokens),0) AS cache_create_1h_tokens
|
|
510
|
+
FROM economy_effective_requests WHERE ${calendarPeriodWhere(period, "timestamp", now)}
|
|
511
|
+
GROUP BY agent, account_key, cost_basis, model, CASE WHEN cost_usd = 0 THEN 1 WHEN cost_usd < 0 THEN 2 ELSE 0 END
|
|
512
|
+
`).all();
|
|
513
|
+
const plans = db.prepare("SELECT * FROM subscriptions WHERE active = 1").all();
|
|
514
|
+
usage.push(...db.prepare(`
|
|
515
|
+
SELECT s.agent, s.account_key, 'estimated' AS cost_basis,
|
|
516
|
+
COALESCE(SUM(s.total_cost_usd), 0) AS cost_usd,
|
|
517
|
+
COALESCE(SUM(s.request_count), 0) AS requests, COUNT(*) AS session_count
|
|
518
|
+
FROM economy_effective_sessions s WHERE ${sessionOverlapWhere(period, "s", now)}
|
|
519
|
+
AND NOT EXISTS (SELECT 1 FROM economy_effective_requests r WHERE r.session_id = s.id)
|
|
520
|
+
GROUP BY s.agent, s.account_key
|
|
521
|
+
`).all());
|
|
522
|
+
const bounds = calendarPeriodBounds(period, now);
|
|
523
|
+
const windowWhere = bounds.start_date === null ? "1=1" : `(
|
|
524
|
+
(window_start IS NOT NULL AND window_end IS NOT NULL
|
|
525
|
+
AND window_start < '${bounds.end_date_exclusive}T00:00:00.000Z'
|
|
526
|
+
AND window_end > '${bounds.start_date}T00:00:00.000Z')
|
|
527
|
+
OR ((window_start IS NULL OR window_end IS NULL) AND ${calendarPeriodWhere(period, "date", now)})
|
|
528
|
+
)`;
|
|
529
|
+
const observations = db.prepare(`
|
|
530
|
+
SELECT * FROM usage_snapshots WHERE metric = 'on_demand_usd'
|
|
531
|
+
AND ${windowWhere}
|
|
532
|
+
`).all();
|
|
533
|
+
return buildSavingsSummary(period, usage, plans, observations, agent, now, listModelPricing(db));
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
// src/lib/billing-diff.ts
|
|
537
|
+
function queryBillingDiff(db, period, thresholdPct = 15) {
|
|
538
|
+
if (!Number.isFinite(thresholdPct) || thresholdPct < 0)
|
|
539
|
+
throw new RangeError("thresholdPct must be a finite nonnegative number");
|
|
540
|
+
const usage = querySummary(db, period, undefined, true);
|
|
541
|
+
const billing = queryBillingSummary(db, period);
|
|
542
|
+
const rows = db.prepare(`
|
|
543
|
+
SELECT agent, COALESCE(SUM(cost_usd), 0) AS estimated_usd
|
|
544
|
+
FROM economy_effective_requests WHERE ${calendarPeriodWhere(period, "timestamp")}
|
|
545
|
+
GROUP BY agent ORDER BY agent
|
|
546
|
+
`).all();
|
|
547
|
+
return requireCurrentBillingDiffSummary({
|
|
548
|
+
period,
|
|
549
|
+
basis: "unmatched_subtotals",
|
|
550
|
+
estimated_usd: usage.total_usd,
|
|
551
|
+
actual_usd: billing.total_usd,
|
|
552
|
+
usage_summary: usage,
|
|
553
|
+
billing_summary: billing,
|
|
554
|
+
delta_usd: null,
|
|
555
|
+
delta_pct: null,
|
|
556
|
+
threshold_pct: thresholdPct,
|
|
557
|
+
is_alert: null,
|
|
558
|
+
comparable: false,
|
|
559
|
+
incomparable_reason: billing.record_count === 0 ? "no_billing_records" : "account_window_source_unverified",
|
|
560
|
+
by_agent: rows.map((row) => ({ ...row, basis: "stored_request_rows_subtotal", actual_usd: null, delta_usd: null, delta_pct: null })),
|
|
561
|
+
by_provider: billing.by_provider
|
|
562
|
+
});
|
|
563
|
+
}
|
|
564
|
+
function requireCurrentBillingDiffSummary(value) {
|
|
565
|
+
const invalid = () => {
|
|
566
|
+
throw new Error("BILLING_COMPARISON_UNAVAILABLE: The service must return scoped unmatched subtotals");
|
|
567
|
+
};
|
|
568
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
569
|
+
return invalid();
|
|
570
|
+
const s = value;
|
|
571
|
+
if (s.basis !== "unmatched_subtotals" || s.comparable !== false || s.delta_usd !== null || s.delta_pct !== null || s.is_alert !== null)
|
|
572
|
+
return invalid();
|
|
573
|
+
if (!["today", "yesterday", "week", "month", "year", "all"].includes(String(s.period)) || typeof s.threshold_pct !== "number" || !Number.isFinite(s.threshold_pct) || s.threshold_pct < 0)
|
|
574
|
+
return invalid();
|
|
575
|
+
let usage, billing;
|
|
576
|
+
try {
|
|
577
|
+
usage = requireCurrentCostSummary(s.usage_summary);
|
|
578
|
+
billing = requireCurrentBillingSummary(s.billing_summary);
|
|
579
|
+
} catch {
|
|
580
|
+
return invalid();
|
|
581
|
+
}
|
|
582
|
+
if (usage.period !== s.period || s.estimated_usd !== usage.total_usd || s.actual_usd !== billing.total_usd || billing.recorded_total_usd !== (billing.record_count ? billing.total_usd : null))
|
|
583
|
+
return invalid();
|
|
584
|
+
if (s.incomparable_reason !== (billing.record_count === 0 ? "no_billing_records" : "account_window_source_unverified"))
|
|
585
|
+
return invalid();
|
|
586
|
+
const providers = s.by_provider;
|
|
587
|
+
if (!providers || typeof providers !== "object" || Array.isArray(providers) || !billing.by_provider || typeof billing.by_provider !== "object")
|
|
588
|
+
return invalid();
|
|
589
|
+
const expected = Object.entries(billing.by_provider);
|
|
590
|
+
if (Object.keys(providers).length !== expected.length || expected.some(([key, amount]) => typeof amount !== "number" || !Number.isFinite(amount) || providers[key] !== amount))
|
|
591
|
+
return invalid();
|
|
592
|
+
if (!Array.isArray(s.by_agent) || s.by_agent.some((row) => !row || typeof row !== "object" || typeof row.agent !== "string" || row.basis !== "stored_request_rows_subtotal" || typeof row.estimated_usd !== "number" || !Number.isFinite(row.estimated_usd) || row.actual_usd !== null || row.delta_usd !== null || row.delta_pct !== null))
|
|
593
|
+
return invalid();
|
|
594
|
+
return value;
|
|
595
|
+
}
|
|
596
|
+
function billingDriftCheck(diff) {
|
|
597
|
+
try {
|
|
598
|
+
requireCurrentBillingDiffSummary(diff);
|
|
599
|
+
} catch {
|
|
600
|
+
return { ok: false, msg: "billing drift: UNKNOWN \u2014 summary coverage unsupported; update the service" };
|
|
601
|
+
}
|
|
602
|
+
const detail = diff.incomparable_reason === "no_billing_records" ? "no provider billing records imported for this period" : "account, billing window, and source coverage are not established";
|
|
603
|
+
return { ok: false, msg: `billing drift ${diff.period}: UNKNOWN \u2014 comparison unavailable; ${detail}; ` + `known usage-value subtotal ${formatReportedUsd(diff.estimated_usd)}; provider rows ${formatBillingValue(diff.billing_summary)}` };
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
// src/lib/brief.ts
|
|
607
|
+
var ZERO_REQUEST_TOTALS = {
|
|
608
|
+
sessions: 0,
|
|
609
|
+
request_sessions: 0,
|
|
610
|
+
requests: 0,
|
|
611
|
+
input_tokens: 0,
|
|
612
|
+
output_tokens: 0,
|
|
613
|
+
cache_read_tokens: 0,
|
|
614
|
+
cache_create_tokens: 0,
|
|
615
|
+
cache_create_5m_tokens: 0,
|
|
616
|
+
cache_create_1h_tokens: 0,
|
|
617
|
+
total_tokens: 0,
|
|
618
|
+
cost_usd: 0,
|
|
619
|
+
last_active: null
|
|
620
|
+
};
|
|
621
|
+
var ZERO_SESSION_TOTALS = {
|
|
622
|
+
sessions: 0,
|
|
623
|
+
requests: 0,
|
|
624
|
+
total_tokens: 0,
|
|
625
|
+
cost_usd: 0,
|
|
626
|
+
last_active: null
|
|
627
|
+
};
|
|
628
|
+
function formatAge(timestamp, now) {
|
|
629
|
+
if (!timestamp)
|
|
630
|
+
return "never";
|
|
631
|
+
const t = new Date(timestamp).getTime();
|
|
632
|
+
if (!Number.isFinite(t))
|
|
633
|
+
return "unknown";
|
|
634
|
+
const ageMs = Math.max(0, now.getTime() - t);
|
|
635
|
+
const minutes = Math.floor(ageMs / 60000);
|
|
636
|
+
if (minutes < 1)
|
|
637
|
+
return "<1m";
|
|
638
|
+
if (minutes < 60)
|
|
639
|
+
return `${minutes}m`;
|
|
640
|
+
const hours = Math.floor(minutes / 60);
|
|
641
|
+
if (hours < 48)
|
|
642
|
+
return `${hours}h`;
|
|
643
|
+
return `${Math.floor(hours / 24)}d`;
|
|
644
|
+
}
|
|
645
|
+
function machineFilter(machine) {
|
|
646
|
+
if (!machine || machine === "all")
|
|
647
|
+
return;
|
|
648
|
+
return machine;
|
|
649
|
+
}
|
|
650
|
+
function requestPeriodWhere(period) {
|
|
651
|
+
return calendarPeriodWhere(period, "timestamp");
|
|
652
|
+
}
|
|
653
|
+
function sessionPeriodWhere(period) {
|
|
654
|
+
return calendarPeriodWhere(period, "started_at");
|
|
655
|
+
}
|
|
656
|
+
function baseRequestTotals(row) {
|
|
657
|
+
return {
|
|
658
|
+
...ZERO_REQUEST_TOTALS,
|
|
659
|
+
...row,
|
|
660
|
+
sessions: Number(row?.request_sessions ?? row?.sessions ?? 0),
|
|
661
|
+
request_sessions: Number(row?.request_sessions ?? row?.sessions ?? 0),
|
|
662
|
+
requests: Number(row?.requests ?? 0),
|
|
663
|
+
input_tokens: Number(row?.input_tokens ?? 0),
|
|
664
|
+
output_tokens: Number(row?.output_tokens ?? 0),
|
|
665
|
+
cache_read_tokens: Number(row?.cache_read_tokens ?? 0),
|
|
666
|
+
cache_create_tokens: Number(row?.cache_create_tokens ?? 0),
|
|
667
|
+
cache_create_5m_tokens: Number(row?.cache_create_5m_tokens ?? 0),
|
|
668
|
+
cache_create_1h_tokens: Number(row?.cache_create_1h_tokens ?? 0),
|
|
669
|
+
total_tokens: Number(row?.total_tokens ?? 0),
|
|
670
|
+
cost_usd: Number(row?.cost_usd ?? 0),
|
|
671
|
+
last_active: row?.last_active ?? null
|
|
672
|
+
};
|
|
673
|
+
}
|
|
674
|
+
function baseSessionTotals(row) {
|
|
675
|
+
return {
|
|
676
|
+
sessions: Number(row?.sessions ?? 0),
|
|
677
|
+
requests: Number(row?.requests ?? 0),
|
|
678
|
+
total_tokens: Number(row?.total_tokens ?? 0),
|
|
679
|
+
cost_usd: Number(row?.cost_usd ?? 0),
|
|
680
|
+
last_active: row?.last_active ?? null
|
|
681
|
+
};
|
|
682
|
+
}
|
|
683
|
+
function latestTimestamp(...values) {
|
|
684
|
+
return values.filter(Boolean).sort().at(-1) ?? null;
|
|
685
|
+
}
|
|
686
|
+
function combineTotals(requests, sessions) {
|
|
687
|
+
return {
|
|
688
|
+
sessions: requests.request_sessions + sessions.sessions,
|
|
689
|
+
requests: requests.requests + sessions.requests,
|
|
690
|
+
input_tokens: requests.input_tokens,
|
|
691
|
+
output_tokens: requests.output_tokens,
|
|
692
|
+
cache_read_tokens: requests.cache_read_tokens,
|
|
693
|
+
cache_create_tokens: requests.cache_create_tokens,
|
|
694
|
+
cache_create_5m_tokens: requests.cache_create_5m_tokens,
|
|
695
|
+
cache_create_1h_tokens: requests.cache_create_1h_tokens,
|
|
696
|
+
total_tokens: requests.total_tokens + sessions.total_tokens,
|
|
697
|
+
cost_usd: requests.cost_usd + sessions.cost_usd,
|
|
698
|
+
last_active: latestTimestamp(requests.last_active, sessions.last_active)
|
|
699
|
+
};
|
|
700
|
+
}
|
|
701
|
+
function emptyTotals() {
|
|
702
|
+
return combineTotals(ZERO_REQUEST_TOTALS, ZERO_SESSION_TOTALS);
|
|
703
|
+
}
|
|
704
|
+
function requestTotalsSql(where, machine) {
|
|
705
|
+
const params = [];
|
|
706
|
+
let machineClause = "";
|
|
707
|
+
if (machine) {
|
|
708
|
+
machineClause = " AND machine_id = ?";
|
|
709
|
+
params.push(machine);
|
|
710
|
+
}
|
|
711
|
+
return {
|
|
712
|
+
sql: `
|
|
713
|
+
SELECT
|
|
714
|
+
COUNT(DISTINCT session_id) as request_sessions,
|
|
715
|
+
COUNT(*) as requests,
|
|
716
|
+
COALESCE(SUM(input_tokens), 0) as input_tokens,
|
|
717
|
+
COALESCE(SUM(output_tokens), 0) as output_tokens,
|
|
718
|
+
COALESCE(SUM(cache_read_tokens), 0) as cache_read_tokens,
|
|
719
|
+
COALESCE(SUM(cache_create_tokens), 0) as cache_create_tokens,
|
|
720
|
+
COALESCE(SUM(COALESCE(cache_create_5m_tokens, cache_create_tokens)), 0) as cache_create_5m_tokens,
|
|
721
|
+
COALESCE(SUM(COALESCE(cache_create_1h_tokens, 0)), 0) as cache_create_1h_tokens,
|
|
722
|
+
COALESCE(SUM(input_tokens + output_tokens + cache_read_tokens + cache_create_tokens), 0) as total_tokens,
|
|
723
|
+
COALESCE(SUM(cost_usd), 0) as cost_usd,
|
|
724
|
+
MAX(timestamp) as last_active
|
|
725
|
+
FROM economy_effective_requests
|
|
726
|
+
WHERE ${where}${machineClause}
|
|
727
|
+
`,
|
|
728
|
+
params
|
|
729
|
+
};
|
|
730
|
+
}
|
|
731
|
+
function sessionOnlyTotalsSql(where, machine) {
|
|
732
|
+
const params = [];
|
|
733
|
+
let machineClause = "";
|
|
734
|
+
if (machine) {
|
|
735
|
+
machineClause = " AND machine_id = ?";
|
|
736
|
+
params.push(machine);
|
|
737
|
+
}
|
|
738
|
+
return {
|
|
739
|
+
sql: `
|
|
740
|
+
SELECT
|
|
741
|
+
COUNT(*) as sessions,
|
|
742
|
+
COALESCE(SUM(request_count), 0) as requests,
|
|
743
|
+
COALESCE(SUM(total_tokens), 0) as total_tokens,
|
|
744
|
+
COALESCE(SUM(total_cost_usd), 0) as cost_usd,
|
|
745
|
+
MAX(started_at) as last_active
|
|
746
|
+
FROM economy_effective_sessions
|
|
747
|
+
WHERE ${where}${machineClause}
|
|
748
|
+
AND NOT EXISTS (SELECT 1 FROM economy_effective_requests r WHERE r.session_id = economy_effective_sessions.id)
|
|
749
|
+
`,
|
|
750
|
+
params
|
|
751
|
+
};
|
|
752
|
+
}
|
|
753
|
+
function queryRequestTotals(db, where, params, machine) {
|
|
754
|
+
const q = requestTotalsSql(where, machine);
|
|
755
|
+
return baseRequestTotals(db.prepare(q.sql).get(...params, ...q.params));
|
|
756
|
+
}
|
|
757
|
+
function querySessionOnlyTotals(db, where, params, machine) {
|
|
758
|
+
const q = sessionOnlyTotalsSql(where, machine);
|
|
759
|
+
return baseSessionTotals(db.prepare(q.sql).get(...params, ...q.params));
|
|
760
|
+
}
|
|
761
|
+
function queryPeriodTotals(db, period, machine) {
|
|
762
|
+
const summary = querySummary(db, period, machine);
|
|
763
|
+
const requests = queryRequestTotals(db, requestPeriodWhere(period), [], machine);
|
|
764
|
+
const sessions = querySessionOnlyTotals(db, sessionPeriodWhere(period), [], machine);
|
|
765
|
+
const totals = combineTotals(requests, sessions);
|
|
766
|
+
return {
|
|
767
|
+
...totals,
|
|
768
|
+
sessions: summary.sessions,
|
|
769
|
+
requests: summary.requests,
|
|
770
|
+
total_tokens: summary.tokens,
|
|
771
|
+
cost_usd: summary.total_usd,
|
|
772
|
+
cost_summary: summary
|
|
773
|
+
};
|
|
774
|
+
}
|
|
775
|
+
function querySinceTotals(db, since, machine) {
|
|
776
|
+
const requests = queryRequestTotals(db, "timestamp >= ?", [since], machine);
|
|
777
|
+
const sessions = querySessionOnlyTotals(db, "started_at >= ?", [since], machine);
|
|
778
|
+
return combineTotals(requests, sessions);
|
|
779
|
+
}
|
|
780
|
+
function parseSinceDate(input, now) {
|
|
781
|
+
const value = input.trim();
|
|
782
|
+
const match = value.match(/^(\d+(?:\.\d+)?)(m|h|d|w)$/i);
|
|
783
|
+
if (match) {
|
|
784
|
+
const amount = Number(match[1]);
|
|
785
|
+
const unit = match[2].toLowerCase();
|
|
786
|
+
const multiplier = unit === "m" ? 60000 : unit === "h" ? 60 * 60000 : unit === "d" ? 24 * 60 * 60000 : 7 * 24 * 60 * 60000;
|
|
787
|
+
return {
|
|
788
|
+
timestamp: new Date(now.getTime() - amount * multiplier).toISOString(),
|
|
789
|
+
label: value
|
|
790
|
+
};
|
|
791
|
+
}
|
|
792
|
+
const parsed = new Date(value);
|
|
793
|
+
if (Number.isNaN(parsed.getTime())) {
|
|
794
|
+
throw new Error("--since must be a duration like 24h, 7d, 30m, 2w, or an ISO date");
|
|
795
|
+
}
|
|
796
|
+
return { timestamp: parsed.toISOString(), label: value };
|
|
797
|
+
}
|
|
798
|
+
function queryMachineSinceRows(db, since, machine) {
|
|
799
|
+
const knownMachines = new Set;
|
|
800
|
+
for (const m of listMachines(db, "all")) {
|
|
801
|
+
if (m.machine_id)
|
|
802
|
+
knownMachines.add(m.machine_id);
|
|
803
|
+
}
|
|
804
|
+
for (const m of listMachineRegistry(db)) {
|
|
805
|
+
if (m.machine_id)
|
|
806
|
+
knownMachines.add(m.machine_id);
|
|
807
|
+
}
|
|
808
|
+
if (machine)
|
|
809
|
+
knownMachines.add(machine);
|
|
810
|
+
const requestMachineClause = machine ? " AND machine_id = ?" : "";
|
|
811
|
+
const requestRows = db.prepare(`
|
|
812
|
+
SELECT
|
|
813
|
+
machine_id,
|
|
814
|
+
COUNT(DISTINCT session_id) as request_sessions,
|
|
815
|
+
COUNT(*) as requests,
|
|
816
|
+
COALESCE(SUM(input_tokens), 0) as input_tokens,
|
|
817
|
+
COALESCE(SUM(output_tokens), 0) as output_tokens,
|
|
818
|
+
COALESCE(SUM(cache_read_tokens), 0) as cache_read_tokens,
|
|
819
|
+
COALESCE(SUM(cache_create_tokens), 0) as cache_create_tokens,
|
|
820
|
+
COALESCE(SUM(COALESCE(cache_create_5m_tokens, cache_create_tokens)), 0) as cache_create_5m_tokens,
|
|
821
|
+
COALESCE(SUM(COALESCE(cache_create_1h_tokens, 0)), 0) as cache_create_1h_tokens,
|
|
822
|
+
COALESCE(SUM(input_tokens + output_tokens + cache_read_tokens + cache_create_tokens), 0) as total_tokens,
|
|
823
|
+
COALESCE(SUM(cost_usd), 0) as cost_usd,
|
|
824
|
+
MAX(timestamp) as last_active
|
|
825
|
+
FROM economy_effective_requests
|
|
826
|
+
WHERE timestamp >= ?
|
|
827
|
+
AND machine_id != ''
|
|
828
|
+
${requestMachineClause}
|
|
829
|
+
GROUP BY machine_id
|
|
830
|
+
`).all(...machine ? [since, machine] : [since]);
|
|
831
|
+
const sessionMachineClause = machine ? " AND machine_id = ?" : "";
|
|
832
|
+
const sessionRows = db.prepare(`
|
|
833
|
+
SELECT
|
|
834
|
+
machine_id,
|
|
835
|
+
COUNT(*) as sessions,
|
|
836
|
+
COALESCE(SUM(request_count), 0) as requests,
|
|
837
|
+
COALESCE(SUM(total_tokens), 0) as total_tokens,
|
|
838
|
+
COALESCE(SUM(total_cost_usd), 0) as cost_usd,
|
|
839
|
+
MAX(started_at) as last_active
|
|
840
|
+
FROM economy_effective_sessions
|
|
841
|
+
WHERE started_at >= ?
|
|
842
|
+
AND machine_id != ''
|
|
843
|
+
${sessionMachineClause}
|
|
844
|
+
AND NOT EXISTS (SELECT 1 FROM economy_effective_requests r WHERE r.session_id = economy_effective_sessions.id)
|
|
845
|
+
GROUP BY machine_id
|
|
846
|
+
`).all(...machine ? [since, machine] : [since]);
|
|
847
|
+
const groups = new Map;
|
|
848
|
+
for (const row of requestRows) {
|
|
849
|
+
knownMachines.add(row.machine_id);
|
|
850
|
+
groups.set(row.machine_id, combineTotals(baseRequestTotals(row), ZERO_SESSION_TOTALS));
|
|
851
|
+
}
|
|
852
|
+
for (const row of sessionRows) {
|
|
853
|
+
knownMachines.add(row.machine_id);
|
|
854
|
+
const current = groups.get(row.machine_id) ?? emptyTotals();
|
|
855
|
+
const sessions = baseSessionTotals(row);
|
|
856
|
+
groups.set(row.machine_id, {
|
|
857
|
+
...current,
|
|
858
|
+
sessions: current.sessions + sessions.sessions,
|
|
859
|
+
requests: current.requests + sessions.requests,
|
|
860
|
+
total_tokens: current.total_tokens + sessions.total_tokens,
|
|
861
|
+
cost_usd: current.cost_usd + sessions.cost_usd,
|
|
862
|
+
last_active: latestTimestamp(current.last_active, sessions.last_active)
|
|
863
|
+
});
|
|
864
|
+
}
|
|
865
|
+
const lastData = queryMachineLastData(db, machine);
|
|
866
|
+
const rows = [...knownMachines].filter((m) => !machine || m === machine).map((machineId) => {
|
|
867
|
+
const totals = groups.get(machineId) ?? emptyTotals();
|
|
868
|
+
const lastDataAt = lastData.get(machineId) ?? totals.last_active;
|
|
869
|
+
return {
|
|
870
|
+
machine_id: machineId,
|
|
871
|
+
...totals,
|
|
872
|
+
last_data_at: lastDataAt,
|
|
873
|
+
last_data_age: ""
|
|
874
|
+
};
|
|
875
|
+
});
|
|
876
|
+
return rows.sort((a, b) => b.cost_usd - a.cost_usd || a.machine_id.localeCompare(b.machine_id));
|
|
877
|
+
}
|
|
878
|
+
function queryMachineLastData(db, machine) {
|
|
879
|
+
const filter = machine ? " AND machine_id = ?" : "";
|
|
880
|
+
const requestRows = db.prepare(`
|
|
881
|
+
SELECT machine_id, MAX(timestamp) as last_at
|
|
882
|
+
FROM economy_effective_requests
|
|
883
|
+
WHERE machine_id != ''${filter}
|
|
884
|
+
GROUP BY machine_id
|
|
885
|
+
`).all(...machine ? [machine] : []);
|
|
886
|
+
const sessionRows = db.prepare(`
|
|
887
|
+
SELECT machine_id, MAX(started_at) as last_at
|
|
888
|
+
FROM economy_effective_sessions
|
|
889
|
+
WHERE machine_id != ''${filter}
|
|
890
|
+
GROUP BY machine_id
|
|
891
|
+
`).all(...machine ? [machine] : []);
|
|
892
|
+
const result = new Map;
|
|
893
|
+
for (const row of [...requestRows, ...sessionRows]) {
|
|
894
|
+
result.set(row.machine_id, latestTimestamp(result.get(row.machine_id), row.last_at));
|
|
895
|
+
}
|
|
896
|
+
return result;
|
|
897
|
+
}
|
|
898
|
+
function queryAgentSinceRows(db, since, machine) {
|
|
899
|
+
const requestMachineClause = machine ? " AND machine_id = ?" : "";
|
|
900
|
+
const sessionMachineClause = machine ? " AND machine_id = ?" : "";
|
|
901
|
+
const rows = db.prepare(`
|
|
902
|
+
WITH request_rows AS (
|
|
903
|
+
SELECT
|
|
904
|
+
agent,
|
|
905
|
+
session_id,
|
|
906
|
+
1 as requests,
|
|
907
|
+
input_tokens,
|
|
908
|
+
output_tokens,
|
|
909
|
+
cache_read_tokens,
|
|
910
|
+
cache_create_tokens,
|
|
911
|
+
COALESCE(cache_create_5m_tokens, cache_create_tokens) as cache_create_5m_tokens,
|
|
912
|
+
COALESCE(cache_create_1h_tokens, 0) as cache_create_1h_tokens,
|
|
913
|
+
input_tokens + output_tokens + cache_read_tokens + cache_create_tokens as total_tokens,
|
|
914
|
+
cost_usd,
|
|
915
|
+
timestamp as last_active
|
|
916
|
+
FROM economy_effective_requests
|
|
917
|
+
WHERE timestamp >= ?${requestMachineClause}
|
|
918
|
+
),
|
|
919
|
+
session_only_rows AS (
|
|
920
|
+
SELECT
|
|
921
|
+
agent,
|
|
922
|
+
id as session_id,
|
|
923
|
+
COALESCE(request_count, 0) as requests,
|
|
924
|
+
0 as input_tokens,
|
|
925
|
+
0 as output_tokens,
|
|
926
|
+
0 as cache_read_tokens,
|
|
927
|
+
0 as cache_create_tokens,
|
|
928
|
+
0 as cache_create_5m_tokens,
|
|
929
|
+
0 as cache_create_1h_tokens,
|
|
930
|
+
COALESCE(total_tokens, 0) as total_tokens,
|
|
931
|
+
COALESCE(total_cost_usd, 0) as cost_usd,
|
|
932
|
+
started_at as last_active
|
|
933
|
+
FROM economy_effective_sessions
|
|
934
|
+
WHERE started_at >= ?${sessionMachineClause}
|
|
935
|
+
AND NOT EXISTS (SELECT 1 FROM economy_effective_requests r WHERE r.session_id = economy_effective_sessions.id)
|
|
936
|
+
),
|
|
937
|
+
combined AS (
|
|
938
|
+
SELECT * FROM request_rows
|
|
939
|
+
UNION ALL
|
|
940
|
+
SELECT * FROM session_only_rows
|
|
941
|
+
)
|
|
942
|
+
SELECT
|
|
943
|
+
agent,
|
|
944
|
+
COUNT(DISTINCT session_id) as sessions,
|
|
945
|
+
COALESCE(SUM(requests), 0) as requests,
|
|
946
|
+
COALESCE(SUM(input_tokens), 0) as input_tokens,
|
|
947
|
+
COALESCE(SUM(output_tokens), 0) as output_tokens,
|
|
948
|
+
COALESCE(SUM(cache_read_tokens), 0) as cache_read_tokens,
|
|
949
|
+
COALESCE(SUM(cache_create_tokens), 0) as cache_create_tokens,
|
|
950
|
+
COALESCE(SUM(cache_create_5m_tokens), 0) as cache_create_5m_tokens,
|
|
951
|
+
COALESCE(SUM(cache_create_1h_tokens), 0) as cache_create_1h_tokens,
|
|
952
|
+
COALESCE(SUM(total_tokens), 0) as total_tokens,
|
|
953
|
+
COALESCE(SUM(cost_usd), 0) as cost_usd,
|
|
954
|
+
MAX(last_active) as last_active
|
|
955
|
+
FROM combined
|
|
956
|
+
GROUP BY agent
|
|
957
|
+
ORDER BY cost_usd DESC
|
|
958
|
+
`).all(...machine ? [since, machine, since, machine] : [since, since]);
|
|
959
|
+
if (rows.length > 0)
|
|
960
|
+
return rows;
|
|
961
|
+
return queryAgentBreakdown(db, "all", machine).filter((row) => row.last_active >= since).map(agentBreakdownToBriefRow);
|
|
962
|
+
}
|
|
963
|
+
function agentBreakdownToBriefRow(row) {
|
|
964
|
+
return {
|
|
965
|
+
agent: row.agent,
|
|
966
|
+
sessions: row.sessions,
|
|
967
|
+
requests: row.requests,
|
|
968
|
+
input_tokens: 0,
|
|
969
|
+
output_tokens: 0,
|
|
970
|
+
cache_read_tokens: 0,
|
|
971
|
+
cache_create_tokens: 0,
|
|
972
|
+
cache_create_5m_tokens: 0,
|
|
973
|
+
cache_create_1h_tokens: 0,
|
|
974
|
+
total_tokens: row.total_tokens,
|
|
975
|
+
cost_usd: row.cost_usd,
|
|
976
|
+
last_active: row.last_active
|
|
977
|
+
};
|
|
978
|
+
}
|
|
979
|
+
function normalizeAccountEmail(email) {
|
|
980
|
+
return email.trim().toLowerCase();
|
|
981
|
+
}
|
|
982
|
+
function accountKey(row) {
|
|
983
|
+
const agent = row.agent || row.account_tool || "unknown";
|
|
984
|
+
const email = normalizeAccountEmail(row.account_email);
|
|
985
|
+
if (email)
|
|
986
|
+
return `${agent}:${email}`;
|
|
987
|
+
if (row.account_name)
|
|
988
|
+
return `${agent}:${row.account_name}`;
|
|
989
|
+
return row.account_key || `${agent}:unknown`;
|
|
990
|
+
}
|
|
991
|
+
function addAccountRow(groups, row) {
|
|
992
|
+
if (!row.account_key && !row.account_tool && !row.account_name && !row.account_email)
|
|
993
|
+
return;
|
|
994
|
+
const key = accountKey(row);
|
|
995
|
+
const email = normalizeAccountEmail(row.account_email);
|
|
996
|
+
const current = groups.get(key) ?? {
|
|
997
|
+
account_key: key,
|
|
998
|
+
account_tool: row.agent || row.account_tool || "unknown",
|
|
999
|
+
account_name: row.account_name || email || row.account_key || "unknown",
|
|
1000
|
+
account_email: email || null,
|
|
1001
|
+
account_source: row.account_source || "unknown",
|
|
1002
|
+
sessions: 0,
|
|
1003
|
+
requests: 0,
|
|
1004
|
+
input_tokens: 0,
|
|
1005
|
+
output_tokens: 0,
|
|
1006
|
+
cache_read_tokens: 0,
|
|
1007
|
+
cache_create_tokens: 0,
|
|
1008
|
+
cache_create_5m_tokens: 0,
|
|
1009
|
+
cache_create_1h_tokens: 0,
|
|
1010
|
+
total_tokens: 0,
|
|
1011
|
+
cost_usd: 0,
|
|
1012
|
+
last_active: null
|
|
1013
|
+
};
|
|
1014
|
+
current.sessions += Number(row.sessions ?? 0);
|
|
1015
|
+
if (!current.account_email && email)
|
|
1016
|
+
current.account_email = email;
|
|
1017
|
+
if ((!current.account_name || current.account_name === "unknown") && (row.account_name || email)) {
|
|
1018
|
+
current.account_name = row.account_name || email;
|
|
1019
|
+
}
|
|
1020
|
+
if ((!current.account_source || current.account_source === "unknown") && row.account_source && row.account_source !== "unknown") {
|
|
1021
|
+
current.account_source = row.account_source;
|
|
1022
|
+
}
|
|
1023
|
+
current.requests += Number(row.requests ?? 0);
|
|
1024
|
+
current.input_tokens += Number(row.input_tokens ?? 0);
|
|
1025
|
+
current.output_tokens += Number(row.output_tokens ?? 0);
|
|
1026
|
+
current.cache_read_tokens += Number(row.cache_read_tokens ?? 0);
|
|
1027
|
+
current.cache_create_tokens += Number(row.cache_create_tokens ?? 0);
|
|
1028
|
+
current.cache_create_5m_tokens += Number(row.cache_create_5m_tokens ?? 0);
|
|
1029
|
+
current.cache_create_1h_tokens += Number(row.cache_create_1h_tokens ?? 0);
|
|
1030
|
+
current.total_tokens += Number(row.total_tokens ?? 0);
|
|
1031
|
+
current.cost_usd += Number(row.cost_usd ?? 0);
|
|
1032
|
+
current.last_active = latestTimestamp(current.last_active, row.last_active);
|
|
1033
|
+
groups.set(key, current);
|
|
1034
|
+
}
|
|
1035
|
+
function queryAccountSinceRows(db, since, machine) {
|
|
1036
|
+
const requestMachineClause = machine ? " AND r.machine_id = ?" : "";
|
|
1037
|
+
const sessionMachineClause = machine ? " AND s.machine_id = ?" : "";
|
|
1038
|
+
const requestRows = db.prepare(`
|
|
1039
|
+
SELECT
|
|
1040
|
+
COALESCE(NULLIF(r.agent, ''), NULLIF(s.agent, ''), '') as agent,
|
|
1041
|
+
COALESCE(NULLIF(r.account_key, ''), NULLIF(s.account_key, ''), '') as account_key,
|
|
1042
|
+
COALESCE(NULLIF(r.account_tool, ''), NULLIF(s.account_tool, ''), '') as account_tool,
|
|
1043
|
+
COALESCE(NULLIF(r.account_name, ''), NULLIF(s.account_name, ''), '') as account_name,
|
|
1044
|
+
COALESCE(NULLIF(r.account_email, ''), NULLIF(s.account_email, ''), '') as account_email,
|
|
1045
|
+
COALESCE(NULLIF(r.account_source, ''), NULLIF(s.account_source, ''), 'unknown') as account_source,
|
|
1046
|
+
COUNT(DISTINCT r.session_id) as sessions,
|
|
1047
|
+
COUNT(*) as requests,
|
|
1048
|
+
COALESCE(SUM(r.input_tokens), 0) as input_tokens,
|
|
1049
|
+
COALESCE(SUM(r.output_tokens), 0) as output_tokens,
|
|
1050
|
+
COALESCE(SUM(r.cache_read_tokens), 0) as cache_read_tokens,
|
|
1051
|
+
COALESCE(SUM(r.cache_create_tokens), 0) as cache_create_tokens,
|
|
1052
|
+
COALESCE(SUM(COALESCE(r.cache_create_5m_tokens, r.cache_create_tokens, 0)), 0) as cache_create_5m_tokens,
|
|
1053
|
+
COALESCE(SUM(COALESCE(r.cache_create_1h_tokens, 0)), 0) as cache_create_1h_tokens,
|
|
1054
|
+
COALESCE(SUM(r.input_tokens + r.output_tokens + r.cache_read_tokens + r.cache_create_tokens), 0) as total_tokens,
|
|
1055
|
+
COALESCE(SUM(r.cost_usd), 0) as cost_usd,
|
|
1056
|
+
MAX(r.timestamp) as last_active
|
|
1057
|
+
FROM economy_effective_requests r
|
|
1058
|
+
LEFT JOIN economy_effective_sessions s ON s.id = r.session_id
|
|
1059
|
+
WHERE r.timestamp >= ?${requestMachineClause}
|
|
1060
|
+
GROUP BY
|
|
1061
|
+
COALESCE(NULLIF(r.agent, ''), NULLIF(s.agent, ''), ''),
|
|
1062
|
+
COALESCE(NULLIF(r.account_key, ''), NULLIF(s.account_key, ''), ''),
|
|
1063
|
+
COALESCE(NULLIF(r.account_tool, ''), NULLIF(s.account_tool, ''), ''),
|
|
1064
|
+
COALESCE(NULLIF(r.account_name, ''), NULLIF(s.account_name, ''), ''),
|
|
1065
|
+
COALESCE(NULLIF(r.account_email, ''), NULLIF(s.account_email, ''), ''),
|
|
1066
|
+
COALESCE(NULLIF(r.account_source, ''), NULLIF(s.account_source, ''), 'unknown')
|
|
1067
|
+
`).all(...machine ? [since, machine] : [since]);
|
|
1068
|
+
const sessionRows = db.prepare(`
|
|
1069
|
+
SELECT
|
|
1070
|
+
s.agent as agent,
|
|
1071
|
+
s.account_key as account_key,
|
|
1072
|
+
s.account_tool as account_tool,
|
|
1073
|
+
s.account_name as account_name,
|
|
1074
|
+
COALESCE(s.account_email, '') as account_email,
|
|
1075
|
+
COALESCE(NULLIF(s.account_source, ''), 'unknown') as account_source,
|
|
1076
|
+
COUNT(DISTINCT s.id) as sessions,
|
|
1077
|
+
COALESCE(SUM(s.request_count), 0) as requests,
|
|
1078
|
+
0 as input_tokens,
|
|
1079
|
+
0 as output_tokens,
|
|
1080
|
+
0 as cache_read_tokens,
|
|
1081
|
+
0 as cache_create_tokens,
|
|
1082
|
+
0 as cache_create_5m_tokens,
|
|
1083
|
+
0 as cache_create_1h_tokens,
|
|
1084
|
+
COALESCE(SUM(s.total_tokens), 0) as total_tokens,
|
|
1085
|
+
COALESCE(SUM(s.total_cost_usd), 0) as cost_usd,
|
|
1086
|
+
MAX(s.started_at) as last_active
|
|
1087
|
+
FROM economy_effective_sessions s
|
|
1088
|
+
WHERE s.started_at >= ?${sessionMachineClause}
|
|
1089
|
+
AND NOT EXISTS (SELECT 1 FROM economy_effective_requests r WHERE r.session_id = s.id)
|
|
1090
|
+
GROUP BY
|
|
1091
|
+
s.agent,
|
|
1092
|
+
s.account_key,
|
|
1093
|
+
s.account_tool,
|
|
1094
|
+
s.account_name,
|
|
1095
|
+
COALESCE(s.account_email, ''),
|
|
1096
|
+
COALESCE(NULLIF(s.account_source, ''), 'unknown')
|
|
1097
|
+
`).all(...machine ? [since, machine] : [since]);
|
|
1098
|
+
const groups = new Map;
|
|
1099
|
+
for (const row of requestRows)
|
|
1100
|
+
addAccountRow(groups, row);
|
|
1101
|
+
for (const row of sessionRows)
|
|
1102
|
+
addAccountRow(groups, row);
|
|
1103
|
+
const rows = [...groups.values()];
|
|
1104
|
+
rows.sort((a, b) => b.cost_usd - a.cost_usd || a.account_key.localeCompare(b.account_key));
|
|
1105
|
+
if (rows.length > 0)
|
|
1106
|
+
return rows;
|
|
1107
|
+
return queryAccountBreakdown(db, "all", machine).filter((row) => row.last_active >= since).map(accountBreakdownToBriefRow);
|
|
1108
|
+
}
|
|
1109
|
+
function accountBreakdownToBriefRow(row) {
|
|
1110
|
+
return {
|
|
1111
|
+
account_key: row.account_key,
|
|
1112
|
+
account_tool: row.account_tool,
|
|
1113
|
+
account_name: row.account_name,
|
|
1114
|
+
account_email: row.account_email,
|
|
1115
|
+
account_source: row.account_source,
|
|
1116
|
+
sessions: row.sessions,
|
|
1117
|
+
requests: row.requests,
|
|
1118
|
+
input_tokens: 0,
|
|
1119
|
+
output_tokens: 0,
|
|
1120
|
+
cache_read_tokens: 0,
|
|
1121
|
+
cache_create_tokens: 0,
|
|
1122
|
+
cache_create_5m_tokens: 0,
|
|
1123
|
+
cache_create_1h_tokens: 0,
|
|
1124
|
+
total_tokens: row.total_tokens,
|
|
1125
|
+
cost_usd: row.cost_usd,
|
|
1126
|
+
last_active: row.last_active
|
|
1127
|
+
};
|
|
1128
|
+
}
|
|
1129
|
+
function queryFreshness(db, now, machine, localSync) {
|
|
1130
|
+
const requestFilter = machine ? " AND machine_id = ?" : "";
|
|
1131
|
+
const requestRows = db.prepare(`
|
|
1132
|
+
SELECT machine_id, MAX(timestamp) as max_request_at
|
|
1133
|
+
FROM economy_effective_requests
|
|
1134
|
+
WHERE machine_id != ''${requestFilter}
|
|
1135
|
+
GROUP BY machine_id
|
|
1136
|
+
`).all(...machine ? [machine] : []);
|
|
1137
|
+
const registryRows = listMachineRegistry(db).filter((row) => !machine || row.machine_id === machine);
|
|
1138
|
+
const machines = new Map;
|
|
1139
|
+
for (const row of requestRows) {
|
|
1140
|
+
machines.set(row.machine_id, {
|
|
1141
|
+
machine_id: row.machine_id,
|
|
1142
|
+
max_request_at: row.max_request_at,
|
|
1143
|
+
request_age: formatAge(row.max_request_at, now),
|
|
1144
|
+
last_merge_sync_at: null,
|
|
1145
|
+
merge_sync_age: "never"
|
|
1146
|
+
});
|
|
1147
|
+
}
|
|
1148
|
+
for (const row of registryRows) {
|
|
1149
|
+
const current = machines.get(row.machine_id) ?? {
|
|
1150
|
+
machine_id: row.machine_id,
|
|
1151
|
+
max_request_at: null,
|
|
1152
|
+
request_age: "never",
|
|
1153
|
+
last_merge_sync_at: null,
|
|
1154
|
+
merge_sync_age: "never"
|
|
1155
|
+
};
|
|
1156
|
+
const syncAt = latestTimestamp(row.last_pull_at, row.last_push_at, row.updated_at);
|
|
1157
|
+
current.last_merge_sync_at = syncAt;
|
|
1158
|
+
current.merge_sync_age = formatAge(syncAt, now);
|
|
1159
|
+
machines.set(row.machine_id, current);
|
|
1160
|
+
}
|
|
1161
|
+
if (localSync && (!machine || machine === localSync.machineId)) {
|
|
1162
|
+
const current = machines.get(localSync.machineId) ?? {
|
|
1163
|
+
machine_id: localSync.machineId,
|
|
1164
|
+
max_request_at: null,
|
|
1165
|
+
request_age: "never",
|
|
1166
|
+
last_merge_sync_at: null,
|
|
1167
|
+
merge_sync_age: "never"
|
|
1168
|
+
};
|
|
1169
|
+
current.last_merge_sync_at = latestTimestamp(current.last_merge_sync_at, localSync.syncedAt);
|
|
1170
|
+
current.merge_sync_age = formatAge(current.last_merge_sync_at, now);
|
|
1171
|
+
machines.set(localSync.machineId, current);
|
|
1172
|
+
}
|
|
1173
|
+
const rows = [...machines.values()].sort((a, b) => a.machine_id.localeCompare(b.machine_id));
|
|
1174
|
+
return {
|
|
1175
|
+
machines: rows,
|
|
1176
|
+
max_request_line: rows.length ? rows.map((row) => `${row.machine_id} ${row.max_request_at ?? "never"} (${row.request_age})`).join("; ") : "none",
|
|
1177
|
+
merge_sync_line: rows.length ? rows.map((row) => `${row.machine_id} ${row.last_merge_sync_at ?? "never"} (${row.merge_sync_age})`).join("; ") : "none"
|
|
1178
|
+
};
|
|
1179
|
+
}
|
|
1180
|
+
function buildBrief(db, opts = {}) {
|
|
1181
|
+
const now = opts.now ?? new Date;
|
|
1182
|
+
const machine = machineFilter(opts.machine);
|
|
1183
|
+
const sinceInput = opts.since ?? "24h";
|
|
1184
|
+
const since = parseSinceDate(sinceInput, now);
|
|
1185
|
+
const summaries = [
|
|
1186
|
+
{
|
|
1187
|
+
label: "Today",
|
|
1188
|
+
period: "today",
|
|
1189
|
+
...queryPeriodTotals(db, "today", machine)
|
|
1190
|
+
},
|
|
1191
|
+
{
|
|
1192
|
+
label: "Week",
|
|
1193
|
+
period: "week",
|
|
1194
|
+
...queryPeriodTotals(db, "week", machine)
|
|
1195
|
+
},
|
|
1196
|
+
{
|
|
1197
|
+
label: `Since ${since.label}`,
|
|
1198
|
+
period: "since",
|
|
1199
|
+
since: since.timestamp,
|
|
1200
|
+
...querySinceTotals(db, since.timestamp, machine)
|
|
1201
|
+
}
|
|
1202
|
+
];
|
|
1203
|
+
const machines = queryMachineSinceRows(db, since.timestamp, machine);
|
|
1204
|
+
for (const row of machines) {
|
|
1205
|
+
row.last_data_age = formatAge(row.last_data_at, now);
|
|
1206
|
+
}
|
|
1207
|
+
return {
|
|
1208
|
+
generated_at: now.toISOString(),
|
|
1209
|
+
machine: machine ?? "all",
|
|
1210
|
+
since: {
|
|
1211
|
+
input: sinceInput,
|
|
1212
|
+
timestamp: since.timestamp,
|
|
1213
|
+
label: since.label
|
|
1214
|
+
},
|
|
1215
|
+
summaries,
|
|
1216
|
+
machines,
|
|
1217
|
+
agents: queryAgentSinceRows(db, since.timestamp, machine),
|
|
1218
|
+
accounts: queryAccountSinceRows(db, since.timestamp, machine),
|
|
1219
|
+
freshness: queryFreshness(db, now, machine, opts.currentMachineId && opts.localSyncAt ? { machineId: opts.currentMachineId, syncedAt: opts.localSyncAt.toISOString() } : undefined)
|
|
1220
|
+
};
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
// src/lib/analytics.ts
|
|
1224
|
+
function queryProjectDetail(db, nameOrPath) {
|
|
1225
|
+
const like = `%${nameOrPath}%`;
|
|
1226
|
+
const sessions = db.prepare(`SELECT id, project_name, project_path, total_cost_usd, total_tokens, started_at
|
|
1227
|
+
FROM economy_effective_sessions
|
|
1228
|
+
WHERE project_name LIKE ? OR project_path LIKE ?
|
|
1229
|
+
ORDER BY started_at DESC`).all(like, like);
|
|
1230
|
+
if (sessions.length === 0)
|
|
1231
|
+
return null;
|
|
1232
|
+
const first = sessions[0];
|
|
1233
|
+
const totalCost = sessions.reduce((s, r) => s + (r.total_cost_usd ?? 0), 0);
|
|
1234
|
+
const totalTokens = sessions.reduce((s, r) => s + (r.total_tokens ?? 0), 0);
|
|
1235
|
+
const daily = db.prepare(`SELECT DATE(r.timestamp) as date, COALESCE(SUM(r.cost_usd), 0) as cost_usd
|
|
1236
|
+
FROM economy_effective_requests r JOIN economy_effective_sessions s ON r.session_id = s.id
|
|
1237
|
+
WHERE (s.project_name LIKE ? OR s.project_path LIKE ?)
|
|
1238
|
+
AND r.timestamp >= DATE('now', '-14 days')
|
|
1239
|
+
GROUP BY date ORDER BY date ASC`).all(like, like);
|
|
1240
|
+
const models = db.prepare(`SELECT r.model as model, COUNT(*) as requests, COALESCE(SUM(r.cost_usd), 0) as cost_usd
|
|
1241
|
+
FROM economy_effective_requests r JOIN economy_effective_sessions s ON r.session_id = s.id
|
|
1242
|
+
WHERE s.project_name LIKE ? OR s.project_path LIKE ?
|
|
1243
|
+
GROUP BY r.model ORDER BY cost_usd DESC LIMIT 5`).all(like, like);
|
|
1244
|
+
const top_sessions = [...sessions].sort((a, b) => (b.total_cost_usd ?? 0) - (a.total_cost_usd ?? 0)).slice(0, 5).map((s) => ({ id: s.id, total_cost_usd: s.total_cost_usd ?? 0, started_at: s.started_at }));
|
|
1245
|
+
return {
|
|
1246
|
+
project_name: first.project_name || nameOrPath,
|
|
1247
|
+
project_path: first.project_path || "",
|
|
1248
|
+
total_cost_usd: totalCost,
|
|
1249
|
+
total_tokens: totalTokens,
|
|
1250
|
+
sessions: sessions.length,
|
|
1251
|
+
daily,
|
|
1252
|
+
models,
|
|
1253
|
+
top_sessions
|
|
1254
|
+
};
|
|
1255
|
+
}
|
|
1256
|
+
function exportDateWhere(column, period) {
|
|
1257
|
+
if (!["today", "yesterday", "week", "month", "year", "all"].includes(period)) {
|
|
1258
|
+
throw new RangeError("Invalid export period");
|
|
1259
|
+
}
|
|
1260
|
+
return calendarPeriodWhere(period, column);
|
|
1261
|
+
}
|
|
1262
|
+
function queryExportRows(db, type, period) {
|
|
1263
|
+
if (type === "requests") {
|
|
1264
|
+
const where2 = exportDateWhere("timestamp", period);
|
|
1265
|
+
return db.prepare(`SELECT * FROM economy_effective_requests WHERE ${where2} ORDER BY timestamp ASC`).all();
|
|
1266
|
+
}
|
|
1267
|
+
const where = exportDateWhere("started_at", period);
|
|
1268
|
+
return db.prepare(`SELECT * FROM economy_effective_sessions WHERE ${where} ORDER BY started_at DESC`).all();
|
|
1269
|
+
}
|
|
1270
|
+
function queryRangeStats(db, from, to) {
|
|
1271
|
+
const r = db.prepare(`SELECT COALESCE(SUM(cost_usd),0) as cost, COUNT(*) as requests,
|
|
1272
|
+
COALESCE(SUM(input_tokens+output_tokens+cache_read_tokens+cache_create_tokens),0) as tokens
|
|
1273
|
+
FROM economy_effective_requests WHERE DATE(timestamp) BETWEEN ? AND ?`).get(from, to);
|
|
1274
|
+
const s = db.prepare(`SELECT COUNT(*) as sessions FROM economy_effective_sessions WHERE DATE(started_at) BETWEEN ? AND ?`).get(from, to);
|
|
1275
|
+
return { cost: r.cost, requests: r.requests, tokens: r.tokens, sessions: s.sessions };
|
|
1276
|
+
}
|
|
1277
|
+
function queryForecast(db, now = new Date) {
|
|
1278
|
+
const daysInMonth = new Date(now.getFullYear(), now.getMonth() + 1, 0).getDate();
|
|
1279
|
+
const dayOfMonth = now.getDate();
|
|
1280
|
+
const monthStart = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-01`;
|
|
1281
|
+
const monthSoFar = db.prepare(`SELECT COALESCE(SUM(cost_usd),0) as cost FROM economy_effective_requests WHERE DATE(timestamp) >= ?`).get(monthStart);
|
|
1282
|
+
const dailyAvg = dayOfMonth > 0 ? monthSoFar.cost / dayOfMonth : 0;
|
|
1283
|
+
const projected = dailyAvg * daysInMonth;
|
|
1284
|
+
const sevenDaysAgo = new Date(now);
|
|
1285
|
+
sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7);
|
|
1286
|
+
const last7 = db.prepare(`SELECT COALESCE(SUM(cost_usd),0) as cost FROM economy_effective_requests WHERE DATE(timestamp) >= ?`).get(sevenDaysAgo.toISOString().substring(0, 10));
|
|
1287
|
+
const last7DailyAvg = last7.cost / 7;
|
|
1288
|
+
const dailyCosts = db.prepare(`SELECT DATE(timestamp) as date, COALESCE(SUM(cost_usd),0) as cost_usd
|
|
1289
|
+
FROM economy_effective_requests WHERE DATE(timestamp) >= ? GROUP BY date ORDER BY cost_usd ASC`).all(monthStart);
|
|
1290
|
+
return {
|
|
1291
|
+
day_of_month: dayOfMonth,
|
|
1292
|
+
days_in_month: daysInMonth,
|
|
1293
|
+
spent_so_far_usd: monthSoFar.cost,
|
|
1294
|
+
daily_avg_usd: dailyAvg,
|
|
1295
|
+
projected_usd: projected,
|
|
1296
|
+
last7_daily_avg_usd: last7DailyAvg,
|
|
1297
|
+
last7_projected_usd: last7DailyAvg * daysInMonth,
|
|
1298
|
+
cheapest_day: dailyCosts[0] ?? null,
|
|
1299
|
+
most_expensive_day: dailyCosts[dailyCosts.length - 1] ?? null
|
|
1300
|
+
};
|
|
1301
|
+
}
|
|
1302
|
+
function queryModelEfficiency(db) {
|
|
1303
|
+
return db.prepare(`SELECT model,
|
|
1304
|
+
COALESCE(SUM(input_tokens),0) as input,
|
|
1305
|
+
COALESCE(SUM(output_tokens),0) as output,
|
|
1306
|
+
COALESCE(SUM(cache_read_tokens),0) as cache_read,
|
|
1307
|
+
COALESCE(SUM(cache_create_tokens),0) as cache_write,
|
|
1308
|
+
COUNT(*) as requests,
|
|
1309
|
+
COALESCE(SUM(cost_usd),0) as cost
|
|
1310
|
+
FROM economy_effective_requests GROUP BY model ORDER BY cost DESC`).all();
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
export { packageMetadata, requireCurrentSavingsReport, querySavingsSummary, queryBillingDiff, requireCurrentBillingDiffSummary, billingDriftCheck, buildBrief, queryProjectDetail, queryExportRows, queryRangeStats, queryForecast, queryModelEfficiency, LOCAL_STORAGE_OPT_IN_KEYS, economyCloudStorage, cloudListItems, cloudObject, economyTransportReport };
|