mirror of
https://github.com/anomalyco/opencode.git
synced 2026-05-27 00:31:00 +00:00
feat: initial datalake and stats site (#28666)
This commit is contained in:
parent
633b5d6208
commit
5b02ac4d33
68 changed files with 8967 additions and 42 deletions
12
.dockerignore
Normal file
12
.dockerignore
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
.git
|
||||
.opencode
|
||||
.sst
|
||||
.turbo
|
||||
.wrangler
|
||||
node_modules
|
||||
**/node_modules
|
||||
**/.output
|
||||
**/dist
|
||||
**/.turbo
|
||||
**/.vite
|
||||
**/coverage
|
||||
12
.github/workflows/deploy.yml
vendored
12
.github/workflows/deploy.yml
vendored
|
|
@ -9,9 +9,15 @@ on:
|
|||
|
||||
concurrency: ${{ github.workflow }}-${{ github.ref }}
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
if: github.repository == 'anomalyco/opencode' && (github.ref_name == 'dev' || github.ref_name == 'production')
|
||||
runs-on: ubuntu-latest
|
||||
environment: ${{ github.ref_name }}
|
||||
steps:
|
||||
- uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0
|
||||
|
||||
|
|
@ -21,6 +27,12 @@ jobs:
|
|||
with:
|
||||
node-version: "24"
|
||||
|
||||
- uses: aws-actions/configure-aws-credentials@7474bc4690e29a8392af63c5b98e7449536d5c3a # v4.3.1
|
||||
with:
|
||||
role-to-assume: ${{ vars.AWS_DEPLOY_ROLE_ARN }}
|
||||
role-session-name: opencode-${{ github.run_id }}
|
||||
aws-region: us-east-1
|
||||
|
||||
- run: bun sst deploy --stage=${{ github.ref_name }}
|
||||
env:
|
||||
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
|
|
|
|||
113
bun.lock
113
bun.lock
|
|
@ -23,7 +23,7 @@
|
|||
"oxlint-tsgolint": "0.21.0",
|
||||
"prettier": "3.6.2",
|
||||
"semver": "^7.6.0",
|
||||
"sst": "4.13.1",
|
||||
"sst": "catalog:",
|
||||
"turbo": "2.8.13",
|
||||
},
|
||||
},
|
||||
|
|
@ -603,6 +603,66 @@
|
|||
"typescript": "catalog:",
|
||||
},
|
||||
},
|
||||
"packages/stats/app": {
|
||||
"name": "@opencode-ai/stats-app",
|
||||
"version": "1.14.50",
|
||||
"dependencies": {
|
||||
"@opencode-ai/stats-core": "workspace:*",
|
||||
"@opencode-ai/ui": "workspace:*",
|
||||
"@solidjs/meta": "catalog:",
|
||||
"@solidjs/router": "catalog:",
|
||||
"@solidjs/start": "catalog:",
|
||||
"d3-scale": "4.0.2",
|
||||
"effect": "catalog:",
|
||||
"nitro": "3.0.1-alpha.1",
|
||||
"solid-js": "catalog:",
|
||||
"vite": "catalog:",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@cloudflare/workers-types": "catalog:",
|
||||
"@types/bun": "catalog:",
|
||||
"@types/d3-scale": "4.0.9",
|
||||
"@typescript/native-preview": "catalog:",
|
||||
"typescript": "catalog:",
|
||||
},
|
||||
},
|
||||
"packages/stats/core": {
|
||||
"name": "@opencode-ai/stats-core",
|
||||
"version": "1.14.50",
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-athena": "3.933.0",
|
||||
"@planetscale/database": "1.19.0",
|
||||
"drizzle-orm": "catalog:",
|
||||
"effect": "catalog:",
|
||||
"sst": "catalog:",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tsconfig/node22": "catalog:",
|
||||
"@types/bun": "catalog:",
|
||||
"@types/node": "catalog:",
|
||||
"@typescript/native-preview": "catalog:",
|
||||
"drizzle-kit": "catalog:",
|
||||
"typescript": "catalog:",
|
||||
},
|
||||
},
|
||||
"packages/stats/server": {
|
||||
"name": "@opencode-ai/stats-server",
|
||||
"version": "1.14.50",
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-firehose": "3.933.0",
|
||||
"@effect/platform-node": "catalog:",
|
||||
"@opencode-ai/stats-core": "workspace:*",
|
||||
"effect": "catalog:",
|
||||
"sst": "catalog:",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tsconfig/node22": "catalog:",
|
||||
"@types/bun": "catalog:",
|
||||
"@types/node": "catalog:",
|
||||
"@typescript/native-preview": "catalog:",
|
||||
"typescript": "catalog:",
|
||||
},
|
||||
},
|
||||
"packages/storybook": {
|
||||
"name": "@opencode-ai/storybook",
|
||||
"devDependencies": {
|
||||
|
|
@ -783,6 +843,7 @@
|
|||
"shiki": "3.20.0",
|
||||
"solid-js": "1.9.10",
|
||||
"solid-list": "0.3.0",
|
||||
"sst": "4.13.1",
|
||||
"tailwindcss": "4.1.11",
|
||||
"typescript": "5.8.2",
|
||||
"ulid": "3.0.1",
|
||||
|
|
@ -908,8 +969,12 @@
|
|||
|
||||
"@aws-crypto/util": ["@aws-crypto/util@5.2.0", "", { "dependencies": { "@aws-sdk/types": "^3.222.0", "@smithy/util-utf8": "^2.0.0", "tslib": "^2.6.2" } }, "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ=="],
|
||||
|
||||
"@aws-sdk/client-athena": ["@aws-sdk/client-athena@3.933.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.932.0", "@aws-sdk/credential-provider-node": "3.933.0", "@aws-sdk/middleware-host-header": "3.930.0", "@aws-sdk/middleware-logger": "3.930.0", "@aws-sdk/middleware-recursion-detection": "3.933.0", "@aws-sdk/middleware-user-agent": "3.932.0", "@aws-sdk/region-config-resolver": "3.930.0", "@aws-sdk/types": "3.930.0", "@aws-sdk/util-endpoints": "3.930.0", "@aws-sdk/util-user-agent-browser": "3.930.0", "@aws-sdk/util-user-agent-node": "3.932.0", "@smithy/config-resolver": "^4.4.3", "@smithy/core": "^3.18.2", "@smithy/fetch-http-handler": "^5.3.6", "@smithy/hash-node": "^4.2.5", "@smithy/invalid-dependency": "^4.2.5", "@smithy/middleware-content-length": "^4.2.5", "@smithy/middleware-endpoint": "^4.3.9", "@smithy/middleware-retry": "^4.4.9", "@smithy/middleware-serde": "^4.2.5", "@smithy/middleware-stack": "^4.2.5", "@smithy/node-config-provider": "^4.3.5", "@smithy/node-http-handler": "^4.4.5", "@smithy/protocol-http": "^5.3.5", "@smithy/smithy-client": "^4.9.5", "@smithy/types": "^4.9.0", "@smithy/url-parser": "^4.2.5", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", "@smithy/util-defaults-mode-browser": "^4.3.8", "@smithy/util-defaults-mode-node": "^4.2.11", "@smithy/util-endpoints": "^3.2.5", "@smithy/util-middleware": "^4.2.5", "@smithy/util-retry": "^4.2.5", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-9eMUCu1Ay3C9ojo+dJcynSdpbxuwDVtZUt/Xhce+c2+mgDsmvRzjww+wfLpZwRNWxBWmeauQQAZk52tCwQgXsQ=="],
|
||||
|
||||
"@aws-sdk/client-cognito-identity": ["@aws-sdk/client-cognito-identity@3.993.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.973.11", "@aws-sdk/credential-provider-node": "^3.972.10", "@aws-sdk/middleware-host-header": "^3.972.3", "@aws-sdk/middleware-logger": "^3.972.3", "@aws-sdk/middleware-recursion-detection": "^3.972.3", "@aws-sdk/middleware-user-agent": "^3.972.11", "@aws-sdk/region-config-resolver": "^3.972.3", "@aws-sdk/types": "^3.973.1", "@aws-sdk/util-endpoints": "3.993.0", "@aws-sdk/util-user-agent-browser": "^3.972.3", "@aws-sdk/util-user-agent-node": "^3.972.9", "@smithy/config-resolver": "^4.4.6", "@smithy/core": "^3.23.2", "@smithy/fetch-http-handler": "^5.3.9", "@smithy/hash-node": "^4.2.8", "@smithy/invalid-dependency": "^4.2.8", "@smithy/middleware-content-length": "^4.2.8", "@smithy/middleware-endpoint": "^4.4.16", "@smithy/middleware-retry": "^4.4.33", "@smithy/middleware-serde": "^4.2.9", "@smithy/middleware-stack": "^4.2.8", "@smithy/node-config-provider": "^4.3.8", "@smithy/node-http-handler": "^4.4.10", "@smithy/protocol-http": "^5.3.8", "@smithy/smithy-client": "^4.11.5", "@smithy/types": "^4.12.0", "@smithy/url-parser": "^4.2.8", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", "@smithy/util-defaults-mode-browser": "^4.3.32", "@smithy/util-defaults-mode-node": "^4.2.35", "@smithy/util-endpoints": "^3.2.8", "@smithy/util-middleware": "^4.2.8", "@smithy/util-retry": "^4.2.8", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-7Ne3Yk/bgQPVebAkv7W+RfhiwTRSbfER9BtbhOa2w/+dIr902LrJf6vrZlxiqaJbGj2ALx8M+ZK1YIHVxSwu9A=="],
|
||||
|
||||
"@aws-sdk/client-firehose": ["@aws-sdk/client-firehose@3.933.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.932.0", "@aws-sdk/credential-provider-node": "3.933.0", "@aws-sdk/middleware-host-header": "3.930.0", "@aws-sdk/middleware-logger": "3.930.0", "@aws-sdk/middleware-recursion-detection": "3.933.0", "@aws-sdk/middleware-user-agent": "3.932.0", "@aws-sdk/region-config-resolver": "3.930.0", "@aws-sdk/types": "3.930.0", "@aws-sdk/util-endpoints": "3.930.0", "@aws-sdk/util-user-agent-browser": "3.930.0", "@aws-sdk/util-user-agent-node": "3.932.0", "@smithy/config-resolver": "^4.4.3", "@smithy/core": "^3.18.2", "@smithy/fetch-http-handler": "^5.3.6", "@smithy/hash-node": "^4.2.5", "@smithy/invalid-dependency": "^4.2.5", "@smithy/middleware-content-length": "^4.2.5", "@smithy/middleware-endpoint": "^4.3.9", "@smithy/middleware-retry": "^4.4.9", "@smithy/middleware-serde": "^4.2.5", "@smithy/middleware-stack": "^4.2.5", "@smithy/node-config-provider": "^4.3.5", "@smithy/node-http-handler": "^4.4.5", "@smithy/protocol-http": "^5.3.5", "@smithy/smithy-client": "^4.9.5", "@smithy/types": "^4.9.0", "@smithy/url-parser": "^4.2.5", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", "@smithy/util-defaults-mode-browser": "^4.3.8", "@smithy/util-defaults-mode-node": "^4.2.11", "@smithy/util-endpoints": "^3.2.5", "@smithy/util-middleware": "^4.2.5", "@smithy/util-retry": "^4.2.5", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-tDrtgczN2lQsflLDPYu/wdOoyCZLVYtgzmWnYzSEOBWd/cp2AbuQ7D+FemSwUTzyoMTuhhIevyEJKzqsF+QYxA=="],
|
||||
|
||||
"@aws-sdk/client-lambda": ["@aws-sdk/client-lambda@3.1048.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.974.11", "@aws-sdk/credential-provider-node": "^3.972.42", "@aws-sdk/types": "^3.973.8", "@smithy/core": "^3.24.2", "@smithy/fetch-http-handler": "^5.4.2", "@smithy/node-http-handler": "^4.7.2", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-ryEYNVdilyWkKsOs/7Xy/l7+qjtSz4sll8NpcWD6AtONxjG/5OMaAhxxDkQb4iBoNMKnISxsARzQAp/Wa8pXIg=="],
|
||||
|
||||
"@aws-sdk/client-s3": ["@aws-sdk/client-s3@3.933.0", "", { "dependencies": { "@aws-crypto/sha1-browser": "5.2.0", "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.932.0", "@aws-sdk/credential-provider-node": "3.933.0", "@aws-sdk/middleware-bucket-endpoint": "3.930.0", "@aws-sdk/middleware-expect-continue": "3.930.0", "@aws-sdk/middleware-flexible-checksums": "3.932.0", "@aws-sdk/middleware-host-header": "3.930.0", "@aws-sdk/middleware-location-constraint": "3.930.0", "@aws-sdk/middleware-logger": "3.930.0", "@aws-sdk/middleware-recursion-detection": "3.933.0", "@aws-sdk/middleware-sdk-s3": "3.932.0", "@aws-sdk/middleware-ssec": "3.930.0", "@aws-sdk/middleware-user-agent": "3.932.0", "@aws-sdk/region-config-resolver": "3.930.0", "@aws-sdk/signature-v4-multi-region": "3.932.0", "@aws-sdk/types": "3.930.0", "@aws-sdk/util-endpoints": "3.930.0", "@aws-sdk/util-user-agent-browser": "3.930.0", "@aws-sdk/util-user-agent-node": "3.932.0", "@smithy/config-resolver": "^4.4.3", "@smithy/core": "^3.18.2", "@smithy/eventstream-serde-browser": "^4.2.5", "@smithy/eventstream-serde-config-resolver": "^4.3.5", "@smithy/eventstream-serde-node": "^4.2.5", "@smithy/fetch-http-handler": "^5.3.6", "@smithy/hash-blob-browser": "^4.2.6", "@smithy/hash-node": "^4.2.5", "@smithy/hash-stream-node": "^4.2.5", "@smithy/invalid-dependency": "^4.2.5", "@smithy/md5-js": "^4.2.5", "@smithy/middleware-content-length": "^4.2.5", "@smithy/middleware-endpoint": "^4.3.9", "@smithy/middleware-retry": "^4.4.9", "@smithy/middleware-serde": "^4.2.5", "@smithy/middleware-stack": "^4.2.5", "@smithy/node-config-provider": "^4.3.5", "@smithy/node-http-handler": "^4.4.5", "@smithy/protocol-http": "^5.3.5", "@smithy/smithy-client": "^4.9.5", "@smithy/types": "^4.9.0", "@smithy/url-parser": "^4.2.5", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", "@smithy/util-defaults-mode-browser": "^4.3.8", "@smithy/util-defaults-mode-node": "^4.2.11", "@smithy/util-endpoints": "^3.2.5", "@smithy/util-middleware": "^4.2.5", "@smithy/util-retry": "^4.2.5", "@smithy/util-stream": "^4.5.6", "@smithy/util-utf8": "^4.2.0", "@smithy/util-waiter": "^4.2.5", "tslib": "^2.6.2" } }, "sha512-KxwZvdxdCeWK6o8mpnb+kk7Kgb8V+8AjTwSXUWH1UAD85B0tjdo1cSfE5zoR5fWGol4Ml5RLez12a6LPhsoTqA=="],
|
||||
|
|
@ -1588,6 +1653,12 @@
|
|||
|
||||
"@opencode-ai/slack": ["@opencode-ai/slack@workspace:packages/slack"],
|
||||
|
||||
"@opencode-ai/stats-app": ["@opencode-ai/stats-app@workspace:packages/stats/app"],
|
||||
|
||||
"@opencode-ai/stats-core": ["@opencode-ai/stats-core@workspace:packages/stats/core"],
|
||||
|
||||
"@opencode-ai/stats-server": ["@opencode-ai/stats-server@workspace:packages/stats/server"],
|
||||
|
||||
"@opencode-ai/storybook": ["@opencode-ai/storybook@workspace:packages/storybook"],
|
||||
|
||||
"@opencode-ai/ui": ["@opencode-ai/ui@workspace:packages/ui"],
|
||||
|
|
@ -2326,6 +2397,10 @@
|
|||
|
||||
"@types/cross-spawn": ["@types/cross-spawn@6.0.6", "", { "dependencies": { "@types/node": "*" } }, "sha512-fXRhhUkG4H3TQk5dBhQ7m/JDdSNHKwR2BBia62lhwEIq9xGiQKLxd6LymNhn47SjXhsUEPmxi+PKw2OkW4LLjA=="],
|
||||
|
||||
"@types/d3-scale": ["@types/d3-scale@4.0.9", "", { "dependencies": { "@types/d3-time": "*" } }, "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw=="],
|
||||
|
||||
"@types/d3-time": ["@types/d3-time@3.0.4", "", {}, "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g=="],
|
||||
|
||||
"@types/debug": ["@types/debug@4.1.13", "", { "dependencies": { "@types/ms": "*" } }, "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw=="],
|
||||
|
||||
"@types/deep-eql": ["@types/deep-eql@4.0.2", "", {}, "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw=="],
|
||||
|
|
@ -2898,6 +2973,20 @@
|
|||
|
||||
"csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="],
|
||||
|
||||
"d3-array": ["d3-array@3.2.4", "", { "dependencies": { "internmap": "1 - 2" } }, "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg=="],
|
||||
|
||||
"d3-color": ["d3-color@3.1.0", "", {}, "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA=="],
|
||||
|
||||
"d3-format": ["d3-format@3.1.2", "", {}, "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg=="],
|
||||
|
||||
"d3-interpolate": ["d3-interpolate@3.0.1", "", { "dependencies": { "d3-color": "1 - 3" } }, "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g=="],
|
||||
|
||||
"d3-scale": ["d3-scale@4.0.2", "", { "dependencies": { "d3-array": "2.10.0 - 3", "d3-format": "1 - 3", "d3-interpolate": "1.2.0 - 3", "d3-time": "2.1.1 - 3", "d3-time-format": "2 - 4" } }, "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ=="],
|
||||
|
||||
"d3-time": ["d3-time@3.1.0", "", { "dependencies": { "d3-array": "2 - 3" } }, "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q=="],
|
||||
|
||||
"d3-time-format": ["d3-time-format@4.1.0", "", { "dependencies": { "d3-time": "1 - 3" } }, "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg=="],
|
||||
|
||||
"data-uri-to-buffer": ["data-uri-to-buffer@4.0.1", "", {}, "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A=="],
|
||||
|
||||
"data-view-buffer": ["data-view-buffer@1.0.2", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-data-view": "^1.0.2" } }, "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ=="],
|
||||
|
|
@ -3472,6 +3561,8 @@
|
|||
|
||||
"internal-slot": ["internal-slot@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "hasown": "^2.0.2", "side-channel": "^1.1.0" } }, "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw=="],
|
||||
|
||||
"internmap": ["internmap@2.0.3", "", {}, "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg=="],
|
||||
|
||||
"ioredis": ["ioredis@5.10.1", "", { "dependencies": { "@ioredis/commands": "1.5.1", "cluster-key-slot": "^1.1.0", "debug": "^4.3.4", "denque": "^2.1.0", "lodash.defaults": "^4.2.0", "lodash.isarguments": "^3.1.0", "redis-errors": "^1.2.0", "redis-parser": "^3.0.0", "standard-as-callback": "^2.1.0" } }, "sha512-HuEDBTI70aYdx1v6U97SbNx9F1+svQKBDo30o0b9fw055LMepzpOOd0Ccg9Q6tbqmBSJaMuY0fB7yw9/vjBYCA=="],
|
||||
|
||||
"ip-address": ["ip-address@10.1.0", "", {}, "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q=="],
|
||||
|
|
@ -5180,6 +5271,16 @@
|
|||
|
||||
"@aws-crypto/util/@smithy/util-utf8": ["@smithy/util-utf8@2.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A=="],
|
||||
|
||||
"@aws-sdk/client-athena/@smithy/core": ["@smithy/core@3.24.3", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-Ep/7tPamGY8mgESE3LyLKtxJyy6U52WWAqr/3wial47Sj4u3PiIF73AOGI27UyLy9duTkhZbgzodOfLV4TduZg=="],
|
||||
|
||||
"@aws-sdk/client-athena/@smithy/fetch-http-handler": ["@smithy/fetch-http-handler@5.4.3", "", { "dependencies": { "@smithy/core": "^3.24.3", "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-F+DRf8IJazRJgYog2A/yJK7eYVc0rqTlRzO+5ZxjJd4WkZoKz0IJRncf7G6t1pdVT3kryJcwuTFhN1c5m6N47A=="],
|
||||
|
||||
"@aws-sdk/client-athena/@smithy/node-http-handler": ["@smithy/node-http-handler@4.7.3", "", { "dependencies": { "@smithy/core": "^3.24.3", "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-/jPhevcTFPMVl6KNjbaI47iOg1zxC7IsnX4PQDGVZKMFceOXtB8IEYaB7a9VvkP/3oC60WzTeKocvSI7vLT0vA=="],
|
||||
|
||||
"@aws-sdk/client-athena/@smithy/types": ["@smithy/types@4.14.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-P+otAxbV4CqBybp7EkcJCrig63yE2E7PuNVOmilVMRcx/O+QDzGULTrKsq4DV13gSfak9ObPrWaHl/9bL5YcWw=="],
|
||||
|
||||
"@aws-sdk/client-athena/@smithy/util-utf8": ["@smithy/util-utf8@4.2.2", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw=="],
|
||||
|
||||
"@aws-sdk/client-cognito-identity/@aws-sdk/core": ["@aws-sdk/core@3.973.27", "", { "dependencies": { "@aws-sdk/types": "^3.973.7", "@aws-sdk/xml-builder": "^3.972.17", "@smithy/core": "^3.23.14", "@smithy/node-config-provider": "^4.3.13", "@smithy/property-provider": "^4.2.13", "@smithy/protocol-http": "^5.3.13", "@smithy/signature-v4": "^5.3.13", "@smithy/smithy-client": "^4.12.9", "@smithy/types": "^4.14.0", "@smithy/util-base64": "^4.3.2", "@smithy/util-middleware": "^4.2.13", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-CUZ5m8hwMCH6OYI4Li/WgMfIEx10Q2PLI9Y3XOUTPGZJ53aZ0007jCv+X/ywsaERyKPdw5MRZWk877roQksQ4A=="],
|
||||
|
||||
"@aws-sdk/client-cognito-identity/@aws-sdk/credential-provider-node": ["@aws-sdk/credential-provider-node@3.972.30", "", { "dependencies": { "@aws-sdk/credential-provider-env": "^3.972.25", "@aws-sdk/credential-provider-http": "^3.972.27", "@aws-sdk/credential-provider-ini": "^3.972.29", "@aws-sdk/credential-provider-process": "^3.972.25", "@aws-sdk/credential-provider-sso": "^3.972.29", "@aws-sdk/credential-provider-web-identity": "^3.972.29", "@aws-sdk/types": "^3.973.7", "@smithy/credential-provider-imds": "^4.2.13", "@smithy/property-provider": "^4.2.13", "@smithy/shared-ini-file-loader": "^4.4.8", "@smithy/types": "^4.14.0", "tslib": "^2.6.2" } }, "sha512-FMnAnWxc8PG+ZrZ2OBKzY4luCUJhe9CG0B9YwYr4pzrYGLXBS2rl+UoUvjGbAwiptxRL6hyA3lFn03Bv1TLqTw=="],
|
||||
|
|
@ -5204,6 +5305,16 @@
|
|||
|
||||
"@aws-sdk/client-cognito-identity/@smithy/util-utf8": ["@smithy/util-utf8@4.2.2", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw=="],
|
||||
|
||||
"@aws-sdk/client-firehose/@smithy/core": ["@smithy/core@3.24.3", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-Ep/7tPamGY8mgESE3LyLKtxJyy6U52WWAqr/3wial47Sj4u3PiIF73AOGI27UyLy9duTkhZbgzodOfLV4TduZg=="],
|
||||
|
||||
"@aws-sdk/client-firehose/@smithy/fetch-http-handler": ["@smithy/fetch-http-handler@5.4.3", "", { "dependencies": { "@smithy/core": "^3.24.3", "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-F+DRf8IJazRJgYog2A/yJK7eYVc0rqTlRzO+5ZxjJd4WkZoKz0IJRncf7G6t1pdVT3kryJcwuTFhN1c5m6N47A=="],
|
||||
|
||||
"@aws-sdk/client-firehose/@smithy/node-http-handler": ["@smithy/node-http-handler@4.7.3", "", { "dependencies": { "@smithy/core": "^3.24.3", "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-/jPhevcTFPMVl6KNjbaI47iOg1zxC7IsnX4PQDGVZKMFceOXtB8IEYaB7a9VvkP/3oC60WzTeKocvSI7vLT0vA=="],
|
||||
|
||||
"@aws-sdk/client-firehose/@smithy/types": ["@smithy/types@4.14.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-P+otAxbV4CqBybp7EkcJCrig63yE2E7PuNVOmilVMRcx/O+QDzGULTrKsq4DV13gSfak9ObPrWaHl/9bL5YcWw=="],
|
||||
|
||||
"@aws-sdk/client-firehose/@smithy/util-utf8": ["@smithy/util-utf8@4.2.2", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw=="],
|
||||
|
||||
"@aws-sdk/client-lambda/@aws-sdk/core": ["@aws-sdk/core@3.974.11", "", { "dependencies": { "@aws-sdk/types": "^3.973.8", "@aws-sdk/xml-builder": "^3.972.24", "@aws/lambda-invoke-store": "^0.2.2", "@smithy/core": "^3.24.2", "@smithy/signature-v4": "^5.4.2", "@smithy/types": "^4.14.1", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-QpnINq5FZH6EOaDEkmHdT7eUunbvD27pDNQypaWjFyYz7Zl1q3UCMQErBZxpmfGfI7MvI2TlK8KTkgNpv8b1ug=="],
|
||||
|
||||
"@aws-sdk/client-lambda/@aws-sdk/credential-provider-node": ["@aws-sdk/credential-provider-node@3.972.42", "", { "dependencies": { "@aws-sdk/credential-provider-env": "^3.972.37", "@aws-sdk/credential-provider-http": "^3.972.39", "@aws-sdk/credential-provider-ini": "^3.972.41", "@aws-sdk/credential-provider-process": "^3.972.37", "@aws-sdk/credential-provider-sso": "^3.972.41", "@aws-sdk/credential-provider-web-identity": "^3.972.41", "@aws-sdk/types": "^3.973.8", "@smithy/core": "^3.24.2", "@smithy/credential-provider-imds": "^4.3.2", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-D4oon2zbqqsWOJUM99Gm3/ZyJ0IJvTXVN3PyloGb3kQEyI36fjCZheZj422lAgTWWd6TSHgiImLt3RIaLdv3dQ=="],
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ export const api = new sst.cloudflare.Worker("Api", {
|
|||
transform: {
|
||||
worker: (args) => {
|
||||
args.logpush = true
|
||||
if ($app.stage === "vimtor") return
|
||||
if ($app.stage === "vimtor" || $app.stage === "adam") return
|
||||
args.bindings = $resolve(args.bindings).apply((bindings) => [
|
||||
...bindings,
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
import { domain } from "./stage"
|
||||
import { deployAws, domain } from "./stage"
|
||||
import { EMAILOCTOPUS_API_KEY } from "./app"
|
||||
import { SECRET } from "./secret"
|
||||
|
||||
const lake = deployAws ? await import("./lake") : undefined
|
||||
|
||||
////////////////
|
||||
// DATABASE
|
||||
////////////////
|
||||
|
|
@ -240,7 +242,7 @@ const SALESFORCE_INSTANCE_URL = new sst.Secret("SALESFORCE_INSTANCE_URL")
|
|||
|
||||
const logProcessor = new sst.cloudflare.Worker("LogProcessor", {
|
||||
handler: "packages/console/function/src/log-processor.ts",
|
||||
link: [new sst.Secret("HONEYCOMB_API_KEY")],
|
||||
link: [SECRET.HoneycombApiKey, ...(lake?.lakeIngest ? [lake.lakeIngest] : [])],
|
||||
})
|
||||
|
||||
new sst.cloudflare.x.SolidStart("Console", {
|
||||
|
|
|
|||
340
infra/lake.ts
Normal file
340
infra/lake.ts
Normal file
|
|
@ -0,0 +1,340 @@
|
|||
import { domain } from "./stage"
|
||||
|
||||
const current = aws.getCallerIdentityOutput({})
|
||||
const partition = aws.getPartitionOutput({})
|
||||
const region = aws.getRegionOutput({})
|
||||
|
||||
const tableBucketName = `opencode-${$app.stage}-lake`
|
||||
const glueCatalogName = "s3tablescatalog"
|
||||
const glueCatalogArn = $interpolate`arn:${partition.partition}:glue:${region.region}:${current.accountId}:catalog`
|
||||
const glueS3TablesCatalogArn = $interpolate`${glueCatalogArn}/${glueCatalogName}`
|
||||
const glueS3TablesChildCatalogArn = $interpolate`${glueS3TablesCatalogArn}/${tableBucketName}`
|
||||
const glueS3TablesDatabaseWildcardArn = $interpolate`arn:${partition.partition}:glue:${region.region}:${current.accountId}:database/${glueCatalogName}/${tableBucketName}/*`
|
||||
const glueS3TablesTableWildcardArn = $interpolate`arn:${partition.partition}:glue:${region.region}:${current.accountId}:table/${glueCatalogName}/${tableBucketName}/*/*`
|
||||
const s3TablesBucketWildcardArn = $interpolate`arn:${partition.partition}:s3tables:${region.region}:${current.accountId}:bucket/*`
|
||||
|
||||
export const tableBucket = new aws.s3tables.TableBucket(
|
||||
"LakeTableBucket",
|
||||
{
|
||||
name: tableBucketName,
|
||||
forceDestroy: $app.stage !== "production",
|
||||
},
|
||||
)
|
||||
|
||||
const s3TablesCatalog = new aws.cloudcontrol.Resource(
|
||||
"LakeS3TablesCatalog",
|
||||
{
|
||||
typeName: "AWS::Glue::Catalog",
|
||||
desiredState: $jsonStringify({
|
||||
Name: glueCatalogName,
|
||||
Description: "Federated catalog for S3 Tables",
|
||||
FederatedCatalog: {
|
||||
Identifier: s3TablesBucketWildcardArn,
|
||||
ConnectionName: "aws:s3tables",
|
||||
},
|
||||
CreateDatabaseDefaultPermissions: [
|
||||
{
|
||||
Principal: {
|
||||
DataLakePrincipalIdentifier: "IAM_ALLOWED_PRINCIPALS",
|
||||
},
|
||||
Permissions: ["ALL"],
|
||||
},
|
||||
],
|
||||
CreateTableDefaultPermissions: [
|
||||
{
|
||||
Principal: {
|
||||
DataLakePrincipalIdentifier: "IAM_ALLOWED_PRINCIPALS",
|
||||
},
|
||||
Permissions: ["ALL"],
|
||||
},
|
||||
],
|
||||
AllowFullTableExternalDataAccess: "True",
|
||||
}),
|
||||
},
|
||||
{ dependsOn: [tableBucket] },
|
||||
)
|
||||
|
||||
const athenaResultsBucket = new aws.s3.Bucket(
|
||||
"LakeAthenaResults",
|
||||
{
|
||||
bucket: `opencode-${$app.stage}-lake-athena-results`,
|
||||
forceDestroy: $app.stage !== "production",
|
||||
},
|
||||
)
|
||||
|
||||
const firehoseErrorBucket = new aws.s3.Bucket(
|
||||
"LakeFirehoseErrors",
|
||||
{
|
||||
bucket: `opencode-${$app.stage}-lake-firehose-errors`,
|
||||
forceDestroy: $app.stage !== "production",
|
||||
},
|
||||
)
|
||||
|
||||
const athenaWorkgroup = new aws.athena.Workgroup(
|
||||
"LakeAthenaWorkgroup",
|
||||
{
|
||||
name: `opencode-${$app.stage}-lake-workgroup`,
|
||||
forceDestroy: $app.stage !== "production",
|
||||
configuration: {
|
||||
enforceWorkgroupConfiguration: true,
|
||||
publishCloudwatchMetricsEnabled: true,
|
||||
resultConfiguration: {
|
||||
outputLocation: $interpolate`s3://${athenaResultsBucket.bucket}/`,
|
||||
},
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
const firehoseRole = new aws.iam.Role(
|
||||
"LakeFirehoseRole",
|
||||
{
|
||||
assumeRolePolicy: aws.iam.getPolicyDocumentOutput({
|
||||
statements: [
|
||||
{
|
||||
effect: "Allow",
|
||||
actions: ["sts:AssumeRole"],
|
||||
principals: [
|
||||
{
|
||||
type: "Service",
|
||||
identifiers: ["firehose.amazonaws.com"],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}).json,
|
||||
},
|
||||
)
|
||||
|
||||
const firehosePolicy = new aws.iam.RolePolicy(
|
||||
"LakeFirehosePolicy",
|
||||
{
|
||||
role: firehoseRole.id,
|
||||
policy: aws.iam.getPolicyDocumentOutput({
|
||||
statements: [
|
||||
{
|
||||
effect: "Allow",
|
||||
actions: [
|
||||
"s3tables:ListTableBuckets",
|
||||
"s3tables:GetTableBucket",
|
||||
"s3tables:GetNamespace",
|
||||
"s3tables:GetTable",
|
||||
"s3tables:GetTableData",
|
||||
"s3tables:GetTableMetadataLocation",
|
||||
"s3tables:ListNamespaces",
|
||||
"s3tables:ListTables",
|
||||
"s3tables:PutTableData",
|
||||
"s3tables:UpdateTableMetadataLocation",
|
||||
],
|
||||
resources: ["*"],
|
||||
},
|
||||
{
|
||||
effect: "Allow",
|
||||
actions: [
|
||||
"glue:GetCatalog",
|
||||
"glue:GetCatalogs",
|
||||
"glue:GetDatabase",
|
||||
"glue:GetDatabases",
|
||||
"glue:GetTable",
|
||||
"glue:GetTables",
|
||||
"glue:UpdateTable",
|
||||
],
|
||||
resources: [
|
||||
glueCatalogArn,
|
||||
glueS3TablesCatalogArn,
|
||||
$interpolate`${glueS3TablesCatalogArn}/*`,
|
||||
glueS3TablesDatabaseWildcardArn,
|
||||
glueS3TablesTableWildcardArn,
|
||||
$interpolate`arn:${partition.partition}:glue:${region.region}:${current.accountId}:database/*`,
|
||||
$interpolate`arn:${partition.partition}:glue:${region.region}:${current.accountId}:table/*/*`,
|
||||
$interpolate`arn:${partition.partition}:glue:${region.region}:${current.accountId}:table/${glueCatalogName}/*`,
|
||||
],
|
||||
},
|
||||
{
|
||||
effect: "Allow",
|
||||
actions: [
|
||||
"s3:AbortMultipartUpload",
|
||||
"s3:GetBucketLocation",
|
||||
"s3:GetObject",
|
||||
"s3:ListBucket",
|
||||
"s3:ListBucketMultipartUploads",
|
||||
"s3:PutObject",
|
||||
],
|
||||
resources: [firehoseErrorBucket.arn, $interpolate`${firehoseErrorBucket.arn}/*`],
|
||||
},
|
||||
{
|
||||
effect: "Allow",
|
||||
actions: ["lakeformation:GetDataAccess"],
|
||||
resources: ["*"],
|
||||
},
|
||||
],
|
||||
}).json,
|
||||
},
|
||||
)
|
||||
|
||||
const firehose = new aws.kinesis.FirehoseDeliveryStream(
|
||||
"LakeFirehose",
|
||||
{
|
||||
name: `opencode-${$app.stage}-lake-ingest`,
|
||||
destination: "iceberg",
|
||||
icebergConfiguration: {
|
||||
appendOnly: true,
|
||||
bufferingInterval: 60,
|
||||
bufferingSize: 1,
|
||||
catalogArn: glueS3TablesChildCatalogArn,
|
||||
processingConfiguration: {
|
||||
enabled: true,
|
||||
processors: [
|
||||
{
|
||||
type: "MetadataExtraction",
|
||||
parameters: [
|
||||
{ parameterName: "JsonParsingEngine", parameterValue: "JQ-1.6" },
|
||||
{
|
||||
parameterName: "MetadataExtractionQuery",
|
||||
parameterValue:
|
||||
'{destinationDatabaseName:._lake_database,destinationTableName:._lake_table,operation:(._lake_operation // "insert")}',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
roleArn: firehoseRole.arn,
|
||||
s3BackupMode: "FailedDataOnly",
|
||||
s3Configuration: {
|
||||
roleArn: firehoseRole.arn,
|
||||
bucketArn: firehoseErrorBucket.arn,
|
||||
errorOutputPrefix: "errors/!{firehose:error-output-type}/",
|
||||
},
|
||||
},
|
||||
},
|
||||
{ dependsOn: [s3TablesCatalog, firehosePolicy] },
|
||||
)
|
||||
|
||||
export const lakeVpc = new sst.aws.Vpc("LakeVpc")
|
||||
export const lakeCluster = new sst.aws.Cluster("LakeCluster", { vpc: lakeVpc })
|
||||
export const lakeRegion = region.region
|
||||
export const lakeCatalog = $interpolate`${glueCatalogName}/${tableBucket.name}`
|
||||
export const lakeAthenaWorkgroup = athenaWorkgroup
|
||||
|
||||
const ingestSecret = new random.RandomPassword("LakeIngestSecret", { length: 32 })
|
||||
|
||||
const ingestConfig = new sst.Linkable("LakeIngestConfig", {
|
||||
properties: {
|
||||
streamName: firehose.name,
|
||||
secret: ingestSecret.result,
|
||||
},
|
||||
})
|
||||
|
||||
const ingestService = new sst.aws.Service("LakeIngestService", {
|
||||
cluster: lakeCluster,
|
||||
architecture: "arm64",
|
||||
cpu: "0.5 vCPU",
|
||||
memory: "1 GB",
|
||||
image: {
|
||||
context: ".",
|
||||
dockerfile: "packages/stats/server/Dockerfile",
|
||||
},
|
||||
link: [ingestConfig],
|
||||
permissions: [
|
||||
{
|
||||
actions: ["firehose:PutRecord", "firehose:PutRecordBatch"],
|
||||
resources: [firehose.arn],
|
||||
},
|
||||
],
|
||||
scaling: {
|
||||
min: $app.stage === "production" ? 2 : 1,
|
||||
max: $app.stage === "production" ? 32 : 4,
|
||||
cpuUtilization: 60,
|
||||
memoryUtilization: 70,
|
||||
},
|
||||
loadBalancer: {
|
||||
domain: {
|
||||
name: `lake.${domain}`,
|
||||
dns: sst.cloudflare.dns(),
|
||||
},
|
||||
rules: [
|
||||
{ listen: "80/http", redirect: "443/https" },
|
||||
{ listen: "443/https", forward: "3000/http" },
|
||||
],
|
||||
health: {
|
||||
"3000/http": {
|
||||
path: "/ready",
|
||||
successCodes: "200-299",
|
||||
},
|
||||
},
|
||||
},
|
||||
health: {
|
||||
command: [
|
||||
"CMD-SHELL",
|
||||
"bun --eval \"fetch('http://localhost:3000/health').then((r) => process.exit(r.ok ? 0 : 1)).catch(() => process.exit(1))\"",
|
||||
],
|
||||
interval: "30 seconds",
|
||||
retries: 3,
|
||||
startPeriod: "30 seconds",
|
||||
timeout: "5 seconds",
|
||||
},
|
||||
dev: {
|
||||
command: "bun run start",
|
||||
directory: "packages/stats/server",
|
||||
url: "http://localhost:3000",
|
||||
},
|
||||
wait: $app.stage === "production",
|
||||
})
|
||||
|
||||
export const lakeIngest = new sst.Linkable("LakeIngest", {
|
||||
properties: {
|
||||
url: ingestService.url,
|
||||
secret: ingestSecret.result,
|
||||
},
|
||||
})
|
||||
|
||||
export const lakeQueryPermissions = [
|
||||
{
|
||||
actions: ["athena:StartQueryExecution", "athena:GetQueryExecution", "athena:GetQueryResults"],
|
||||
resources: [athenaWorkgroup.arn],
|
||||
},
|
||||
{
|
||||
actions: [
|
||||
"glue:GetCatalog",
|
||||
"glue:GetCatalogs",
|
||||
"glue:GetDatabase",
|
||||
"glue:GetDatabases",
|
||||
"glue:GetTable",
|
||||
"glue:GetTables",
|
||||
"glue:GetPartitions",
|
||||
],
|
||||
resources: [
|
||||
glueCatalogArn,
|
||||
glueS3TablesCatalogArn,
|
||||
$interpolate`${glueS3TablesCatalogArn}/*`,
|
||||
glueS3TablesDatabaseWildcardArn,
|
||||
glueS3TablesTableWildcardArn,
|
||||
$interpolate`arn:${partition.partition}:glue:${region.region}:${current.accountId}:database/*`,
|
||||
$interpolate`arn:${partition.partition}:glue:${region.region}:${current.accountId}:table/*/*`,
|
||||
$interpolate`arn:${partition.partition}:glue:${region.region}:${current.accountId}:table/${glueCatalogName}/*`,
|
||||
],
|
||||
},
|
||||
{
|
||||
actions: ["s3:GetBucketLocation", "s3:ListBucket"],
|
||||
resources: [athenaResultsBucket.arn],
|
||||
},
|
||||
{
|
||||
actions: ["s3:GetObject", "s3:PutObject", "s3:AbortMultipartUpload", "s3:ListBucketMultipartUploads"],
|
||||
resources: [$interpolate`${athenaResultsBucket.arn}/*`],
|
||||
},
|
||||
{
|
||||
actions: [
|
||||
"s3tables:GetTableBucket",
|
||||
"s3tables:GetNamespace",
|
||||
"s3tables:GetTable",
|
||||
"s3tables:GetTableData",
|
||||
"s3tables:GetTableMetadataLocation",
|
||||
"s3tables:ListNamespaces",
|
||||
"s3tables:ListTables",
|
||||
],
|
||||
resources: ["*"],
|
||||
},
|
||||
{
|
||||
actions: ["lakeformation:GetDataAccess"],
|
||||
resources: ["*"],
|
||||
},
|
||||
]
|
||||
|
|
@ -7,6 +7,7 @@ sst.Linkable.wrap(random.RandomPassword, (resource) => ({
|
|||
export const SECRET = {
|
||||
R2AccessKey: new sst.Secret("R2AccessKey", "unknown"),
|
||||
R2SecretKey: new sst.Secret("R2SecretKey", "unknown"),
|
||||
HoneycombApiKey: new sst.Secret("HONEYCOMB_API_KEY"),
|
||||
HoneycombWebhookSecret: new random.RandomPassword("HoneycombWebhookSecret", { length: 24 }),
|
||||
UpstashRedisRestUrl: new sst.Secret("UpstashRedisRestUrl"),
|
||||
UpstashRedisRestToken: new sst.Secret("UpstashRedisRestToken"),
|
||||
|
|
|
|||
|
|
@ -5,6 +5,50 @@ export const domain = (() => {
|
|||
})()
|
||||
|
||||
export const zoneID = "430ba34c138cfb5360826c4909f99be8"
|
||||
export const deployAws = $app.stage === "production" || $app.stage === "dev" || $app.stage === "adam"
|
||||
|
||||
const githubActionsDeployRole = (() => {
|
||||
if ($app.stage !== "dev" && $app.stage !== "production") return
|
||||
|
||||
const provider = new aws.iam.OpenIdConnectProvider("GithubActionsOidcProvider", {
|
||||
url: "https://token.actions.githubusercontent.com",
|
||||
clientIdLists: ["sts.amazonaws.com"],
|
||||
})
|
||||
const role = new aws.iam.Role("GithubActionsDeployRole", {
|
||||
name: `opencode-${$app.stage}-github-actions-deploy`,
|
||||
maxSessionDuration: 3600,
|
||||
assumeRolePolicy: aws.iam.getPolicyDocumentOutput({
|
||||
statements: [
|
||||
{
|
||||
effect: "Allow",
|
||||
actions: ["sts:AssumeRoleWithWebIdentity"],
|
||||
principals: [{ type: "Federated", identifiers: [provider.arn] }],
|
||||
conditions: [
|
||||
{
|
||||
test: "StringEquals",
|
||||
variable: "token.actions.githubusercontent.com:aud",
|
||||
values: ["sts.amazonaws.com"],
|
||||
},
|
||||
{
|
||||
test: "StringEquals",
|
||||
variable: "token.actions.githubusercontent.com:sub",
|
||||
values: [`repo:anomalyco/opencode:environment:${$app.stage}`],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}).json,
|
||||
})
|
||||
|
||||
new aws.iam.RolePolicyAttachment("GithubActionsDeployRoleAdmin", {
|
||||
role: role.name,
|
||||
policyArn: "arn:aws:iam::aws:policy/AdministratorAccess",
|
||||
})
|
||||
|
||||
return role
|
||||
})()
|
||||
|
||||
export const githubActionsDeployRoleArn = githubActionsDeployRole?.arn
|
||||
|
||||
new cloudflare.RegionalHostname("RegionalHostname", {
|
||||
hostname: domain,
|
||||
|
|
|
|||
207
infra/stats.ts
Normal file
207
infra/stats.ts
Normal file
|
|
@ -0,0 +1,207 @@
|
|||
import { lakeAthenaWorkgroup, lakeCatalog, lakeCluster, lakeQueryPermissions, lakeRegion, tableBucket } from "./lake"
|
||||
|
||||
const domain = (() => {
|
||||
if ($app.stage === "production") return "stats.opencode.ai"
|
||||
if ($app.stage === "dev") return "stats.dev.opencode.ai"
|
||||
return `stats.${$app.stage}.dev.opencode.ai`
|
||||
})()
|
||||
|
||||
////////////////
|
||||
// LAKE
|
||||
////////////////
|
||||
|
||||
const inferenceNamespace = new aws.s3tables.Namespace("LakeInferenceNamespace", {
|
||||
namespace: "inference",
|
||||
tableBucketArn: tableBucket.arn,
|
||||
})
|
||||
|
||||
const inferenceEventTable = new aws.s3tables.Table(
|
||||
"LakeInferenceEventTable",
|
||||
{
|
||||
name: "event",
|
||||
namespace: inferenceNamespace.namespace,
|
||||
tableBucketArn: inferenceNamespace.tableBucketArn,
|
||||
format: "ICEBERG",
|
||||
metadata: {
|
||||
iceberg: {
|
||||
schema: {
|
||||
fields: [
|
||||
{ name: "event_timestamp", type: "string", required: false },
|
||||
{ name: "event_date", type: "string", required: false },
|
||||
{ name: "event_type", type: "string", required: false },
|
||||
{ name: "dataset", type: "string", required: false },
|
||||
{ name: "cf_continent", type: "string", required: false },
|
||||
{ name: "cf_country", type: "string", required: false },
|
||||
{ name: "cf_city", type: "string", required: false },
|
||||
{ name: "cf_region", type: "string", required: false },
|
||||
{ name: "cf_latitude", type: "double", required: false },
|
||||
{ name: "cf_longitude", type: "double", required: false },
|
||||
{ name: "cf_timezone", type: "string", required: false },
|
||||
{ name: "duration", type: "double", required: false },
|
||||
{ name: "request_length", type: "long", required: false },
|
||||
{ name: "status", type: "int", required: false },
|
||||
{ name: "ip", type: "string", required: false },
|
||||
{ name: "is_stream", type: "boolean", required: false },
|
||||
{ name: "session", type: "string", required: false },
|
||||
{ name: "request", type: "string", required: false },
|
||||
{ name: "client", type: "string", required: false },
|
||||
{ name: "user_agent", type: "string", required: false },
|
||||
{ name: "model_variant", type: "string", required: false },
|
||||
{ name: "source", type: "string", required: false },
|
||||
{ name: "provider", type: "string", required: false },
|
||||
{ name: "provider_model", type: "string", required: false },
|
||||
{ name: "model", type: "string", required: false },
|
||||
{ name: "llm_error_code", type: "int", required: false },
|
||||
{ name: "llm_error_message", type: "string", required: false },
|
||||
{ name: "error_response", type: "string", required: false },
|
||||
{ name: "error_type", type: "string", required: false },
|
||||
{ name: "error_message", type: "string", required: false },
|
||||
{ name: "error_cause", type: "string", required: false },
|
||||
{ name: "error_cause2", type: "string", required: false },
|
||||
{ name: "api_key", type: "string", required: false },
|
||||
{ name: "workspace", type: "string", required: false },
|
||||
{ name: "is_subscription", type: "boolean", required: false },
|
||||
{ name: "subscription", type: "string", required: false },
|
||||
{ name: "response_length", type: "long", required: false },
|
||||
{ name: "time_to_first_byte", type: "long", required: false },
|
||||
{ name: "timestamp_first_byte", type: "long", required: false },
|
||||
{ name: "timestamp_last_byte", type: "long", required: false },
|
||||
{ name: "tokens_input", type: "long", required: false },
|
||||
{ name: "tokens_output", type: "long", required: false },
|
||||
{ name: "tokens_reasoning", type: "long", required: false },
|
||||
{ name: "tokens_cache_read", type: "long", required: false },
|
||||
{ name: "tokens_cache_write_5m", type: "long", required: false },
|
||||
{ name: "tokens_cache_write_1h", type: "long", required: false },
|
||||
{ name: "cost_input_microcents", type: "long", required: false },
|
||||
{ name: "cost_output_microcents", type: "long", required: false },
|
||||
{ name: "cost_cache_read_microcents", type: "long", required: false },
|
||||
{ name: "cost_cache_write_microcents", type: "long", required: false },
|
||||
{ name: "cost_total_microcents", type: "long", required: false },
|
||||
{ name: "cost_input", type: "long", required: false },
|
||||
{ name: "cost_output", type: "long", required: false },
|
||||
{ name: "cost_cache_read", type: "long", required: false },
|
||||
{ name: "cost_cache_write_5m", type: "long", required: false },
|
||||
{ name: "cost_cache_write_1h", type: "long", required: false },
|
||||
{ name: "cost_total", type: "long", required: false },
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{ deleteBeforeReplace: $app.stage !== "production" },
|
||||
)
|
||||
|
||||
export const inferenceEvent = new sst.Linkable("InferenceEvent", {
|
||||
properties: {
|
||||
region: lakeRegion,
|
||||
catalog: lakeCatalog,
|
||||
database: inferenceNamespace.namespace,
|
||||
table: inferenceEventTable.name,
|
||||
tableBucket: tableBucket.name,
|
||||
workgroup: lakeAthenaWorkgroup.name,
|
||||
},
|
||||
})
|
||||
|
||||
////////////////
|
||||
// DATABASE
|
||||
////////////////
|
||||
|
||||
const cluster = planetscale.getDatabaseOutput({
|
||||
name: "opencode-stats",
|
||||
organization: "anomalyco",
|
||||
})
|
||||
|
||||
const branch =
|
||||
$app.stage === "production"
|
||||
? planetscale.getBranchOutput({
|
||||
name: "production",
|
||||
organization: cluster.organization,
|
||||
database: cluster.name,
|
||||
})
|
||||
: new planetscale.Branch("StatsDatabaseBranch", {
|
||||
database: cluster.name,
|
||||
organization: cluster.organization,
|
||||
name: $app.stage,
|
||||
parentBranch: "production",
|
||||
})
|
||||
|
||||
const password = new planetscale.Password("StatsDatabasePassword", {
|
||||
name: $app.stage,
|
||||
database: cluster.name,
|
||||
organization: cluster.organization,
|
||||
branch: branch.name,
|
||||
})
|
||||
|
||||
const databaseUrl = $interpolate`mysql://${password.username.apply(encodeURIComponent)}:${password.plaintext.apply(
|
||||
encodeURIComponent,
|
||||
)}@${password.accessHostUrl}/${cluster.name}`
|
||||
|
||||
export const database = new sst.Linkable("StatsDatabase", {
|
||||
properties: {
|
||||
host: password.accessHostUrl,
|
||||
database: cluster.name,
|
||||
username: password.username,
|
||||
password: password.plaintext,
|
||||
port: 3306,
|
||||
url: databaseUrl,
|
||||
},
|
||||
})
|
||||
|
||||
new sst.x.DevCommand("StatsStudio", {
|
||||
link: [database],
|
||||
environment: {
|
||||
DATABASE_URL: databaseUrl,
|
||||
},
|
||||
dev: {
|
||||
command: "bun db:studio",
|
||||
directory: "packages/stats/core",
|
||||
autostart: false,
|
||||
},
|
||||
})
|
||||
|
||||
////////////////
|
||||
// APP
|
||||
////////////////
|
||||
|
||||
// export const app = new sst.cloudflare.x.SolidStart("Stats", {
|
||||
// path: "packages/stats/app",
|
||||
// buildCommand: "bun run build",
|
||||
// domain,
|
||||
// link: [database],
|
||||
// environment: {
|
||||
// PUBLIC_URL: `https://${domain}`,
|
||||
// },
|
||||
// })
|
||||
|
||||
////////////////
|
||||
// SERVICES
|
||||
////////////////
|
||||
|
||||
const statsSyncConfig = new sst.Linkable("StatsSyncConfig", {
|
||||
properties: {
|
||||
dataset: "zen",
|
||||
},
|
||||
})
|
||||
|
||||
export const statSync = new sst.aws.Service("StatsSyncService", {
|
||||
cluster: lakeCluster,
|
||||
architecture: "arm64",
|
||||
cpu: "0.25 vCPU",
|
||||
memory: "0.5 GB",
|
||||
image: {
|
||||
context: ".",
|
||||
dockerfile: "packages/stats/server/Dockerfile",
|
||||
},
|
||||
command: ["bun", "src/stat-sync.ts"],
|
||||
link: [database, inferenceEvent, statsSyncConfig],
|
||||
permissions: lakeQueryPermissions,
|
||||
scaling: {
|
||||
min: 1,
|
||||
max: 1,
|
||||
},
|
||||
dev: {
|
||||
command: "bun src/stat-sync.ts",
|
||||
directory: "packages/stats/server",
|
||||
autostart: false,
|
||||
},
|
||||
})
|
||||
|
|
@ -10,6 +10,7 @@
|
|||
"dev:desktop": "bun --cwd packages/desktop dev",
|
||||
"dev:web": "bun --cwd packages/app dev",
|
||||
"dev:console": "ulimit -n 10240 2>/dev/null; bun run --cwd packages/console/app dev",
|
||||
"dev:stats": "bun run --cwd packages/stats/app dev",
|
||||
"dev:storybook": "bun --cwd packages/storybook storybook",
|
||||
"lint": "oxlint",
|
||||
"typecheck": "bun turbo typecheck",
|
||||
|
|
@ -17,13 +18,14 @@
|
|||
"postinstall": "bun run --cwd packages/opencode fix-node-pty",
|
||||
"prepare": "husky",
|
||||
"random": "echo 'Random script'",
|
||||
"hello": "echo 'Hello World!'",
|
||||
"sso": "aws sso login --sso-session=opencode --no-browser",
|
||||
"test": "echo 'do not run tests from root' && exit 1"
|
||||
},
|
||||
"workspaces": {
|
||||
"packages": [
|
||||
"packages/*",
|
||||
"packages/console/*",
|
||||
"packages/stats/*",
|
||||
"packages/sdk/js",
|
||||
"packages/slack"
|
||||
],
|
||||
|
|
@ -72,6 +74,7 @@
|
|||
"@typescript/native-preview": "7.0.0-dev.20251207.1",
|
||||
"zod": "4.1.8",
|
||||
"remeda": "2.26.0",
|
||||
"sst": "4.13.1",
|
||||
"shiki": "3.20.0",
|
||||
"solid-list": "0.3.0",
|
||||
"tailwindcss": "4.1.11",
|
||||
|
|
@ -98,7 +101,7 @@
|
|||
"oxlint-tsgolint": "0.21.0",
|
||||
"prettier": "3.6.2",
|
||||
"semver": "^7.6.0",
|
||||
"sst": "4.13.1",
|
||||
"sst": "catalog:",
|
||||
"turbo": "2.8.13"
|
||||
},
|
||||
"dependencies": {
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ export const logger = {
|
|||
},
|
||||
log: console.log,
|
||||
debug: (message: string) => {
|
||||
if (Resource.App.stage === "production") return
|
||||
if (Resource.App.stage === "production" || Resource.App.stage === "adam") return
|
||||
console.debug(message)
|
||||
},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ export default defineConfig({
|
|||
}) as PluginOption,
|
||||
nitro({
|
||||
compatibilityDate: "2024-09-19",
|
||||
preset: "cloudflare_module",
|
||||
preset: "cloudflare-module",
|
||||
cloudflare: {
|
||||
nodeCompat: true,
|
||||
},
|
||||
|
|
|
|||
146
packages/console/core/script/create-api-key.ts
Normal file
146
packages/console/core/script/create-api-key.ts
Normal file
|
|
@ -0,0 +1,146 @@
|
|||
import { Resource } from "@opencode-ai/console-resource"
|
||||
import { and, Database, eq, isNull } from "../src/drizzle/index.js"
|
||||
import { Identifier } from "../src/identifier.js"
|
||||
import { AccountTable } from "../src/schema/account.sql.js"
|
||||
import { AuthTable } from "../src/schema/auth.sql.js"
|
||||
import { BillingTable } from "../src/schema/billing.sql.js"
|
||||
import { KeyTable } from "../src/schema/key.sql.js"
|
||||
import { UserTable } from "../src/schema/user.sql.js"
|
||||
import { WorkspaceTable } from "../src/schema/workspace.sql.js"
|
||||
import { centsToMicroCents } from "../src/util/price.js"
|
||||
|
||||
const args = parseArgs(process.argv.slice(2))
|
||||
if (!args.email) {
|
||||
console.error(
|
||||
"Usage: bun script/create-api-key.ts --email <email> [--workspace-id <wrk_...>] [--workspace-name <name>] [--key-name <name>] [--balance-dollars <amount>] [--allow-production]",
|
||||
)
|
||||
process.exit(1)
|
||||
}
|
||||
if (Resource.App.stage === "production" && !args.allowProduction) {
|
||||
throw new Error("Refusing to create a production API key without --allow-production")
|
||||
}
|
||||
|
||||
const result = await Database.transaction(async (tx) => {
|
||||
const auth = await tx
|
||||
.select()
|
||||
.from(AuthTable)
|
||||
.where(and(eq(AuthTable.provider, "email"), eq(AuthTable.subject, args.email)))
|
||||
.then((rows) => rows[0])
|
||||
const accountID = auth?.accountID ?? Identifier.create("account")
|
||||
if (!auth) {
|
||||
await tx.insert(AccountTable).values({ id: accountID })
|
||||
await tx.insert(AuthTable).values({
|
||||
id: Identifier.create("auth"),
|
||||
provider: "email",
|
||||
subject: args.email,
|
||||
accountID,
|
||||
})
|
||||
}
|
||||
|
||||
const workspace = args.workspaceID
|
||||
? await tx
|
||||
.select()
|
||||
.from(WorkspaceTable)
|
||||
.where(eq(WorkspaceTable.id, args.workspaceID))
|
||||
.then((rows) => rows[0])
|
||||
: await tx
|
||||
.select({ workspace: WorkspaceTable })
|
||||
.from(UserTable)
|
||||
.innerJoin(WorkspaceTable, eq(WorkspaceTable.id, UserTable.workspaceID))
|
||||
.where(and(eq(UserTable.accountID, accountID), isNull(UserTable.timeDeleted)))
|
||||
.then((rows) => rows[0]?.workspace)
|
||||
if (args.workspaceID && !workspace) throw new Error(`Workspace not found: ${args.workspaceID}`)
|
||||
const workspaceID = workspace?.id ?? Identifier.create("workspace")
|
||||
if (!workspace) {
|
||||
await tx.insert(WorkspaceTable).values({
|
||||
id: workspaceID,
|
||||
slug: null,
|
||||
name: args.workspaceName ?? `${args.email} manual`,
|
||||
})
|
||||
}
|
||||
|
||||
const user = await tx
|
||||
.select()
|
||||
.from(UserTable)
|
||||
.where(
|
||||
and(eq(UserTable.workspaceID, workspaceID), eq(UserTable.accountID, accountID), isNull(UserTable.timeDeleted)),
|
||||
)
|
||||
.then((rows) => rows[0])
|
||||
const userID = user?.id ?? Identifier.create("user")
|
||||
if (!user) {
|
||||
await tx.insert(UserTable).values({
|
||||
id: userID,
|
||||
workspaceID,
|
||||
accountID,
|
||||
email: args.email,
|
||||
name: args.email,
|
||||
role: "admin",
|
||||
})
|
||||
}
|
||||
|
||||
const balance = centsToMicroCents(args.balanceDollars * 100)
|
||||
const billing = await tx
|
||||
.select()
|
||||
.from(BillingTable)
|
||||
.where(eq(BillingTable.workspaceID, workspaceID))
|
||||
.then((rows) => rows[0])
|
||||
if (!billing) {
|
||||
await tx.insert(BillingTable).values({
|
||||
id: Identifier.create("billing"),
|
||||
workspaceID,
|
||||
balance,
|
||||
})
|
||||
} else if (billing.balance < balance) {
|
||||
await tx.update(BillingTable).set({ balance }).where(eq(BillingTable.workspaceID, workspaceID))
|
||||
}
|
||||
|
||||
const secretKey = createSecretKey()
|
||||
const keyID = Identifier.create("key")
|
||||
await tx.insert(KeyTable).values({
|
||||
id: keyID,
|
||||
workspaceID,
|
||||
userID,
|
||||
name: args.keyName ?? "Manual API Key",
|
||||
key: secretKey,
|
||||
timeUsed: null,
|
||||
})
|
||||
|
||||
return { accountID, workspaceID, userID, keyID, secretKey }
|
||||
})
|
||||
|
||||
console.log(JSON.stringify({ stage: Resource.App.stage, ...result }, null, 2))
|
||||
|
||||
function createSecretKey() {
|
||||
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
|
||||
const values = new Uint32Array(64)
|
||||
crypto.getRandomValues(values)
|
||||
return `sk-${Array.from(values, (value) => chars[value % chars.length]).join("")}`
|
||||
}
|
||||
|
||||
function parseArgs(argv: string[]) {
|
||||
const parsed = {
|
||||
email: "",
|
||||
workspaceID: "",
|
||||
workspaceName: "",
|
||||
keyName: "",
|
||||
balanceDollars: 100,
|
||||
allowProduction: false,
|
||||
}
|
||||
for (let index = 0; index < argv.length; index++) {
|
||||
const arg = argv[index]
|
||||
if (arg === "--email") parsed.email = requiredValue(argv, ++index, arg)
|
||||
if (arg === "--workspace-id") parsed.workspaceID = requiredValue(argv, ++index, arg)
|
||||
if (arg === "--workspace-name") parsed.workspaceName = requiredValue(argv, ++index, arg)
|
||||
if (arg === "--key-name") parsed.keyName = requiredValue(argv, ++index, arg)
|
||||
if (arg === "--balance-dollars") parsed.balanceDollars = Number(requiredValue(argv, ++index, arg))
|
||||
if (arg === "--allow-production") parsed.allowProduction = true
|
||||
}
|
||||
if (!Number.isFinite(parsed.balanceDollars) || parsed.balanceDollars < 0) throw new Error("Invalid --balance-dollars")
|
||||
return parsed
|
||||
}
|
||||
|
||||
function requiredValue(argv: string[], index: number, arg: string) {
|
||||
const value = argv[index]
|
||||
if (!value || value.startsWith("--")) throw new Error(`Missing value for ${arg}`)
|
||||
return value
|
||||
}
|
||||
|
|
@ -21,7 +21,7 @@ export default {
|
|||
)
|
||||
continue
|
||||
|
||||
let data = {
|
||||
let data: Record<string, unknown> = {
|
||||
"cf.continent": event.event.request.cf?.continent,
|
||||
"cf.country": event.event.request.cf?.country,
|
||||
"cf.city": event.event.request.cf?.city,
|
||||
|
|
@ -35,30 +35,152 @@ export default {
|
|||
ip: event.event.request.headers["x-real-ip"],
|
||||
}
|
||||
const time = new Date(event.eventTimestamp ?? Date.now()).toISOString()
|
||||
const events = []
|
||||
for (const log of event.logs) {
|
||||
for (const message of log.message) {
|
||||
if (!message.startsWith("_metric:")) continue
|
||||
const json = JSON.parse(message.slice(8))
|
||||
data = { ...data, ...json }
|
||||
if ("llm.error.code" in json) {
|
||||
events.push({ time, data: { ...data, event_type: "llm.error" } })
|
||||
}
|
||||
}
|
||||
}
|
||||
events.push({ time, data: { ...data, event_type: "completions" } })
|
||||
const events = [
|
||||
...event.logs.flatMap((log) =>
|
||||
log.message.flatMap((message: string) => {
|
||||
if (!message.startsWith("_metric:")) return []
|
||||
const json = JSON.parse(message.slice(8)) as Record<string, unknown>
|
||||
data = { ...data, ...json }
|
||||
if ("llm.error.code" in json) {
|
||||
return [{ time, data: { ...data, event_type: "llm.error" } }]
|
||||
}
|
||||
return []
|
||||
}),
|
||||
),
|
||||
{ time, data: { ...data, event_type: "completions" } },
|
||||
]
|
||||
console.log(JSON.stringify(data, null, 2))
|
||||
|
||||
const ret = await fetch("https://api.honeycomb.io/1/batch/zen", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-Honeycomb-Team": Resource.HONEYCOMB_API_KEY.value,
|
||||
},
|
||||
body: JSON.stringify(events),
|
||||
})
|
||||
console.log(ret.status)
|
||||
console.log(await ret.text())
|
||||
const lakeIngest = getLakeIngest()
|
||||
const [honeycomb, lake] = await Promise.all([
|
||||
fetch("https://api.honeycomb.io/1/batch/zen", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-Honeycomb-Team": Resource.HONEYCOMB_API_KEY.value,
|
||||
},
|
||||
body: JSON.stringify(events),
|
||||
}),
|
||||
...(lakeIngest
|
||||
? [
|
||||
fetch(lakeIngest.url, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${lakeIngest.secret}`,
|
||||
},
|
||||
body: JSON.stringify({ events: events.map((event) => toLakeEvent(event.time, event.data)) }),
|
||||
}),
|
||||
]
|
||||
: []),
|
||||
])
|
||||
console.log(honeycomb.status)
|
||||
console.log(await honeycomb.text())
|
||||
if (lake) {
|
||||
console.log(lake.status)
|
||||
console.log(await lake.text())
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
function getLakeIngest(): { url: string; secret: string } | undefined {
|
||||
try {
|
||||
return Resource.LakeIngest
|
||||
} catch {
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
function toLakeEvent(time: string, data: Record<string, unknown>) {
|
||||
return {
|
||||
_datalake_key: "inference.event",
|
||||
event_timestamp: time,
|
||||
event_date: time.slice(0, 10),
|
||||
event_type: string(data, "event_type"),
|
||||
dataset: "zen",
|
||||
cf_continent: string(data, "cf.continent"),
|
||||
cf_country: string(data, "cf.country"),
|
||||
cf_city: string(data, "cf.city"),
|
||||
cf_region: string(data, "cf.region"),
|
||||
cf_latitude: number(data, "cf.latitude"),
|
||||
cf_longitude: number(data, "cf.longitude"),
|
||||
cf_timezone: string(data, "cf.timezone"),
|
||||
duration: number(data, "duration"),
|
||||
request_length: integer(data, "request_length"),
|
||||
status: integer(data, "status"),
|
||||
ip: string(data, "ip"),
|
||||
is_stream: boolean(data, "is_stream"),
|
||||
session: string(data, "session"),
|
||||
request: string(data, "request"),
|
||||
client: string(data, "client"),
|
||||
user_agent: string(data, "user_agent"),
|
||||
model_variant: string(data, "model.variant"),
|
||||
source: string(data, "source"),
|
||||
provider: string(data, "provider"),
|
||||
provider_model: string(data, "provider.model"),
|
||||
model: string(data, "model"),
|
||||
llm_error_code: integer(data, "llm.error.code"),
|
||||
llm_error_message: string(data, "llm.error.message"),
|
||||
error_response: string(data, "error.response"),
|
||||
error_type: string(data, "error.type"),
|
||||
error_message: string(data, "error.message"),
|
||||
error_cause: string(data, "error.cause"),
|
||||
error_cause2: string(data, "error.cause2"),
|
||||
api_key: string(data, "api_key"),
|
||||
workspace: string(data, "workspace"),
|
||||
is_subscription: boolean(data, "isSubscription"),
|
||||
subscription: string(data, "subscription"),
|
||||
response_length: integer(data, "response_length"),
|
||||
time_to_first_byte: integer(data, "time_to_first_byte"),
|
||||
timestamp_first_byte: integer(data, "timestamp.first_byte"),
|
||||
timestamp_last_byte: integer(data, "timestamp.last_byte"),
|
||||
tokens_input: integer(data, "tokens.input"),
|
||||
tokens_output: integer(data, "tokens.output"),
|
||||
tokens_reasoning: integer(data, "tokens.reasoning"),
|
||||
tokens_cache_read: integer(data, "tokens.cache_read"),
|
||||
tokens_cache_write_5m: integer(data, "tokens.cache_write_5m"),
|
||||
tokens_cache_write_1h: integer(data, "tokens.cache_write_1h"),
|
||||
cost_input_microcents: integer(data, "cost.input.microcents"),
|
||||
cost_output_microcents: integer(data, "cost.output.microcents"),
|
||||
cost_cache_read_microcents: integer(data, "cost.cache_read.microcents"),
|
||||
cost_cache_write_microcents: integer(data, "cost.cache_write.microcents"),
|
||||
cost_total_microcents: integer(data, "cost.total.microcents"),
|
||||
cost_input: integer(data, "cost.input"),
|
||||
cost_output: integer(data, "cost.output"),
|
||||
cost_cache_read: integer(data, "cost.cache_read"),
|
||||
cost_cache_write_5m: integer(data, "cost.cache_write_5m"),
|
||||
cost_cache_write_1h: integer(data, "cost.cache_write_1h"),
|
||||
cost_total: integer(data, "cost.total"),
|
||||
}
|
||||
}
|
||||
|
||||
function string(data: Record<string, unknown>, key: string) {
|
||||
const value = data[key]
|
||||
if (typeof value === "string") return value
|
||||
if (typeof value === "number" || typeof value === "boolean") return String(value)
|
||||
return undefined
|
||||
}
|
||||
|
||||
function boolean(data: Record<string, unknown>, key: string) {
|
||||
const value = data[key]
|
||||
if (typeof value === "boolean") return value
|
||||
if (typeof value === "string") return value === "true" ? true : value === "false" ? false : undefined
|
||||
return undefined
|
||||
}
|
||||
|
||||
function integer(data: Record<string, unknown>, key: string) {
|
||||
const value = number(data, key)
|
||||
if (value === undefined) return undefined
|
||||
return Math.round(value)
|
||||
}
|
||||
|
||||
function number(data: Record<string, unknown>, key: string) {
|
||||
const value = data[key]
|
||||
if (typeof value === "number") return Number.isFinite(value) ? value : undefined
|
||||
if (typeof value === "string") {
|
||||
const parsed = Number(value)
|
||||
return Number.isFinite(parsed) ? parsed : undefined
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ export const Resource = new Proxy(
|
|||
{
|
||||
get(_target, prop: keyof typeof ResourceBase) {
|
||||
const value = ResourceBase[prop]
|
||||
const secrets = ResourceBase as unknown as Record<string, { value: string }>
|
||||
if ("type" in value) {
|
||||
// @ts-ignore
|
||||
if (value.type === "sst.cloudflare.Bucket") {
|
||||
|
|
@ -21,11 +22,11 @@ export const Resource = new Proxy(
|
|||
// @ts-ignore
|
||||
if (value.type === "sst.cloudflare.Kv") {
|
||||
const client = new Cloudflare({
|
||||
apiToken: ResourceBase.CLOUDFLARE_API_TOKEN.value,
|
||||
apiToken: secrets.CLOUDFLARE_API_TOKEN.value,
|
||||
})
|
||||
// @ts-ignore
|
||||
const namespaceId = value.namespaceId
|
||||
const accountId = ResourceBase.CLOUDFLARE_DEFAULT_ACCOUNT_ID.value
|
||||
const accountId = secrets.CLOUDFLARE_DEFAULT_ACCOUNT_ID.value
|
||||
return {
|
||||
get: (k: string | string[]) => {
|
||||
const isMulti = Array.isArray(k)
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ const nitroConfig: any = (() => {
|
|||
if (target === "cloudflare") {
|
||||
return {
|
||||
compatibilityDate: "2024-09-19",
|
||||
preset: "cloudflare_module",
|
||||
preset: "cloudflare-module",
|
||||
cloudflare: {
|
||||
nodeCompat: true,
|
||||
},
|
||||
|
|
|
|||
16
packages/stats/README.md
Normal file
16
packages/stats/README.md
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
# OpenCode Stats
|
||||
|
||||
Stats is a separate site from the console. Runtime, database, and domain services live in `core`; the SolidStart website lives in `app`; deployable Lambda entrypoints live in `function`.
|
||||
|
||||
## Packages
|
||||
|
||||
- `app`: SolidStart frontend/site.
|
||||
- `core`: Effect services, app config, Drizzle schema/migrations, and stats domains.
|
||||
- `function`: Lambda handlers that call into `core` services.
|
||||
|
||||
## Commands
|
||||
|
||||
- `bun run dev:stats` from the repo root starts the SolidStart app.
|
||||
- `bun run --cwd packages/stats/app typecheck` typechecks the site.
|
||||
- `bun run --cwd packages/stats/core typecheck` typechecks the Effect/database package.
|
||||
- `bun run --cwd packages/stats/function typecheck` typechecks Lambda entrypoints.
|
||||
17
packages/stats/app/.gitignore
vendored
Normal file
17
packages/stats/app/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
dist
|
||||
.wrangler
|
||||
.output
|
||||
.vercel
|
||||
.netlify
|
||||
app.config.timestamp_*.js
|
||||
|
||||
# Environment
|
||||
.env
|
||||
.env*.local
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
|
||||
# System Files
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
5
packages/stats/app/app.config.ts
Normal file
5
packages/stats/app/app.config.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
export default {
|
||||
server: {
|
||||
preset: "cloudflare-module",
|
||||
},
|
||||
}
|
||||
36
packages/stats/app/package.json
Normal file
36
packages/stats/app/package.json
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode-ai/stats-app",
|
||||
"version": "1.14.50",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"typecheck": "tsgo --noEmit",
|
||||
"dev": "vite dev --host 0.0.0.0",
|
||||
"build": "vite build",
|
||||
"start": "vite start"
|
||||
},
|
||||
"dependencies": {
|
||||
"@opencode-ai/stats-core": "workspace:*",
|
||||
"@opencode-ai/ui": "workspace:*",
|
||||
"@solidjs/meta": "catalog:",
|
||||
"@solidjs/router": "catalog:",
|
||||
"@solidjs/start": "catalog:",
|
||||
"d3-scale": "4.0.2",
|
||||
"effect": "catalog:",
|
||||
"nitro": "3.0.1-alpha.1",
|
||||
"solid-js": "catalog:",
|
||||
"vite": "catalog:"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@cloudflare/workers-types": "catalog:",
|
||||
"@types/bun": "catalog:",
|
||||
"@types/d3-scale": "4.0.9",
|
||||
"@typescript/native-preview": "catalog:",
|
||||
"typescript": "catalog:"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=22"
|
||||
}
|
||||
}
|
||||
123
packages/stats/app/src/app.css
Normal file
123
packages/stats/app/src/app.css
Normal file
|
|
@ -0,0 +1,123 @@
|
|||
:root {
|
||||
color-scheme: light dark;
|
||||
--stats-bg: #f8f5ee;
|
||||
--stats-ink: #16110d;
|
||||
--stats-muted: #6d6257;
|
||||
--stats-line: #ded5c9;
|
||||
--stats-panel: #fffaf1;
|
||||
--stats-accent: #2357ff;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--stats-bg: #11100e;
|
||||
--stats-ink: #f7efe4;
|
||||
--stats-muted: #b8aa99;
|
||||
--stats-line: #322d27;
|
||||
--stats-panel: #1a1714;
|
||||
--stats-accent: #86a2ff;
|
||||
}
|
||||
}
|
||||
|
||||
html {
|
||||
line-height: 1;
|
||||
background: var(--stats-bg);
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
min-width: 320px;
|
||||
background:
|
||||
radial-gradient(circle at top left, color-mix(in srgb, var(--stats-accent) 16%, transparent), transparent 32rem),
|
||||
var(--stats-bg);
|
||||
color: var(--stats-ink);
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.shell {
|
||||
box-sizing: border-box;
|
||||
min-height: 100vh;
|
||||
padding: 2rem clamp(1rem, 4vw, 4rem);
|
||||
}
|
||||
|
||||
.panel {
|
||||
display: grid;
|
||||
gap: clamp(2rem, 8vw, 5rem);
|
||||
box-sizing: border-box;
|
||||
width: min(100%, 72rem);
|
||||
margin: 0 auto;
|
||||
padding: clamp(1.25rem, 4vw, 3rem);
|
||||
border: 1px solid var(--stats-line);
|
||||
border-radius: 1.5rem;
|
||||
background: color-mix(in srgb, var(--stats-panel) 88%, transparent);
|
||||
}
|
||||
|
||||
.eyebrow {
|
||||
margin: 0 0 1rem;
|
||||
color: var(--stats-muted);
|
||||
font-size: 0.75rem;
|
||||
letter-spacing: 0.14em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
h1 {
|
||||
max-width: 11ch;
|
||||
margin: 0;
|
||||
font-size: clamp(3rem, 14vw, 9rem);
|
||||
line-height: 0.85;
|
||||
letter-spacing: -0.08em;
|
||||
}
|
||||
|
||||
.summary {
|
||||
max-width: 42rem;
|
||||
margin: 1.5rem 0 0;
|
||||
color: var(--stats-muted);
|
||||
font-size: clamp(1rem, 2vw, 1.25rem);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 1px;
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--stats-line);
|
||||
border-radius: 1rem;
|
||||
background: var(--stats-line);
|
||||
}
|
||||
|
||||
.metric {
|
||||
padding: 1rem;
|
||||
background: var(--stats-panel);
|
||||
}
|
||||
|
||||
.metric b {
|
||||
display: block;
|
||||
margin-bottom: 0.5rem;
|
||||
font-size: clamp(1.5rem, 4vw, 3rem);
|
||||
letter-spacing: -0.05em;
|
||||
}
|
||||
|
||||
.metric span {
|
||||
color: var(--stats-muted);
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
|
||||
.link {
|
||||
display: inline-flex;
|
||||
width: fit-content;
|
||||
margin-top: 1.5rem;
|
||||
color: var(--stats-accent);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
.grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
30
packages/stats/app/src/app.tsx
Normal file
30
packages/stats/app/src/app.tsx
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
import { MetaProvider, Meta, Title } from "@solidjs/meta"
|
||||
import { Router } from "@solidjs/router"
|
||||
import { FileRoutes } from "@solidjs/start/router"
|
||||
import { Suspense } from "solid-js"
|
||||
import "./app.css"
|
||||
|
||||
function AppMeta() {
|
||||
return (
|
||||
<>
|
||||
<Title>opencode stats</Title>
|
||||
<Meta name="description" content="OpenCode usage and stats." />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default function App() {
|
||||
return (
|
||||
<Router
|
||||
explicitLinks={true}
|
||||
root={(props) => (
|
||||
<MetaProvider>
|
||||
<AppMeta />
|
||||
<Suspense>{props.children}</Suspense>
|
||||
</MetaProvider>
|
||||
)}
|
||||
>
|
||||
<FileRoutes />
|
||||
</Router>
|
||||
)
|
||||
}
|
||||
18
packages/stats/app/src/asset/logo-ornate-dark.svg
Normal file
18
packages/stats/app/src/asset/logo-ornate-dark.svg
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
<svg width="234" height="42" viewBox="0 0 234 42" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M18 30H6V18H18V30Z" fill="#4B4646"/>
|
||||
<path d="M18 12H6V30H18V12ZM24 36H0V6H24V36Z" fill="#B7B1B1"/>
|
||||
<path d="M48 30H36V18H48V30Z" fill="#4B4646"/>
|
||||
<path d="M36 30H48V12H36V30ZM54 36H36V42H30V6H54V36Z" fill="#B7B1B1"/>
|
||||
<path d="M84 24V30H66V24H84Z" fill="#4B4646"/>
|
||||
<path d="M84 24H66V30H84V36H60V6H84V24ZM66 18H78V12H66V18Z" fill="#B7B1B1"/>
|
||||
<path d="M108 36H96V18H108V36Z" fill="#4B4646"/>
|
||||
<path d="M108 12H96V36H90V6H108V12ZM114 36H108V12H114V36Z" fill="#B7B1B1"/>
|
||||
<path d="M144 30H126V18H144V30Z" fill="#4B4646"/>
|
||||
<path d="M144 12H126V30H144V36H120V6H144V12Z" fill="#F1ECEC"/>
|
||||
<path d="M168 30H156V18H168V30Z" fill="#4B4646"/>
|
||||
<path d="M168 12H156V30H168V12ZM174 36H150V6H174V36Z" fill="#F1ECEC"/>
|
||||
<path d="M198 30H186V18H198V30Z" fill="#4B4646"/>
|
||||
<path d="M198 12H186V30H198V12ZM204 36H180V6H198V0H204V36Z" fill="#F1ECEC"/>
|
||||
<path d="M234 24V30H216V24H234Z" fill="#4B4646"/>
|
||||
<path d="M216 12V18H228V12H216ZM234 24H216V30H234V36H210V6H234V24Z" fill="#F1ECEC"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
18
packages/stats/app/src/asset/logo-ornate-light.svg
Normal file
18
packages/stats/app/src/asset/logo-ornate-light.svg
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
<svg width="234" height="42" viewBox="0 0 234 42" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M18 30H6V18H18V30Z" fill="#CFCECD"/>
|
||||
<path d="M18 12H6V30H18V12ZM24 36H0V6H24V36Z" fill="#656363"/>
|
||||
<path d="M48 30H36V18H48V30Z" fill="#CFCECD"/>
|
||||
<path d="M36 30H48V12H36V30ZM54 36H36V42H30V6H54V36Z" fill="#656363"/>
|
||||
<path d="M84 24V30H66V24H84Z" fill="#CFCECD"/>
|
||||
<path d="M84 24H66V30H84V36H60V6H84V24ZM66 18H78V12H66V18Z" fill="#656363"/>
|
||||
<path d="M108 36H96V18H108V36Z" fill="#CFCECD"/>
|
||||
<path d="M108 12H96V36H90V6H108V12ZM114 36H108V12H114V36Z" fill="#656363"/>
|
||||
<path d="M144 30H126V18H144V30Z" fill="#CFCECD"/>
|
||||
<path d="M144 12H126V30H144V36H120V6H144V12Z" fill="#211E1E"/>
|
||||
<path d="M168 30H156V18H168V30Z" fill="#CFCECD"/>
|
||||
<path d="M168 12H156V30H168V12ZM174 36H150V6H174V36Z" fill="#211E1E"/>
|
||||
<path d="M198 30H186V18H198V30Z" fill="#CFCECD"/>
|
||||
<path d="M198 12H186V30H198V12ZM204 36H180V6H198V0H204V36Z" fill="#211E1E"/>
|
||||
<path d="M234 24V30H216V24H234Z" fill="#CFCECD"/>
|
||||
<path d="M216 12V18H228V12H216ZM234 24H216V30H234V36H210V6H234V24Z" fill="#211E1E"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
7
packages/stats/app/src/entry-client.tsx
Normal file
7
packages/stats/app/src/entry-client.tsx
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
// @refresh reload
|
||||
import { mount, StartClient } from "@solidjs/start/client"
|
||||
|
||||
const root = document.getElementById("app")
|
||||
if (!root) throw new Error("Root element #app not found")
|
||||
|
||||
mount(() => <StartClient />, root)
|
||||
25
packages/stats/app/src/entry-server.tsx
Normal file
25
packages/stats/app/src/entry-server.tsx
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
// @refresh reload
|
||||
import { createHandler, StartServer } from "@solidjs/start/server"
|
||||
|
||||
export default createHandler(
|
||||
() => (
|
||||
<StartServer
|
||||
document={({ assets, children, scripts }) => (
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
{assets}
|
||||
</head>
|
||||
<body>
|
||||
<div id="app">{children}</div>
|
||||
{scripts}
|
||||
</body>
|
||||
</html>
|
||||
)}
|
||||
/>
|
||||
),
|
||||
{
|
||||
mode: "async",
|
||||
},
|
||||
)
|
||||
1
packages/stats/app/src/global.d.ts
vendored
Normal file
1
packages/stats/app/src/global.d.ts
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
/// <reference types="@solidjs/start/env" />
|
||||
19
packages/stats/app/src/routes/api/health.ts
Normal file
19
packages/stats/app/src/routes/api/health.ts
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
import { AppConfig } from "@opencode-ai/stats-core/config"
|
||||
import { runtime } from "@opencode-ai/stats-core/runtime"
|
||||
import { Effect } from "effect"
|
||||
|
||||
export async function GET() {
|
||||
return Response.json(
|
||||
await runtime.runPromise(
|
||||
Effect.gen(function* () {
|
||||
const config = yield* AppConfig
|
||||
return {
|
||||
ok: true,
|
||||
app: "stats",
|
||||
stage: config.stage,
|
||||
publicUrl: config.publicUrl,
|
||||
}
|
||||
}),
|
||||
),
|
||||
)
|
||||
}
|
||||
1316
packages/stats/app/src/routes/index.css
Normal file
1316
packages/stats/app/src/routes/index.css
Normal file
File diff suppressed because it is too large
Load diff
971
packages/stats/app/src/routes/index.tsx
Normal file
971
packages/stats/app/src/routes/index.tsx
Normal file
|
|
@ -0,0 +1,971 @@
|
|||
import "./index.css"
|
||||
import { Meta, Title } from "@solidjs/meta"
|
||||
import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
|
||||
import {
|
||||
type CountryEntry,
|
||||
getStatsHomeData,
|
||||
type LeaderboardEntry,
|
||||
type MarketDay,
|
||||
type StatsHomeData,
|
||||
type SessionCostEntry,
|
||||
type TokenCostEntry,
|
||||
type UsagePoint,
|
||||
} from "@opencode-ai/stats-core/domain/home"
|
||||
import { runtime } from "@opencode-ai/stats-core/runtime"
|
||||
import { createAsync, query } from "@solidjs/router"
|
||||
import { scaleBand, scaleLinear } from "d3-scale"
|
||||
import { createMemo, createSignal, For, Show, type JSX } from "solid-js"
|
||||
import { getRequestEvent } from "solid-js/web"
|
||||
import logoDark from "../asset/logo-ornate-dark.svg"
|
||||
import logoLight from "../asset/logo-ornate-light.svg"
|
||||
|
||||
const products = ["All Users", "Zen", "Go", "Enterprise"] as const
|
||||
const tokenProducts = ["Zen", "Go", "Enterprise"] as const
|
||||
const ranges = ["1D", "1W", "1M", "3M", "YTD", "ALL"] as const
|
||||
const usageColors = ["#ff5d64", "#ff8a00", "#8bef00", "#12c8b3", "#18c7dc", "#6c7dff", "#9d73f7"]
|
||||
const marketColors = ["#ed6aff", "#a684ff", "#7c86ff", "#51a2ff", "#00d3f2", "#00d5be", "#00bc7d", "#9ae600", "#ffb900"]
|
||||
const countryPositions = [
|
||||
{ x: 112, y: 96 },
|
||||
{ x: 284, y: 144 },
|
||||
{ x: 472, y: 92 },
|
||||
{ x: 642, y: 154 },
|
||||
{ x: 800, y: 96 },
|
||||
{ x: 172, y: 234 },
|
||||
{ x: 362, y: 250 },
|
||||
{ x: 552, y: 236 },
|
||||
{ x: 744, y: 252 },
|
||||
{ x: 48, y: 184 },
|
||||
{ x: 892, y: 198 },
|
||||
{ x: 456, y: 176 },
|
||||
] as const
|
||||
|
||||
type UsageProduct = (typeof products)[number]
|
||||
type TokenProduct = (typeof tokenProducts)[number]
|
||||
type UsageRange = (typeof ranges)[number]
|
||||
|
||||
const getData = query(async () => {
|
||||
"use server"
|
||||
return runtime.runPromise(getStatsHomeData())
|
||||
}, "getStatsHomeData")
|
||||
|
||||
export default function StatsHome() {
|
||||
getRequestEvent()?.response.headers.set(
|
||||
"Cache-Control",
|
||||
"public, max-age=60, s-maxage=300, stale-while-revalidate=86400",
|
||||
)
|
||||
const data = createAsync(() => getData())
|
||||
|
||||
return (
|
||||
<main data-page="stats">
|
||||
<Title>OpenCode Stats</Title>
|
||||
<Meta name="description" content="OpenCode usage, market share, token cost, and session cost stats." />
|
||||
<div data-component="container">
|
||||
<Header />
|
||||
<div data-component="content">
|
||||
<Show when={data()} fallback={<StatsLoading />}>
|
||||
{(stats) => (
|
||||
<>
|
||||
<Hero updatedAt={stats().updatedAt} />
|
||||
<UsageSection data={stats().usage} />
|
||||
<LeaderboardSection data={stats().leaderboard} />
|
||||
<MarketShareSection data={stats().market} />
|
||||
<TokenCostSection data={stats().tokenCost} />
|
||||
<SessionCostSection data={stats().sessionCost} />
|
||||
<CountrySection data={stats().country} />
|
||||
<Newsletter />
|
||||
</>
|
||||
)}
|
||||
</Show>
|
||||
</div>
|
||||
<Footer />
|
||||
</div>
|
||||
<Legal />
|
||||
</main>
|
||||
)
|
||||
}
|
||||
|
||||
function Hero(props: { updatedAt: string | null }) {
|
||||
return (
|
||||
<section data-section="hero">
|
||||
<div>
|
||||
<h1>OpenCode Stats</h1>
|
||||
<p data-slot="meta">
|
||||
<svg aria-hidden="true" width="16" height="16" viewBox="0 0 16 16">
|
||||
<rect x="3" y="3" width="10" height="10" fill="currentColor" />
|
||||
<rect x="7" y="6.5" width="2" height="4.5" fill="var(--stats-layer-2)" />
|
||||
<rect x="7" y="5" width="2" height="1" fill="var(--stats-layer-2)" />
|
||||
</svg>
|
||||
<span>OpenCode data</span> <b>·</b>{" "}
|
||||
<em>{props.updatedAt ? `Updated ${formatUpdatedAt(props.updatedAt)}` : "No rows yet"}</em>
|
||||
</p>
|
||||
</div>
|
||||
<p>See how model usage, provider share, cost, and geography move across OpenCode traffic.</p>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
function StatsLoading() {
|
||||
return (
|
||||
<>
|
||||
<Hero updatedAt={null} />
|
||||
<ChartSection title="Usage">
|
||||
<EmptyState title="Loading stats" description="Reading model aggregates from model_stat." />
|
||||
</ChartSection>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function ChartSection(props: { title: string; description?: string; controls?: JSX.Element; children: JSX.Element }) {
|
||||
return (
|
||||
<section data-section="chart">
|
||||
<div data-slot="section-header">
|
||||
<div>
|
||||
<h2>{props.title}</h2>
|
||||
{props.description && <p>{props.description}</p>}
|
||||
</div>
|
||||
{props.controls}
|
||||
</div>
|
||||
{props.children}
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
function EmptyState(props: { title: string; description: string }) {
|
||||
return (
|
||||
<div data-component="empty-state">
|
||||
<strong>{props.title}</strong>
|
||||
<p>{props.description}</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function formatUpdatedAt(value: string) {
|
||||
const date = new Date(value)
|
||||
if (Number.isNaN(date.getTime())) return "just now"
|
||||
return new Intl.DateTimeFormat("en", {
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
hour: "numeric",
|
||||
minute: "2-digit",
|
||||
timeZone: "UTC",
|
||||
timeZoneName: "short",
|
||||
}).format(date)
|
||||
}
|
||||
|
||||
function UsageSection(props: { data: StatsHomeData["usage"] }) {
|
||||
const [product, setProduct] = createSignal<UsageProduct>("All Users")
|
||||
const [range, setRange] = createSignal<UsageRange>("1W")
|
||||
const data = createMemo(() => props.data[product()][range()])
|
||||
|
||||
return (
|
||||
<ChartSection title="Usage">
|
||||
<Show
|
||||
when={data().some((item) => usageTotal(item) > 0)}
|
||||
fallback={<EmptyState title="No usage data" description="No model_stat rows matched this product and range." />}
|
||||
>
|
||||
<UsageChart data={data()} />
|
||||
</Show>
|
||||
<div data-slot="chart-footer">
|
||||
<StatsFilters product={product()} range={range()} onProductSelect={setProduct} onRangeSelect={setRange} />
|
||||
</div>
|
||||
</ChartSection>
|
||||
)
|
||||
}
|
||||
|
||||
function StatsFilters(props: {
|
||||
product: UsageProduct
|
||||
range: UsageRange
|
||||
onProductSelect: (product: UsageProduct) => void
|
||||
onRangeSelect: (range: UsageRange) => void
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
<FilterPills
|
||||
items={products}
|
||||
selected={props.product}
|
||||
label="Product filter"
|
||||
variant="product"
|
||||
onSelect={props.onProductSelect}
|
||||
/>
|
||||
<FilterPills
|
||||
items={ranges}
|
||||
selected={props.range}
|
||||
label="Date range"
|
||||
variant="range"
|
||||
onSelect={props.onRangeSelect}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function FilterPills<T extends string>(props: {
|
||||
items: readonly T[]
|
||||
selected: T
|
||||
label: string
|
||||
variant: "product" | "range"
|
||||
onSelect: (item: T) => void
|
||||
}) {
|
||||
return (
|
||||
<div data-component="usage-filter" data-variant={props.variant} role="radiogroup" aria-label={props.label}>
|
||||
<For each={props.items}>
|
||||
{(item) => (
|
||||
<button
|
||||
type="button"
|
||||
role="radio"
|
||||
aria-checked={props.selected === item}
|
||||
data-active={props.selected === item ? "true" : undefined}
|
||||
onClick={() => props.onSelect(item)}
|
||||
>
|
||||
{item}
|
||||
</button>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function UsageChart(props: { data: UsagePoint[] }) {
|
||||
const [activeIndex, setActiveIndex] = createSignal<number>()
|
||||
const [activeSegment, setActiveSegment] = createSignal<number>()
|
||||
const height = 434
|
||||
const width = 920
|
||||
const headerOffset = 46
|
||||
const segmentGap = 2
|
||||
const maxTotal = createMemo(() => Math.max(1, Math.max(...props.data.map((item) => usageTotal(item))) * 1.02))
|
||||
const activePoint = createMemo(() => props.data[activeIndex() ?? -1])
|
||||
const y = createMemo(() => scaleLinear([0, maxTotal()], [height, 0]))
|
||||
const x = createMemo(() =>
|
||||
scaleBand(
|
||||
props.data.map((_, index) => String(index)),
|
||||
[0, width],
|
||||
).paddingInner(0.08),
|
||||
)
|
||||
const activeBar = createMemo(() => {
|
||||
const index = activeIndex()
|
||||
const point = activePoint()
|
||||
if (index === undefined) return
|
||||
if (!point) return
|
||||
return {
|
||||
point,
|
||||
x: x()(String(index)) ?? 0,
|
||||
width: x().bandwidth(),
|
||||
}
|
||||
})
|
||||
|
||||
return (
|
||||
<div data-component="usage-chart">
|
||||
<svg viewBox={`0 0 ${width} ${height + headerOffset}`} role="img" aria-label="Stacked usage chart">
|
||||
<defs>
|
||||
<pattern id="stats-usage-dot-grid" width="6" height="6" patternUnits="userSpaceOnUse">
|
||||
<rect x="1" y="1" width="2" height="2" fill="var(--stats-dot)" />
|
||||
</pattern>
|
||||
</defs>
|
||||
<For each={props.data}>
|
||||
{(day, dayIndex) => {
|
||||
const barX = x()(String(dayIndex())) ?? 0
|
||||
const barWidth = x().bandwidth()
|
||||
const stackTop = y()(usageTotal(day))
|
||||
return (
|
||||
<g
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
aria-label={`${day.date} ${formatTokens(usageTotal(day))}`}
|
||||
data-active={activeIndex() === dayIndex() ? "true" : undefined}
|
||||
onPointerEnter={() => {
|
||||
setActiveIndex(dayIndex())
|
||||
setActiveSegment(undefined)
|
||||
}}
|
||||
onPointerLeave={(event) => {
|
||||
if (event.pointerType === "touch") return
|
||||
setActiveIndex(undefined)
|
||||
setActiveSegment(undefined)
|
||||
}}
|
||||
onClick={() => setActiveIndex(dayIndex())}
|
||||
onFocus={() => {
|
||||
setActiveIndex(dayIndex())
|
||||
setActiveSegment(undefined)
|
||||
}}
|
||||
onBlur={() => {
|
||||
setActiveIndex(undefined)
|
||||
setActiveSegment(undefined)
|
||||
}}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key !== "Enter" && event.key !== " ") return
|
||||
event.preventDefault()
|
||||
setActiveIndex(dayIndex())
|
||||
}}
|
||||
>
|
||||
<rect
|
||||
x={barX}
|
||||
y="0"
|
||||
width={barWidth}
|
||||
height={height + headerOffset}
|
||||
fill="transparent"
|
||||
pointer-events="all"
|
||||
/>
|
||||
<text x={barX} y="17" class="chart-total">
|
||||
{formatTokens(usageTotal(day))}
|
||||
</text>
|
||||
<text x={barX} y="34" class="chart-date">
|
||||
{day.date}
|
||||
</text>
|
||||
<rect x={barX} y={headerOffset} width={barWidth} height={stackTop} fill="url(#stats-usage-dot-grid)" />
|
||||
<For each={day.segments}>
|
||||
{(segment, index) => {
|
||||
const previous = day.segments.slice(0, index()).reduce((sum, item) => sum + item.value, 0)
|
||||
const segmentHeight = y()(previous) - y()(previous + segment.value)
|
||||
const segmentInset = index() === day.segments.length - 1 ? 0 : segmentGap
|
||||
return (
|
||||
<rect
|
||||
x={barX}
|
||||
y={headerOffset + y()(previous + segment.value) + segmentInset}
|
||||
width={barWidth}
|
||||
height={Math.max(segmentHeight - segmentInset, 0)}
|
||||
data-segment-active={
|
||||
activeIndex() === dayIndex() && activeSegment() === index() ? "true" : undefined
|
||||
}
|
||||
opacity={getUsageSegmentOpacity(activeIndex() === dayIndex(), activeSegment(), index())}
|
||||
fill={activeIndex() === dayIndex() ? usageColors[index()] : "var(--stats-bar-idle)"}
|
||||
onPointerEnter={(event) => {
|
||||
event.stopPropagation()
|
||||
setActiveIndex(dayIndex())
|
||||
setActiveSegment(index())
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}}
|
||||
</For>
|
||||
</g>
|
||||
)
|
||||
}}
|
||||
</For>
|
||||
</svg>
|
||||
<Show when={activeBar()}>
|
||||
{(bar) => (
|
||||
<div
|
||||
data-component="chart-tooltip"
|
||||
data-placement={bar().x > width * 0.62 ? "left" : "right"}
|
||||
style={getUsageTooltipStyle(bar().x, bar().width, width)}
|
||||
>
|
||||
<strong>{bar().point.date}</strong>
|
||||
<span>{formatTokens(usageTotal(bar().point))} total</span>
|
||||
<div data-slot="tooltip-divider" />
|
||||
<For each={bar().point.segments}>
|
||||
{(segment, index) => (
|
||||
<p data-active={activeSegment() === index() ? "true" : undefined}>
|
||||
<span data-slot="tooltip-label">
|
||||
<i style={{ background: usageColors[index()] }} /> {segment.model}
|
||||
</span>
|
||||
<b>{formatTokens(segment.value)}</b>
|
||||
</p>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
)}
|
||||
</Show>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function getUsageTooltipStyle(barX: number, barWidth: number, width: number) {
|
||||
if (barX > width * 0.62) return { left: "auto", right: `${((width - barX + 12) / width) * 100}%` }
|
||||
return { left: `${((barX + barWidth + 12) / width) * 100}%`, right: "auto" }
|
||||
}
|
||||
|
||||
function getUsageSegmentOpacity(isActiveBar: boolean, activeSegment: number | undefined, index: number) {
|
||||
if (!isActiveBar) return 1
|
||||
if (activeSegment === undefined) return 1
|
||||
return activeSegment === index ? 1 : 0.38
|
||||
}
|
||||
|
||||
function usageTotal(point: UsagePoint) {
|
||||
return point.segments.reduce((sum, item) => sum + item.value, 0)
|
||||
}
|
||||
|
||||
function formatTokens(value: number) {
|
||||
if (value >= 1) return `${value.toFixed(value >= 10 ? 0 : 1)}T`
|
||||
return `${Math.round(value * 1000)}B`
|
||||
}
|
||||
|
||||
function LeaderboardSection(props: { data: StatsHomeData["leaderboard"] }) {
|
||||
const [product, setProduct] = createSignal<UsageProduct>("All Users")
|
||||
const [range, setRange] = createSignal<UsageRange>("1W")
|
||||
const data = createMemo(() => props.data[product()][range()])
|
||||
|
||||
return (
|
||||
<ChartSection
|
||||
title="Leaderboard"
|
||||
description="Shown are the sum of prompt and completion tokens per model, including reasoning tokens."
|
||||
>
|
||||
<Show
|
||||
when={data().length > 0}
|
||||
fallback={
|
||||
<EmptyState title="No leaderboard data" description="No model_stat rows matched this product and range." />
|
||||
}
|
||||
>
|
||||
<Leaderboard data={data()} />
|
||||
</Show>
|
||||
<div data-slot="chart-footer">
|
||||
<StatsFilters product={product()} range={range()} onProductSelect={setProduct} onRangeSelect={setRange} />
|
||||
</div>
|
||||
</ChartSection>
|
||||
)
|
||||
}
|
||||
|
||||
function Leaderboard(props: { data: LeaderboardEntry[] }) {
|
||||
return (
|
||||
<div data-component="leaderboard" aria-label="Model token leaderboard">
|
||||
<div data-slot="leaderboard-grid">
|
||||
<div data-slot="leaderboard-featured">
|
||||
<For each={props.data.slice(0, 3)}>{(entry) => <LeaderboardCard entry={entry} size="featured" />}</For>
|
||||
</div>
|
||||
<div data-slot="leaderboard-compact">
|
||||
<For each={props.data.slice(3)}>{(entry) => <LeaderboardCard entry={entry} size="compact" />}</For>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function LeaderboardCard(props: { entry: LeaderboardEntry; size: "featured" | "compact" }) {
|
||||
return (
|
||||
<article data-component="leader-card" data-size={props.size}>
|
||||
<span data-slot="rank">{String(props.entry.rank).padStart(2, "0")}</span>
|
||||
<ProviderIcon data-slot="leader-watermark" aria-hidden="true" id={getProviderIconId(props.entry.author)} />
|
||||
<div data-slot="leader-body">
|
||||
<ProviderIcon data-slot="leader-avatar" aria-hidden="true" id={getProviderIconId(props.entry.author)} />
|
||||
<div data-slot="leader-copy">
|
||||
<div>
|
||||
<strong>{props.entry.model}</strong>
|
||||
<span>{formatBillions(props.entry.tokens)}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span>{props.entry.author}</span>
|
||||
<span data-slot="delta" data-negative={props.entry.change < 0 ? "true" : undefined}>
|
||||
{formatChange(props.entry.change)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
)
|
||||
}
|
||||
|
||||
function getProviderIconId(author: string) {
|
||||
if (author === "MiniMax") return "minimax"
|
||||
if (author === "Moonshot") return "moonshotai"
|
||||
if (author === "Zhipu") return "zhipuai"
|
||||
return author.toLowerCase()
|
||||
}
|
||||
|
||||
function formatBillions(value: number) {
|
||||
if (value >= 1000) return `${(value / 1000).toFixed(value >= 10000 ? 0 : 1)}T`
|
||||
return `${value}B`
|
||||
}
|
||||
|
||||
function formatChange(value: number) {
|
||||
if (value > 0) return `+${value}%`
|
||||
return `${value}%`
|
||||
}
|
||||
|
||||
function MarketShareSection(props: { data: StatsHomeData["market"] }) {
|
||||
const [range, setRange] = createSignal<UsageRange>("1W")
|
||||
const [activeIndex, setActiveIndex] = createSignal(2)
|
||||
const data = createMemo(() => props.data[range()])
|
||||
const selectedIndex = createMemo(() => Math.min(activeIndex(), Math.max(data().length - 1, 0)))
|
||||
const activeDay = createMemo(() => data()[selectedIndex()])
|
||||
|
||||
return (
|
||||
<ChartSection title="Market Share" description="Compare token share by model author.">
|
||||
<Show
|
||||
when={activeDay()}
|
||||
fallback={<EmptyState title="No market data" description="No model_stat rows matched this range." />}
|
||||
>
|
||||
{(day) => (
|
||||
<>
|
||||
<MarketShare data={data()} activeIndex={selectedIndex()} onActiveIndexChange={setActiveIndex} />
|
||||
<MarketShareList data={day().authors} />
|
||||
</>
|
||||
)}
|
||||
</Show>
|
||||
<div data-slot="market-footer">
|
||||
<p>
|
||||
<span>[*]</span>
|
||||
<strong>{activeDay()?.date ?? "No data"}</strong>
|
||||
</p>
|
||||
<FilterPills items={ranges} selected={range()} label="Date range" variant="range" onSelect={setRange} />
|
||||
</div>
|
||||
</ChartSection>
|
||||
)
|
||||
}
|
||||
|
||||
function MarketShare(props: { data: MarketDay[]; activeIndex: number; onActiveIndexChange: (index: number) => void }) {
|
||||
return (
|
||||
<div data-component="market-share" role="img" aria-label="Market share by model author">
|
||||
<div data-slot="market-labels">
|
||||
<For each={props.data}>
|
||||
{(day, index) => (
|
||||
<button
|
||||
type="button"
|
||||
data-active={props.activeIndex === index() ? "true" : undefined}
|
||||
onClick={() => props.onActiveIndexChange(index())}
|
||||
>
|
||||
<span>{formatTrillions(day.total)}</span>
|
||||
<span>{day.date}</span>
|
||||
</button>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
<div data-slot="market-bars">
|
||||
<For each={props.data}>
|
||||
{(day, index) => (
|
||||
<button
|
||||
type="button"
|
||||
aria-label={`${day.date} ${formatTrillions(day.total)}`}
|
||||
data-active={props.activeIndex === index() ? "true" : undefined}
|
||||
onClick={() => props.onActiveIndexChange(index())}
|
||||
>
|
||||
<For each={day.authors}>
|
||||
{(author, authorIndex) => (
|
||||
<span
|
||||
style={{
|
||||
"background-color": props.activeIndex === index() ? marketColors[authorIndex()] : undefined,
|
||||
"flex-grow": author.share,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</For>
|
||||
</button>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function MarketShareList(props: { data: MarketDay["authors"] }) {
|
||||
return (
|
||||
<ol data-component="market-share-list">
|
||||
<For each={props.data}>
|
||||
{(item, index) => (
|
||||
<li>
|
||||
<span>{String(index() + 1).padStart(2, "0")}</span>
|
||||
<i style={{ background: marketColors[index()] }} />
|
||||
<strong>{item.author}</strong>
|
||||
<em>{formatTrillions(item.tokens)}</em>
|
||||
<b>{item.share.toFixed(1)}%</b>
|
||||
</li>
|
||||
)}
|
||||
</For>
|
||||
</ol>
|
||||
)
|
||||
}
|
||||
|
||||
function formatTrillions(value: number) {
|
||||
return `${value.toFixed(value >= 10 ? 0 : 1)}T`
|
||||
}
|
||||
|
||||
function TokenCostSection(props: { data: StatsHomeData["tokenCost"] }) {
|
||||
const [product, setProduct] = createSignal<TokenProduct>("Zen")
|
||||
const [activeIndex, setActiveIndex] = createSignal(2)
|
||||
const data = createMemo(() => props.data[product()])
|
||||
const selectedIndex = createMemo(() => Math.min(activeIndex(), Math.max(data().length - 1, 0)))
|
||||
|
||||
return (
|
||||
<ChartSection title="Token Cost" description="Price per 1M tokens.">
|
||||
<Show
|
||||
when={data().length > 0}
|
||||
fallback={
|
||||
<EmptyState title="No token cost data" description="No cost-bearing model_stat rows matched this product." />
|
||||
}
|
||||
>
|
||||
<TokenCostChart data={data()} activeIndex={selectedIndex()} onActiveIndexChange={setActiveIndex} />
|
||||
</Show>
|
||||
<div data-slot="token-footer">
|
||||
<FilterPills
|
||||
items={tokenProducts}
|
||||
selected={product()}
|
||||
label="Product filter"
|
||||
variant="product"
|
||||
onSelect={setProduct}
|
||||
/>
|
||||
</div>
|
||||
</ChartSection>
|
||||
)
|
||||
}
|
||||
|
||||
function TokenCostChart(props: {
|
||||
data: TokenCostEntry[]
|
||||
activeIndex: number
|
||||
onActiveIndexChange: (index: number) => void
|
||||
}) {
|
||||
const max = createMemo(() => Math.max(1, ...props.data.map((item) => item.total)))
|
||||
const active = createMemo(() => props.data[props.activeIndex] ?? props.data[0])
|
||||
|
||||
return (
|
||||
<div data-component="token-cost">
|
||||
<For each={props.data}>
|
||||
{(item, index) => (
|
||||
<button
|
||||
type="button"
|
||||
data-component="token-row"
|
||||
data-active={props.activeIndex === index() ? "true" : undefined}
|
||||
onClick={() => props.onActiveIndexChange(index())}
|
||||
onPointerEnter={() => props.onActiveIndexChange(index())}
|
||||
>
|
||||
<strong>{formatDollars(item.total)}</strong>
|
||||
<span>{item.model}</span>
|
||||
<MetricBar value={item.total} max={max()} active={props.activeIndex === index()} />
|
||||
</button>
|
||||
)}
|
||||
</For>
|
||||
<Show when={active()}>
|
||||
{(item) => (
|
||||
<div data-component="token-tooltip" style={{ top: `${props.activeIndex * 28 + 2}px` }}>
|
||||
<p>
|
||||
<span>Input</span>
|
||||
<strong>{formatDollars(item().input)}</strong>
|
||||
</p>
|
||||
<p>
|
||||
<span>Output</span>
|
||||
<strong>{formatDollars(item().output)}</strong>
|
||||
</p>
|
||||
<p>
|
||||
<span>Cached</span>
|
||||
<strong>{formatDollars(item().cached)}</strong>
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</Show>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function formatDollars(value: number) {
|
||||
return `$${value.toFixed(2)}`
|
||||
}
|
||||
|
||||
function MetricBar(props: { value: number; max: number; active: boolean }) {
|
||||
return (
|
||||
<i data-component="metric-bar" data-active={props.active ? "true" : undefined}>
|
||||
<b style={{ "flex-grow": Math.max(props.value / Math.max(props.max, 1), 0.05) }} />
|
||||
<em />
|
||||
</i>
|
||||
)
|
||||
}
|
||||
|
||||
function SessionCostSection(props: { data: StatsHomeData["sessionCost"] }) {
|
||||
const [product, setProduct] = createSignal<TokenProduct>("Zen")
|
||||
const [activeIndex, setActiveIndex] = createSignal(2)
|
||||
const data = createMemo(() => props.data[product()])
|
||||
const selectedIndex = createMemo(() => Math.min(activeIndex(), Math.max(data().length - 1, 0)))
|
||||
|
||||
return (
|
||||
<ChartSection title="Session Cost" description="Average cost per session.">
|
||||
<Show
|
||||
when={data().length > 0}
|
||||
fallback={
|
||||
<EmptyState
|
||||
title="No session cost data"
|
||||
description="No session-bearing model_stat rows matched this product."
|
||||
/>
|
||||
}
|
||||
>
|
||||
<SessionCostChart data={data()} activeIndex={selectedIndex()} onActiveIndexChange={setActiveIndex} />
|
||||
</Show>
|
||||
<div data-slot="token-footer">
|
||||
<FilterPills
|
||||
items={tokenProducts}
|
||||
selected={product()}
|
||||
label="Product filter"
|
||||
variant="product"
|
||||
onSelect={setProduct}
|
||||
/>
|
||||
</div>
|
||||
</ChartSection>
|
||||
)
|
||||
}
|
||||
|
||||
function SessionCostChart(props: {
|
||||
data: SessionCostEntry[]
|
||||
activeIndex: number
|
||||
onActiveIndexChange: (index: number) => void
|
||||
}) {
|
||||
const maxCost = createMemo(() => Math.max(1, ...props.data.map((item) => item.cost)))
|
||||
const maxTokens = createMemo(() => Math.max(1, ...props.data.map((item) => item.tokens)))
|
||||
const active = createMemo(() => props.data[props.activeIndex] ?? props.data[0])
|
||||
|
||||
return (
|
||||
<div data-component="session-cost">
|
||||
<div data-slot="session-heading">
|
||||
<span />
|
||||
<p>COST / SESSION</p>
|
||||
<p>TOKENS / SESSIONS</p>
|
||||
</div>
|
||||
<For each={props.data}>
|
||||
{(item, index) => (
|
||||
<button
|
||||
type="button"
|
||||
data-component="token-row"
|
||||
data-variant="session"
|
||||
data-active={props.activeIndex === index() ? "true" : undefined}
|
||||
onClick={() => props.onActiveIndexChange(index())}
|
||||
onPointerEnter={() => props.onActiveIndexChange(index())}
|
||||
>
|
||||
<strong>{formatSessionCost(item.cost)}</strong>
|
||||
<span>{item.model}</span>
|
||||
<MetricBar value={item.cost} max={maxCost()} active={props.activeIndex === index()} />
|
||||
<MetricBar value={item.tokens} max={maxTokens()} active={props.activeIndex === index()} />
|
||||
</button>
|
||||
)}
|
||||
</For>
|
||||
<Show when={active()}>
|
||||
{(item) => (
|
||||
<div
|
||||
data-component="token-tooltip"
|
||||
data-variant="session"
|
||||
style={{ top: `${props.activeIndex * 28 + 21}px` }}
|
||||
>
|
||||
<p>
|
||||
<span>Cost/Session</span>
|
||||
<strong>{formatSessionCost(item().cost)}</strong>
|
||||
</p>
|
||||
<p>
|
||||
<span>Tokens/Session</span>
|
||||
<strong>{formatTokenCount(item().tokens)}</strong>
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</Show>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function formatTokenCount(value: number) {
|
||||
if (value >= 1_000_000) return `${Number((value / 1_000_000).toFixed(1))}M`
|
||||
return `${Math.round(value / 1_000)}K`
|
||||
}
|
||||
|
||||
function formatSessionCost(value: number) {
|
||||
return `$${value.toFixed(4)}`
|
||||
}
|
||||
|
||||
function CountrySection(props: { data: StatsHomeData["country"] }) {
|
||||
const [range, setRange] = createSignal<UsageRange>("1W")
|
||||
const data = createMemo(() => props.data[range()])
|
||||
|
||||
return (
|
||||
<ChartSection title="Token by Country" description="Country-level token totals from geo_stat.">
|
||||
<Show
|
||||
when={data().length > 0}
|
||||
fallback={<EmptyState title="No country data" description="No geo_stat rows matched this range." />}
|
||||
>
|
||||
<CountryChart data={data()} />
|
||||
</Show>
|
||||
<div data-slot="country-footer">
|
||||
<p>
|
||||
<span>[*]</span>
|
||||
<strong>Top countries by tokens</strong>
|
||||
</p>
|
||||
<FilterPills items={ranges} selected={range()} label="Date range" variant="range" onSelect={setRange} />
|
||||
</div>
|
||||
</ChartSection>
|
||||
)
|
||||
}
|
||||
|
||||
function CountryChart(props: { data: CountryEntry[] }) {
|
||||
const [activeIndex, setActiveIndex] = createSignal(0)
|
||||
const selectedIndex = createMemo(() => Math.min(activeIndex(), Math.max(props.data.length - 1, 0)))
|
||||
const active = createMemo(() => props.data[selectedIndex()])
|
||||
const max = createMemo(() => Math.max(0.0001, ...props.data.map((item) => item.tokens)))
|
||||
|
||||
return (
|
||||
<div data-component="country-map">
|
||||
<svg viewBox="0 0 920 320" role="img" aria-label="Country token share bubble chart">
|
||||
<For each={props.data.slice(0, countryPositions.length)}>
|
||||
{(item, index) => {
|
||||
const position = countryPositions[index()]
|
||||
const radius = 18 + Math.sqrt(item.tokens / max()) * 58
|
||||
return (
|
||||
<g
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
aria-label={`${formatCountry(item.country)} ${formatTokens(item.tokens)}`}
|
||||
data-active={selectedIndex() === index() ? "true" : undefined}
|
||||
onPointerEnter={() => setActiveIndex(index())}
|
||||
onClick={() => setActiveIndex(index())}
|
||||
onFocus={() => setActiveIndex(index())}
|
||||
>
|
||||
<circle cx={position.x} cy={position.y} r={radius} />
|
||||
<text x={position.x} y={position.y + 4} text-anchor="middle">
|
||||
{item.country}
|
||||
</text>
|
||||
</g>
|
||||
)
|
||||
}}
|
||||
</For>
|
||||
</svg>
|
||||
<Show when={active()}>
|
||||
{(item) => (
|
||||
<div data-component="map-tooltip">
|
||||
<strong>{formatCountry(item().country)}</strong>
|
||||
<span>{item().continent || "Unknown region"}</span>
|
||||
<p>
|
||||
<b>{formatTokens(item().tokens)}</b>
|
||||
<em>{item().share.toFixed(1)}%</em>
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</Show>
|
||||
<CountryList data={props.data.slice(0, 8)} activeIndex={selectedIndex()} onActiveIndexChange={setActiveIndex} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function CountryList(props: {
|
||||
data: CountryEntry[]
|
||||
activeIndex: number
|
||||
onActiveIndexChange: (index: number) => void
|
||||
}) {
|
||||
return (
|
||||
<ol data-component="country-list">
|
||||
<For each={props.data}>
|
||||
{(item, index) => (
|
||||
<li>
|
||||
<button
|
||||
type="button"
|
||||
data-active={props.activeIndex === index() ? "true" : undefined}
|
||||
onClick={() => props.onActiveIndexChange(index())}
|
||||
onPointerEnter={() => props.onActiveIndexChange(index())}
|
||||
>
|
||||
<span>{String(item.rank).padStart(2, "0")}</span>
|
||||
<strong>{formatCountry(item.country)}</strong>
|
||||
<em>{formatTokens(item.tokens)}</em>
|
||||
<b>{item.share.toFixed(1)}%</b>
|
||||
</button>
|
||||
</li>
|
||||
)}
|
||||
</For>
|
||||
</ol>
|
||||
)
|
||||
}
|
||||
|
||||
function formatCountry(country: string) {
|
||||
const known: Record<string, string> = {
|
||||
AU: "Australia",
|
||||
BR: "Brazil",
|
||||
CA: "Canada",
|
||||
CN: "China",
|
||||
DE: "Germany",
|
||||
FR: "France",
|
||||
GB: "United Kingdom",
|
||||
IN: "India",
|
||||
JP: "Japan",
|
||||
KR: "South Korea",
|
||||
NL: "Netherlands",
|
||||
SG: "Singapore",
|
||||
US: "United States",
|
||||
ZZ: "Unknown",
|
||||
}
|
||||
return known[country] ?? country
|
||||
}
|
||||
|
||||
function Newsletter() {
|
||||
return (
|
||||
<section data-section="newsletter">
|
||||
<div>
|
||||
<h2>Be the first to know when we release new products</h2>
|
||||
<p>Join the waitlist for early access.</p>
|
||||
</div>
|
||||
<form>
|
||||
<input type="email" placeholder="Email address" />
|
||||
<button>Subscribe</button>
|
||||
</form>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
function Header() {
|
||||
return (
|
||||
<section data-component="top">
|
||||
<a data-slot="brand" href="https://opencode.ai/" aria-label="OpenCode home">
|
||||
<img data-slot="logo light" src={logoLight} alt="OpenCode" width="234" height="42" />
|
||||
<img data-slot="logo dark" src={logoDark} alt="OpenCode" width="234" height="42" />
|
||||
</a>
|
||||
<nav data-component="nav-desktop" aria-label="Main navigation">
|
||||
<ul>
|
||||
<li>
|
||||
<a href="https://github.com/sst/opencode" target="_blank" rel="noreferrer">
|
||||
GitHub
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://opencode.ai/docs">Docs</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://opencode.ai/zen">Zen</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://opencode.ai/go">Go</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://opencode.ai/enterprise">Enterprise</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://opencode.ai/download" data-slot="cta-button">
|
||||
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" aria-hidden="true">
|
||||
<path
|
||||
d="M12.1875 9.75L9.00001 12.9375L5.8125 9.75M9.00001 2.0625L9 12.375M14.4375 15.9375H3.5625"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.5"
|
||||
stroke-linecap="square"
|
||||
/>
|
||||
</svg>
|
||||
Download
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
function Footer() {
|
||||
return (
|
||||
<footer data-component="footer">
|
||||
<div data-slot="cell">
|
||||
<a href="https://github.com/sst/opencode" target="_blank" rel="noreferrer">
|
||||
GitHub
|
||||
</a>
|
||||
</div>
|
||||
<div data-slot="cell">
|
||||
<a href="https://opencode.ai/docs">Docs</a>
|
||||
</div>
|
||||
<div data-slot="cell">
|
||||
<a href="https://opencode.ai/changelog">Changelog</a>
|
||||
</div>
|
||||
<div data-slot="cell">
|
||||
<a href="https://x.com/opencode_ai">X</a>
|
||||
</div>
|
||||
</footer>
|
||||
)
|
||||
}
|
||||
|
||||
function Legal() {
|
||||
return (
|
||||
<div data-component="legal">
|
||||
<span>
|
||||
©{new Date().getFullYear()} <a href="https://anoma.ly">Anomaly</a>
|
||||
</span>
|
||||
<span>
|
||||
<a href="https://opencode.ai/brand">Brand</a>
|
||||
</span>
|
||||
<span>
|
||||
<a href="https://opencode.ai/legal/privacy-policy">Privacy</a>
|
||||
</span>
|
||||
<span>
|
||||
<a href="https://opencode.ai/legal/terms-of-service">Terms</a>
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
10
packages/stats/app/sst-env.d.ts
vendored
Normal file
10
packages/stats/app/sst-env.d.ts
vendored
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
/* This file is auto-generated by SST. Do not edit. */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
/* deno-fmt-ignore-file */
|
||||
/* biome-ignore-all lint: auto-generated */
|
||||
|
||||
/// <reference path="../../../sst-env.d.ts" />
|
||||
|
||||
import "sst"
|
||||
export {}
|
||||
21
packages/stats/app/tsconfig.json
Normal file
21
packages/stats/app/tsconfig.json
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
"moduleResolution": "bundler",
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"esModuleInterop": true,
|
||||
"jsx": "preserve",
|
||||
"jsxImportSource": "solid-js",
|
||||
"allowJs": true,
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"types": ["vite/client", "bun"],
|
||||
"isolatedModules": true,
|
||||
"paths": {
|
||||
"~/*": ["./src/*"]
|
||||
}
|
||||
}
|
||||
}
|
||||
22
packages/stats/app/vite.config.ts
Normal file
22
packages/stats/app/vite.config.ts
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
import { solidStart } from "@solidjs/start/config"
|
||||
import { nitro } from "nitro/vite"
|
||||
import { defineConfig, type PluginOption } from "vite"
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
solidStart() as PluginOption,
|
||||
nitro({
|
||||
compatibilityDate: "2024-09-19",
|
||||
preset: "cloudflare-module",
|
||||
cloudflare: {
|
||||
nodeCompat: true,
|
||||
},
|
||||
}),
|
||||
],
|
||||
server: {
|
||||
allowedHosts: true,
|
||||
},
|
||||
build: {
|
||||
minify: false,
|
||||
},
|
||||
})
|
||||
21
packages/stats/core/drizzle.config.ts
Normal file
21
packages/stats/core/drizzle.config.ts
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
import { Resource } from "sst/resource"
|
||||
import { defineConfig } from "drizzle-kit"
|
||||
|
||||
export default defineConfig({
|
||||
dialect: "mysql",
|
||||
schema: ["./src/database/schema.ts"],
|
||||
// schema: ["./src/**/*.sql.ts"],
|
||||
out: "./migrations/",
|
||||
strict: true,
|
||||
verbose: true,
|
||||
dbCredentials: {
|
||||
database: Resource.StatsDatabase.database,
|
||||
host: Resource.StatsDatabase.host,
|
||||
user: Resource.StatsDatabase.username,
|
||||
password: Resource.StatsDatabase.password,
|
||||
port: Resource.StatsDatabase.port,
|
||||
ssl: {
|
||||
rejectUnauthorized: false,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
CREATE TABLE `stat` (
|
||||
`id` bigint AUTO_INCREMENT PRIMARY KEY,
|
||||
`grain` varchar(16) NOT NULL,
|
||||
`period_start` datetime NOT NULL,
|
||||
`period_end` datetime NOT NULL,
|
||||
`dataset` varchar(64) NOT NULL DEFAULT 'all',
|
||||
`tier` varchar(64) NOT NULL DEFAULT 'all',
|
||||
`client` varchar(64) NOT NULL DEFAULT 'all',
|
||||
`source` varchar(64) NOT NULL DEFAULT 'all',
|
||||
`provider` varchar(128) NOT NULL,
|
||||
`model` varchar(256) NOT NULL,
|
||||
`provider_model` varchar(256) NOT NULL DEFAULT '',
|
||||
`sessions` bigint NOT NULL DEFAULT 0,
|
||||
`requests` bigint NOT NULL DEFAULT 0,
|
||||
`input_tokens` bigint NOT NULL DEFAULT 0,
|
||||
`output_tokens` bigint NOT NULL DEFAULT 0,
|
||||
`reasoning_tokens` bigint NOT NULL DEFAULT 0,
|
||||
`cache_read_tokens` bigint NOT NULL DEFAULT 0,
|
||||
`total_tokens` bigint NOT NULL DEFAULT 0,
|
||||
`input_cost_microcents` bigint NOT NULL DEFAULT 0,
|
||||
`output_cost_microcents` bigint NOT NULL DEFAULT 0,
|
||||
`total_cost_microcents` bigint NOT NULL DEFAULT 0,
|
||||
`avg_duration_ms` decimal(12,2),
|
||||
`p50_duration_ms` int,
|
||||
`p95_duration_ms` int,
|
||||
`avg_ttfb_ms` decimal(12,2),
|
||||
`p50_ttfb_ms` int,
|
||||
`p95_ttfb_ms` int,
|
||||
`avg_output_tps` decimal(12,4),
|
||||
`success_count` bigint NOT NULL DEFAULT 0,
|
||||
`error_count` bigint NOT NULL DEFAULT 0,
|
||||
`sample_count` bigint NOT NULL DEFAULT 0,
|
||||
`rank_by_tokens` int,
|
||||
`rank_by_requests` int,
|
||||
`rank_by_cost` int,
|
||||
`created_at` datetime NOT NULL DEFAULT (now()),
|
||||
`updated_at` datetime NOT NULL DEFAULT (now()) ON UPDATE CURRENT_TIMESTAMP,
|
||||
CONSTRAINT `uniq_model_period` UNIQUE INDEX(`grain`,`period_start`,`dataset`,`tier`,`client`,`source`,`provider`,`model`)
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE INDEX `idx_leaderboard_tokens` ON `stat` (`grain`,`period_start`,`dataset`,`tier`,`total_tokens`);--> statement-breakpoint
|
||||
CREATE INDEX `idx_model` ON `stat` (`model`,`grain`,`period_start`);
|
||||
|
|
@ -0,0 +1,627 @@
|
|||
{
|
||||
"version": "6",
|
||||
"dialect": "mysql",
|
||||
"id": "72655266-65da-408e-bfd8-9f3a4ad817a5",
|
||||
"prevIds": [
|
||||
"00000000-0000-0000-0000-000000000000"
|
||||
],
|
||||
"ddl": [
|
||||
{
|
||||
"name": "stat",
|
||||
"entityType": "tables"
|
||||
},
|
||||
{
|
||||
"type": "bigint",
|
||||
"notNull": true,
|
||||
"autoIncrement": true,
|
||||
"default": null,
|
||||
"onUpdateNow": false,
|
||||
"onUpdateNowFsp": null,
|
||||
"charSet": null,
|
||||
"collation": null,
|
||||
"generated": null,
|
||||
"name": "id",
|
||||
"entityType": "columns",
|
||||
"table": "stat"
|
||||
},
|
||||
{
|
||||
"type": "varchar(16)",
|
||||
"notNull": true,
|
||||
"autoIncrement": false,
|
||||
"default": null,
|
||||
"onUpdateNow": false,
|
||||
"onUpdateNowFsp": null,
|
||||
"charSet": null,
|
||||
"collation": null,
|
||||
"generated": null,
|
||||
"name": "grain",
|
||||
"entityType": "columns",
|
||||
"table": "stat"
|
||||
},
|
||||
{
|
||||
"type": "datetime",
|
||||
"notNull": true,
|
||||
"autoIncrement": false,
|
||||
"default": null,
|
||||
"onUpdateNow": false,
|
||||
"onUpdateNowFsp": null,
|
||||
"charSet": null,
|
||||
"collation": null,
|
||||
"generated": null,
|
||||
"name": "period_start",
|
||||
"entityType": "columns",
|
||||
"table": "stat"
|
||||
},
|
||||
{
|
||||
"type": "datetime",
|
||||
"notNull": true,
|
||||
"autoIncrement": false,
|
||||
"default": null,
|
||||
"onUpdateNow": false,
|
||||
"onUpdateNowFsp": null,
|
||||
"charSet": null,
|
||||
"collation": null,
|
||||
"generated": null,
|
||||
"name": "period_end",
|
||||
"entityType": "columns",
|
||||
"table": "stat"
|
||||
},
|
||||
{
|
||||
"type": "varchar(64)",
|
||||
"notNull": true,
|
||||
"autoIncrement": false,
|
||||
"default": "'all'",
|
||||
"onUpdateNow": false,
|
||||
"onUpdateNowFsp": null,
|
||||
"charSet": null,
|
||||
"collation": null,
|
||||
"generated": null,
|
||||
"name": "dataset",
|
||||
"entityType": "columns",
|
||||
"table": "stat"
|
||||
},
|
||||
{
|
||||
"type": "varchar(64)",
|
||||
"notNull": true,
|
||||
"autoIncrement": false,
|
||||
"default": "'all'",
|
||||
"onUpdateNow": false,
|
||||
"onUpdateNowFsp": null,
|
||||
"charSet": null,
|
||||
"collation": null,
|
||||
"generated": null,
|
||||
"name": "tier",
|
||||
"entityType": "columns",
|
||||
"table": "stat"
|
||||
},
|
||||
{
|
||||
"type": "varchar(64)",
|
||||
"notNull": true,
|
||||
"autoIncrement": false,
|
||||
"default": "'all'",
|
||||
"onUpdateNow": false,
|
||||
"onUpdateNowFsp": null,
|
||||
"charSet": null,
|
||||
"collation": null,
|
||||
"generated": null,
|
||||
"name": "client",
|
||||
"entityType": "columns",
|
||||
"table": "stat"
|
||||
},
|
||||
{
|
||||
"type": "varchar(64)",
|
||||
"notNull": true,
|
||||
"autoIncrement": false,
|
||||
"default": "'all'",
|
||||
"onUpdateNow": false,
|
||||
"onUpdateNowFsp": null,
|
||||
"charSet": null,
|
||||
"collation": null,
|
||||
"generated": null,
|
||||
"name": "source",
|
||||
"entityType": "columns",
|
||||
"table": "stat"
|
||||
},
|
||||
{
|
||||
"type": "varchar(128)",
|
||||
"notNull": true,
|
||||
"autoIncrement": false,
|
||||
"default": null,
|
||||
"onUpdateNow": false,
|
||||
"onUpdateNowFsp": null,
|
||||
"charSet": null,
|
||||
"collation": null,
|
||||
"generated": null,
|
||||
"name": "provider",
|
||||
"entityType": "columns",
|
||||
"table": "stat"
|
||||
},
|
||||
{
|
||||
"type": "varchar(256)",
|
||||
"notNull": true,
|
||||
"autoIncrement": false,
|
||||
"default": null,
|
||||
"onUpdateNow": false,
|
||||
"onUpdateNowFsp": null,
|
||||
"charSet": null,
|
||||
"collation": null,
|
||||
"generated": null,
|
||||
"name": "model",
|
||||
"entityType": "columns",
|
||||
"table": "stat"
|
||||
},
|
||||
{
|
||||
"type": "varchar(256)",
|
||||
"notNull": true,
|
||||
"autoIncrement": false,
|
||||
"default": "''",
|
||||
"onUpdateNow": false,
|
||||
"onUpdateNowFsp": null,
|
||||
"charSet": null,
|
||||
"collation": null,
|
||||
"generated": null,
|
||||
"name": "provider_model",
|
||||
"entityType": "columns",
|
||||
"table": "stat"
|
||||
},
|
||||
{
|
||||
"type": "bigint",
|
||||
"notNull": true,
|
||||
"autoIncrement": false,
|
||||
"default": "0",
|
||||
"onUpdateNow": false,
|
||||
"onUpdateNowFsp": null,
|
||||
"charSet": null,
|
||||
"collation": null,
|
||||
"generated": null,
|
||||
"name": "sessions",
|
||||
"entityType": "columns",
|
||||
"table": "stat"
|
||||
},
|
||||
{
|
||||
"type": "bigint",
|
||||
"notNull": true,
|
||||
"autoIncrement": false,
|
||||
"default": "0",
|
||||
"onUpdateNow": false,
|
||||
"onUpdateNowFsp": null,
|
||||
"charSet": null,
|
||||
"collation": null,
|
||||
"generated": null,
|
||||
"name": "requests",
|
||||
"entityType": "columns",
|
||||
"table": "stat"
|
||||
},
|
||||
{
|
||||
"type": "bigint",
|
||||
"notNull": true,
|
||||
"autoIncrement": false,
|
||||
"default": "0",
|
||||
"onUpdateNow": false,
|
||||
"onUpdateNowFsp": null,
|
||||
"charSet": null,
|
||||
"collation": null,
|
||||
"generated": null,
|
||||
"name": "input_tokens",
|
||||
"entityType": "columns",
|
||||
"table": "stat"
|
||||
},
|
||||
{
|
||||
"type": "bigint",
|
||||
"notNull": true,
|
||||
"autoIncrement": false,
|
||||
"default": "0",
|
||||
"onUpdateNow": false,
|
||||
"onUpdateNowFsp": null,
|
||||
"charSet": null,
|
||||
"collation": null,
|
||||
"generated": null,
|
||||
"name": "output_tokens",
|
||||
"entityType": "columns",
|
||||
"table": "stat"
|
||||
},
|
||||
{
|
||||
"type": "bigint",
|
||||
"notNull": true,
|
||||
"autoIncrement": false,
|
||||
"default": "0",
|
||||
"onUpdateNow": false,
|
||||
"onUpdateNowFsp": null,
|
||||
"charSet": null,
|
||||
"collation": null,
|
||||
"generated": null,
|
||||
"name": "reasoning_tokens",
|
||||
"entityType": "columns",
|
||||
"table": "stat"
|
||||
},
|
||||
{
|
||||
"type": "bigint",
|
||||
"notNull": true,
|
||||
"autoIncrement": false,
|
||||
"default": "0",
|
||||
"onUpdateNow": false,
|
||||
"onUpdateNowFsp": null,
|
||||
"charSet": null,
|
||||
"collation": null,
|
||||
"generated": null,
|
||||
"name": "cache_read_tokens",
|
||||
"entityType": "columns",
|
||||
"table": "stat"
|
||||
},
|
||||
{
|
||||
"type": "bigint",
|
||||
"notNull": true,
|
||||
"autoIncrement": false,
|
||||
"default": "0",
|
||||
"onUpdateNow": false,
|
||||
"onUpdateNowFsp": null,
|
||||
"charSet": null,
|
||||
"collation": null,
|
||||
"generated": null,
|
||||
"name": "total_tokens",
|
||||
"entityType": "columns",
|
||||
"table": "stat"
|
||||
},
|
||||
{
|
||||
"type": "bigint",
|
||||
"notNull": true,
|
||||
"autoIncrement": false,
|
||||
"default": "0",
|
||||
"onUpdateNow": false,
|
||||
"onUpdateNowFsp": null,
|
||||
"charSet": null,
|
||||
"collation": null,
|
||||
"generated": null,
|
||||
"name": "input_cost_microcents",
|
||||
"entityType": "columns",
|
||||
"table": "stat"
|
||||
},
|
||||
{
|
||||
"type": "bigint",
|
||||
"notNull": true,
|
||||
"autoIncrement": false,
|
||||
"default": "0",
|
||||
"onUpdateNow": false,
|
||||
"onUpdateNowFsp": null,
|
||||
"charSet": null,
|
||||
"collation": null,
|
||||
"generated": null,
|
||||
"name": "output_cost_microcents",
|
||||
"entityType": "columns",
|
||||
"table": "stat"
|
||||
},
|
||||
{
|
||||
"type": "bigint",
|
||||
"notNull": true,
|
||||
"autoIncrement": false,
|
||||
"default": "0",
|
||||
"onUpdateNow": false,
|
||||
"onUpdateNowFsp": null,
|
||||
"charSet": null,
|
||||
"collation": null,
|
||||
"generated": null,
|
||||
"name": "total_cost_microcents",
|
||||
"entityType": "columns",
|
||||
"table": "stat"
|
||||
},
|
||||
{
|
||||
"type": "decimal(12,2)",
|
||||
"notNull": false,
|
||||
"autoIncrement": false,
|
||||
"default": null,
|
||||
"onUpdateNow": false,
|
||||
"onUpdateNowFsp": null,
|
||||
"charSet": null,
|
||||
"collation": null,
|
||||
"generated": null,
|
||||
"name": "avg_duration_ms",
|
||||
"entityType": "columns",
|
||||
"table": "stat"
|
||||
},
|
||||
{
|
||||
"type": "int",
|
||||
"notNull": false,
|
||||
"autoIncrement": false,
|
||||
"default": null,
|
||||
"onUpdateNow": false,
|
||||
"onUpdateNowFsp": null,
|
||||
"charSet": null,
|
||||
"collation": null,
|
||||
"generated": null,
|
||||
"name": "p50_duration_ms",
|
||||
"entityType": "columns",
|
||||
"table": "stat"
|
||||
},
|
||||
{
|
||||
"type": "int",
|
||||
"notNull": false,
|
||||
"autoIncrement": false,
|
||||
"default": null,
|
||||
"onUpdateNow": false,
|
||||
"onUpdateNowFsp": null,
|
||||
"charSet": null,
|
||||
"collation": null,
|
||||
"generated": null,
|
||||
"name": "p95_duration_ms",
|
||||
"entityType": "columns",
|
||||
"table": "stat"
|
||||
},
|
||||
{
|
||||
"type": "decimal(12,2)",
|
||||
"notNull": false,
|
||||
"autoIncrement": false,
|
||||
"default": null,
|
||||
"onUpdateNow": false,
|
||||
"onUpdateNowFsp": null,
|
||||
"charSet": null,
|
||||
"collation": null,
|
||||
"generated": null,
|
||||
"name": "avg_ttfb_ms",
|
||||
"entityType": "columns",
|
||||
"table": "stat"
|
||||
},
|
||||
{
|
||||
"type": "int",
|
||||
"notNull": false,
|
||||
"autoIncrement": false,
|
||||
"default": null,
|
||||
"onUpdateNow": false,
|
||||
"onUpdateNowFsp": null,
|
||||
"charSet": null,
|
||||
"collation": null,
|
||||
"generated": null,
|
||||
"name": "p50_ttfb_ms",
|
||||
"entityType": "columns",
|
||||
"table": "stat"
|
||||
},
|
||||
{
|
||||
"type": "int",
|
||||
"notNull": false,
|
||||
"autoIncrement": false,
|
||||
"default": null,
|
||||
"onUpdateNow": false,
|
||||
"onUpdateNowFsp": null,
|
||||
"charSet": null,
|
||||
"collation": null,
|
||||
"generated": null,
|
||||
"name": "p95_ttfb_ms",
|
||||
"entityType": "columns",
|
||||
"table": "stat"
|
||||
},
|
||||
{
|
||||
"type": "decimal(12,4)",
|
||||
"notNull": false,
|
||||
"autoIncrement": false,
|
||||
"default": null,
|
||||
"onUpdateNow": false,
|
||||
"onUpdateNowFsp": null,
|
||||
"charSet": null,
|
||||
"collation": null,
|
||||
"generated": null,
|
||||
"name": "avg_output_tps",
|
||||
"entityType": "columns",
|
||||
"table": "stat"
|
||||
},
|
||||
{
|
||||
"type": "bigint",
|
||||
"notNull": true,
|
||||
"autoIncrement": false,
|
||||
"default": "0",
|
||||
"onUpdateNow": false,
|
||||
"onUpdateNowFsp": null,
|
||||
"charSet": null,
|
||||
"collation": null,
|
||||
"generated": null,
|
||||
"name": "success_count",
|
||||
"entityType": "columns",
|
||||
"table": "stat"
|
||||
},
|
||||
{
|
||||
"type": "bigint",
|
||||
"notNull": true,
|
||||
"autoIncrement": false,
|
||||
"default": "0",
|
||||
"onUpdateNow": false,
|
||||
"onUpdateNowFsp": null,
|
||||
"charSet": null,
|
||||
"collation": null,
|
||||
"generated": null,
|
||||
"name": "error_count",
|
||||
"entityType": "columns",
|
||||
"table": "stat"
|
||||
},
|
||||
{
|
||||
"type": "bigint",
|
||||
"notNull": true,
|
||||
"autoIncrement": false,
|
||||
"default": "0",
|
||||
"onUpdateNow": false,
|
||||
"onUpdateNowFsp": null,
|
||||
"charSet": null,
|
||||
"collation": null,
|
||||
"generated": null,
|
||||
"name": "sample_count",
|
||||
"entityType": "columns",
|
||||
"table": "stat"
|
||||
},
|
||||
{
|
||||
"type": "int",
|
||||
"notNull": false,
|
||||
"autoIncrement": false,
|
||||
"default": null,
|
||||
"onUpdateNow": false,
|
||||
"onUpdateNowFsp": null,
|
||||
"charSet": null,
|
||||
"collation": null,
|
||||
"generated": null,
|
||||
"name": "rank_by_tokens",
|
||||
"entityType": "columns",
|
||||
"table": "stat"
|
||||
},
|
||||
{
|
||||
"type": "int",
|
||||
"notNull": false,
|
||||
"autoIncrement": false,
|
||||
"default": null,
|
||||
"onUpdateNow": false,
|
||||
"onUpdateNowFsp": null,
|
||||
"charSet": null,
|
||||
"collation": null,
|
||||
"generated": null,
|
||||
"name": "rank_by_requests",
|
||||
"entityType": "columns",
|
||||
"table": "stat"
|
||||
},
|
||||
{
|
||||
"type": "int",
|
||||
"notNull": false,
|
||||
"autoIncrement": false,
|
||||
"default": null,
|
||||
"onUpdateNow": false,
|
||||
"onUpdateNowFsp": null,
|
||||
"charSet": null,
|
||||
"collation": null,
|
||||
"generated": null,
|
||||
"name": "rank_by_cost",
|
||||
"entityType": "columns",
|
||||
"table": "stat"
|
||||
},
|
||||
{
|
||||
"type": "datetime",
|
||||
"notNull": true,
|
||||
"autoIncrement": false,
|
||||
"default": "(now())",
|
||||
"onUpdateNow": false,
|
||||
"onUpdateNowFsp": null,
|
||||
"charSet": null,
|
||||
"collation": null,
|
||||
"generated": null,
|
||||
"name": "created_at",
|
||||
"entityType": "columns",
|
||||
"table": "stat"
|
||||
},
|
||||
{
|
||||
"type": "datetime",
|
||||
"notNull": true,
|
||||
"autoIncrement": false,
|
||||
"default": "(now())",
|
||||
"onUpdateNow": true,
|
||||
"onUpdateNowFsp": null,
|
||||
"charSet": null,
|
||||
"collation": null,
|
||||
"generated": null,
|
||||
"name": "updated_at",
|
||||
"entityType": "columns",
|
||||
"table": "stat"
|
||||
},
|
||||
{
|
||||
"columns": [
|
||||
"id"
|
||||
],
|
||||
"name": "PRIMARY",
|
||||
"table": "stat",
|
||||
"entityType": "pks"
|
||||
},
|
||||
{
|
||||
"columns": [
|
||||
{
|
||||
"value": "grain",
|
||||
"isExpression": false
|
||||
},
|
||||
{
|
||||
"value": "period_start",
|
||||
"isExpression": false
|
||||
},
|
||||
{
|
||||
"value": "dataset",
|
||||
"isExpression": false
|
||||
},
|
||||
{
|
||||
"value": "tier",
|
||||
"isExpression": false
|
||||
},
|
||||
{
|
||||
"value": "client",
|
||||
"isExpression": false
|
||||
},
|
||||
{
|
||||
"value": "source",
|
||||
"isExpression": false
|
||||
},
|
||||
{
|
||||
"value": "provider",
|
||||
"isExpression": false
|
||||
},
|
||||
{
|
||||
"value": "model",
|
||||
"isExpression": false
|
||||
}
|
||||
],
|
||||
"isUnique": true,
|
||||
"using": null,
|
||||
"algorithm": null,
|
||||
"lock": null,
|
||||
"nameExplicit": true,
|
||||
"name": "uniq_model_period",
|
||||
"entityType": "indexes",
|
||||
"table": "stat"
|
||||
},
|
||||
{
|
||||
"columns": [
|
||||
{
|
||||
"value": "grain",
|
||||
"isExpression": false
|
||||
},
|
||||
{
|
||||
"value": "period_start",
|
||||
"isExpression": false
|
||||
},
|
||||
{
|
||||
"value": "dataset",
|
||||
"isExpression": false
|
||||
},
|
||||
{
|
||||
"value": "tier",
|
||||
"isExpression": false
|
||||
},
|
||||
{
|
||||
"value": "total_tokens",
|
||||
"isExpression": false
|
||||
}
|
||||
],
|
||||
"isUnique": false,
|
||||
"using": null,
|
||||
"algorithm": null,
|
||||
"lock": null,
|
||||
"nameExplicit": true,
|
||||
"name": "idx_leaderboard_tokens",
|
||||
"entityType": "indexes",
|
||||
"table": "stat"
|
||||
},
|
||||
{
|
||||
"columns": [
|
||||
{
|
||||
"value": "model",
|
||||
"isExpression": false
|
||||
},
|
||||
{
|
||||
"value": "grain",
|
||||
"isExpression": false
|
||||
},
|
||||
{
|
||||
"value": "period_start",
|
||||
"isExpression": false
|
||||
}
|
||||
],
|
||||
"isUnique": false,
|
||||
"using": null,
|
||||
"algorithm": null,
|
||||
"lock": null,
|
||||
"nameExplicit": true,
|
||||
"name": "idx_model",
|
||||
"entityType": "indexes",
|
||||
"table": "stat"
|
||||
}
|
||||
],
|
||||
"renames": []
|
||||
}
|
||||
|
|
@ -0,0 +1,94 @@
|
|||
CREATE TABLE `geo_stat` (
|
||||
`id` bigint AUTO_INCREMENT PRIMARY KEY,
|
||||
`grain` varchar(16) NOT NULL,
|
||||
`period_start` datetime NOT NULL,
|
||||
`period_end` datetime NOT NULL,
|
||||
`dataset` varchar(64) NOT NULL DEFAULT 'all',
|
||||
`tier` varchar(64) NOT NULL DEFAULT 'all',
|
||||
`client` varchar(64) NOT NULL DEFAULT 'all',
|
||||
`source` varchar(64) NOT NULL DEFAULT 'all',
|
||||
`country` char(2) NOT NULL,
|
||||
`continent` varchar(8) NOT NULL DEFAULT '',
|
||||
`sessions` bigint NOT NULL DEFAULT 0,
|
||||
`requests` bigint NOT NULL DEFAULT 0,
|
||||
`input_tokens` bigint NOT NULL DEFAULT 0,
|
||||
`output_tokens` bigint NOT NULL DEFAULT 0,
|
||||
`reasoning_tokens` bigint NOT NULL DEFAULT 0,
|
||||
`cache_read_tokens` bigint NOT NULL DEFAULT 0,
|
||||
`total_tokens` bigint NOT NULL DEFAULT 0,
|
||||
`input_cost_microcents` bigint NOT NULL DEFAULT 0,
|
||||
`output_cost_microcents` bigint NOT NULL DEFAULT 0,
|
||||
`total_cost_microcents` bigint NOT NULL DEFAULT 0,
|
||||
`avg_duration_ms` decimal(12,2),
|
||||
`p50_duration_ms` int,
|
||||
`p95_duration_ms` int,
|
||||
`avg_ttfb_ms` decimal(12,2),
|
||||
`p50_ttfb_ms` int,
|
||||
`p95_ttfb_ms` int,
|
||||
`avg_output_tps` decimal(12,4),
|
||||
`success_count` bigint NOT NULL DEFAULT 0,
|
||||
`error_count` bigint NOT NULL DEFAULT 0,
|
||||
`sample_count` bigint NOT NULL DEFAULT 0,
|
||||
`market_share_tokens` decimal(10,6),
|
||||
`market_share_requests` decimal(10,6),
|
||||
`market_share_sessions` decimal(10,6),
|
||||
`rank_by_tokens` int,
|
||||
`rank_by_requests` int,
|
||||
`rank_by_sessions` int,
|
||||
`rank_by_cost` int,
|
||||
`created_at` datetime NOT NULL DEFAULT (now()),
|
||||
`updated_at` datetime NOT NULL DEFAULT (now()) ON UPDATE CURRENT_TIMESTAMP,
|
||||
CONSTRAINT `uniq_country_period` UNIQUE INDEX(`grain`,`period_start`,`dataset`,`tier`,`client`,`source`,`country`)
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE `provider_stat` (
|
||||
`id` bigint AUTO_INCREMENT PRIMARY KEY,
|
||||
`grain` varchar(16) NOT NULL,
|
||||
`period_start` datetime NOT NULL,
|
||||
`period_end` datetime NOT NULL,
|
||||
`dataset` varchar(64) NOT NULL DEFAULT 'all',
|
||||
`tier` varchar(64) NOT NULL DEFAULT 'all',
|
||||
`client` varchar(64) NOT NULL DEFAULT 'all',
|
||||
`source` varchar(64) NOT NULL DEFAULT 'all',
|
||||
`provider` varchar(128) NOT NULL,
|
||||
`sessions` bigint NOT NULL DEFAULT 0,
|
||||
`requests` bigint NOT NULL DEFAULT 0,
|
||||
`input_tokens` bigint NOT NULL DEFAULT 0,
|
||||
`output_tokens` bigint NOT NULL DEFAULT 0,
|
||||
`reasoning_tokens` bigint NOT NULL DEFAULT 0,
|
||||
`cache_read_tokens` bigint NOT NULL DEFAULT 0,
|
||||
`total_tokens` bigint NOT NULL DEFAULT 0,
|
||||
`input_cost_microcents` bigint NOT NULL DEFAULT 0,
|
||||
`output_cost_microcents` bigint NOT NULL DEFAULT 0,
|
||||
`total_cost_microcents` bigint NOT NULL DEFAULT 0,
|
||||
`avg_duration_ms` decimal(12,2),
|
||||
`p50_duration_ms` int,
|
||||
`p95_duration_ms` int,
|
||||
`avg_ttfb_ms` decimal(12,2),
|
||||
`p50_ttfb_ms` int,
|
||||
`p95_ttfb_ms` int,
|
||||
`avg_output_tps` decimal(12,4),
|
||||
`success_count` bigint NOT NULL DEFAULT 0,
|
||||
`error_count` bigint NOT NULL DEFAULT 0,
|
||||
`sample_count` bigint NOT NULL DEFAULT 0,
|
||||
`market_share_tokens` decimal(10,6),
|
||||
`market_share_requests` decimal(10,6),
|
||||
`market_share_sessions` decimal(10,6),
|
||||
`rank_by_tokens` int,
|
||||
`rank_by_requests` int,
|
||||
`rank_by_sessions` int,
|
||||
`rank_by_cost` int,
|
||||
`created_at` datetime NOT NULL DEFAULT (now()),
|
||||
`updated_at` datetime NOT NULL DEFAULT (now()) ON UPDATE CURRENT_TIMESTAMP,
|
||||
CONSTRAINT `uniq_provider_period` UNIQUE INDEX(`grain`,`period_start`,`dataset`,`tier`,`client`,`source`,`provider`)
|
||||
);
|
||||
--> statement-breakpoint
|
||||
RENAME TABLE `stat` TO `model_stat`;--> statement-breakpoint
|
||||
CREATE INDEX `idx_country_map_tokens` ON `geo_stat` (`grain`,`period_start`,`dataset`,`tier`,`total_tokens`);--> statement-breakpoint
|
||||
CREATE INDEX `idx_country_rank` ON `geo_stat` (`grain`,`period_start`,`dataset`,`tier`,`rank_by_tokens`);--> statement-breakpoint
|
||||
CREATE INDEX `idx_country` ON `geo_stat` (`country`,`grain`,`period_start`);--> statement-breakpoint
|
||||
CREATE INDEX `idx_continent` ON `geo_stat` (`continent`,`grain`,`period_start`);--> statement-breakpoint
|
||||
CREATE INDEX `idx_provider_leaderboard_tokens` ON `provider_stat` (`grain`,`period_start`,`dataset`,`tier`,`total_tokens`);--> statement-breakpoint
|
||||
CREATE INDEX `idx_provider_market_share` ON `provider_stat` (`grain`,`period_start`,`dataset`,`tier`,`market_share_tokens`);--> statement-breakpoint
|
||||
CREATE INDEX `idx_provider_rank` ON `provider_stat` (`grain`,`period_start`,`dataset`,`tier`,`rank_by_tokens`);--> statement-breakpoint
|
||||
CREATE INDEX `idx_provider` ON `provider_stat` (`provider`,`grain`,`period_start`);
|
||||
File diff suppressed because it is too large
Load diff
43
packages/stats/core/package.json
Normal file
43
packages/stats/core/package.json
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode-ai/stats-core",
|
||||
"version": "1.14.50",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"exports": {
|
||||
".": "./src/index.ts",
|
||||
"./athena": "./src/athena.ts",
|
||||
"./config": "./src/config.ts",
|
||||
"./database": "./src/database.ts",
|
||||
"./database/*": "./src/database/*.ts",
|
||||
"./domain/*": "./src/domain/*.ts",
|
||||
"./runtime": "./src/runtime.ts",
|
||||
"./stat-sync": "./src/stat-sync.ts"
|
||||
},
|
||||
"scripts": {
|
||||
"db:generate": "drizzle-kit generate --config=drizzle.config.ts",
|
||||
"db:migrate": "bun src/migrate.ts",
|
||||
"db:push": "drizzle-kit push --config=drizzle.config.ts",
|
||||
"db:studio": "drizzle-kit studio --config=drizzle.config.ts",
|
||||
"typecheck": "tsgo --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-athena": "3.933.0",
|
||||
"@planetscale/database": "1.19.0",
|
||||
"drizzle-orm": "catalog:",
|
||||
"effect": "catalog:",
|
||||
"sst": "catalog:"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tsconfig/node22": "catalog:",
|
||||
"@types/bun": "catalog:",
|
||||
"@types/node": "catalog:",
|
||||
"@typescript/native-preview": "catalog:",
|
||||
"drizzle-kit": "catalog:",
|
||||
"typescript": "catalog:"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=22"
|
||||
}
|
||||
}
|
||||
139
packages/stats/core/src/athena.ts
Normal file
139
packages/stats/core/src/athena.ts
Normal file
|
|
@ -0,0 +1,139 @@
|
|||
import {
|
||||
AthenaClient as AwsAthenaClient,
|
||||
GetQueryExecutionCommand,
|
||||
GetQueryResultsCommand,
|
||||
StartQueryExecutionCommand,
|
||||
type Row,
|
||||
} from "@aws-sdk/client-athena"
|
||||
import { Effect, Layer, Schema } from "effect"
|
||||
import * as Context from "effect/Context"
|
||||
import { Resource } from "sst/resource"
|
||||
|
||||
const ATHENA_MAX_POLL_ATTEMPTS = 60
|
||||
const ATHENA_PAGE_SIZE = 1000
|
||||
|
||||
export type AthenaData = Record<string, string>
|
||||
|
||||
export class AthenaQueryError extends Schema.TaggedErrorClass<AthenaQueryError>()("AthenaQueryError", {
|
||||
message: Schema.String,
|
||||
queryExecutionId: Schema.optional(Schema.String),
|
||||
cause: Schema.optional(Schema.Defect),
|
||||
}) {}
|
||||
|
||||
export class AthenaQueryTimeoutError extends Schema.TaggedErrorClass<AthenaQueryTimeoutError>()(
|
||||
"AthenaQueryTimeoutError",
|
||||
{
|
||||
message: Schema.String,
|
||||
queryExecutionId: Schema.String,
|
||||
},
|
||||
) {}
|
||||
|
||||
export declare namespace Athena {
|
||||
export interface Service {
|
||||
readonly query: (query: string) => Effect.Effect<AthenaData[], AthenaQueryError | AthenaQueryTimeoutError>
|
||||
}
|
||||
}
|
||||
|
||||
export class Athena extends Context.Service<Athena, Athena.Service>()("@opencode/stats/Athena") {
|
||||
static readonly layer: Layer.Layer<Athena> = Layer.effect(
|
||||
Athena,
|
||||
Effect.sync(() => {
|
||||
const client = new AwsAthenaClient({ region: Resource.InferenceEvent.region })
|
||||
|
||||
const query = Effect.fn("Athena.query")(function* (query: string) {
|
||||
const started = yield* Effect.tryPromise({
|
||||
try: () =>
|
||||
client.send(
|
||||
new StartQueryExecutionCommand({
|
||||
QueryString: query,
|
||||
WorkGroup: Resource.InferenceEvent.workgroup,
|
||||
QueryExecutionContext: {
|
||||
Catalog: Resource.InferenceEvent.catalog,
|
||||
Database: Resource.InferenceEvent.database,
|
||||
},
|
||||
}),
|
||||
),
|
||||
catch: (cause) => new AthenaQueryError({ message: "Failed to start Athena stats query", cause }),
|
||||
})
|
||||
const queryExecutionId = started.QueryExecutionId
|
||||
if (!queryExecutionId)
|
||||
return yield* new AthenaQueryError({ message: "Athena did not return a query execution id" })
|
||||
|
||||
yield* poll(client, queryExecutionId)
|
||||
return yield* results(client, queryExecutionId)
|
||||
})
|
||||
|
||||
return Athena.of({ query })
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
const poll: (
|
||||
client: AwsAthenaClient,
|
||||
queryExecutionId: string,
|
||||
attempt?: number,
|
||||
) => Effect.Effect<void, AthenaQueryError | AthenaQueryTimeoutError> = Effect.fn("Athena.poll")(function* (
|
||||
client: AwsAthenaClient,
|
||||
queryExecutionId: string,
|
||||
attempt = 0,
|
||||
) {
|
||||
if (attempt > 0) yield* Effect.sleep("2 seconds")
|
||||
|
||||
const result = yield* Effect.tryPromise({
|
||||
try: () => client.send(new GetQueryExecutionCommand({ QueryExecutionId: queryExecutionId })),
|
||||
catch: (cause) => new AthenaQueryError({ message: "Failed to poll Athena stats query", queryExecutionId, cause }),
|
||||
})
|
||||
const status = result.QueryExecution?.Status
|
||||
|
||||
if (status?.State === "SUCCEEDED") return
|
||||
if (status?.State === "FAILED" || status?.State === "CANCELLED")
|
||||
return yield* new AthenaQueryError({
|
||||
message: `Athena stats query ${status.State.toLowerCase()}: ${status.StateChangeReason ?? "unknown reason"}`,
|
||||
queryExecutionId,
|
||||
})
|
||||
|
||||
if (attempt >= ATHENA_MAX_POLL_ATTEMPTS - 1)
|
||||
return yield* new AthenaQueryTimeoutError({
|
||||
message: `Athena stats query ${queryExecutionId} did not complete`,
|
||||
queryExecutionId,
|
||||
})
|
||||
|
||||
return yield* poll(client, queryExecutionId, attempt + 1)
|
||||
})
|
||||
|
||||
const results: (
|
||||
client: AwsAthenaClient,
|
||||
queryExecutionId: string,
|
||||
nextToken?: string,
|
||||
) => Effect.Effect<AthenaData[], AthenaQueryError> = Effect.fn("Athena.results")(function* (
|
||||
client: AwsAthenaClient,
|
||||
queryExecutionId: string,
|
||||
nextToken?: string,
|
||||
) {
|
||||
const result = yield* Effect.tryPromise({
|
||||
try: () =>
|
||||
client.send(
|
||||
new GetQueryResultsCommand({
|
||||
QueryExecutionId: queryExecutionId,
|
||||
NextToken: nextToken,
|
||||
MaxResults: ATHENA_PAGE_SIZE,
|
||||
}),
|
||||
),
|
||||
catch: (cause) => new AthenaQueryError({ message: "Failed to read Athena stats results", queryExecutionId, cause }),
|
||||
})
|
||||
const columns = result.ResultSet?.ResultSetMetadata?.ColumnInfo?.map((item) => item.Name ?? "") ?? []
|
||||
const rows = (result.ResultSet?.Rows ?? []).slice(nextToken ? 0 : 1).map((row) => rowData(columns, row))
|
||||
|
||||
if (!result.NextToken) return rows
|
||||
return [...rows, ...(yield* results(client, queryExecutionId, result.NextToken))]
|
||||
})
|
||||
|
||||
function rowData(columns: string[], row: Row): AthenaData {
|
||||
return Object.fromEntries(
|
||||
columns.flatMap((column, index) => {
|
||||
const value = row.Data?.[index]?.VarCharValue
|
||||
if (!column || value === undefined) return []
|
||||
return [[column, value]]
|
||||
}),
|
||||
)
|
||||
}
|
||||
23
packages/stats/core/src/config.ts
Normal file
23
packages/stats/core/src/config.ts
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
import { Config, ConfigProvider, Effect, Layer, Schema } from "effect"
|
||||
import * as Context from "effect/Context"
|
||||
import { Resource } from "sst/resource"
|
||||
|
||||
export class AppConfigValue extends Schema.Class<AppConfigValue>("AppConfigValue")({
|
||||
stage: Schema.NonEmptyString,
|
||||
publicUrl: Schema.NonEmptyString,
|
||||
}) {}
|
||||
|
||||
const decodeAppConfigValue = Schema.decodeUnknownSync(AppConfigValue)
|
||||
|
||||
const config = Config.all({
|
||||
stage: Config.succeed(Resource.App.stage),
|
||||
publicUrl: Config.string("PUBLIC_URL").pipe(Config.withDefault("http://localhost:3000")),
|
||||
}).pipe(Config.map(decodeAppConfigValue))
|
||||
|
||||
export class AppConfig extends Context.Service<AppConfig, AppConfigValue>()("@opencode/stats/AppConfig") {
|
||||
static readonly config = config
|
||||
static readonly layer: Layer.Layer<AppConfig, never, never> = Layer.effect(
|
||||
AppConfig,
|
||||
config.parse(ConfigProvider.fromEnv()).pipe(Effect.orDie),
|
||||
)
|
||||
}
|
||||
79
packages/stats/core/src/database.ts
Normal file
79
packages/stats/core/src/database.ts
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
import { Client } from "@planetscale/database"
|
||||
import { drizzle } from "drizzle-orm/planetscale-serverless"
|
||||
import { migrate as drizzleMigrate } from "drizzle-orm/planetscale-serverless/migrator"
|
||||
import { Config, ConfigProvider, Effect, Layer, Schema } from "effect"
|
||||
import * as Context from "effect/Context"
|
||||
import * as schema from "./database/schema"
|
||||
import { Resource } from "sst/resource"
|
||||
|
||||
export const DatabaseUrl = Schema.NonEmptyString.pipe(Schema.brand("DatabaseUrl"))
|
||||
export type DatabaseUrl = typeof DatabaseUrl.Type
|
||||
|
||||
export class DatabaseSettings extends Schema.Class<DatabaseSettings>("DatabaseSettings")({
|
||||
url: DatabaseUrl,
|
||||
migrationsDir: Schema.NonEmptyString,
|
||||
}) {}
|
||||
|
||||
const decodeDatabaseSettings = Schema.decodeUnknownSync(DatabaseSettings)
|
||||
|
||||
const config = Config.all({
|
||||
url: Config.nonEmptyString("DATABASE_URL").pipe(Config.withDefault(Resource.StatsDatabase.url)),
|
||||
migrationsDir: Config.nonEmptyString("DATABASE_MIGRATIONS_DIR").pipe(Config.withDefault("./migrations")),
|
||||
}).pipe(Config.map(decodeDatabaseSettings))
|
||||
|
||||
export class DatabaseConfig extends Context.Service<DatabaseConfig, DatabaseSettings>()(
|
||||
"@opencode/stats/DatabaseConfig",
|
||||
) {
|
||||
static readonly config = config
|
||||
static readonly layer: Layer.Layer<DatabaseConfig, never, never> = Layer.effect(
|
||||
DatabaseConfig,
|
||||
config.parse(ConfigProvider.fromEnv()).pipe(Effect.orDie),
|
||||
)
|
||||
}
|
||||
|
||||
function makeDrizzle(settings: DatabaseSettings) {
|
||||
return drizzle({ client: new Client({ url: settings.url }), schema })
|
||||
}
|
||||
|
||||
export type Drizzle = ReturnType<typeof makeDrizzle>
|
||||
|
||||
export class DrizzleClient extends Context.Service<DrizzleClient, Drizzle>()("@opencode/stats/DrizzleClient") {
|
||||
static readonly layer: Layer.Layer<DrizzleClient, never, DatabaseConfig> = Layer.effect(
|
||||
DrizzleClient,
|
||||
Effect.map(DatabaseConfig, makeDrizzle),
|
||||
)
|
||||
}
|
||||
|
||||
export class DatabaseError extends Schema.TaggedErrorClass<DatabaseError>()("DatabaseError", {
|
||||
cause: Schema.Defect,
|
||||
}) {}
|
||||
|
||||
export const catchDbError = Effect.mapError((cause) => DatabaseError.make({ cause }))
|
||||
|
||||
export class MigrationError extends Schema.TaggedErrorClass<MigrationError>()("MigrationError", {
|
||||
message: Schema.String,
|
||||
cause: Schema.optional(Schema.Defect),
|
||||
}) {}
|
||||
|
||||
export const migrate = Effect.fn("Database.migrate")(function* () {
|
||||
const settings = yield* DatabaseConfig
|
||||
yield* Effect.logInfo("applying database migrations").pipe(
|
||||
Effect.annotateLogs({ migrationsDir: settings.migrationsDir }),
|
||||
)
|
||||
const result = yield* Effect.tryPromise({
|
||||
try: () =>
|
||||
drizzleMigrate(drizzle({ client: new Client({ url: settings.url }) }), {
|
||||
migrationsFolder: settings.migrationsDir,
|
||||
}),
|
||||
catch: (cause) => new MigrationError({ message: "Failed to apply database migrations", cause }),
|
||||
})
|
||||
if (result)
|
||||
return yield* new MigrationError({
|
||||
message: `Failed to initialize database migrations: ${result.exitCode}`,
|
||||
})
|
||||
yield* Effect.logInfo("database migrations complete").pipe(
|
||||
Effect.annotateLogs({ migrationsDir: settings.migrationsDir }),
|
||||
)
|
||||
})
|
||||
|
||||
export const layer = Layer.mergeAll(DatabaseConfig.layer, DrizzleClient.layer.pipe(Layer.provide(DatabaseConfig.layer)))
|
||||
156
packages/stats/core/src/database/schema.ts
Normal file
156
packages/stats/core/src/database/schema.ts
Normal file
|
|
@ -0,0 +1,156 @@
|
|||
import { bigint, char, datetime, decimal, index, int, mysqlTable, uniqueIndex, varchar } from "drizzle-orm/mysql-core"
|
||||
|
||||
export const modelStat = mysqlTable(
|
||||
"model_stat",
|
||||
{
|
||||
...periodColumns(),
|
||||
provider: varchar({ length: 128 }).notNull(),
|
||||
model: varchar({ length: 256 }).notNull(),
|
||||
provider_model: varchar({ length: 256 }).notNull().default(""),
|
||||
...metricColumns(),
|
||||
rank_by_tokens: int(),
|
||||
rank_by_requests: int(),
|
||||
rank_by_cost: int(),
|
||||
...timestampColumns(),
|
||||
},
|
||||
(table) => [
|
||||
uniqueIndex("uniq_model_period").on(
|
||||
table.grain,
|
||||
table.period_start,
|
||||
table.dataset,
|
||||
table.tier,
|
||||
table.client,
|
||||
table.source,
|
||||
table.provider,
|
||||
table.model,
|
||||
),
|
||||
index("idx_leaderboard_tokens").on(table.grain, table.period_start, table.dataset, table.tier, table.total_tokens),
|
||||
index("idx_model").on(table.model, table.grain, table.period_start),
|
||||
],
|
||||
)
|
||||
|
||||
export const providerStat = mysqlTable(
|
||||
"provider_stat",
|
||||
{
|
||||
...periodColumns(),
|
||||
provider: varchar({ length: 128 }).notNull(),
|
||||
...metricColumns(),
|
||||
...marketShareColumns(),
|
||||
rank_by_tokens: int(),
|
||||
rank_by_requests: int(),
|
||||
rank_by_sessions: int(),
|
||||
rank_by_cost: int(),
|
||||
...timestampColumns(),
|
||||
},
|
||||
(table) => [
|
||||
uniqueIndex("uniq_provider_period").on(
|
||||
table.grain,
|
||||
table.period_start,
|
||||
table.dataset,
|
||||
table.tier,
|
||||
table.client,
|
||||
table.source,
|
||||
table.provider,
|
||||
),
|
||||
index("idx_provider_leaderboard_tokens").on(
|
||||
table.grain,
|
||||
table.period_start,
|
||||
table.dataset,
|
||||
table.tier,
|
||||
table.total_tokens,
|
||||
),
|
||||
index("idx_provider_market_share").on(
|
||||
table.grain,
|
||||
table.period_start,
|
||||
table.dataset,
|
||||
table.tier,
|
||||
table.market_share_tokens,
|
||||
),
|
||||
index("idx_provider_rank").on(table.grain, table.period_start, table.dataset, table.tier, table.rank_by_tokens),
|
||||
index("idx_provider").on(table.provider, table.grain, table.period_start),
|
||||
],
|
||||
)
|
||||
|
||||
export const geoStat = mysqlTable(
|
||||
"geo_stat",
|
||||
{
|
||||
...periodColumns(),
|
||||
country: char({ length: 2 }).notNull(),
|
||||
continent: varchar({ length: 8 }).notNull().default(""),
|
||||
...metricColumns(),
|
||||
...marketShareColumns(),
|
||||
rank_by_tokens: int(),
|
||||
rank_by_requests: int(),
|
||||
rank_by_sessions: int(),
|
||||
rank_by_cost: int(),
|
||||
...timestampColumns(),
|
||||
},
|
||||
(table) => [
|
||||
uniqueIndex("uniq_country_period").on(
|
||||
table.grain,
|
||||
table.period_start,
|
||||
table.dataset,
|
||||
table.tier,
|
||||
table.client,
|
||||
table.source,
|
||||
table.country,
|
||||
),
|
||||
index("idx_country_map_tokens").on(table.grain, table.period_start, table.dataset, table.tier, table.total_tokens),
|
||||
index("idx_country_rank").on(table.grain, table.period_start, table.dataset, table.tier, table.rank_by_tokens),
|
||||
index("idx_country").on(table.country, table.grain, table.period_start),
|
||||
index("idx_continent").on(table.continent, table.grain, table.period_start),
|
||||
],
|
||||
)
|
||||
|
||||
function periodColumns() {
|
||||
return {
|
||||
id: bigint({ mode: "number" }).autoincrement().primaryKey(),
|
||||
grain: varchar({ length: 16 }).notNull(),
|
||||
period_start: datetime({ mode: "date" }).notNull(),
|
||||
period_end: datetime({ mode: "date" }).notNull(),
|
||||
dataset: varchar({ length: 64 }).notNull().default("all"),
|
||||
tier: varchar({ length: 64 }).notNull().default("all"),
|
||||
client: varchar({ length: 64 }).notNull().default("all"),
|
||||
source: varchar({ length: 64 }).notNull().default("all"),
|
||||
}
|
||||
}
|
||||
|
||||
function metricColumns() {
|
||||
return {
|
||||
sessions: bigint({ mode: "number" }).notNull().default(0),
|
||||
requests: bigint({ mode: "number" }).notNull().default(0),
|
||||
input_tokens: bigint({ mode: "number" }).notNull().default(0),
|
||||
output_tokens: bigint({ mode: "number" }).notNull().default(0),
|
||||
reasoning_tokens: bigint({ mode: "number" }).notNull().default(0),
|
||||
cache_read_tokens: bigint({ mode: "number" }).notNull().default(0),
|
||||
total_tokens: bigint({ mode: "number" }).notNull().default(0),
|
||||
input_cost_microcents: bigint({ mode: "number" }).notNull().default(0),
|
||||
output_cost_microcents: bigint({ mode: "number" }).notNull().default(0),
|
||||
total_cost_microcents: bigint({ mode: "number" }).notNull().default(0),
|
||||
avg_duration_ms: decimal({ precision: 12, scale: 2, mode: "number" }),
|
||||
p50_duration_ms: int(),
|
||||
p95_duration_ms: int(),
|
||||
avg_ttfb_ms: decimal({ precision: 12, scale: 2, mode: "number" }),
|
||||
p50_ttfb_ms: int(),
|
||||
p95_ttfb_ms: int(),
|
||||
avg_output_tps: decimal({ precision: 12, scale: 4, mode: "number" }),
|
||||
success_count: bigint({ mode: "number" }).notNull().default(0),
|
||||
error_count: bigint({ mode: "number" }).notNull().default(0),
|
||||
sample_count: bigint({ mode: "number" }).notNull().default(0),
|
||||
}
|
||||
}
|
||||
|
||||
function marketShareColumns() {
|
||||
return {
|
||||
market_share_tokens: decimal({ precision: 10, scale: 6, mode: "number" }),
|
||||
market_share_requests: decimal({ precision: 10, scale: 6, mode: "number" }),
|
||||
market_share_sessions: decimal({ precision: 10, scale: 6, mode: "number" }),
|
||||
}
|
||||
}
|
||||
|
||||
function timestampColumns() {
|
||||
return {
|
||||
created_at: datetime({ mode: "date" }).notNull().defaultNow(),
|
||||
updated_at: datetime({ mode: "date" }).notNull().defaultNow().onUpdateNow(),
|
||||
}
|
||||
}
|
||||
171
packages/stats/core/src/domain/geo.ts
Normal file
171
packages/stats/core/src/domain/geo.ts
Normal file
|
|
@ -0,0 +1,171 @@
|
|||
import { and, asc, eq } from "drizzle-orm"
|
||||
import { Effect, Layer } from "effect"
|
||||
import * as Context from "effect/Context"
|
||||
import { DatabaseError, DrizzleClient } from "../database"
|
||||
import { geoStat } from "../database/schema"
|
||||
import {
|
||||
chunks,
|
||||
collapseRows,
|
||||
inserted,
|
||||
rankRowsWithMarketShare,
|
||||
synthesizeAllTierRows,
|
||||
toStatBaseRow,
|
||||
UPSERT_CHUNK_SIZE,
|
||||
type StatBaseAggregate,
|
||||
} from "./stat"
|
||||
|
||||
export type GeoStatRow = typeof geoStat.$inferInsert
|
||||
export type GeoStatAggregate = StatBaseAggregate & { country: string; continent: string }
|
||||
export type GeoStatMetric = {
|
||||
periodStart: Date
|
||||
periodEnd: Date
|
||||
tier: string
|
||||
country: string
|
||||
continent: string
|
||||
totalTokens: number
|
||||
}
|
||||
|
||||
export declare namespace GeoStatRepo {
|
||||
export interface Service {
|
||||
readonly listDaily: () => Effect.Effect<GeoStatMetric[], DatabaseError>
|
||||
readonly listByPeriod: (opts: {
|
||||
readonly grain: string
|
||||
readonly periodStart: Date
|
||||
readonly dataset?: string
|
||||
readonly tier?: string
|
||||
readonly client?: string
|
||||
readonly source?: string
|
||||
}) => Effect.Effect<GeoStatRow[], DatabaseError>
|
||||
readonly upsert: (rows: GeoStatRow[]) => Effect.Effect<void, DatabaseError>
|
||||
}
|
||||
}
|
||||
|
||||
export class GeoStatRepo extends Context.Service<GeoStatRepo, GeoStatRepo.Service>()("@opencode/stats/GeoStatRepo") {
|
||||
static readonly layer: Layer.Layer<GeoStatRepo, never, DrizzleClient> = Layer.effect(
|
||||
GeoStatRepo,
|
||||
Effect.gen(function* () {
|
||||
const db = yield* DrizzleClient
|
||||
|
||||
const listDaily = Effect.fn("GeoStatRepo.listDaily")(function* () {
|
||||
return yield* Effect.tryPromise({
|
||||
try: () =>
|
||||
db
|
||||
.select({
|
||||
periodStart: geoStat.period_start,
|
||||
periodEnd: geoStat.period_end,
|
||||
tier: geoStat.tier,
|
||||
country: geoStat.country,
|
||||
continent: geoStat.continent,
|
||||
totalTokens: geoStat.total_tokens,
|
||||
})
|
||||
.from(geoStat)
|
||||
.where(and(eq(geoStat.grain, "day"), eq(geoStat.client, "all"), eq(geoStat.source, "all")))
|
||||
.orderBy(asc(geoStat.period_start)),
|
||||
catch: (cause) => DatabaseError.make({ cause }),
|
||||
})
|
||||
})
|
||||
|
||||
const listByPeriod = Effect.fn("GeoStatRepo.listByPeriod")(function* (opts: {
|
||||
readonly grain: string
|
||||
readonly periodStart: Date
|
||||
readonly dataset?: string
|
||||
readonly tier?: string
|
||||
readonly client?: string
|
||||
readonly source?: string
|
||||
}) {
|
||||
return yield* Effect.tryPromise({
|
||||
try: () =>
|
||||
db
|
||||
.select()
|
||||
.from(geoStat)
|
||||
.where(
|
||||
and(
|
||||
eq(geoStat.grain, opts.grain),
|
||||
eq(geoStat.period_start, opts.periodStart),
|
||||
eq(geoStat.dataset, opts.dataset ?? "zen"),
|
||||
eq(geoStat.tier, opts.tier ?? "all"),
|
||||
eq(geoStat.client, opts.client ?? "all"),
|
||||
eq(geoStat.source, opts.source ?? "all"),
|
||||
),
|
||||
),
|
||||
catch: (cause) => DatabaseError.make({ cause }),
|
||||
})
|
||||
})
|
||||
|
||||
const upsert = Effect.fn("GeoStatRepo.upsert")(function* (rows: GeoStatRow[]) {
|
||||
yield* Effect.forEach(
|
||||
chunks(rows, UPSERT_CHUNK_SIZE),
|
||||
(chunk) =>
|
||||
Effect.tryPromise({
|
||||
try: () =>
|
||||
db
|
||||
.insert(geoStat)
|
||||
.values(chunk)
|
||||
.onDuplicateKeyUpdate({
|
||||
set: {
|
||||
period_end: inserted("period_end"),
|
||||
continent: inserted("continent"),
|
||||
sessions: inserted("sessions"),
|
||||
requests: inserted("requests"),
|
||||
input_tokens: inserted("input_tokens"),
|
||||
output_tokens: inserted("output_tokens"),
|
||||
reasoning_tokens: inserted("reasoning_tokens"),
|
||||
cache_read_tokens: inserted("cache_read_tokens"),
|
||||
total_tokens: inserted("total_tokens"),
|
||||
input_cost_microcents: inserted("input_cost_microcents"),
|
||||
output_cost_microcents: inserted("output_cost_microcents"),
|
||||
total_cost_microcents: inserted("total_cost_microcents"),
|
||||
avg_duration_ms: inserted("avg_duration_ms"),
|
||||
p50_duration_ms: inserted("p50_duration_ms"),
|
||||
p95_duration_ms: inserted("p95_duration_ms"),
|
||||
avg_ttfb_ms: inserted("avg_ttfb_ms"),
|
||||
p50_ttfb_ms: inserted("p50_ttfb_ms"),
|
||||
p95_ttfb_ms: inserted("p95_ttfb_ms"),
|
||||
avg_output_tps: inserted("avg_output_tps"),
|
||||
success_count: inserted("success_count"),
|
||||
error_count: inserted("error_count"),
|
||||
sample_count: inserted("sample_count"),
|
||||
market_share_tokens: inserted("market_share_tokens"),
|
||||
market_share_requests: inserted("market_share_requests"),
|
||||
market_share_sessions: inserted("market_share_sessions"),
|
||||
rank_by_tokens: inserted("rank_by_tokens"),
|
||||
rank_by_requests: inserted("rank_by_requests"),
|
||||
rank_by_sessions: inserted("rank_by_sessions"),
|
||||
rank_by_cost: inserted("rank_by_cost"),
|
||||
},
|
||||
}),
|
||||
catch: (cause) => DatabaseError.make({ cause }),
|
||||
}),
|
||||
{ discard: true },
|
||||
)
|
||||
})
|
||||
|
||||
return GeoStatRepo.of({ listDaily, listByPeriod, upsert })
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
export function rowsFromAggregates(aggregates: GeoStatAggregate[]) {
|
||||
return rankRowsWithMarketShare([
|
||||
...synthesizeAllTierRows(
|
||||
collapseRows(aggregates.filter((item) => item.grain === "week").map(toRow), dimensionKey),
|
||||
dimensionKey,
|
||||
),
|
||||
...synthesizeAllTierRows(
|
||||
collapseRows(aggregates.filter((item) => item.grain === "day").map(toRow), dimensionKey),
|
||||
dimensionKey,
|
||||
),
|
||||
])
|
||||
}
|
||||
|
||||
function toRow(data: GeoStatAggregate): GeoStatRow {
|
||||
return {
|
||||
...toStatBaseRow(data),
|
||||
country: data.country,
|
||||
continent: data.continent,
|
||||
}
|
||||
}
|
||||
|
||||
function dimensionKey(row: GeoStatRow) {
|
||||
return row.country
|
||||
}
|
||||
467
packages/stats/core/src/domain/home.ts
Normal file
467
packages/stats/core/src/domain/home.ts
Normal file
|
|
@ -0,0 +1,467 @@
|
|||
import { Effect } from "effect"
|
||||
import { DatabaseError } from "../database"
|
||||
import { GeoStatRepo, type GeoStatMetric } from "./geo"
|
||||
import { ModelStatRepo, type ModelStatMetric } from "./model"
|
||||
import { ProviderStatRepo, type ProviderStatMetric } from "./provider"
|
||||
|
||||
export type UsageProduct = "All Users" | "Zen" | "Go" | "Enterprise"
|
||||
export type TokenProduct = "Zen" | "Go" | "Enterprise"
|
||||
export type UsageRange = "1D" | "1W" | "1M" | "3M" | "YTD" | "ALL"
|
||||
export type UsagePoint = { date: string; segments: { model: string; value: number }[] }
|
||||
export type MarketDay = { date: string; total: number; authors: { author: string; share: number; tokens: number }[] }
|
||||
export type LeaderboardEntry = { model: string; author: string; tokens: number; change: number; rank: number }
|
||||
export type TokenCostEntry = { model: string; total: number; input: number; output: number; cached: number }
|
||||
export type SessionCostEntry = { model: string; cost: number; tokens: number }
|
||||
export type CountryEntry = { country: string; continent: string; tokens: number; share: number; rank: number }
|
||||
export type StatsHomeData = {
|
||||
updatedAt: string | null
|
||||
usage: Record<UsageProduct, Record<UsageRange, UsagePoint[]>>
|
||||
leaderboard: Record<UsageProduct, Record<UsageRange, LeaderboardEntry[]>>
|
||||
market: Record<UsageRange, MarketDay[]>
|
||||
tokenCost: Record<TokenProduct, TokenCostEntry[]>
|
||||
sessionCost: Record<TokenProduct, SessionCostEntry[]>
|
||||
country: Record<UsageRange, CountryEntry[]>
|
||||
}
|
||||
|
||||
const DAY_MS = 86_400_000
|
||||
const TOKEN_SCALE = 1_000_000
|
||||
const DOLLARS_PER_MICROCENT = 1 / 100_000_000
|
||||
const months = ["JAN", "FEB", "MAR", "APR", "MAY", "JUN", "JUL", "AUG", "SEP", "OCT", "NOV", "DEC"] as const
|
||||
|
||||
type StatMetricRow = Omit<ModelStatMetric, "periodStart" | "periodEnd"> & {
|
||||
periodStart: number
|
||||
periodEnd: number
|
||||
}
|
||||
type ProviderMetricRow = Omit<ProviderStatMetric, "periodStart" | "periodEnd"> & {
|
||||
periodStart: number
|
||||
periodEnd: number
|
||||
}
|
||||
type GeoMetricRow = Omit<GeoStatMetric, "periodStart" | "periodEnd"> & {
|
||||
periodStart: number
|
||||
periodEnd: number
|
||||
}
|
||||
|
||||
type DateWindow = { start: number; end: number; previousStart: number; previousEnd: number }
|
||||
type Bucket = { start: number; end: number; label: string }
|
||||
type ModelAggregate = {
|
||||
model: string
|
||||
provider: string
|
||||
sessions: number
|
||||
inputTokens: number
|
||||
outputTokens: number
|
||||
reasoningTokens: number
|
||||
cacheReadTokens: number
|
||||
totalTokens: number
|
||||
inputCostMicrocents: number
|
||||
outputCostMicrocents: number
|
||||
totalCostMicrocents: number
|
||||
}
|
||||
|
||||
export const getStatsHomeData: () => Effect.Effect<
|
||||
StatsHomeData,
|
||||
DatabaseError,
|
||||
ModelStatRepo | ProviderStatRepo | GeoStatRepo
|
||||
> = Effect.fn("StatsHome.getData")(function* () {
|
||||
const modelStats = yield* ModelStatRepo
|
||||
const providerStats = yield* ProviderStatRepo
|
||||
const geoStats = yield* GeoStatRepo
|
||||
const [modelRows, providerRows, geoRows] = yield* Effect.all(
|
||||
[modelStats.listDaily(), providerStats.listDaily(), geoStats.listDaily()],
|
||||
{ concurrency: "unbounded" },
|
||||
)
|
||||
return buildStatsHomeData(modelRows, providerRows, geoRows)
|
||||
})
|
||||
|
||||
function buildStatsHomeData(
|
||||
modelRows: ModelStatMetric[],
|
||||
providerRows: ProviderStatMetric[],
|
||||
geoRows: GeoStatMetric[],
|
||||
): StatsHomeData {
|
||||
const normalized = modelRows.flatMap(normalizeStatRow)
|
||||
const providers = providerRows.flatMap(normalizeProviderRow)
|
||||
const geo = geoRows.flatMap(normalizeGeoRow)
|
||||
const periods = [...normalized, ...providers, ...geo]
|
||||
if (periods.length === 0) return emptyStatsHomeData()
|
||||
|
||||
const earliest = Math.min(...periods.map((row) => row.periodStart))
|
||||
const latest = Math.max(...periods.map((row) => row.periodStart))
|
||||
const latestEnd = Math.max(...periods.map((row) => row.periodEnd))
|
||||
|
||||
return {
|
||||
updatedAt: new Date(latestEnd).toISOString(),
|
||||
usage: createUsageProductRecord((product) =>
|
||||
createRangeRecord((range) => buildUsagePoints(normalized, product, range, getWindow(range, earliest, latest))),
|
||||
),
|
||||
leaderboard: createUsageProductRecord((product) =>
|
||||
createRangeRecord((range) => buildLeaderboard(normalized, product, getWindow(range, earliest, latest))),
|
||||
),
|
||||
market: createRangeRecord((range) => buildMarketShare(providers, range, getWindow(range, earliest, latest))),
|
||||
tokenCost: createTokenProductRecord((product) =>
|
||||
buildTokenCost(normalized, product, getWindow("1W", earliest, latest)),
|
||||
),
|
||||
sessionCost: createTokenProductRecord((product) =>
|
||||
buildSessionCost(normalized, product, getWindow("1W", earliest, latest)),
|
||||
),
|
||||
country: createRangeRecord((range) => buildCountryStats(geo, getWindow(range, earliest, latest))),
|
||||
}
|
||||
}
|
||||
|
||||
function emptyStatsHomeData(): StatsHomeData {
|
||||
return {
|
||||
updatedAt: null,
|
||||
usage: createUsageProductRecord(() => createRangeRecord(() => [])),
|
||||
leaderboard: createUsageProductRecord(() => createRangeRecord(() => [])),
|
||||
market: createRangeRecord(() => []),
|
||||
tokenCost: createTokenProductRecord(() => []),
|
||||
sessionCost: createTokenProductRecord(() => []),
|
||||
country: createRangeRecord(() => []),
|
||||
}
|
||||
}
|
||||
|
||||
function buildUsagePoints(rows: StatMetricRow[], product: UsageProduct, range: UsageRange, window: DateWindow) {
|
||||
const windowRows = rowsForProduct(rows, product, window.start, window.end)
|
||||
const modelOrder = aggregateByModel(windowRows)
|
||||
.toSorted((a, b) => b.totalTokens - a.totalTokens)
|
||||
.slice(0, 6)
|
||||
.map((item) => ({ key: modelKey(item.provider, item.model), model: item.model }))
|
||||
|
||||
return createBuckets(window, range).map((bucket) => {
|
||||
const bucketRows = aggregateByModel(rowsForProduct(rows, product, bucket.start, bucket.end))
|
||||
const byModel = new Map(bucketRows.map((item) => [modelKey(item.provider, item.model), item.totalTokens]))
|
||||
const segmentTokens = modelOrder.map((model) => ({ model: model.model, tokens: byModel.get(model.key) ?? 0 }))
|
||||
const knownTokens = segmentTokens.reduce((sum, item) => sum + item.tokens, 0)
|
||||
const totalTokens = bucketRows.reduce((sum, item) => sum + item.totalTokens, 0)
|
||||
return {
|
||||
date: bucket.label,
|
||||
segments: [
|
||||
...segmentTokens.map((item) => ({ model: item.model, value: round(item.tokens / 1_000_000_000_000, 2) })),
|
||||
{ model: "Other", value: round(Math.max(totalTokens - knownTokens, 0) / 1_000_000_000_000, 2) },
|
||||
].filter((item) => item.value > 0),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function buildLeaderboard(rows: StatMetricRow[], product: UsageProduct, window: DateWindow) {
|
||||
const previous = new Map(
|
||||
aggregateByModel(rowsForProduct(rows, product, window.previousStart, window.previousEnd)).map((item) => [
|
||||
modelKey(item.provider, item.model),
|
||||
item.totalTokens,
|
||||
]),
|
||||
)
|
||||
|
||||
return aggregateByModel(rowsForProduct(rows, product, window.start, window.end))
|
||||
.toSorted((a, b) => b.totalTokens - a.totalTokens)
|
||||
.slice(0, 13)
|
||||
.map((item, index) => ({
|
||||
model: item.model,
|
||||
author: formatProvider(item.provider),
|
||||
tokens: Math.round(item.totalTokens / 1_000_000_000),
|
||||
change: percentChange(item.totalTokens, previous.get(modelKey(item.provider, item.model)) ?? 0),
|
||||
rank: index + 1,
|
||||
}))
|
||||
}
|
||||
|
||||
function buildMarketShare(rows: ProviderMetricRow[], range: UsageRange, window: DateWindow) {
|
||||
return createBuckets(window, range).flatMap((bucket) => {
|
||||
const total = aggregateByProvider(rowsForProduct(rows, "All Users", bucket.start, bucket.end)).toSorted(
|
||||
(a, b) => b.tokens - a.tokens,
|
||||
)
|
||||
const totalTokens = total.reduce((sum, item) => sum + item.tokens, 0)
|
||||
if (totalTokens === 0) return []
|
||||
|
||||
const authors = total.slice(0, 8)
|
||||
const knownTokens = authors.reduce((sum, item) => sum + item.tokens, 0)
|
||||
const withOther = [...authors, { provider: "Other", tokens: Math.max(totalTokens - knownTokens, 0) }].filter(
|
||||
(item) => item.tokens > 0,
|
||||
)
|
||||
|
||||
return [
|
||||
{
|
||||
date: bucket.label,
|
||||
total: round(totalTokens / 1_000_000_000_000, 2),
|
||||
authors: withOther.map((item) => ({
|
||||
author: item.provider === "Other" ? "Other" : formatProvider(item.provider),
|
||||
share: round((item.tokens / totalTokens) * 100, 1),
|
||||
tokens: round(item.tokens / 1_000_000_000_000, 2),
|
||||
})),
|
||||
},
|
||||
]
|
||||
})
|
||||
}
|
||||
|
||||
function buildCountryStats(rows: GeoMetricRow[], window: DateWindow) {
|
||||
const countries = aggregateByCountry(rowsForProduct(rows, "All Users", window.start, window.end))
|
||||
.filter((item) => item.tokens > 0)
|
||||
.toSorted((a, b) => b.tokens - a.tokens)
|
||||
const totalTokens = countries.reduce((sum, item) => sum + item.tokens, 0)
|
||||
if (totalTokens === 0) return []
|
||||
|
||||
return countries.slice(0, 16).map((item, index) => ({
|
||||
country: item.country,
|
||||
continent: item.continent,
|
||||
tokens: round(item.tokens / 1_000_000_000_000, 4),
|
||||
share: round((item.tokens / totalTokens) * 100, 1),
|
||||
rank: index + 1,
|
||||
}))
|
||||
}
|
||||
|
||||
function buildTokenCost(rows: StatMetricRow[], product: TokenProduct, window: DateWindow) {
|
||||
return aggregateByModel(rowsForProduct(rows, product, window.start, window.end))
|
||||
.flatMap((item) => {
|
||||
const total = costPerMillion(item.totalCostMicrocents, item.totalTokens)
|
||||
if (total === 0) return []
|
||||
return [
|
||||
{
|
||||
model: item.model,
|
||||
total,
|
||||
input: costPerMillion(item.inputCostMicrocents, item.inputTokens),
|
||||
output: costPerMillion(item.outputCostMicrocents, item.outputTokens + item.reasoningTokens),
|
||||
cached: costPerMillion(item.inputCostMicrocents, item.inputTokens + item.cacheReadTokens),
|
||||
},
|
||||
]
|
||||
})
|
||||
.toSorted((a, b) => a.total - b.total)
|
||||
.slice(0, 17)
|
||||
}
|
||||
|
||||
function buildSessionCost(rows: StatMetricRow[], product: TokenProduct, window: DateWindow) {
|
||||
return aggregateByModel(rowsForProduct(rows, product, window.start, window.end))
|
||||
.flatMap((item) => {
|
||||
if (item.sessions === 0) return []
|
||||
const cost = round(microcentsToDollars(item.totalCostMicrocents) / item.sessions, 4)
|
||||
if (cost === 0) return []
|
||||
return [{ model: item.model, cost, tokens: Math.round(item.totalTokens / item.sessions) }]
|
||||
})
|
||||
.toSorted((a, b) => a.cost - b.cost)
|
||||
.slice(0, 17)
|
||||
}
|
||||
|
||||
function rowsForProduct<T extends { periodStart: number; tier: string }>(
|
||||
rows: T[],
|
||||
product: UsageProduct,
|
||||
start: number,
|
||||
end: number,
|
||||
) {
|
||||
const windowRows = rows.filter((row) => row.periodStart >= start && row.periodStart < end)
|
||||
if (product !== "All Users") return windowRows.filter((row) => row.tier === product)
|
||||
|
||||
const allRows = windowRows.filter((row) => row.tier === "all")
|
||||
if (allRows.length > 0) return allRows
|
||||
return windowRows.filter((row) => row.tier !== "all")
|
||||
}
|
||||
|
||||
function aggregateByModel(rows: StatMetricRow[]) {
|
||||
return Object.values(
|
||||
rows.reduce<Record<string, ModelAggregate>>((result, row) => {
|
||||
const key = modelKey(row.provider, row.model)
|
||||
result[key] = combineModelAggregate(result[key], row)
|
||||
return result
|
||||
}, {}),
|
||||
)
|
||||
}
|
||||
|
||||
function aggregateByProvider(rows: ProviderMetricRow[]) {
|
||||
return Object.values(
|
||||
rows.reduce<Record<string, { provider: string; tokens: number }>>((result, row) => {
|
||||
result[row.provider] = {
|
||||
provider: row.provider,
|
||||
tokens: (result[row.provider]?.tokens ?? 0) + row.totalTokens,
|
||||
}
|
||||
return result
|
||||
}, {}),
|
||||
)
|
||||
}
|
||||
|
||||
function aggregateByCountry(rows: GeoMetricRow[]) {
|
||||
return Object.values(
|
||||
rows.reduce<Record<string, { country: string; continent: string; tokens: number }>>((result, row) => {
|
||||
result[row.country] = {
|
||||
country: row.country,
|
||||
continent: result[row.country]?.continent || row.continent,
|
||||
tokens: (result[row.country]?.tokens ?? 0) + row.totalTokens,
|
||||
}
|
||||
return result
|
||||
}, {}),
|
||||
)
|
||||
}
|
||||
|
||||
function combineModelAggregate(current: ModelAggregate | undefined, row: StatMetricRow): ModelAggregate {
|
||||
return {
|
||||
model: row.model,
|
||||
provider: row.provider,
|
||||
sessions: (current?.sessions ?? 0) + row.sessions,
|
||||
inputTokens: (current?.inputTokens ?? 0) + row.inputTokens,
|
||||
outputTokens: (current?.outputTokens ?? 0) + row.outputTokens,
|
||||
reasoningTokens: (current?.reasoningTokens ?? 0) + row.reasoningTokens,
|
||||
cacheReadTokens: (current?.cacheReadTokens ?? 0) + row.cacheReadTokens,
|
||||
totalTokens: (current?.totalTokens ?? 0) + row.totalTokens,
|
||||
inputCostMicrocents: (current?.inputCostMicrocents ?? 0) + row.inputCostMicrocents,
|
||||
outputCostMicrocents: (current?.outputCostMicrocents ?? 0) + row.outputCostMicrocents,
|
||||
totalCostMicrocents: (current?.totalCostMicrocents ?? 0) + row.totalCostMicrocents,
|
||||
}
|
||||
}
|
||||
|
||||
function getWindow(range: UsageRange, earliest: number, latest: number): DateWindow {
|
||||
const end = latest + DAY_MS
|
||||
const start = Math.max(
|
||||
earliest,
|
||||
range === "1D"
|
||||
? latest
|
||||
: range === "1W"
|
||||
? latest - 6 * DAY_MS
|
||||
: range === "1M"
|
||||
? latest - 29 * DAY_MS
|
||||
: range === "3M"
|
||||
? latest - 89 * DAY_MS
|
||||
: range === "YTD"
|
||||
? Date.UTC(new Date(latest).getUTCFullYear(), 0, 1)
|
||||
: earliest,
|
||||
)
|
||||
const duration = end - start
|
||||
return { start, end, previousStart: start - duration, previousEnd: start }
|
||||
}
|
||||
|
||||
function createBuckets(window: DateWindow, range: UsageRange): Bucket[] {
|
||||
const span = Math.max(window.end - window.start, DAY_MS)
|
||||
const count = Math.max(1, Math.min(7, Math.ceil(span / DAY_MS)))
|
||||
const size = span / count
|
||||
return Array.from({ length: count }, (_, index) => {
|
||||
const start = window.start + index * size
|
||||
const end = index === count - 1 ? window.end : window.start + (index + 1) * size
|
||||
return { start, end, label: formatBucketLabel(start, range) }
|
||||
})
|
||||
}
|
||||
|
||||
function createUsageProductRecord<T>(value: (product: UsageProduct) => T): Record<UsageProduct, T> {
|
||||
return {
|
||||
"All Users": value("All Users"),
|
||||
Zen: value("Zen"),
|
||||
Go: value("Go"),
|
||||
Enterprise: value("Enterprise"),
|
||||
}
|
||||
}
|
||||
|
||||
function createTokenProductRecord<T>(value: (product: TokenProduct) => T): Record<TokenProduct, T> {
|
||||
return {
|
||||
Zen: value("Zen"),
|
||||
Go: value("Go"),
|
||||
Enterprise: value("Enterprise"),
|
||||
}
|
||||
}
|
||||
|
||||
function createRangeRecord<T>(value: (range: UsageRange) => T): Record<UsageRange, T> {
|
||||
return {
|
||||
"1D": value("1D"),
|
||||
"1W": value("1W"),
|
||||
"1M": value("1M"),
|
||||
"3M": value("3M"),
|
||||
YTD: value("YTD"),
|
||||
ALL: value("ALL"),
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeStatRow(row: ModelStatMetric): StatMetricRow[] {
|
||||
const periodStart = dateTime(row.periodStart)
|
||||
const periodEnd = dateTime(row.periodEnd)
|
||||
if (!Number.isFinite(periodStart) || !Number.isFinite(periodEnd)) return []
|
||||
return [
|
||||
{
|
||||
...row,
|
||||
periodStart,
|
||||
periodEnd,
|
||||
tier: normalizeTier(row.tier),
|
||||
provider: row.provider || "unknown",
|
||||
model: row.model || "unknown",
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
function normalizeProviderRow(row: ProviderStatMetric): ProviderMetricRow[] {
|
||||
const periodStart = dateTime(row.periodStart)
|
||||
const periodEnd = dateTime(row.periodEnd)
|
||||
if (!Number.isFinite(periodStart) || !Number.isFinite(periodEnd)) return []
|
||||
return [
|
||||
{
|
||||
...row,
|
||||
periodStart,
|
||||
periodEnd,
|
||||
tier: normalizeTier(row.tier),
|
||||
provider: row.provider || "unknown",
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
function normalizeGeoRow(row: GeoStatMetric): GeoMetricRow[] {
|
||||
const periodStart = dateTime(row.periodStart)
|
||||
const periodEnd = dateTime(row.periodEnd)
|
||||
if (!Number.isFinite(periodStart) || !Number.isFinite(periodEnd)) return []
|
||||
return [
|
||||
{
|
||||
...row,
|
||||
periodStart,
|
||||
periodEnd,
|
||||
tier: normalizeTier(row.tier),
|
||||
country: row.country || "ZZ",
|
||||
continent: row.continent || "",
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
function normalizeTier(value: string) {
|
||||
const normalized = value.toLowerCase()
|
||||
if (normalized === "paid" || normalized === "zen") return "Zen"
|
||||
if (normalized === "go") return "Go"
|
||||
if (normalized === "enterprise") return "Enterprise"
|
||||
if (normalized === "all") return "all"
|
||||
return value
|
||||
}
|
||||
|
||||
function dateTime(value: Date | string) {
|
||||
return (value instanceof Date ? value : new Date(value)).getTime()
|
||||
}
|
||||
|
||||
function formatBucketLabel(value: number, range: UsageRange) {
|
||||
const date = new Date(value)
|
||||
if (range === "YTD") return months[date.getUTCMonth()]
|
||||
if (range === "ALL")
|
||||
return date.getUTCFullYear() === new Date().getUTCFullYear()
|
||||
? months[date.getUTCMonth()]
|
||||
: String(date.getUTCFullYear())
|
||||
return `${months[date.getUTCMonth()]} ${date.getUTCDate()}`
|
||||
}
|
||||
|
||||
function formatProvider(provider: string) {
|
||||
const known: Record<string, string> = {
|
||||
anthropic: "Anthropic",
|
||||
google: "Google",
|
||||
minimax: "MiniMax",
|
||||
moonshotai: "Moonshot",
|
||||
nvidia: "Nvidia",
|
||||
openai: "OpenAI",
|
||||
zhipuai: "Zhipu",
|
||||
}
|
||||
const normalized = provider.toLowerCase().replace(/[^a-z0-9]/g, "")
|
||||
return known[normalized] ?? provider.replace(/[-_]/g, " ").replace(/\b\w/g, (letter) => letter.toUpperCase())
|
||||
}
|
||||
|
||||
function modelKey(provider: string, model: string) {
|
||||
return `${provider}\u0000${model}`
|
||||
}
|
||||
|
||||
function costPerMillion(costMicrocents: number, tokens: number) {
|
||||
if (tokens <= 0 || costMicrocents <= 0) return 0
|
||||
return round((microcentsToDollars(costMicrocents) / tokens) * TOKEN_SCALE, 2)
|
||||
}
|
||||
|
||||
function microcentsToDollars(value: number) {
|
||||
return value * DOLLARS_PER_MICROCENT
|
||||
}
|
||||
|
||||
function percentChange(current: number, previous: number) {
|
||||
if (previous <= 0) return current > 0 ? 100 : 0
|
||||
return Math.round(((current - previous) / previous) * 100)
|
||||
}
|
||||
|
||||
function round(value: number, digits: number) {
|
||||
return Number(value.toFixed(digits))
|
||||
}
|
||||
216
packages/stats/core/src/domain/inference.ts
Normal file
216
packages/stats/core/src/domain/inference.ts
Normal file
|
|
@ -0,0 +1,216 @@
|
|||
import { Resource } from "sst/resource"
|
||||
import type { AthenaData } from "../athena"
|
||||
import type { GeoStatAggregate } from "./geo"
|
||||
import type { ModelStatAggregate } from "./model"
|
||||
import type { ProviderStatAggregate } from "./provider"
|
||||
import { normalizeCountry, normalizeTier, type StatBaseAggregate } from "./stat"
|
||||
|
||||
export type StatDimension = "model" | "provider" | "geo"
|
||||
|
||||
export function buildStatsQuery(periodStart: Date, periodEnd: Date, dimension: StatDimension) {
|
||||
const periodStartValue = sqlString(periodStart.toISOString())
|
||||
const periodEndValue = sqlString(periodEnd.toISOString())
|
||||
const sourceTable = [
|
||||
Resource.InferenceEvent.catalog,
|
||||
Resource.InferenceEvent.database,
|
||||
Resource.InferenceEvent.table,
|
||||
]
|
||||
.map(sqlIdentifier)
|
||||
.join(".")
|
||||
const dimensionSql = (() => {
|
||||
if (dimension === "model")
|
||||
return {
|
||||
select: "provider, model, COALESCE(MAX(NULLIF(provider_model, '')), '') AS provider_model",
|
||||
groupBy: "provider, model",
|
||||
}
|
||||
if (dimension === "provider") return { select: "provider", groupBy: "provider" }
|
||||
return {
|
||||
select: "country, COALESCE(MAX(NULLIF(continent, '')), '') AS continent",
|
||||
groupBy: "country",
|
||||
}
|
||||
})()
|
||||
const aggregateColumns = `
|
||||
COUNT(DISTINCT session) AS sessions,
|
||||
COUNT(*) AS requests,
|
||||
COALESCE(SUM(tokens_input), 0) AS input_tokens,
|
||||
COALESCE(SUM(tokens_output), 0) AS output_tokens,
|
||||
COALESCE(SUM(tokens_reasoning), 0) AS reasoning_tokens,
|
||||
COALESCE(SUM(tokens_cache_read), 0) AS cache_read_tokens,
|
||||
COALESCE(SUM(tokens_total), 0) AS total_tokens,
|
||||
COALESCE(SUM(cost_input_microcents), 0) AS input_cost_microcents,
|
||||
COALESCE(SUM(cost_output_microcents), 0) AS output_cost_microcents,
|
||||
COALESCE(SUM(cost_total_microcents), 0) AS total_cost_microcents,
|
||||
AVG(duration_ms) AS avg_duration_ms,
|
||||
approx_percentile(CAST(duration_ms AS double), 0.5) AS p50_duration_ms,
|
||||
approx_percentile(CAST(duration_ms AS double), 0.95) AS p95_duration_ms,
|
||||
AVG(ttfb_ms) AS avg_ttfb_ms,
|
||||
approx_percentile(CAST(ttfb_ms AS double), 0.5) AS p50_ttfb_ms,
|
||||
approx_percentile(CAST(ttfb_ms AS double), 0.95) AS p95_ttfb_ms,
|
||||
AVG(output_tps) AS avg_output_tps,
|
||||
SUM(CASE WHEN status >= 200 AND status < 400 THEN 1 ELSE 0 END) AS success_count,
|
||||
SUM(CASE WHEN status >= 400 THEN 1 ELSE 0 END) AS error_count,
|
||||
COUNT(*) AS sample_count`
|
||||
|
||||
return `
|
||||
WITH filtered AS (
|
||||
SELECT
|
||||
from_iso8601_timestamp(event_timestamp) AS event_time,
|
||||
CASE
|
||||
WHEN source = 'lite' THEN 'Go'
|
||||
WHEN model IN ('gpt-5-nano', 'grok-code', 'big-pickle') OR model LIKE '%-free' THEN 'Free'
|
||||
ELSE 'Paid'
|
||||
END AS tier,
|
||||
COALESCE(NULLIF(
|
||||
CASE
|
||||
WHEN starts_with(provider, 'minimax-plan') THEN 'minimax-plan'
|
||||
WHEN starts_with(provider, 'zai-plan') THEN 'zai-plan'
|
||||
WHEN starts_with(provider, 'azure-databricks') THEN 'azure-databricks'
|
||||
WHEN regexp_like(provider, '^azure[0-9]+') THEN 'azure-openai'
|
||||
ELSE provider
|
||||
END,
|
||||
''
|
||||
), 'unknown') AS provider,
|
||||
COALESCE(NULLIF(provider_model, ''), '') AS provider_model,
|
||||
COALESCE(NULLIF(model, ''), 'unknown') AS model,
|
||||
UPPER(COALESCE(NULLIF(cf_country, ''), 'ZZ')) AS country,
|
||||
COALESCE(NULLIF(cf_continent, ''), '') AS continent,
|
||||
session,
|
||||
status,
|
||||
duration AS duration_ms,
|
||||
time_to_first_byte AS ttfb_ms,
|
||||
CASE
|
||||
WHEN timestamp_last_byte - timestamp_first_byte < 100 THEN null
|
||||
ELSE CAST(tokens_output AS double) / (timestamp_last_byte - timestamp_first_byte) * 1000
|
||||
END AS output_tps,
|
||||
tokens_input,
|
||||
tokens_output,
|
||||
tokens_reasoning,
|
||||
tokens_cache_read,
|
||||
COALESCE(tokens_cache_read, 0) + COALESCE(tokens_cache_write_5m, 0) + COALESCE(tokens_input, 0) + COALESCE(tokens_output, 0) AS tokens_total,
|
||||
COALESCE(cost_input_microcents, cost_input * 1000000) AS cost_input_microcents,
|
||||
COALESCE(cost_output_microcents, cost_output * 1000000) AS cost_output_microcents,
|
||||
COALESCE(cost_total_microcents, cost_total * 1000000) AS cost_total_microcents
|
||||
FROM ${sourceTable}
|
||||
WHERE event_type = 'completions'
|
||||
AND model IS NOT NULL
|
||||
AND model <> ''
|
||||
AND (strpos(COALESCE(user_agent, ''), 'ai-sdk') > 0 OR strpos(COALESCE(user_agent, ''), 'opencode') > 0)
|
||||
AND event_timestamp >= ${periodStartValue}
|
||||
AND event_timestamp < ${periodEndValue}
|
||||
), daily AS (
|
||||
SELECT date_trunc('day', event_time) AS day, *
|
||||
FROM filtered
|
||||
)
|
||||
SELECT
|
||||
'week' AS grain,
|
||||
${periodStartValue} AS period_start,
|
||||
${periodEndValue} AS period_end,
|
||||
${sqlString(Resource.StatsSyncConfig.dataset)} AS dataset,
|
||||
tier,
|
||||
${dimensionSql.select},
|
||||
${aggregateColumns}
|
||||
FROM filtered
|
||||
GROUP BY tier, ${dimensionSql.groupBy}
|
||||
UNION ALL
|
||||
SELECT
|
||||
'day' AS grain,
|
||||
to_iso8601(day) AS period_start,
|
||||
to_iso8601(least(day + INTERVAL '1' DAY, from_iso8601_timestamp(${periodEndValue}))) AS period_end,
|
||||
${sqlString(Resource.StatsSyncConfig.dataset)} AS dataset,
|
||||
tier,
|
||||
${dimensionSql.select},
|
||||
${aggregateColumns}
|
||||
FROM daily
|
||||
GROUP BY day, tier, ${dimensionSql.groupBy}
|
||||
ORDER BY grain, period_start, total_tokens DESC
|
||||
`
|
||||
}
|
||||
|
||||
export function toModelAggregate(data: AthenaData): ModelStatAggregate[] {
|
||||
return toStatBaseAggregate(data).flatMap((base) => [
|
||||
{
|
||||
...base,
|
||||
provider: data.provider || "unknown",
|
||||
model: data.model || "unknown",
|
||||
provider_model: data.provider_model || "",
|
||||
},
|
||||
])
|
||||
}
|
||||
|
||||
export function toProviderAggregate(data: AthenaData): ProviderStatAggregate[] {
|
||||
return toStatBaseAggregate(data).flatMap((base) => [{ ...base, provider: data.provider || "unknown" }])
|
||||
}
|
||||
|
||||
export function toGeoAggregate(data: AthenaData): GeoStatAggregate[] {
|
||||
return toStatBaseAggregate(data).flatMap((base) => [
|
||||
{
|
||||
...base,
|
||||
country: normalizeCountry(data.country),
|
||||
continent: data.continent || "",
|
||||
},
|
||||
])
|
||||
}
|
||||
|
||||
function toStatBaseAggregate(data: AthenaData): StatBaseAggregate[] {
|
||||
const grain = data.grain === "day" || data.grain === "week" ? data.grain : undefined
|
||||
const periodStart = new Date(data.period_start ?? "")
|
||||
const periodEnd = new Date(data.period_end ?? "")
|
||||
if (!grain || Number.isNaN(periodStart.getTime()) || Number.isNaN(periodEnd.getTime())) return []
|
||||
|
||||
return [
|
||||
{
|
||||
grain,
|
||||
period_start: periodStart,
|
||||
period_end: periodEnd,
|
||||
dataset: data.dataset || Resource.StatsSyncConfig.dataset,
|
||||
tier: normalizeTier(data.tier || "unknown"),
|
||||
sessions: integer(data, "sessions"),
|
||||
requests: integer(data, "requests"),
|
||||
input_tokens: integer(data, "input_tokens"),
|
||||
output_tokens: integer(data, "output_tokens"),
|
||||
reasoning_tokens: integer(data, "reasoning_tokens"),
|
||||
cache_read_tokens: integer(data, "cache_read_tokens"),
|
||||
total_tokens: integer(data, "total_tokens"),
|
||||
input_cost_microcents: integer(data, "input_cost_microcents"),
|
||||
output_cost_microcents: integer(data, "output_cost_microcents"),
|
||||
total_cost_microcents: integer(data, "total_cost_microcents"),
|
||||
avg_duration_ms: nullableNumber(data, "avg_duration_ms"),
|
||||
p50_duration_ms: nullableInteger(data, "p50_duration_ms"),
|
||||
p95_duration_ms: nullableInteger(data, "p95_duration_ms"),
|
||||
avg_ttfb_ms: nullableNumber(data, "avg_ttfb_ms"),
|
||||
p50_ttfb_ms: nullableInteger(data, "p50_ttfb_ms"),
|
||||
p95_ttfb_ms: nullableInteger(data, "p95_ttfb_ms"),
|
||||
avg_output_tps: nullableNumber(data, "avg_output_tps"),
|
||||
success_count: integer(data, "success_count"),
|
||||
error_count: integer(data, "error_count"),
|
||||
sample_count: integer(data, "sample_count"),
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
function integer(data: AthenaData, key: string) {
|
||||
return Math.round(number(data, key))
|
||||
}
|
||||
|
||||
function nullableNumber(data: AthenaData, key: string) {
|
||||
if (data[key] === undefined || data[key] === "") return null
|
||||
return Number(number(data, key).toFixed(2))
|
||||
}
|
||||
|
||||
function nullableInteger(data: AthenaData, key: string) {
|
||||
if (data[key] === undefined || data[key] === "") return null
|
||||
return Math.round(number(data, key))
|
||||
}
|
||||
|
||||
function number(data: AthenaData, key: string) {
|
||||
const value = Number(data[key])
|
||||
return Number.isFinite(value) ? value : 0
|
||||
}
|
||||
|
||||
function sqlIdentifier(value: string) {
|
||||
return `"${value.replace(/"/g, '""')}"`
|
||||
}
|
||||
|
||||
function sqlString(value: string) {
|
||||
return `'${value.replace(/'/g, "''")}'`
|
||||
}
|
||||
173
packages/stats/core/src/domain/model.ts
Normal file
173
packages/stats/core/src/domain/model.ts
Normal file
|
|
@ -0,0 +1,173 @@
|
|||
import { and, asc, eq } from "drizzle-orm"
|
||||
import { Effect, Layer } from "effect"
|
||||
import * as Context from "effect/Context"
|
||||
import { DatabaseError, DrizzleClient } from "../database"
|
||||
import { modelStat } from "../database/schema"
|
||||
import {
|
||||
chunks,
|
||||
collapseRows,
|
||||
inserted,
|
||||
rankBy,
|
||||
statPeriodKey,
|
||||
synthesizeAllTierRows,
|
||||
toStatBaseRow,
|
||||
UPSERT_CHUNK_SIZE,
|
||||
type StatBaseAggregate,
|
||||
} from "./stat"
|
||||
|
||||
export type ModelStatRow = typeof modelStat.$inferInsert
|
||||
export type ModelStatAggregate = StatBaseAggregate & { provider: string; model: string; provider_model: string }
|
||||
|
||||
export type ModelStatMetric = {
|
||||
periodStart: Date
|
||||
periodEnd: Date
|
||||
tier: string
|
||||
provider: string
|
||||
model: string
|
||||
sessions: number
|
||||
inputTokens: number
|
||||
outputTokens: number
|
||||
reasoningTokens: number
|
||||
cacheReadTokens: number
|
||||
totalTokens: number
|
||||
inputCostMicrocents: number
|
||||
outputCostMicrocents: number
|
||||
totalCostMicrocents: number
|
||||
}
|
||||
|
||||
export declare namespace ModelStatRepo {
|
||||
export interface Service {
|
||||
readonly listDaily: () => Effect.Effect<ModelStatMetric[], DatabaseError>
|
||||
readonly upsert: (rows: ModelStatRow[]) => Effect.Effect<void, DatabaseError>
|
||||
}
|
||||
}
|
||||
|
||||
export class ModelStatRepo extends Context.Service<ModelStatRepo, ModelStatRepo.Service>()(
|
||||
"@opencode/stats/ModelStatRepo",
|
||||
) {
|
||||
static readonly layer: Layer.Layer<ModelStatRepo, never, DrizzleClient> = Layer.effect(
|
||||
ModelStatRepo,
|
||||
Effect.gen(function* () {
|
||||
const db = yield* DrizzleClient
|
||||
|
||||
const listDaily = Effect.fn("ModelStatRepo.listDaily")(function* () {
|
||||
return yield* Effect.tryPromise({
|
||||
try: () =>
|
||||
db
|
||||
.select({
|
||||
periodStart: modelStat.period_start,
|
||||
periodEnd: modelStat.period_end,
|
||||
tier: modelStat.tier,
|
||||
provider: modelStat.provider,
|
||||
model: modelStat.model,
|
||||
sessions: modelStat.sessions,
|
||||
inputTokens: modelStat.input_tokens,
|
||||
outputTokens: modelStat.output_tokens,
|
||||
reasoningTokens: modelStat.reasoning_tokens,
|
||||
cacheReadTokens: modelStat.cache_read_tokens,
|
||||
totalTokens: modelStat.total_tokens,
|
||||
inputCostMicrocents: modelStat.input_cost_microcents,
|
||||
outputCostMicrocents: modelStat.output_cost_microcents,
|
||||
totalCostMicrocents: modelStat.total_cost_microcents,
|
||||
})
|
||||
.from(modelStat)
|
||||
.where(and(eq(modelStat.grain, "day"), eq(modelStat.client, "all"), eq(modelStat.source, "all")))
|
||||
.orderBy(asc(modelStat.period_start)),
|
||||
catch: (cause) => DatabaseError.make({ cause }),
|
||||
})
|
||||
})
|
||||
|
||||
const upsert = Effect.fn("ModelStatRepo.upsert")(function* (rows: ModelStatRow[]) {
|
||||
yield* Effect.forEach(
|
||||
chunks(rows, UPSERT_CHUNK_SIZE),
|
||||
(chunk) =>
|
||||
Effect.tryPromise({
|
||||
try: () =>
|
||||
db
|
||||
.insert(modelStat)
|
||||
.values(chunk)
|
||||
.onDuplicateKeyUpdate({
|
||||
set: {
|
||||
period_end: inserted("period_end"),
|
||||
provider_model: inserted("provider_model"),
|
||||
sessions: inserted("sessions"),
|
||||
requests: inserted("requests"),
|
||||
input_tokens: inserted("input_tokens"),
|
||||
output_tokens: inserted("output_tokens"),
|
||||
reasoning_tokens: inserted("reasoning_tokens"),
|
||||
cache_read_tokens: inserted("cache_read_tokens"),
|
||||
total_tokens: inserted("total_tokens"),
|
||||
input_cost_microcents: inserted("input_cost_microcents"),
|
||||
output_cost_microcents: inserted("output_cost_microcents"),
|
||||
total_cost_microcents: inserted("total_cost_microcents"),
|
||||
avg_duration_ms: inserted("avg_duration_ms"),
|
||||
p50_duration_ms: inserted("p50_duration_ms"),
|
||||
p95_duration_ms: inserted("p95_duration_ms"),
|
||||
avg_ttfb_ms: inserted("avg_ttfb_ms"),
|
||||
p50_ttfb_ms: inserted("p50_ttfb_ms"),
|
||||
p95_ttfb_ms: inserted("p95_ttfb_ms"),
|
||||
avg_output_tps: inserted("avg_output_tps"),
|
||||
success_count: inserted("success_count"),
|
||||
error_count: inserted("error_count"),
|
||||
sample_count: inserted("sample_count"),
|
||||
rank_by_tokens: inserted("rank_by_tokens"),
|
||||
rank_by_requests: inserted("rank_by_requests"),
|
||||
rank_by_cost: inserted("rank_by_cost"),
|
||||
},
|
||||
}),
|
||||
catch: (cause) => DatabaseError.make({ cause }),
|
||||
}),
|
||||
{ discard: true },
|
||||
)
|
||||
})
|
||||
|
||||
return ModelStatRepo.of({ listDaily, upsert })
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
export function rowsFromAggregates(aggregates: ModelStatAggregate[]) {
|
||||
return rankRows([
|
||||
...synthesizeAllTierRows(
|
||||
collapseRows(aggregates.filter((item) => item.grain === "week").map(toRow), dimensionKey),
|
||||
dimensionKey,
|
||||
),
|
||||
...synthesizeAllTierRows(
|
||||
collapseRows(aggregates.filter((item) => item.grain === "day").map(toRow), dimensionKey),
|
||||
dimensionKey,
|
||||
),
|
||||
])
|
||||
}
|
||||
|
||||
function toRow(data: ModelStatAggregate): ModelStatRow {
|
||||
return {
|
||||
...toStatBaseRow(data),
|
||||
provider: data.provider,
|
||||
model: data.model,
|
||||
provider_model: data.provider_model,
|
||||
}
|
||||
}
|
||||
|
||||
function rankRows(rows: ModelStatRow[]) {
|
||||
return Object.values(
|
||||
rows.reduce<Record<string, ModelStatRow[]>>((result, row) => {
|
||||
const key = statPeriodKey(row)
|
||||
result[key] = [...(result[key] ?? []), row]
|
||||
return result
|
||||
}, {}),
|
||||
).flatMap((group) => {
|
||||
const tokenRanks = rankBy(group, (row) => row.total_tokens ?? 0)
|
||||
const requestRanks = rankBy(group, (row) => row.requests ?? 0)
|
||||
const costRanks = rankBy(group, (row) => row.total_cost_microcents ?? 0)
|
||||
return group.map((row) => ({
|
||||
...row,
|
||||
rank_by_tokens: tokenRanks.get(row) ?? null,
|
||||
rank_by_requests: requestRanks.get(row) ?? null,
|
||||
rank_by_cost: costRanks.get(row) ?? null,
|
||||
}))
|
||||
})
|
||||
}
|
||||
|
||||
function dimensionKey(row: ModelStatRow) {
|
||||
return [row.provider, row.model].join("\u0000")
|
||||
}
|
||||
169
packages/stats/core/src/domain/provider.ts
Normal file
169
packages/stats/core/src/domain/provider.ts
Normal file
|
|
@ -0,0 +1,169 @@
|
|||
import { and, asc, eq } from "drizzle-orm"
|
||||
import { Effect, Layer } from "effect"
|
||||
import * as Context from "effect/Context"
|
||||
import { DatabaseError, DrizzleClient } from "../database"
|
||||
import { providerStat } from "../database/schema"
|
||||
import {
|
||||
chunks,
|
||||
collapseRows,
|
||||
inserted,
|
||||
rankRowsWithMarketShare,
|
||||
synthesizeAllTierRows,
|
||||
toStatBaseRow,
|
||||
UPSERT_CHUNK_SIZE,
|
||||
type StatBaseAggregate,
|
||||
} from "./stat"
|
||||
|
||||
export type ProviderStatRow = typeof providerStat.$inferInsert
|
||||
export type ProviderStatAggregate = StatBaseAggregate & { provider: string }
|
||||
export type ProviderStatMetric = {
|
||||
periodStart: Date
|
||||
periodEnd: Date
|
||||
tier: string
|
||||
provider: string
|
||||
totalTokens: number
|
||||
}
|
||||
|
||||
export declare namespace ProviderStatRepo {
|
||||
export interface Service {
|
||||
readonly listDaily: () => Effect.Effect<ProviderStatMetric[], DatabaseError>
|
||||
readonly listByPeriod: (opts: {
|
||||
readonly grain: string
|
||||
readonly periodStart: Date
|
||||
readonly dataset?: string
|
||||
readonly tier?: string
|
||||
readonly client?: string
|
||||
readonly source?: string
|
||||
}) => Effect.Effect<ProviderStatRow[], DatabaseError>
|
||||
readonly upsert: (rows: ProviderStatRow[]) => Effect.Effect<void, DatabaseError>
|
||||
}
|
||||
}
|
||||
|
||||
export class ProviderStatRepo extends Context.Service<ProviderStatRepo, ProviderStatRepo.Service>()(
|
||||
"@opencode/stats/ProviderStatRepo",
|
||||
) {
|
||||
static readonly layer: Layer.Layer<ProviderStatRepo, never, DrizzleClient> = Layer.effect(
|
||||
ProviderStatRepo,
|
||||
Effect.gen(function* () {
|
||||
const db = yield* DrizzleClient
|
||||
|
||||
const listDaily = Effect.fn("ProviderStatRepo.listDaily")(function* () {
|
||||
return yield* Effect.tryPromise({
|
||||
try: () =>
|
||||
db
|
||||
.select({
|
||||
periodStart: providerStat.period_start,
|
||||
periodEnd: providerStat.period_end,
|
||||
tier: providerStat.tier,
|
||||
provider: providerStat.provider,
|
||||
totalTokens: providerStat.total_tokens,
|
||||
})
|
||||
.from(providerStat)
|
||||
.where(and(eq(providerStat.grain, "day"), eq(providerStat.client, "all"), eq(providerStat.source, "all")))
|
||||
.orderBy(asc(providerStat.period_start)),
|
||||
catch: (cause) => DatabaseError.make({ cause }),
|
||||
})
|
||||
})
|
||||
|
||||
const listByPeriod = Effect.fn("ProviderStatRepo.listByPeriod")(function* (opts: {
|
||||
readonly grain: string
|
||||
readonly periodStart: Date
|
||||
readonly dataset?: string
|
||||
readonly tier?: string
|
||||
readonly client?: string
|
||||
readonly source?: string
|
||||
}) {
|
||||
return yield* Effect.tryPromise({
|
||||
try: () =>
|
||||
db
|
||||
.select()
|
||||
.from(providerStat)
|
||||
.where(
|
||||
and(
|
||||
eq(providerStat.grain, opts.grain),
|
||||
eq(providerStat.period_start, opts.periodStart),
|
||||
eq(providerStat.dataset, opts.dataset ?? "zen"),
|
||||
eq(providerStat.tier, opts.tier ?? "all"),
|
||||
eq(providerStat.client, opts.client ?? "all"),
|
||||
eq(providerStat.source, opts.source ?? "all"),
|
||||
),
|
||||
),
|
||||
catch: (cause) => DatabaseError.make({ cause }),
|
||||
})
|
||||
})
|
||||
|
||||
const upsert = Effect.fn("ProviderStatRepo.upsert")(function* (rows: ProviderStatRow[]) {
|
||||
yield* Effect.forEach(
|
||||
chunks(rows, UPSERT_CHUNK_SIZE),
|
||||
(chunk) =>
|
||||
Effect.tryPromise({
|
||||
try: () =>
|
||||
db
|
||||
.insert(providerStat)
|
||||
.values(chunk)
|
||||
.onDuplicateKeyUpdate({
|
||||
set: {
|
||||
period_end: inserted("period_end"),
|
||||
sessions: inserted("sessions"),
|
||||
requests: inserted("requests"),
|
||||
input_tokens: inserted("input_tokens"),
|
||||
output_tokens: inserted("output_tokens"),
|
||||
reasoning_tokens: inserted("reasoning_tokens"),
|
||||
cache_read_tokens: inserted("cache_read_tokens"),
|
||||
total_tokens: inserted("total_tokens"),
|
||||
input_cost_microcents: inserted("input_cost_microcents"),
|
||||
output_cost_microcents: inserted("output_cost_microcents"),
|
||||
total_cost_microcents: inserted("total_cost_microcents"),
|
||||
avg_duration_ms: inserted("avg_duration_ms"),
|
||||
p50_duration_ms: inserted("p50_duration_ms"),
|
||||
p95_duration_ms: inserted("p95_duration_ms"),
|
||||
avg_ttfb_ms: inserted("avg_ttfb_ms"),
|
||||
p50_ttfb_ms: inserted("p50_ttfb_ms"),
|
||||
p95_ttfb_ms: inserted("p95_ttfb_ms"),
|
||||
avg_output_tps: inserted("avg_output_tps"),
|
||||
success_count: inserted("success_count"),
|
||||
error_count: inserted("error_count"),
|
||||
sample_count: inserted("sample_count"),
|
||||
market_share_tokens: inserted("market_share_tokens"),
|
||||
market_share_requests: inserted("market_share_requests"),
|
||||
market_share_sessions: inserted("market_share_sessions"),
|
||||
rank_by_tokens: inserted("rank_by_tokens"),
|
||||
rank_by_requests: inserted("rank_by_requests"),
|
||||
rank_by_sessions: inserted("rank_by_sessions"),
|
||||
rank_by_cost: inserted("rank_by_cost"),
|
||||
},
|
||||
}),
|
||||
catch: (cause) => DatabaseError.make({ cause }),
|
||||
}),
|
||||
{ discard: true },
|
||||
)
|
||||
})
|
||||
|
||||
return ProviderStatRepo.of({ listDaily, listByPeriod, upsert })
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
export function rowsFromAggregates(aggregates: ProviderStatAggregate[]) {
|
||||
return rankRowsWithMarketShare([
|
||||
...synthesizeAllTierRows(
|
||||
collapseRows(aggregates.filter((item) => item.grain === "week").map(toRow), dimensionKey),
|
||||
dimensionKey,
|
||||
),
|
||||
...synthesizeAllTierRows(
|
||||
collapseRows(aggregates.filter((item) => item.grain === "day").map(toRow), dimensionKey),
|
||||
dimensionKey,
|
||||
),
|
||||
])
|
||||
}
|
||||
|
||||
function toRow(data: ProviderStatAggregate): ProviderStatRow {
|
||||
return {
|
||||
...toStatBaseRow(data),
|
||||
provider: data.provider,
|
||||
}
|
||||
}
|
||||
|
||||
function dimensionKey(row: ProviderStatRow) {
|
||||
return row.provider
|
||||
}
|
||||
233
packages/stats/core/src/domain/stat.ts
Normal file
233
packages/stats/core/src/domain/stat.ts
Normal file
|
|
@ -0,0 +1,233 @@
|
|||
import { sql } from "drizzle-orm"
|
||||
|
||||
export const UPSERT_CHUNK_SIZE = 500
|
||||
|
||||
export type StatGrain = "day" | "week"
|
||||
|
||||
export type StatBaseAggregate = {
|
||||
grain: StatGrain
|
||||
period_start: Date
|
||||
period_end: Date
|
||||
dataset: string
|
||||
tier: string
|
||||
sessions: number
|
||||
requests: number
|
||||
input_tokens: number
|
||||
output_tokens: number
|
||||
reasoning_tokens: number
|
||||
cache_read_tokens: number
|
||||
total_tokens: number
|
||||
input_cost_microcents: number
|
||||
output_cost_microcents: number
|
||||
total_cost_microcents: number
|
||||
avg_duration_ms: number | null
|
||||
p50_duration_ms: number | null
|
||||
p95_duration_ms: number | null
|
||||
avg_ttfb_ms: number | null
|
||||
p50_ttfb_ms: number | null
|
||||
p95_ttfb_ms: number | null
|
||||
avg_output_tps: number | null
|
||||
success_count: number
|
||||
error_count: number
|
||||
sample_count: number
|
||||
}
|
||||
|
||||
export type StatBaseRow = {
|
||||
grain: string
|
||||
period_start: Date
|
||||
period_end: Date
|
||||
dataset?: string
|
||||
tier?: string
|
||||
client?: string
|
||||
source?: string
|
||||
sessions?: number
|
||||
requests?: number
|
||||
input_tokens?: number
|
||||
output_tokens?: number
|
||||
reasoning_tokens?: number
|
||||
cache_read_tokens?: number
|
||||
total_tokens?: number
|
||||
input_cost_microcents?: number
|
||||
output_cost_microcents?: number
|
||||
total_cost_microcents?: number
|
||||
avg_duration_ms?: number | null
|
||||
p50_duration_ms?: number | null
|
||||
p95_duration_ms?: number | null
|
||||
avg_ttfb_ms?: number | null
|
||||
p50_ttfb_ms?: number | null
|
||||
p95_ttfb_ms?: number | null
|
||||
avg_output_tps?: number | null
|
||||
success_count?: number
|
||||
error_count?: number
|
||||
sample_count?: number
|
||||
}
|
||||
|
||||
export function toStatBaseRow(data: StatBaseAggregate) {
|
||||
return {
|
||||
grain: data.grain,
|
||||
period_start: data.period_start,
|
||||
period_end: data.period_end,
|
||||
dataset: data.dataset,
|
||||
tier: data.tier,
|
||||
client: "all",
|
||||
source: "all",
|
||||
sessions: data.sessions,
|
||||
requests: data.requests,
|
||||
input_tokens: data.input_tokens,
|
||||
output_tokens: data.output_tokens,
|
||||
reasoning_tokens: data.reasoning_tokens,
|
||||
cache_read_tokens: data.cache_read_tokens,
|
||||
total_tokens: data.total_tokens,
|
||||
input_cost_microcents: data.input_cost_microcents,
|
||||
output_cost_microcents: data.output_cost_microcents,
|
||||
total_cost_microcents: data.total_cost_microcents,
|
||||
avg_duration_ms: data.avg_duration_ms,
|
||||
p50_duration_ms: data.p50_duration_ms,
|
||||
p95_duration_ms: data.p95_duration_ms,
|
||||
avg_ttfb_ms: data.avg_ttfb_ms,
|
||||
p50_ttfb_ms: data.p50_ttfb_ms,
|
||||
p95_ttfb_ms: data.p95_ttfb_ms,
|
||||
avg_output_tps: data.avg_output_tps,
|
||||
success_count: data.success_count,
|
||||
error_count: data.error_count,
|
||||
sample_count: data.sample_count,
|
||||
}
|
||||
}
|
||||
|
||||
export function synthesizeAllTierRows<T extends StatBaseRow>(rows: T[], dimensionKey: (row: T) => string) {
|
||||
return [
|
||||
...rows,
|
||||
...Object.values(
|
||||
rows.reduce<Record<string, T>>((result, row) => {
|
||||
const key = [
|
||||
row.grain,
|
||||
row.period_start.toISOString(),
|
||||
row.dataset,
|
||||
row.client,
|
||||
row.source,
|
||||
dimensionKey(row),
|
||||
].join("\u0000")
|
||||
result[key] = result[key] ? combineRows(result[key], row) : { ...row, tier: "all" }
|
||||
return result
|
||||
}, {}),
|
||||
),
|
||||
]
|
||||
}
|
||||
|
||||
export function collapseRows<T extends StatBaseRow>(rows: T[], dimensionKey: (row: T) => string) {
|
||||
return Object.values(
|
||||
rows.reduce<Record<string, T>>((result, row) => {
|
||||
const key = [
|
||||
row.grain,
|
||||
row.period_start.toISOString(),
|
||||
row.dataset,
|
||||
row.tier,
|
||||
row.client,
|
||||
row.source,
|
||||
dimensionKey(row),
|
||||
].join("\u0000")
|
||||
result[key] = result[key] ? combineRows(result[key], row) : row
|
||||
return result
|
||||
}, {}),
|
||||
)
|
||||
}
|
||||
|
||||
export function combineRows<T extends StatBaseRow>(left: T, right: T): T {
|
||||
return {
|
||||
...left,
|
||||
period_end: right.period_end > left.period_end ? right.period_end : left.period_end,
|
||||
sessions: (left.sessions ?? 0) + (right.sessions ?? 0),
|
||||
requests: (left.requests ?? 0) + (right.requests ?? 0),
|
||||
input_tokens: (left.input_tokens ?? 0) + (right.input_tokens ?? 0),
|
||||
output_tokens: (left.output_tokens ?? 0) + (right.output_tokens ?? 0),
|
||||
reasoning_tokens: (left.reasoning_tokens ?? 0) + (right.reasoning_tokens ?? 0),
|
||||
cache_read_tokens: (left.cache_read_tokens ?? 0) + (right.cache_read_tokens ?? 0),
|
||||
total_tokens: (left.total_tokens ?? 0) + (right.total_tokens ?? 0),
|
||||
input_cost_microcents: (left.input_cost_microcents ?? 0) + (right.input_cost_microcents ?? 0),
|
||||
output_cost_microcents: (left.output_cost_microcents ?? 0) + (right.output_cost_microcents ?? 0),
|
||||
total_cost_microcents: (left.total_cost_microcents ?? 0) + (right.total_cost_microcents ?? 0),
|
||||
avg_duration_ms: weightedAverage(left.avg_duration_ms, left.requests, right.avg_duration_ms, right.requests),
|
||||
p50_duration_ms: null,
|
||||
p95_duration_ms: null,
|
||||
avg_ttfb_ms: weightedAverage(left.avg_ttfb_ms, left.requests, right.avg_ttfb_ms, right.requests),
|
||||
p50_ttfb_ms: null,
|
||||
p95_ttfb_ms: null,
|
||||
avg_output_tps: weightedAverage(left.avg_output_tps, left.requests, right.avg_output_tps, right.requests),
|
||||
success_count: (left.success_count ?? 0) + (right.success_count ?? 0),
|
||||
error_count: (left.error_count ?? 0) + (right.error_count ?? 0),
|
||||
sample_count: (left.sample_count ?? 0) + (right.sample_count ?? 0),
|
||||
}
|
||||
}
|
||||
|
||||
export function statPeriodKey(row: StatBaseRow) {
|
||||
return [row.grain, row.period_start.toISOString(), row.dataset, row.tier, row.client, row.source].join("\u0000")
|
||||
}
|
||||
|
||||
export function rankBy<T extends StatBaseRow>(rows: T[], value: (row: T) => number) {
|
||||
return new Map(rows.toSorted((a, b) => value(b) - value(a)).map((row, index) => [row, index + 1]))
|
||||
}
|
||||
|
||||
export function rankRowsWithMarketShare<T extends StatBaseRow>(rows: T[]) {
|
||||
return Object.values(
|
||||
rows.reduce<Record<string, T[]>>((result, row) => {
|
||||
const key = statPeriodKey(row)
|
||||
result[key] = [...(result[key] ?? []), row]
|
||||
return result
|
||||
}, {}),
|
||||
).flatMap((group) => {
|
||||
const tokens = group.reduce((sum, row) => sum + (row.total_tokens ?? 0), 0)
|
||||
const requests = group.reduce((sum, row) => sum + (row.requests ?? 0), 0)
|
||||
const sessions = group.reduce((sum, row) => sum + (row.sessions ?? 0), 0)
|
||||
const tokenRanks = rankBy(group, (row) => row.total_tokens ?? 0)
|
||||
const requestRanks = rankBy(group, (row) => row.requests ?? 0)
|
||||
const sessionRanks = rankBy(group, (row) => row.sessions ?? 0)
|
||||
const costRanks = rankBy(group, (row) => row.total_cost_microcents ?? 0)
|
||||
return group.map((row) => ({
|
||||
...row,
|
||||
market_share_tokens: share(row.total_tokens, tokens),
|
||||
market_share_requests: share(row.requests, requests),
|
||||
market_share_sessions: share(row.sessions, sessions),
|
||||
rank_by_tokens: tokenRanks.get(row) ?? null,
|
||||
rank_by_requests: requestRanks.get(row) ?? null,
|
||||
rank_by_sessions: sessionRanks.get(row) ?? null,
|
||||
rank_by_cost: costRanks.get(row) ?? null,
|
||||
}))
|
||||
})
|
||||
}
|
||||
|
||||
export function share(value: number | null | undefined, total: number) {
|
||||
if (total <= 0) return null
|
||||
return Number(((value ?? 0) / total).toFixed(6))
|
||||
}
|
||||
|
||||
export function chunks<T>(items: T[], size: number) {
|
||||
return Array.from({ length: Math.ceil(items.length / size) }, (_, index) =>
|
||||
items.slice(index * size, (index + 1) * size),
|
||||
)
|
||||
}
|
||||
|
||||
export function inserted(column: string) {
|
||||
return sql.raw(`values(\`${column}\`)`)
|
||||
}
|
||||
|
||||
export function weightedAverage(
|
||||
left: number | null | undefined,
|
||||
leftWeight = 0,
|
||||
right: number | null | undefined,
|
||||
rightWeight = 0,
|
||||
) {
|
||||
const totalWeight =
|
||||
(left === null || left === undefined ? 0 : leftWeight) + (right === null || right === undefined ? 0 : rightWeight)
|
||||
if (totalWeight === 0) return null
|
||||
return Number((((left ?? 0) * leftWeight + (right ?? 0) * rightWeight) / totalWeight).toFixed(2))
|
||||
}
|
||||
|
||||
export function normalizeTier(value: string) {
|
||||
if (value === "Paid") return "Zen"
|
||||
return value
|
||||
}
|
||||
|
||||
export function normalizeCountry(value: string | undefined) {
|
||||
if (!value || value.length !== 2) return "ZZ"
|
||||
return value.toUpperCase()
|
||||
}
|
||||
11
packages/stats/core/src/index.ts
Normal file
11
packages/stats/core/src/index.ts
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
export * as Athena from "./athena"
|
||||
export * as AppConfig from "./config"
|
||||
export * as Database from "./database"
|
||||
export * as GeoStat from "./domain/geo"
|
||||
export * as StatsHome from "./domain/home"
|
||||
export * as Inference from "./domain/inference"
|
||||
export * as ModelStat from "./domain/model"
|
||||
export * as ProviderStat from "./domain/provider"
|
||||
export * as Stat from "./domain/stat"
|
||||
export * as Runtime from "./runtime"
|
||||
export * as StatSync from "./stat-sync"
|
||||
4
packages/stats/core/src/migrate.ts
Normal file
4
packages/stats/core/src/migrate.ts
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
import { Effect } from "effect"
|
||||
import { layer, migrate } from "./database"
|
||||
|
||||
await Effect.runPromise(migrate().pipe(Effect.provide(layer)))
|
||||
28
packages/stats/core/src/resource.d.ts
vendored
Normal file
28
packages/stats/core/src/resource.d.ts
vendored
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
import "sst/resource"
|
||||
|
||||
declare module "sst/resource" {
|
||||
export interface Resource {
|
||||
InferenceEvent: {
|
||||
catalog: string
|
||||
database: string
|
||||
region: string
|
||||
table: string
|
||||
tableBucket: string
|
||||
type: "sst.sst.Linkable"
|
||||
workgroup: string
|
||||
}
|
||||
StatsSyncConfig: {
|
||||
dataset: string
|
||||
type: "sst.sst.Linkable"
|
||||
}
|
||||
StatsDatabase: {
|
||||
database: string
|
||||
host: string
|
||||
password: string
|
||||
port: number
|
||||
type: "sst.sst.Linkable"
|
||||
url: string
|
||||
username: string
|
||||
}
|
||||
}
|
||||
}
|
||||
14
packages/stats/core/src/runtime.ts
Normal file
14
packages/stats/core/src/runtime.ts
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
import { Layer, ManagedRuntime } from "effect"
|
||||
import { AppConfig } from "./config"
|
||||
import { layer as databaseLayer } from "./database"
|
||||
import { GeoStatRepo } from "./domain/geo"
|
||||
import { ModelStatRepo } from "./domain/model"
|
||||
import { ProviderStatRepo } from "./domain/provider"
|
||||
|
||||
const repoLayer = Layer.mergeAll(ModelStatRepo.layer, ProviderStatRepo.layer, GeoStatRepo.layer).pipe(
|
||||
Layer.provide(databaseLayer),
|
||||
)
|
||||
|
||||
export const layer = Layer.mergeAll(AppConfig.layer, databaseLayer, repoLayer)
|
||||
export const runtime = ManagedRuntime.make(layer)
|
||||
export type RuntimeServices = ManagedRuntime.ManagedRuntime.Services<typeof runtime>
|
||||
88
packages/stats/core/src/stat-sync.ts
Normal file
88
packages/stats/core/src/stat-sync.ts
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
import { DateTime, Effect } from "effect"
|
||||
import { Resource } from "sst/resource"
|
||||
import { Athena, AthenaQueryError, AthenaQueryTimeoutError } from "./athena"
|
||||
import { DatabaseError } from "./database"
|
||||
import { GeoStatRepo, rowsFromAggregates as geoRowsFromAggregates } from "./domain/geo"
|
||||
import { buildStatsQuery, toGeoAggregate, toModelAggregate, toProviderAggregate } from "./domain/inference"
|
||||
import { ModelStatRepo, rowsFromAggregates as modelRowsFromAggregates } from "./domain/model"
|
||||
import { ProviderStatRepo, rowsFromAggregates as providerRowsFromAggregates } from "./domain/provider"
|
||||
|
||||
const DATALAKE_INGESTION_LAG_MS = 5 * 60_000
|
||||
|
||||
export type SyncStatsResult = { ok: true; rows: number; startedAt: string; periodStart: string; periodEnd: string }
|
||||
export type SyncStatsError = AthenaQueryError | AthenaQueryTimeoutError | DatabaseError
|
||||
|
||||
export const syncStats: () => Effect.Effect<
|
||||
SyncStatsResult,
|
||||
SyncStatsError,
|
||||
Athena | ModelStatRepo | ProviderStatRepo | GeoStatRepo
|
||||
> = Effect.fn("StatSync.sync")(function* () {
|
||||
const startedAt = yield* DateTime.nowAsDate
|
||||
const periodEnd = new Date(Math.floor((startedAt.getTime() - DATALAKE_INGESTION_LAG_MS) / 60_000) * 60_000)
|
||||
const periodStart = new Date(
|
||||
Date.UTC(periodEnd.getUTCFullYear(), periodEnd.getUTCMonth(), periodEnd.getUTCDate() - 6),
|
||||
)
|
||||
const athena = yield* Athena
|
||||
const modelStats = yield* ModelStatRepo
|
||||
const providerStats = yield* ProviderStatRepo
|
||||
const geoStats = yield* GeoStatRepo
|
||||
|
||||
yield* logRuntimeCheck()
|
||||
|
||||
const [modelAggregates, providerAggregates, geoAggregates] = yield* Effect.all(
|
||||
[
|
||||
athena
|
||||
.query(buildStatsQuery(periodStart, periodEnd, "model"))
|
||||
.pipe(Effect.map((rows) => rows.flatMap(toModelAggregate))),
|
||||
athena
|
||||
.query(buildStatsQuery(periodStart, periodEnd, "provider"))
|
||||
.pipe(Effect.map((rows) => rows.flatMap(toProviderAggregate))),
|
||||
athena
|
||||
.query(buildStatsQuery(periodStart, periodEnd, "geo"))
|
||||
.pipe(Effect.map((rows) => rows.flatMap(toGeoAggregate))),
|
||||
],
|
||||
{ concurrency: "unbounded" },
|
||||
)
|
||||
const modelRows = modelRowsFromAggregates(modelAggregates)
|
||||
const providerRows = providerRowsFromAggregates(providerAggregates)
|
||||
const geoRows = geoRowsFromAggregates(geoAggregates)
|
||||
|
||||
yield* Effect.all([modelStats.upsert(modelRows), providerStats.upsert(providerRows), geoStats.upsert(geoRows)], {
|
||||
concurrency: "unbounded",
|
||||
discard: true,
|
||||
})
|
||||
|
||||
yield* Effect.logInfo("stats sync complete").pipe(
|
||||
Effect.annotateLogs({
|
||||
startedAt: startedAt.toISOString(),
|
||||
periodStart: periodStart.toISOString(),
|
||||
periodEnd: periodEnd.toISOString(),
|
||||
rows: modelRows.length,
|
||||
providerRows: providerRows.length,
|
||||
geoRows: geoRows.length,
|
||||
stage: Resource.App.stage,
|
||||
}),
|
||||
)
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
rows: modelRows.length,
|
||||
startedAt: startedAt.toISOString(),
|
||||
periodStart: periodStart.toISOString(),
|
||||
periodEnd: periodEnd.toISOString(),
|
||||
}
|
||||
})
|
||||
|
||||
function logRuntimeCheck() {
|
||||
return Effect.logInfo("athena stats runtime check").pipe(
|
||||
Effect.annotateLogs({
|
||||
catalog: Resource.InferenceEvent.catalog,
|
||||
database: Resource.InferenceEvent.database,
|
||||
dataset: Resource.StatsSyncConfig.dataset,
|
||||
table: Resource.InferenceEvent.table,
|
||||
workgroup: Resource.InferenceEvent.workgroup,
|
||||
region: Resource.InferenceEvent.region,
|
||||
stage: Resource.App.stage,
|
||||
}),
|
||||
)
|
||||
}
|
||||
10
packages/stats/core/sst-env.d.ts
vendored
Normal file
10
packages/stats/core/sst-env.d.ts
vendored
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
/* This file is auto-generated by SST. Do not edit. */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
/* deno-fmt-ignore-file */
|
||||
/* biome-ignore-all lint: auto-generated */
|
||||
|
||||
/// <reference path="../../../sst-env.d.ts" />
|
||||
|
||||
import "sst"
|
||||
export {}
|
||||
11
packages/stats/core/tsconfig.json
Normal file
11
packages/stats/core/tsconfig.json
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"extends": "@tsconfig/node22/tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"types": ["bun", "node"]
|
||||
}
|
||||
}
|
||||
43
packages/stats/server/Dockerfile
Normal file
43
packages/stats/server/Dockerfile
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
FROM oven/bun:1.3.14-alpine
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
ENV NODE_ENV=production
|
||||
ENV BUN_RUNTIME_TRANSPILER_CACHE_PATH=0
|
||||
|
||||
COPY package.json bun.lock ./
|
||||
COPY patches ./patches
|
||||
COPY packages/app/package.json ./packages/app/package.json
|
||||
COPY packages/console/app/package.json ./packages/console/app/package.json
|
||||
COPY packages/console/core/package.json ./packages/console/core/package.json
|
||||
COPY packages/console/function/package.json ./packages/console/function/package.json
|
||||
COPY packages/console/mail/package.json ./packages/console/mail/package.json
|
||||
COPY packages/console/resource/package.json ./packages/console/resource/package.json
|
||||
COPY packages/core/package.json ./packages/core/package.json
|
||||
COPY packages/desktop/package.json ./packages/desktop/package.json
|
||||
COPY packages/effect-drizzle-sqlite/package.json ./packages/effect-drizzle-sqlite/package.json
|
||||
COPY packages/enterprise/package.json ./packages/enterprise/package.json
|
||||
COPY packages/function/package.json ./packages/function/package.json
|
||||
COPY packages/http-recorder/package.json ./packages/http-recorder/package.json
|
||||
COPY packages/llm/package.json ./packages/llm/package.json
|
||||
COPY packages/opencode/package.json ./packages/opencode/package.json
|
||||
COPY packages/plugin/package.json ./packages/plugin/package.json
|
||||
COPY packages/script/package.json ./packages/script/package.json
|
||||
COPY packages/sdk/js/package.json ./packages/sdk/js/package.json
|
||||
COPY packages/slack/package.json ./packages/slack/package.json
|
||||
COPY packages/stats/app/package.json ./packages/stats/app/package.json
|
||||
COPY packages/stats/core/package.json ./packages/stats/core/package.json
|
||||
COPY packages/stats/server/package.json ./packages/stats/server/package.json
|
||||
COPY packages/storybook/package.json ./packages/storybook/package.json
|
||||
COPY packages/ui/package.json ./packages/ui/package.json
|
||||
COPY packages/web/package.json ./packages/web/package.json
|
||||
|
||||
RUN bun install --frozen-lockfile --production --ignore-scripts
|
||||
|
||||
COPY packages ./packages
|
||||
|
||||
WORKDIR /app/packages/stats/server
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
CMD ["bun", "src/server.ts"]
|
||||
33
packages/stats/server/package.json
Normal file
33
packages/stats/server/package.json
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode-ai/stats-server",
|
||||
"version": "1.14.50",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"main": "./src/server.ts",
|
||||
"exports": {
|
||||
".": "./src/server.ts"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "bun src/server.ts",
|
||||
"typecheck": "tsgo --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-firehose": "3.933.0",
|
||||
"@effect/platform-node": "catalog:",
|
||||
"@opencode-ai/stats-core": "workspace:*",
|
||||
"effect": "catalog:",
|
||||
"sst": "catalog:"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tsconfig/node22": "catalog:",
|
||||
"@types/bun": "catalog:",
|
||||
"@types/node": "catalog:",
|
||||
"@typescript/native-preview": "catalog:",
|
||||
"typescript": "catalog:"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=22"
|
||||
}
|
||||
}
|
||||
110
packages/stats/server/src/ingest.ts
Normal file
110
packages/stats/server/src/ingest.ts
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
import { Buffer } from "node:buffer"
|
||||
import { FirehoseClient, PutRecordBatchCommand } from "@aws-sdk/client-firehose"
|
||||
import { Effect, Layer, Schema } from "effect"
|
||||
import * as Context from "effect/Context"
|
||||
import { Resource } from "sst/resource"
|
||||
|
||||
const MAX_FIREHOSE_BATCH_SIZE = 500
|
||||
const MAX_FIREHOSE_ATTEMPTS = 3
|
||||
const LAKE_TYPE = /^([A-Za-z0-9_]+)\.([A-Za-z0-9_]+)$/
|
||||
|
||||
type IngestEvent = Record<string, unknown>
|
||||
type RoutedEvent = IngestEvent & { _lake_database: string; _lake_table: string; _lake_operation: "insert" }
|
||||
type FirehoseRecord = { Data: Uint8Array }
|
||||
|
||||
export class IngestError extends Schema.TaggedErrorClass<IngestError>()("IngestError", {
|
||||
message: Schema.String,
|
||||
failed: Schema.Number,
|
||||
cause: Schema.optional(Schema.Defect),
|
||||
}) {}
|
||||
|
||||
export declare namespace Ingest {
|
||||
export interface Service {
|
||||
readonly write: (events: IngestEvent[]) => Effect.Effect<{ records: number }, IngestError>
|
||||
}
|
||||
}
|
||||
|
||||
export class Ingest extends Context.Service<Ingest, Ingest.Service>()("@opencode/stats/Ingest") {
|
||||
static readonly layer: Layer.Layer<Ingest> = Layer.effect(
|
||||
Ingest,
|
||||
Effect.sync(() => {
|
||||
const client = new FirehoseClient({})
|
||||
|
||||
const write = Effect.fn("Ingest.write")(function* (events: IngestEvent[]) {
|
||||
if (events.length === 0) return { records: 0 }
|
||||
const records = events.map(routeEvent).filter((event): event is RoutedEvent => Boolean(event))
|
||||
if (records.length !== events.length) {
|
||||
return yield* new IngestError({
|
||||
message: "Unsupported lake event type",
|
||||
failed: events.length - records.length,
|
||||
})
|
||||
}
|
||||
|
||||
const failed = (
|
||||
yield* Effect.all(
|
||||
chunks(
|
||||
records.map((event) => ({ Data: Buffer.from(JSON.stringify(event)) })),
|
||||
MAX_FIREHOSE_BATCH_SIZE,
|
||||
).map((batch) => putRecords(client, Resource.LakeIngestConfig.streamName, batch)),
|
||||
{ concurrency: 8 },
|
||||
)
|
||||
).reduce((sum, item) => sum + item, 0)
|
||||
|
||||
if (failed > 0) {
|
||||
return yield* new IngestError({ message: "Failed to ingest all lake records", failed })
|
||||
}
|
||||
|
||||
return { records: records.length }
|
||||
})
|
||||
|
||||
return Ingest.of({ write })
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
const putRecords: (
|
||||
client: FirehoseClient,
|
||||
streamName: string,
|
||||
records: FirehoseRecord[],
|
||||
attempt?: number,
|
||||
) => Effect.Effect<number, IngestError> = Effect.fn("Ingest.putRecords")(function* (
|
||||
client,
|
||||
streamName,
|
||||
records,
|
||||
attempt = 1,
|
||||
) {
|
||||
const result = yield* Effect.tryPromise({
|
||||
try: () => client.send(new PutRecordBatchCommand({ DeliveryStreamName: streamName, Records: records })),
|
||||
catch: (cause) => new IngestError({ message: "Failed to write lake records to Firehose", failed: records.length, cause }),
|
||||
})
|
||||
const failed =
|
||||
result.RequestResponses?.flatMap((item, index) => {
|
||||
const record = records[index]
|
||||
if (!item.ErrorCode || !record) return []
|
||||
return [record]
|
||||
}) ?? []
|
||||
|
||||
if (failed.length === 0) return 0
|
||||
if (attempt >= MAX_FIREHOSE_ATTEMPTS) return failed.length
|
||||
|
||||
yield* Effect.sleep(`${250 * 2 ** (attempt - 1)} millis`)
|
||||
return yield* putRecords(client, streamName, failed, attempt + 1)
|
||||
})
|
||||
|
||||
function routeEvent(event: IngestEvent): RoutedEvent | undefined {
|
||||
if (typeof event._datalake_key !== "string") return
|
||||
const match = event._datalake_key.match(LAKE_TYPE)
|
||||
if (!match?.[1] || !match[2]) return
|
||||
return {
|
||||
...Object.fromEntries(Object.entries(event).filter(([key]) => key !== "_datalake_key")),
|
||||
_lake_database: match[1],
|
||||
_lake_table: match[2],
|
||||
_lake_operation: "insert" as const,
|
||||
}
|
||||
}
|
||||
|
||||
function chunks<T>(items: T[], size: number) {
|
||||
return Array.from({ length: Math.ceil(items.length / size) }, (_, index) =>
|
||||
items.slice(index * size, (index + 1) * size),
|
||||
)
|
||||
}
|
||||
11
packages/stats/server/src/resource.d.ts
vendored
Normal file
11
packages/stats/server/src/resource.d.ts
vendored
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
import "sst/resource"
|
||||
|
||||
declare module "sst/resource" {
|
||||
export interface Resource {
|
||||
LakeIngestConfig: {
|
||||
secret: string
|
||||
streamName: string
|
||||
type: "sst.sst.Linkable"
|
||||
}
|
||||
}
|
||||
}
|
||||
62
packages/stats/server/src/router.ts
Normal file
62
packages/stats/server/src/router.ts
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
import { Buffer } from "node:buffer"
|
||||
import { timingSafeEqual } from "node:crypto"
|
||||
import { Effect, Schema } from "effect"
|
||||
import { HttpRouter, HttpServerRequest, HttpServerResponse } from "effect/unstable/http"
|
||||
import { Resource } from "sst/resource"
|
||||
import { Ingest } from "./ingest"
|
||||
import { isShuttingDown } from "./shutdown"
|
||||
|
||||
const IngestPayload = Schema.Struct({
|
||||
events: Schema.optional(Schema.Unknown),
|
||||
})
|
||||
|
||||
export const Routes = HttpRouter.use((router) =>
|
||||
Effect.gen(function* () {
|
||||
const ingestService = yield* Ingest
|
||||
|
||||
yield* Effect.all(
|
||||
[
|
||||
router.add("GET", "/health", () => json(200, { ok: true })),
|
||||
router.add("GET", "/ready", () => json(isShuttingDown() ? 503 : 200, { ok: !isShuttingDown() })),
|
||||
router.add("POST", "/", ingest(ingestService)),
|
||||
],
|
||||
{ discard: true },
|
||||
)
|
||||
}),
|
||||
)
|
||||
|
||||
const ingest = (ingestService: Ingest.Service) => Effect.gen(function* () {
|
||||
const request = yield* HttpServerRequest.HttpServerRequest
|
||||
if (!isAuthorized(request.headers)) return yield* json(401, { ok: false, error: "Unauthorized" })
|
||||
|
||||
const payload = yield* HttpServerRequest.schemaBodyJson(IngestPayload).pipe(
|
||||
Effect.match({
|
||||
onFailure: () => undefined,
|
||||
onSuccess: (value) => value,
|
||||
}),
|
||||
)
|
||||
if (!payload) return yield* json(400, { ok: false, error: "Invalid JSON body" })
|
||||
|
||||
const events = Array.isArray(payload.events) ? payload.events.filter(isRecord) : []
|
||||
if (events.length === 0) return yield* json(202, { ok: true, records: 0 })
|
||||
|
||||
return yield* ingestService.write(events).pipe(
|
||||
Effect.flatMap((result) => json(202, { ok: true, records: result.records })),
|
||||
Effect.catchTag("IngestError", (error) => json(502, { ok: false, records: events.length, failed: error.failed })),
|
||||
)
|
||||
})
|
||||
|
||||
function isAuthorized(headers: Record<string, string | undefined>) {
|
||||
const actual = Buffer.from(headers.authorization ?? headers.Authorization ?? "")
|
||||
const expected = Buffer.from(`Bearer ${Resource.LakeIngestConfig.secret}`)
|
||||
if (actual.length !== expected.length) return false
|
||||
return timingSafeEqual(actual, expected)
|
||||
}
|
||||
|
||||
function isRecord(item: unknown): item is Record<string, unknown> {
|
||||
return Boolean(item) && typeof item === "object" && !Array.isArray(item)
|
||||
}
|
||||
|
||||
function json(status: number, body: Record<string, unknown>) {
|
||||
return HttpServerResponse.json(body, { status }).pipe(Effect.orDie)
|
||||
}
|
||||
28
packages/stats/server/src/server.ts
Normal file
28
packages/stats/server/src/server.ts
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
import * as NodeHttpServer from "@effect/platform-node/NodeHttpServer"
|
||||
import * as NodeRuntime from "@effect/platform-node/NodeRuntime"
|
||||
import { Config, Layer } from "effect"
|
||||
import { HttpRouter } from "effect/unstable/http"
|
||||
import { createServer } from "node:http"
|
||||
import { Ingest } from "./ingest"
|
||||
import { Routes } from "./router"
|
||||
import { registerShutdownSignalHandlers } from "./shutdown"
|
||||
|
||||
registerShutdownSignalHandlers()
|
||||
|
||||
const ServerLive = NodeHttpServer.layerConfig(
|
||||
() => createServer(),
|
||||
Config.all({
|
||||
port: Config.number("PORT").pipe(Config.withDefault(3000)),
|
||||
host: Config.string("HOST").pipe(Config.withDefault("0.0.0.0")),
|
||||
}),
|
||||
)
|
||||
|
||||
const runtimeLayer = Ingest.layer
|
||||
const programLayer = Routes.pipe(Layer.provide(runtimeLayer))
|
||||
const main = Layer.launch(
|
||||
HttpRouter.serve(programLayer, {
|
||||
disableLogger: true,
|
||||
}).pipe(Layer.provideMerge(ServerLive)),
|
||||
)
|
||||
|
||||
NodeRuntime.runMain(main, { disableErrorReporting: true })
|
||||
17
packages/stats/server/src/shutdown.ts
Normal file
17
packages/stats/server/src/shutdown.ts
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
let shuttingDown = false
|
||||
let signalHandlersRegistered = false
|
||||
|
||||
export function isShuttingDown() {
|
||||
return shuttingDown
|
||||
}
|
||||
|
||||
export function registerShutdownSignalHandlers() {
|
||||
if (signalHandlersRegistered) return
|
||||
signalHandlersRegistered = true
|
||||
process.once("SIGTERM", markShuttingDown)
|
||||
process.once("SIGINT", markShuttingDown)
|
||||
}
|
||||
|
||||
function markShuttingDown() {
|
||||
shuttingDown = true
|
||||
}
|
||||
22
packages/stats/server/src/stat-sync.ts
Normal file
22
packages/stats/server/src/stat-sync.ts
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
import * as NodeRuntime from "@effect/platform-node/NodeRuntime"
|
||||
import { Athena } from "@opencode-ai/stats-core/athena"
|
||||
import { layer as statsLayer } from "@opencode-ai/stats-core/runtime"
|
||||
import { syncStats } from "@opencode-ai/stats-core/stat-sync"
|
||||
import { Cause, Effect, Layer, Schedule } from "effect"
|
||||
|
||||
const SYNC_INTERVAL = "1 hour"
|
||||
|
||||
const runtimeLayer = Layer.mergeAll(statsLayer, Athena.layer)
|
||||
const syncPass = syncStats().pipe(
|
||||
Effect.catchCause((cause) =>
|
||||
Effect.logWarning("stats sync failed").pipe(Effect.annotateLogs({ cause: Cause.pretty(cause) })),
|
||||
),
|
||||
)
|
||||
const daemon = Effect.logInfo("stats sync daemon started").pipe(
|
||||
Effect.andThen(syncPass.pipe(Effect.repeat(Schedule.fixed(SYNC_INTERVAL)))),
|
||||
Effect.forkScoped,
|
||||
)
|
||||
|
||||
NodeRuntime.runMain(Layer.launch(Layer.effectDiscard(daemon).pipe(Layer.provide(runtimeLayer))), {
|
||||
disableErrorReporting: true,
|
||||
})
|
||||
10
packages/stats/server/sst-env.d.ts
vendored
Normal file
10
packages/stats/server/sst-env.d.ts
vendored
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
/* This file is auto-generated by SST. Do not edit. */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
/* deno-fmt-ignore-file */
|
||||
/* biome-ignore-all lint: auto-generated */
|
||||
|
||||
/// <reference path="../../../sst-env.d.ts" />
|
||||
|
||||
import "sst"
|
||||
export {}
|
||||
12
packages/stats/server/tsconfig.json
Normal file
12
packages/stats/server/tsconfig.json
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"extends": "@tsconfig/node22/tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"types": ["bun", "node"]
|
||||
},
|
||||
"include": ["src", "../core/src/resource.d.ts"]
|
||||
}
|
||||
56
sst-env.d.ts
vendored
56
sst-env.d.ts
vendored
|
|
@ -26,14 +26,6 @@ declare module "sst" {
|
|||
"AuthApi": import("@cloudflare/workers-types").Service
|
||||
"AuthStorage": import("@cloudflare/workers-types").KVNamespace
|
||||
"Bucket": import("@cloudflare/workers-types").R2Bucket
|
||||
"CLOUDFLARE_API_TOKEN": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
"CLOUDFLARE_DEFAULT_ACCOUNT_ID": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
"Console": {
|
||||
"type": "sst.cloudflare.SolidStart"
|
||||
"url": string
|
||||
|
|
@ -99,6 +91,37 @@ declare module "sst" {
|
|||
"type": "random.index/randomPassword.RandomPassword"
|
||||
"value": string
|
||||
}
|
||||
"InferenceEvent": {
|
||||
"catalog": string
|
||||
"database": string
|
||||
"region": string
|
||||
"table": string
|
||||
"tableBucket": string
|
||||
"type": "sst.sst.Linkable"
|
||||
"workgroup": string
|
||||
}
|
||||
"LakeIngest": {
|
||||
"secret": string
|
||||
"type": "sst.sst.Linkable"
|
||||
"url": string
|
||||
}
|
||||
"LakeIngestConfig": {
|
||||
"secret": string
|
||||
"streamName": string
|
||||
"type": "sst.sst.Linkable"
|
||||
}
|
||||
"LakeIngestSecret": {
|
||||
"type": "random.index/randomPassword.RandomPassword"
|
||||
"value": string
|
||||
}
|
||||
"LakeIngestService": {
|
||||
"service": string
|
||||
"type": "sst.aws.Service"
|
||||
"url": string
|
||||
}
|
||||
"LakeVpc": {
|
||||
"type": "sst.aws.Vpc"
|
||||
}
|
||||
"LogProcessor": import("@cloudflare/workers-types").Service
|
||||
"R2AccessKey": {
|
||||
"type": "sst.sst.Secret"
|
||||
|
|
@ -133,6 +156,23 @@ declare module "sst" {
|
|||
"value": string
|
||||
}
|
||||
"Stat": import("@cloudflare/workers-types").Service
|
||||
"StatsDatabase": {
|
||||
"database": string
|
||||
"host": string
|
||||
"password": string
|
||||
"port": number
|
||||
"type": "sst.sst.Linkable"
|
||||
"url": string
|
||||
"username": string
|
||||
}
|
||||
"StatsSyncConfig": {
|
||||
"dataset": string
|
||||
"type": "sst.sst.Linkable"
|
||||
}
|
||||
"StatsSyncService": {
|
||||
"service": string
|
||||
"type": "sst.aws.Service"
|
||||
}
|
||||
"Teams": {
|
||||
"type": "sst.cloudflare.SolidStart"
|
||||
"url": string
|
||||
|
|
|
|||
|
|
@ -2,12 +2,26 @@
|
|||
|
||||
export default $config({
|
||||
app(input) {
|
||||
const deployAws = input.stage === "production" || input.stage === "dev" || input.stage === "adam"
|
||||
return {
|
||||
name: "opencode",
|
||||
removal: input?.stage === "production" ? "retain" : "remove",
|
||||
protect: ["production"].includes(input?.stage),
|
||||
home: "cloudflare",
|
||||
providers: {
|
||||
...(deployAws
|
||||
? {
|
||||
aws: {
|
||||
version: "7.30.0",
|
||||
region: "us-east-1",
|
||||
profile: process.env.GITHUB_ACTIONS
|
||||
? undefined
|
||||
: input.stage === "production"
|
||||
? "opencode-production"
|
||||
: "opencode-dev",
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
stripe: {
|
||||
version: "0.0.28",
|
||||
apiKey: process.env.STRIPE_SECRET_KEY!,
|
||||
|
|
@ -19,7 +33,12 @@ export default $config({
|
|||
}
|
||||
},
|
||||
async run() {
|
||||
const stage = await import("./infra/stage.js")
|
||||
await import("./infra/app.js")
|
||||
if (stage.deployAws) {
|
||||
await import("./infra/lake.js")
|
||||
await import("./infra/stats.js")
|
||||
}
|
||||
const { stat } = await import("./infra/console.js")
|
||||
await import("./infra/enterprise.js")
|
||||
if ($app.stage === "production" || $app.stage === "vimtor") {
|
||||
|
|
@ -28,6 +47,8 @@ export default $config({
|
|||
|
||||
return {
|
||||
StatWorkerUrl: stat.url,
|
||||
// StatsUrl: stats.app.url,
|
||||
...(stage.githubActionsDeployRoleArn ? { GithubActionsDeployRoleArn: stage.githubActionsDeployRoleArn } : {}),
|
||||
}
|
||||
},
|
||||
})
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue