diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 000000000000..b6d8d73512ff --- /dev/null +++ b/.dockerignore @@ -0,0 +1,12 @@ +.git +.opencode +.sst +.turbo +.wrangler +node_modules +**/node_modules +**/.output +**/dist +**/.turbo +**/.vite +**/coverage diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 68f00dd4a4f4..18e6cf7acb44 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -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 }} diff --git a/bun.lock b/bun.lock index 740bbf0c4509..c899ce9c48bf 100644 --- a/bun.lock +++ b/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=="], diff --git a/infra/app.ts b/infra/app.ts index 2ede5a1f4a29..7b532bcb23f3 100644 --- a/infra/app.ts +++ b/infra/app.ts @@ -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, { diff --git a/infra/console.ts b/infra/console.ts index cf1fba823728..0a304a7be309 100644 --- a/infra/console.ts +++ b/infra/console.ts @@ -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", { diff --git a/infra/lake.ts b/infra/lake.ts new file mode 100644 index 000000000000..cf6211e01c64 --- /dev/null +++ b/infra/lake.ts @@ -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: ["*"], + }, +] diff --git a/infra/secret.ts b/infra/secret.ts index eafbd91ed293..65ada2f1f64d 100644 --- a/infra/secret.ts +++ b/infra/secret.ts @@ -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"), diff --git a/infra/stage.ts b/infra/stage.ts index f9a6fd75529c..27df5f300c79 100644 --- a/infra/stage.ts +++ b/infra/stage.ts @@ -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, diff --git a/infra/stats.ts b/infra/stats.ts new file mode 100644 index 000000000000..28d50fe0e340 --- /dev/null +++ b/infra/stats.ts @@ -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, + }, +}) diff --git a/package.json b/package.json index 86f5a1eace79..fcf3873d377c 100644 --- a/package.json +++ b/package.json @@ -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": { diff --git a/packages/console/app/src/routes/zen/util/logger.ts b/packages/console/app/src/routes/zen/util/logger.ts index aef46ddd0e61..c4fd422962af 100644 --- a/packages/console/app/src/routes/zen/util/logger.ts +++ b/packages/console/app/src/routes/zen/util/logger.ts @@ -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) }, } diff --git a/packages/console/app/vite.config.ts b/packages/console/app/vite.config.ts index 951c9a4276c0..fb753b6be7ce 100644 --- a/packages/console/app/vite.config.ts +++ b/packages/console/app/vite.config.ts @@ -9,7 +9,7 @@ export default defineConfig({ }) as PluginOption, nitro({ compatibilityDate: "2024-09-19", - preset: "cloudflare_module", + preset: "cloudflare-module", cloudflare: { nodeCompat: true, }, diff --git a/packages/console/core/script/create-api-key.ts b/packages/console/core/script/create-api-key.ts new file mode 100644 index 000000000000..dba2ee946c40 --- /dev/null +++ b/packages/console/core/script/create-api-key.ts @@ -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 [--workspace-id ] [--workspace-name ] [--key-name ] [--balance-dollars ] [--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 +} diff --git a/packages/console/function/src/log-processor.ts b/packages/console/function/src/log-processor.ts index 2bb741b7aa34..25e1838ed77b 100644 --- a/packages/console/function/src/log-processor.ts +++ b/packages/console/function/src/log-processor.ts @@ -21,7 +21,7 @@ export default { ) continue - let data = { + let data: Record = { "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 + 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) { + 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, 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, 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, key: string) { + const value = number(data, key) + if (value === undefined) return undefined + return Math.round(value) +} + +function number(data: Record, 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 +} diff --git a/packages/console/resource/resource.node.ts b/packages/console/resource/resource.node.ts index 1470bacf2652..ce11abcc4469 100644 --- a/packages/console/resource/resource.node.ts +++ b/packages/console/resource/resource.node.ts @@ -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 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) diff --git a/packages/enterprise/vite.config.ts b/packages/enterprise/vite.config.ts index 11ca1729dfe4..531732c2befc 100644 --- a/packages/enterprise/vite.config.ts +++ b/packages/enterprise/vite.config.ts @@ -8,7 +8,7 @@ const nitroConfig: any = (() => { if (target === "cloudflare") { return { compatibilityDate: "2024-09-19", - preset: "cloudflare_module", + preset: "cloudflare-module", cloudflare: { nodeCompat: true, }, diff --git a/packages/stats/README.md b/packages/stats/README.md new file mode 100644 index 000000000000..6c66684cc529 --- /dev/null +++ b/packages/stats/README.md @@ -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. diff --git a/packages/stats/app/.gitignore b/packages/stats/app/.gitignore new file mode 100644 index 000000000000..60a72e7e6c23 --- /dev/null +++ b/packages/stats/app/.gitignore @@ -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 diff --git a/packages/stats/app/app.config.ts b/packages/stats/app/app.config.ts new file mode 100644 index 000000000000..40a103295f0c --- /dev/null +++ b/packages/stats/app/app.config.ts @@ -0,0 +1,5 @@ +export default { + server: { + preset: "cloudflare-module", + }, +} diff --git a/packages/stats/app/package.json b/packages/stats/app/package.json new file mode 100644 index 000000000000..c090cf3339fe --- /dev/null +++ b/packages/stats/app/package.json @@ -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" + } +} diff --git a/packages/stats/app/src/app.css b/packages/stats/app/src/app.css new file mode 100644 index 000000000000..6b7652a0337c --- /dev/null +++ b/packages/stats/app/src/app.css @@ -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; + } +} diff --git a/packages/stats/app/src/app.tsx b/packages/stats/app/src/app.tsx new file mode 100644 index 000000000000..3a356119b03e --- /dev/null +++ b/packages/stats/app/src/app.tsx @@ -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 ( + <> + opencode stats + + + ) +} + +export default function App() { + return ( + ( + + + {props.children} + + )} + > + + + ) +} diff --git a/packages/stats/app/src/asset/logo-ornate-dark.svg b/packages/stats/app/src/asset/logo-ornate-dark.svg new file mode 100644 index 000000000000..a1582732423a --- /dev/null +++ b/packages/stats/app/src/asset/logo-ornate-dark.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/packages/stats/app/src/asset/logo-ornate-light.svg b/packages/stats/app/src/asset/logo-ornate-light.svg new file mode 100644 index 000000000000..2a856dccefe8 --- /dev/null +++ b/packages/stats/app/src/asset/logo-ornate-light.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/packages/stats/app/src/entry-client.tsx b/packages/stats/app/src/entry-client.tsx new file mode 100644 index 000000000000..8187b9eadc49 --- /dev/null +++ b/packages/stats/app/src/entry-client.tsx @@ -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(() => , root) diff --git a/packages/stats/app/src/entry-server.tsx b/packages/stats/app/src/entry-server.tsx new file mode 100644 index 000000000000..34fb5af26515 --- /dev/null +++ b/packages/stats/app/src/entry-server.tsx @@ -0,0 +1,25 @@ +// @refresh reload +import { createHandler, StartServer } from "@solidjs/start/server" + +export default createHandler( + () => ( + ( + + + + + {assets} + + +
{children}
+ {scripts} + + + )} + /> + ), + { + mode: "async", + }, +) diff --git a/packages/stats/app/src/global.d.ts b/packages/stats/app/src/global.d.ts new file mode 100644 index 000000000000..dc6f10c226c0 --- /dev/null +++ b/packages/stats/app/src/global.d.ts @@ -0,0 +1 @@ +/// diff --git a/packages/stats/app/src/routes/api/health.ts b/packages/stats/app/src/routes/api/health.ts new file mode 100644 index 000000000000..fe7abc9a7f05 --- /dev/null +++ b/packages/stats/app/src/routes/api/health.ts @@ -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, + } + }), + ), + ) +} diff --git a/packages/stats/app/src/routes/index.css b/packages/stats/app/src/routes/index.css new file mode 100644 index 000000000000..2e292346d847 --- /dev/null +++ b/packages/stats/app/src/routes/index.css @@ -0,0 +1,1316 @@ +[data-page="stats"] { + --color-background: #ffffff; + --color-background-weak: #fafafa; + --color-background-weak-hover: #eeeeee; + --color-background-strong: #161616; + --color-background-strong-hover: #242424; + --color-text: #5c5c5c; + --color-text-weak: #808080; + --color-text-strong: #161616; + --color-text-inverted: #ffffff; + --color-border-weak: #0000001a; + --stats-bg: #ffffff; + --stats-layer: #fafafa; + --stats-layer-2: #eeeeee; + --stats-line: #0000001a; + --stats-line-strong: #00000033; + --stats-text: #161616; + --stats-muted: #5c5c5c; + --stats-faint: #808080; + --stats-accent: #3b5cf6; + --stats-accent-text: #6c7dff; + --stats-bar-idle: #d4d4d4; + --stats-dot: #d4d4d4; + --stats-page-padding: 5rem; + --stats-section-padding: 6rem; + min-height: 100vh; + display: flex; + flex-direction: column; + gap: 4rem; + padding-bottom: 5rem; + background: var(--stats-bg); +} + +[data-page="stats"] [data-component="content"] { + color: var(--stats-text); + font-family: + "IBM Plex Mono", + var(--font-mono, ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace); +} + +[data-page="stats"] [data-component="container"] { + max-width: 67.5rem; + margin: 0 auto; + border-left: 1px solid var(--stats-line); + border-right: 1px solid var(--stats-line); +} + +[data-page="stats"] [data-component="top"] { + position: sticky; + top: 0; + z-index: 10; + display: flex; + align-items: center; + justify-content: space-between; + height: 80px; + min-height: 80px; + padding: 24px var(--stats-page-padding); + color: var(--stats-muted); + background: var(--stats-bg); + border-bottom: 1px solid var(--stats-line); + font-family: + "IBM Plex Mono", + var(--font-mono, ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace); +} + +[data-page="stats"] [data-component="top"] a { + color: var(--stats-text); + font-size: 14px; + line-height: 18px; + text-decoration: none; +} + +[data-page="stats"] [data-component="top"] a:hover { + text-decoration: underline; + text-underline-offset: 4px; +} + +[data-page="stats"] [data-slot="brand"] { + display: flex; + align-items: center; +} + +[data-page="stats"] [data-slot="brand"] img { + width: auto; + height: 34px; +} + +[data-page="stats"] [data-slot="logo dark"] { + display: none; +} + +[data-page="stats"] [data-component="nav-desktop"] ul { + display: flex; + align-items: center; + gap: 32px; + margin: 0; + padding: 0; + list-style: none; +} + +[data-page="stats"] [data-component="nav-desktop"] a span { + color: var(--stats-faint); +} + +[data-page="stats"] [data-component="nav-desktop"] [data-slot="cta-button"] { + display: flex; + align-items: center; + gap: 8px; + padding: 8px 16px 8px 10px; + border-radius: 4px; + background: var(--color-background-strong); + color: var(--color-text-inverted); + font-weight: 500; + white-space: nowrap; +} + +[data-page="stats"] [data-component="nav-desktop"] [data-slot="cta-button"]:hover { + background: var(--color-background-strong-hover); + text-decoration: none; +} + +[data-page="stats"] [data-component="footer"] { + display: flex; + border-top: 1px solid var(--stats-line); + color: var(--stats-text); + font-family: + "IBM Plex Mono", + var(--font-mono, ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace); + font-size: 14px; +} + +[data-page="stats"] [data-component="footer"] [data-slot="cell"] { + flex: 1; + text-align: center; +} + +[data-page="stats"] [data-component="footer"] [data-slot="cell"] + [data-slot="cell"] { + border-left: 1px solid var(--stats-line); +} + +[data-page="stats"] [data-component="footer"] a { + display: block; + width: 100%; + padding: 2rem 0; + color: var(--stats-text); + text-decoration: none; +} + +[data-page="stats"] [data-component="footer"] a:hover { + background: var(--stats-layer); + text-decoration: underline; + text-underline-offset: 4px; +} + +[data-page="stats"] [data-component="legal"] { + display: flex; + justify-content: center; + gap: 32px; + color: var(--stats-faint); + text-align: center; + font-family: + "IBM Plex Mono", + var(--font-mono, ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace); + font-size: 14px; +} + +[data-page="stats"] [data-component="legal"] a { + color: var(--stats-faint); + text-decoration: none; +} + +[data-page="stats"] [data-component="legal"] a:hover { + color: var(--stats-muted); + text-decoration: underline; +} + +[data-page="stats"] [data-component="content"] a { + color: var(--stats-text); + text-decoration: none; +} + +[data-page="stats"] [data-component="content"] a:hover { + text-decoration: underline; + text-underline-offset: 4px; +} + +[data-page="stats"] [data-section="hero"], +[data-page="stats"] [data-section="chart"], +[data-page="stats"] [data-section="newsletter"] { + border-bottom: 1px solid var(--stats-line); + padding: var(--stats-section-padding) var(--stats-page-padding); +} + +[data-page="stats"] [data-section="hero"] { + min-height: 270px; + display: grid; + grid-template-columns: 1fr 1fr; + gap: 64px; + align-items: start; +} + +[data-page="stats"] h1, +[data-page="stats"] h2, +[data-page="stats"] p { + margin: 0; +} + +[data-page="stats"] h1 { + font-size: 38px; + line-height: 1; + letter-spacing: normal; + font-weight: 600; +} + +[data-page="stats"] h2 { + font-size: 24px; + line-height: 1; + letter-spacing: -0.03em; + font-weight: 500; +} + +[data-page="stats"] [data-section="hero"] > p, +[data-page="stats"] [data-slot="section-header"] p { + color: var(--stats-muted); + font-size: 16px; + line-height: 1.5; +} + +[data-page="stats"] [data-section="hero"] > div { + display: flex; + flex-direction: column; + gap: 16px; + min-width: 0; +} + +[data-page="stats"] [data-slot="meta"] { + display: flex; + align-items: center; + gap: 4px; + width: fit-content; + height: 24px; + padding: 0 8px 0 4px; + background: var(--stats-layer-2); + color: var(--stats-faint); + font-size: 13px; + font-weight: 600; + line-height: 1.1; + overflow: hidden; + white-space: nowrap; +} + +[data-page="stats"] [data-slot="meta"] svg { + width: 16px; + height: 16px; + color: var(--stats-faint); + flex: 0 0 auto; +} + +[data-page="stats"] [data-slot="meta"] span { + color: var(--stats-muted); +} + +[data-page="stats"] [data-slot="meta"] b, +[data-page="stats"] [data-slot="meta"] em { + color: var(--stats-faint); + font-style: normal; + font-weight: 600; +} + +[data-page="stats"] [data-section="chart"] { + min-height: 0; +} + +[data-page="stats"] [data-slot="section-header"] { + display: flex; + justify-content: space-between; + align-items: flex-start; + gap: 32px; + margin-bottom: 32px; +} + +[data-page="stats"] [data-slot="section-header"] > div { + display: grid; + gap: 18px; + max-width: 460px; +} + +[data-page="stats"] [data-component="controls"] { + display: flex; + flex-direction: column; + align-items: flex-end; + gap: 10px; +} + +[data-page="stats"] [data-component="pills"], +[data-page="stats"] [data-component="toggle"] { + display: flex; + gap: 4px; + padding: 4px; + background: var(--stats-layer); + border: 1px solid var(--stats-line); + border-radius: 6px; +} + +[data-page="stats"] [data-component="content"] button { + font: inherit; + color: var(--stats-muted); + background: transparent; + border: 0; + border-radius: 4px; + padding: 7px 10px; + cursor: pointer; +} + +[data-page="stats"] [data-component="content"] button[data-active="true"] { + color: #fff; + background: var(--stats-text); +} + +[data-page="stats"] [data-component="usage-chart"], +[data-page="stats"] [data-component="country-map"] { + position: relative; +} + +[data-page="stats"] [data-component="usage-chart"] { + display: grid; +} + +[data-page="stats"] [data-component="empty-state"] { + display: grid; + place-content: center; + gap: 12px; + min-height: 280px; + padding: 32px; + background: var(--stats-layer); + border: 1px solid var(--stats-line); + color: var(--stats-muted); + text-align: center; +} + +[data-page="stats"] [data-component="empty-state"] strong { + color: var(--stats-text); + font-weight: 600; +} + +[data-page="stats"] [data-component="empty-state"] p { + max-width: 34rem; + color: var(--stats-muted); + font-size: 13px; + line-height: 1.5; +} + +[data-page="stats"] [data-component="usage-chart"] svg, +[data-page="stats"] [data-component="country-map"] svg { + display: block; + width: 100%; + overflow: visible; +} + +[data-page="stats"] .chart-total, +[data-page="stats"] .chart-date { + font-size: 14px; + fill: var(--stats-faint); +} + +[data-page="stats"] .chart-date { + font-size: 12px; +} + +[data-page="stats"] [data-component="usage-chart"] g[role="button"] { + outline: none; + cursor: pointer; +} + +[data-page="stats"] [data-component="usage-chart"] g[role="button"] rect { + transition: + fill 120ms ease, + opacity 120ms ease; +} + +[data-page="stats"] [data-component="usage-chart"] g[role="button"][data-active="true"] .chart-total, +[data-page="stats"] [data-component="usage-chart"] g[role="button"][data-active="true"] .chart-date { + fill: var(--stats-text); +} + +[data-page="stats"] [data-slot="chart-footer"] { + display: flex; + align-items: center; + justify-content: space-between; + gap: 24px; + margin-top: 32px; +} + +[data-page="stats"] [data-component="usage-filter"] { + display: flex; + align-items: center; +} + +[data-page="stats"] [data-component="usage-filter"][data-variant="product"] { + gap: 24px; +} + +[data-page="stats"] [data-component="usage-filter"][data-variant="range"] { + gap: 4px; +} + +[data-page="stats"] [data-component="usage-filter"] button { + color: var(--stats-faint); + background: transparent; + border: 1px solid transparent; + border-radius: 0; + padding: 0; + font-size: 13px; + line-height: 17px; +} + +[data-page="stats"] [data-component="usage-filter"] button[data-active="true"] { + color: var(--stats-text); + background: transparent; +} + +[data-page="stats"] [data-component="usage-filter"][data-variant="product"] button[data-active="true"] { + display: flex; + align-items: center; + gap: 8px; +} + +[data-page="stats"] [data-component="usage-filter"][data-variant="product"] button[data-active="true"]::before { + content: ""; + width: 8px; + height: 8px; + background: var(--stats-text); +} + +[data-page="stats"] [data-component="usage-filter"][data-variant="range"] button { + min-width: 34px; + padding: 3px 7px; +} + +[data-page="stats"] [data-component="usage-filter"][data-variant="range"] button[data-active="true"] { + border-color: var(--stats-line-strong); +} + +[data-page="stats"] [data-component="chart-tooltip"], +[data-page="stats"] [data-component="map-tooltip"] { + position: absolute; + right: auto; + top: 96px; + display: grid; + gap: 8px; + min-width: 220px; + padding: 16px; + background: #fffffff2; + border: 1px solid var(--stats-line-strong); + box-shadow: + 0 8px 16px #0000000a, + 0 4px 8px #00000014; + pointer-events: none; + z-index: 2; +} + +[data-page="stats"] [data-component="chart-tooltip"] p { + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + align-items: center; + gap: 18px; + margin: 0 -4px; + padding: 2px 4px; + color: var(--stats-muted); + font-size: 12px; + line-height: 17px; +} + +[data-page="stats"] [data-component="chart-tooltip"] p[data-active="true"] { + background: var(--stats-layer-2); + color: var(--stats-text); +} + +[data-page="stats"] [data-component="chart-tooltip"] [data-slot="tooltip-label"] { + display: grid; + grid-template-columns: 9px minmax(0, 1fr); + gap: 8px; + min-width: 0; + align-items: center; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +[data-page="stats"] [data-component="chart-tooltip"] [data-slot="tooltip-divider"] { + height: 1px; + margin: 4px -16px 2px; + background: var(--stats-line); +} + +[data-page="stats"] [data-component="chart-tooltip"] i { + width: 9px; + height: 9px; +} + +[data-page="stats"] [data-component="chart-tooltip"] b { + color: var(--stats-text); +} + +[data-page="stats"] [data-component="leaderboard"] { + display: block; +} + +[data-page="stats"] [data-slot="leaderboard-grid"] { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 8px; + min-height: 447px; +} + +[data-page="stats"] [data-slot="leaderboard-featured"], +[data-page="stats"] [data-slot="leaderboard-compact"] { + display: grid; + gap: 8px; +} + +[data-page="stats"] [data-slot="leaderboard-compact"] { + grid-template-columns: repeat(2, minmax(0, 1fr)); +} + +[data-page="stats"] [data-component="leader-card"] { + container-type: inline-size; + position: relative; + display: flex; + flex-direction: column; + justify-content: space-between; + min-width: 0; + overflow: hidden; + padding: 16px; + border: 1px solid var(--stats-line); + background: linear-gradient(180deg, #ffffff0a 0%, #ffffff00 100%), var(--stats-layer); + font-size: 13px; + line-height: 1.1; +} + +[data-page="stats"] [data-slot="rank"], +[data-page="stats"] [data-slot="delta"][data-negative="true"] { + color: var(--stats-faint); +} + +[data-page="stats"] [data-component="leader-card"][data-size="featured"] { + min-height: 143px; +} + +[data-page="stats"] [data-component="leader-card"][data-size="compact"] { + min-height: 82px; + box-shadow: + 0 0 0 0.5px #0000001f, + 0 1px 2px -1px #00000014, + 0 2px 4px #0000000a; +} + +[data-page="stats"] [data-slot="leader-watermark"] { + position: absolute; + right: -44px; + top: 50%; + display: none; + width: 210px; + height: 210px; + transform: translateY(-50%); + color: var(--stats-line); + opacity: 0.5; + pointer-events: none; +} + +@container (min-width: 280px) { + [data-page="stats"] [data-slot="leader-watermark"] { + display: block; + } +} + +[data-page="stats"] [data-slot="leader-body"] { + position: relative; + display: flex; + align-items: flex-end; + gap: 12px; + min-width: 0; + z-index: 1; +} + +[data-page="stats"] [data-component="leader-card"][data-size="featured"] [data-slot="leader-body"] { + flex-direction: column; + align-items: flex-start; + gap: 16px; + width: 100%; +} + +[data-page="stats"] [data-slot="leader-avatar"] { + display: grid; + place-items: center; + flex: 0 0 auto; + box-sizing: border-box; + width: 20px; + height: 20px; + padding: 3px; + border: 0.5px solid var(--stats-line-strong); + border-radius: 4px; + background: var(--stats-bg); + color: var(--stats-muted); + font-family: var(--font-mono); + font-size: 10px; + font-weight: 600; +} + +[data-page="stats"] [data-component="leader-card"][data-size="featured"] [data-slot="leader-avatar"] { + width: 28px; + height: 28px; + padding: 4px; + border-radius: 6px; + font-size: 13px; +} + +[data-page="stats"] [data-slot="leader-copy"] { + display: grid; + gap: 4px; + flex: 1; + min-width: 0; + width: 100%; +} + +[data-page="stats"] [data-slot="leader-copy"] div { + display: flex; + gap: 8px; + min-width: 0; +} + +[data-page="stats"] [data-component="leader-card"][data-size="featured"] [data-slot="leader-copy"] div { + gap: 4px; +} + +[data-page="stats"] [data-slot="leader-copy"] strong, +[data-page="stats"] [data-slot="leader-copy"] div > span:first-child { + flex: 1; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +[data-page="stats"] [data-slot="leader-copy"] strong { + color: var(--stats-text); + font-weight: 600; +} + +[data-page="stats"] [data-slot="leader-copy"] span { + color: var(--stats-muted); +} + +[data-page="stats"] [data-slot="delta"] { + color: #198b43; +} + +[data-page="stats"] [data-slot="leader-copy"] [data-slot="delta"][data-negative="true"] { + color: #b82d35; +} + +[data-page="stats"] [data-component="market-share"] { + display: grid; + grid-template-rows: auto minmax(0, 1fr); + gap: 12px; + height: 280px; +} + +[data-page="stats"] [data-slot="market-labels"], +[data-page="stats"] [data-slot="market-bars"] { + display: grid; + grid-template-columns: repeat(7, minmax(0, 1fr)); + gap: 12px; +} + +[data-page="stats"] [data-slot="market-labels"] button, +[data-page="stats"] [data-slot="market-bars"] button { + display: flex; + min-width: 0; + padding: 0; + border: 0; + border-radius: 0; + background: transparent; + text-align: left; +} + +[data-page="stats"] [data-slot="market-labels"] button { + flex-direction: column; + align-items: flex-start; + color: var(--stats-faint); + font-size: 11px; + font-weight: 600; + line-height: 1.5; +} + +[data-page="stats"] [data-slot="market-labels"] button[data-active="true"] { + color: var(--stats-text); + background: transparent; +} + +[data-page="stats"] [data-slot="market-bars"] { + flex: 1; + min-height: 0; +} + +[data-page="stats"] [data-slot="market-bars"] button { + flex-direction: column; + gap: 2px; + height: 100%; +} + +[data-page="stats"] [data-slot="market-bars"] button[data-active="true"] { + background: transparent; +} + +[data-page="stats"] [data-slot="market-bars"] span { + width: 100%; + min-height: 1px; + background: var(--stats-layer-2); +} + +[data-page="stats"] [data-component="market-share-list"] { + display: grid; + grid-auto-flow: column; + grid-template-columns: repeat(3, minmax(0, 1fr)); + grid-template-rows: repeat(3, auto); + gap: 8px; + margin: 20px 0 0; + padding: 0; + list-style: none; +} + +[data-page="stats"] [data-component="market-share-list"] li { + display: flex; + align-items: center; + gap: 6px; + min-width: 0; + padding: 8px 12px 8px 8px; + background: var(--stats-layer); + border: 1px solid var(--stats-line); + font-size: 13px; + line-height: 1.1; +} + +[data-page="stats"] [data-component="market-share-list"] span { + width: 20px; + flex: 0 0 auto; + color: var(--stats-muted); + font-weight: 600; + text-align: center; +} + +[data-page="stats"] [data-component="market-share-list"] i { + width: 6px; + height: 6px; + flex: 0 0 auto; +} + +[data-page="stats"] [data-component="market-share-list"] strong { + flex: 1; + min-width: 0; + overflow: hidden; + color: var(--stats-text); + font-weight: 600; + text-overflow: ellipsis; + white-space: nowrap; +} + +[data-page="stats"] [data-component="market-share-list"] em, +[data-page="stats"] [data-component="market-share-list"] b, +[data-page="stats"] [data-slot="market-footer"] span { + color: var(--stats-faint); + font-style: normal; +} + +[data-page="stats"] [data-component="market-share-list"] b { + color: var(--stats-muted); + font-weight: 600; +} + +[data-page="stats"] [data-slot="market-footer"] { + display: flex; + align-items: center; + justify-content: space-between; + gap: 64px; + margin-top: 24px; +} + +[data-page="stats"] [data-slot="market-footer"] p { + display: flex; + align-items: center; + gap: 8px; + width: 166px; + font-size: 13px; + line-height: 1.1; + white-space: nowrap; +} + +[data-page="stats"] [data-component="token-cost"] { + position: relative; + display: grid; + gap: 8px; + width: 100%; +} + +[data-page="stats"] button[data-component="token-row"] { + display: flex; + align-items: center; + gap: 12px; + width: 100%; + height: 28px; + padding: 0; + border: 0; + border-radius: 0; + background: transparent; + color: var(--stats-text); + font-size: 13px; + line-height: 1.5; + text-align: left; +} + +[data-page="stats"] button[data-component="token-row"][data-active="true"] { + background: #0000000a; + color: var(--stats-text); +} + +[data-page="stats"] [data-component="token-row"] strong { + flex: 0 0 48px; + font-weight: 600; + white-space: nowrap; +} + +[data-page="stats"] button[data-component="token-row"][data-active="true"] strong { + color: var(--stats-accent-text); +} + +[data-page="stats"] [data-component="token-row"] > span { + flex: 0 0 133px; + overflow: hidden; + color: var(--stats-muted); + font-weight: 400; + text-overflow: ellipsis; + white-space: nowrap; +} + +[data-page="stats"] [data-component="token-row"][data-variant="session"] > span { + flex-basis: 150px; +} + +[data-page="stats"] [data-component="metric-bar"] { + display: flex; + align-items: center; + gap: 4px; + flex: 1; + min-width: 0; + height: 6px; + font-style: normal; +} + +[data-page="stats"] [data-component="metric-bar"] b, +[data-page="stats"] [data-component="metric-bar"] em { + display: block; + height: 6px; +} + +[data-page="stats"] [data-component="metric-bar"] b { + flex-basis: 0; + background: var(--stats-text); +} + +[data-page="stats"] [data-component="metric-bar"][data-active="true"] b { + background: var(--stats-accent); +} + +[data-page="stats"] [data-component="metric-bar"] em { + flex: 1; + min-width: 12px; + background: var(--stats-layer-2); +} + +[data-page="stats"] [data-component="metric-bar"][data-active="true"] em { + background: var(--stats-line-strong); +} + +[data-page="stats"] [data-slot="session-heading"] { + display: flex; + align-items: flex-end; + gap: 12px; + width: 100%; +} + +[data-page="stats"] [data-slot="session-heading"] span { + flex: 0 0 210px; +} + +[data-page="stats"] [data-slot="session-heading"] p { + flex: 1; + min-width: 0; + color: var(--stats-faint); + font-size: 11px; + font-weight: 600; + line-height: 1.5; +} + +[data-page="stats"] [data-component="token-tooltip"] { + position: absolute; + left: 32%; + z-index: 2; + display: grid; + gap: 8px; + width: 192px; + padding: 8px; + background: var(--stats-layer); + box-shadow: + 0 0 0 0.5px #0000001f, + 0 4px 8px #00000014, + 0 8px 16px #0000000a; + pointer-events: none; +} + +[data-page="stats"] [data-component="token-tooltip"] p { + display: flex; + justify-content: space-between; + gap: 12px; + font-size: 11px; + font-weight: 600; + line-height: 1.1; +} + +[data-page="stats"] [data-component="token-tooltip"] span { + color: var(--stats-muted); +} + +[data-page="stats"] [data-slot="token-footer"] { + display: flex; + justify-content: space-between; + gap: 24px; + margin-top: 32px; +} + +[data-page="stats"] button[data-component="live-filter"] { + display: flex; + align-items: center; + gap: 6px; + padding: 0; + color: var(--stats-muted); + font-size: 13px; + font-weight: 600; + line-height: 1.1; +} + +[data-page="stats"] button[data-component="live-filter"][data-active="true"] { + color: var(--stats-muted); + background: transparent; +} + +[data-page="stats"] [data-component="live-filter"]::before { + content: ""; + width: 6px; + height: 6px; + background: #198b43; +} + +[data-page="stats"] [data-component="session-cost"] { + position: relative; + display: grid; + gap: 8px; + width: 100%; +} + +[data-page="stats"] [data-component="toggle"] { + width: fit-content; +} + +[data-page="stats"] [data-component="country-map"] circle { + fill: var(--stats-accent); + fill-opacity: 0.14; + stroke: var(--stats-accent); + stroke-width: 1.5; + transition: + fill-opacity 120ms ease, + stroke-width 120ms ease; +} + +[data-page="stats"] [data-component="country-map"] g[role="button"] { + cursor: pointer; + outline: none; +} + +[data-page="stats"] [data-component="country-map"] g[role="button"][data-active="true"] circle { + fill-opacity: 0.28; + stroke-width: 3; +} + +[data-page="stats"] [data-component="country-map"] text { + fill: var(--stats-text); + font-size: 12px; + font-weight: 600; + pointer-events: none; +} + +[data-page="stats"] [data-component="map-tooltip"] { + top: 24px; + left: 24px; + right: auto; + min-width: 168px; +} + +[data-page="stats"] [data-component="map-tooltip"] span, +[data-page="stats"] [data-component="map-tooltip"] em { + color: var(--stats-muted); + font-size: 12px; + font-style: normal; +} + +[data-page="stats"] [data-component="map-tooltip"] p { + display: flex; + justify-content: space-between; + gap: 16px; + color: var(--stats-text); + font-size: 12px; +} + +[data-page="stats"] [data-component="country-list"] { + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 8px; + margin: 20px 0 0; + padding: 0; + list-style: none; +} + +[data-page="stats"] [data-component="country-list"] button { + display: grid; + grid-template-columns: 24px minmax(0, 1fr) auto auto; + align-items: center; + gap: 8px; + width: 100%; + padding: 8px; + border: 1px solid var(--stats-line); + border-radius: 0; + background: var(--stats-layer); + color: var(--stats-muted); + font-size: 13px; + line-height: 1.1; + text-align: left; +} + +[data-page="stats"] [data-component="country-list"] button[data-active="true"] { + border-color: var(--stats-line-strong); + background: var(--stats-layer-2); + color: var(--stats-muted); +} + +[data-page="stats"] [data-component="country-list"] span, +[data-page="stats"] [data-component="country-list"] em { + color: var(--stats-faint); + font-style: normal; +} + +[data-page="stats"] [data-component="country-list"] strong { + min-width: 0; + overflow: hidden; + color: var(--stats-text); + font-weight: 600; + text-overflow: ellipsis; + white-space: nowrap; +} + +[data-page="stats"] [data-component="country-list"] b { + color: var(--stats-muted); + font-weight: 600; +} + +[data-page="stats"] [data-slot="country-footer"] { + display: flex; + align-items: center; + justify-content: space-between; + gap: 24px; + margin-top: 32px; +} + +[data-page="stats"] [data-slot="country-footer"] p { + display: flex; + align-items: center; + gap: 8px; + color: var(--stats-muted); + font-size: 13px; +} + +[data-page="stats"] [data-slot="country-footer"] span { + color: var(--stats-faint); +} + +[data-page="stats"] [data-section="newsletter"] { + min-height: 272px; + display: grid; + grid-template-columns: minmax(0, 1fr) 420px; + align-items: center; + gap: 48px; +} + +[data-page="stats"] [data-section="newsletter"] div { + display: grid; + gap: 16px; +} + +[data-page="stats"] [data-section="newsletter"] p { + color: var(--stats-muted); +} + +[data-page="stats"] form { + display: flex; + border: 1px solid var(--stats-line); + background: var(--stats-layer); + padding: 4px; +} + +[data-page="stats"] input { + min-width: 0; + flex: 1; + border: 0; + background: transparent; + color: var(--stats-text); + font: inherit; + padding: 12px; +} + +[data-page="stats"] form button { + background: var(--stats-text); + color: #fff; + padding: 12px 16px; +} + +@media (prefers-color-scheme: dark) { + [data-page="stats"] { + --color-background: #161616; + --color-background-weak: #242424; + --color-background-weak-hover: #303030; + --color-background-strong: #ffffff; + --color-background-strong-hover: #eeeeee; + --color-text: #d4d4d4; + --color-text-weak: #808080; + --color-text-strong: #ffffff; + --color-text-inverted: #161616; + --color-border-weak: #ffffff1a; + --stats-bg: #161616; + --stats-layer: #242424; + --stats-layer-2: #303030; + --stats-line: #ffffff1a; + --stats-line-strong: #ffffff33; + --stats-text: #ffffff; + --stats-muted: #d4d4d4; + --stats-faint: #808080; + --stats-bar-idle: #303030; + --stats-dot: #303030; + } + + [data-page="stats"] [data-component="chart-tooltip"], + [data-page="stats"] [data-component="map-tooltip"] { + background: #242424f2; + } + + [data-page="stats"] [data-component="leader-card"][data-size="compact"] { + box-shadow: + 0 0 0 0.5px #ffffff1f, + 0 1px 2px -1px #00000052, + 0 2px 4px #0000003d; + } + + [data-page="stats"] [data-slot="logo light"] { + display: none; + } + + [data-page="stats"] [data-slot="logo dark"] { + display: block; + } +} + +@media (max-width: 74rem) { + [data-page="stats"] [data-component="container"] { + border: 0; + } +} + +@media (max-width: 58rem) { + [data-page="stats"] { + --stats-page-padding: 24px; + --stats-section-padding: 4rem; + } + + [data-page="stats"] [data-component="top"] { + padding-left: 24px; + padding-right: 24px; + } + + [data-page="stats"] [data-component="nav-desktop"] ul { + gap: 18px; + } + + [data-page="stats"] [data-section="hero"], + [data-page="stats"] [data-section="chart"], + [data-page="stats"] [data-section="newsletter"] { + padding-left: 24px; + padding-right: 24px; + } + + [data-page="stats"] [data-section="hero"], + [data-page="stats"] [data-section="newsletter"] { + grid-template-columns: 1fr; + } + + [data-page="stats"] [data-slot="section-header"] { + flex-direction: column; + } + + [data-page="stats"] [data-component="controls"] { + align-items: flex-start; + } + + [data-page="stats"] [data-component="pills"] { + flex-wrap: wrap; + } + + [data-page="stats"] [data-slot="chart-footer"] { + align-items: flex-start; + flex-direction: column; + } + + [data-page="stats"] [data-component="usage-filter"] { + flex-wrap: wrap; + } + + [data-page="stats"] [data-slot="leaderboard-grid"], + [data-page="stats"] [data-slot="leaderboard-compact"] { + grid-template-columns: 1fr; + min-height: 0; + } + + [data-page="stats"] [data-component="leader-card"][data-size="featured"], + [data-page="stats"] [data-component="leader-card"][data-size="compact"] { + min-height: 96px; + } + + [data-page="stats"] [data-component="leader-card"][data-size="featured"] [data-slot="leader-body"] { + flex-direction: row; + align-items: flex-end; + gap: 12px; + } + + [data-page="stats"] [data-slot="leader-avatar"] { + width: 28px; + height: 28px; + padding: 4px; + border-radius: 6px; + font-size: 13px; + } + + [data-page="stats"] [data-slot="leader-watermark"] { + right: -64px; + width: 180px; + height: 180px; + font-size: 96px; + } + + [data-page="stats"] [data-slot="market-labels"], + [data-page="stats"] [data-slot="market-bars"] { + gap: 8px; + } + + [data-page="stats"] [data-component="market-share-list"] { + grid-auto-flow: row; + grid-template-columns: 1fr; + grid-template-rows: none; + } + + [data-page="stats"] [data-component="country-list"] { + grid-template-columns: 1fr; + } + + [data-page="stats"] [data-slot="market-footer"] { + align-items: flex-start; + flex-direction: column; + gap: 24px; + } + + [data-page="stats"] [data-slot="country-footer"] { + align-items: flex-start; + flex-direction: column; + } + + [data-page="stats"] [data-component="chart-tooltip"], + [data-page="stats"] [data-component="map-tooltip"] { + position: static; + margin-top: 16px; + } + + [data-page="stats"] [data-section="newsletter"] form { + width: 100%; + } +} + +@media (max-width: 40rem) { + [data-page="stats"] [data-component="nav-desktop"] li:not(:last-child) { + display: none; + } + + [data-page="stats"] [data-component="footer"], + [data-page="stats"] [data-component="legal"] { + flex-wrap: wrap; + } + + [data-page="stats"] [data-component="footer"] [data-slot="cell"] { + flex-basis: 50%; + } +} diff --git a/packages/stats/app/src/routes/index.tsx b/packages/stats/app/src/routes/index.tsx new file mode 100644 index 000000000000..01c02d4717c3 --- /dev/null +++ b/packages/stats/app/src/routes/index.tsx @@ -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 ( +
+ OpenCode Stats + +
+
+
+ }> + {(stats) => ( + <> + + + + + + + + + + )} + +
+
+
+ +
+ ) +} + +function Hero(props: { updatedAt: string | null }) { + return ( +
+
+

OpenCode Stats

+

+ + OpenCode data ·{" "} + {props.updatedAt ? `Updated ${formatUpdatedAt(props.updatedAt)}` : "No rows yet"} +

+
+

See how model usage, provider share, cost, and geography move across OpenCode traffic.

+
+ ) +} + +function StatsLoading() { + return ( + <> + + + + + + ) +} + +function ChartSection(props: { title: string; description?: string; controls?: JSX.Element; children: JSX.Element }) { + return ( +
+
+
+

{props.title}

+ {props.description &&

{props.description}

} +
+ {props.controls} +
+ {props.children} +
+ ) +} + +function EmptyState(props: { title: string; description: string }) { + return ( +
+ {props.title} +

{props.description}

+
+ ) +} + +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("All Users") + const [range, setRange] = createSignal("1W") + const data = createMemo(() => props.data[product()][range()]) + + return ( + + usageTotal(item) > 0)} + fallback={} + > + + +
+ +
+
+ ) +} + +function StatsFilters(props: { + product: UsageProduct + range: UsageRange + onProductSelect: (product: UsageProduct) => void + onRangeSelect: (range: UsageRange) => void +}) { + return ( + <> + + + + ) +} + +function FilterPills(props: { + items: readonly T[] + selected: T + label: string + variant: "product" | "range" + onSelect: (item: T) => void +}) { + return ( +
+ + {(item) => ( + + )} + +
+ ) +} + +function UsageChart(props: { data: UsagePoint[] }) { + const [activeIndex, setActiveIndex] = createSignal() + const [activeSegment, setActiveSegment] = createSignal() + 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 ( +
+ + + + + + + + {(day, dayIndex) => { + const barX = x()(String(dayIndex())) ?? 0 + const barWidth = x().bandwidth() + const stackTop = y()(usageTotal(day)) + return ( + { + 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()) + }} + > + + + {formatTokens(usageTotal(day))} + + + {day.date} + + + + {(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 ( + { + event.stopPropagation() + setActiveIndex(dayIndex()) + setActiveSegment(index()) + }} + /> + ) + }} + + + ) + }} + + + + {(bar) => ( +
width * 0.62 ? "left" : "right"} + style={getUsageTooltipStyle(bar().x, bar().width, width)} + > + {bar().point.date} + {formatTokens(usageTotal(bar().point))} total +
+ + {(segment, index) => ( +

+ + {segment.model} + + {formatTokens(segment.value)} +

+ )} +
+
+ )} + +
+ ) +} + +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("All Users") + const [range, setRange] = createSignal("1W") + const data = createMemo(() => props.data[product()][range()]) + + return ( + + 0} + fallback={ + + } + > + + +
+ +
+
+ ) +} + +function Leaderboard(props: { data: LeaderboardEntry[] }) { + return ( +
+
+
+ {(entry) => } +
+
+ {(entry) => } +
+
+
+ ) +} + +function LeaderboardCard(props: { entry: LeaderboardEntry; size: "featured" | "compact" }) { + return ( +
+ {String(props.entry.rank).padStart(2, "0")} +
+ ) +} + +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("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 ( + + } + > + {(day) => ( + <> + + + + )} + +
+

+ [*] + {activeDay()?.date ?? "No data"} +

+ +
+
+ ) +} + +function MarketShare(props: { data: MarketDay[]; activeIndex: number; onActiveIndexChange: (index: number) => void }) { + return ( +
+
+ + {(day, index) => ( + + )} + +
+
+ + {(day, index) => ( + + )} + +
+
+ ) +} + +function MarketShareList(props: { data: MarketDay["authors"] }) { + return ( +
    + + {(item, index) => ( +
  1. + {String(index() + 1).padStart(2, "0")} + + {item.author} + {formatTrillions(item.tokens)} + {item.share.toFixed(1)}% +
  2. + )} +
    +
+ ) +} + +function formatTrillions(value: number) { + return `${value.toFixed(value >= 10 ? 0 : 1)}T` +} + +function TokenCostSection(props: { data: StatsHomeData["tokenCost"] }) { + const [product, setProduct] = createSignal("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 ( + + 0} + fallback={ + + } + > + + +
+ +
+
+ ) +} + +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 ( +
+ + {(item, index) => ( + + )} + + + {(item) => ( +
+

+ Input + {formatDollars(item().input)} +

+

+ Output + {formatDollars(item().output)} +

+

+ Cached + {formatDollars(item().cached)} +

+
+ )} +
+
+ ) +} + +function formatDollars(value: number) { + return `$${value.toFixed(2)}` +} + +function MetricBar(props: { value: number; max: number; active: boolean }) { + return ( + + + + + ) +} + +function SessionCostSection(props: { data: StatsHomeData["sessionCost"] }) { + const [product, setProduct] = createSignal("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 ( + + 0} + fallback={ + + } + > + + +
+ +
+
+ ) +} + +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 ( +
+
+ +

COST / SESSION

+

TOKENS / SESSIONS

+
+ + {(item, index) => ( + + )} + + + {(item) => ( +
+

+ Cost/Session + {formatSessionCost(item().cost)} +

+

+ Tokens/Session + {formatTokenCount(item().tokens)} +

+
+ )} +
+
+ ) +} + +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("1W") + const data = createMemo(() => props.data[range()]) + + return ( + + 0} + fallback={} + > + + +
+

+ [*] + Top countries by tokens +

+ +
+
+ ) +} + +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 ( +
+ + + {(item, index) => { + const position = countryPositions[index()] + const radius = 18 + Math.sqrt(item.tokens / max()) * 58 + return ( + setActiveIndex(index())} + onClick={() => setActiveIndex(index())} + onFocus={() => setActiveIndex(index())} + > + + + {item.country} + + + ) + }} + + + + {(item) => ( +
+ {formatCountry(item().country)} + {item().continent || "Unknown region"} +

+ {formatTokens(item().tokens)} + {item().share.toFixed(1)}% +

+
+ )} +
+ +
+ ) +} + +function CountryList(props: { + data: CountryEntry[] + activeIndex: number + onActiveIndexChange: (index: number) => void +}) { + return ( +
    + + {(item, index) => ( +
  1. + +
  2. + )} +
    +
+ ) +} + +function formatCountry(country: string) { + const known: Record = { + 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 ( +
+
+

Be the first to know when we release new products

+

Join the waitlist for early access.

+
+
+ + +
+
+ ) +} + +function Header() { + return ( +
+ + OpenCode + OpenCode + + +
+ ) +} + +function Footer() { + return ( + + ) +} + +function Legal() { + return ( +
+ + ©{new Date().getFullYear()} Anomaly + + + Brand + + + Privacy + + + Terms + +
+ ) +} diff --git a/packages/stats/app/sst-env.d.ts b/packages/stats/app/sst-env.d.ts new file mode 100644 index 000000000000..301538ccb214 --- /dev/null +++ b/packages/stats/app/sst-env.d.ts @@ -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 */ + +/// + +import "sst" +export {} \ No newline at end of file diff --git a/packages/stats/app/tsconfig.json b/packages/stats/app/tsconfig.json new file mode 100644 index 000000000000..0f96f182cee8 --- /dev/null +++ b/packages/stats/app/tsconfig.json @@ -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/*"] + } + } +} diff --git a/packages/stats/app/vite.config.ts b/packages/stats/app/vite.config.ts new file mode 100644 index 000000000000..573939143f25 --- /dev/null +++ b/packages/stats/app/vite.config.ts @@ -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, + }, +}) diff --git a/packages/stats/core/drizzle.config.ts b/packages/stats/core/drizzle.config.ts new file mode 100644 index 000000000000..6fca8235afbd --- /dev/null +++ b/packages/stats/core/drizzle.config.ts @@ -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, + }, + }, +}) diff --git a/packages/stats/core/migrations/20260522121617_common_dust/migration.sql b/packages/stats/core/migrations/20260522121617_common_dust/migration.sql new file mode 100644 index 000000000000..0194c4878d14 --- /dev/null +++ b/packages/stats/core/migrations/20260522121617_common_dust/migration.sql @@ -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`); \ No newline at end of file diff --git a/packages/stats/core/migrations/20260522121617_common_dust/snapshot.json b/packages/stats/core/migrations/20260522121617_common_dust/snapshot.json new file mode 100644 index 000000000000..6088cfa0c15c --- /dev/null +++ b/packages/stats/core/migrations/20260522121617_common_dust/snapshot.json @@ -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": [] +} \ No newline at end of file diff --git a/packages/stats/core/migrations/20260523110335_cool_vin_gonzales/migration.sql b/packages/stats/core/migrations/20260523110335_cool_vin_gonzales/migration.sql new file mode 100644 index 000000000000..4d5c0abce6c2 --- /dev/null +++ b/packages/stats/core/migrations/20260523110335_cool_vin_gonzales/migration.sql @@ -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`); \ No newline at end of file diff --git a/packages/stats/core/migrations/20260523110335_cool_vin_gonzales/snapshot.json b/packages/stats/core/migrations/20260523110335_cool_vin_gonzales/snapshot.json new file mode 100644 index 000000000000..02ce4b4d18ff --- /dev/null +++ b/packages/stats/core/migrations/20260523110335_cool_vin_gonzales/snapshot.json @@ -0,0 +1,2041 @@ +{ + "version": "6", + "dialect": "mysql", + "id": "e246639a-0da0-4fbd-b7bb-f1781d407780", + "prevIds": [ + "72655266-65da-408e-bfd8-9f3a4ad817a5" + ], + "ddl": [ + { + "name": "geo_stat", + "entityType": "tables" + }, + { + "name": "model_stat", + "entityType": "tables" + }, + { + "name": "provider_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": "geo_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": "geo_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": "geo_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": "geo_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": "geo_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": "geo_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": "geo_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": "geo_stat" + }, + { + "type": "char(2)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "country", + "entityType": "columns", + "table": "geo_stat" + }, + { + "type": "varchar(8)", + "notNull": true, + "autoIncrement": false, + "default": "''", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "continent", + "entityType": "columns", + "table": "geo_stat" + }, + { + "type": "bigint", + "notNull": true, + "autoIncrement": false, + "default": "0", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "sessions", + "entityType": "columns", + "table": "geo_stat" + }, + { + "type": "bigint", + "notNull": true, + "autoIncrement": false, + "default": "0", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "requests", + "entityType": "columns", + "table": "geo_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": "geo_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": "geo_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": "geo_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": "geo_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": "geo_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": "geo_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": "geo_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": "geo_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": "geo_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": "geo_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": "geo_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": "geo_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": "geo_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": "geo_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": "geo_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": "geo_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": "geo_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": "geo_stat" + }, + { + "type": "decimal(10,6)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "market_share_tokens", + "entityType": "columns", + "table": "geo_stat" + }, + { + "type": "decimal(10,6)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "market_share_requests", + "entityType": "columns", + "table": "geo_stat" + }, + { + "type": "decimal(10,6)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "market_share_sessions", + "entityType": "columns", + "table": "geo_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": "geo_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": "geo_stat" + }, + { + "type": "int", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "rank_by_sessions", + "entityType": "columns", + "table": "geo_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": "geo_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": "geo_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": "geo_stat" + }, + { + "type": "bigint", + "notNull": true, + "autoIncrement": true, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "model_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": "model_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": "model_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": "model_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": "model_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": "model_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": "model_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": "model_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": "model_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": "model_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": "model_stat" + }, + { + "type": "bigint", + "notNull": true, + "autoIncrement": false, + "default": "0", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "sessions", + "entityType": "columns", + "table": "model_stat" + }, + { + "type": "bigint", + "notNull": true, + "autoIncrement": false, + "default": "0", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "requests", + "entityType": "columns", + "table": "model_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": "model_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": "model_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": "model_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": "model_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": "model_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": "model_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": "model_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": "model_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": "model_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": "model_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": "model_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": "model_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": "model_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": "model_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": "model_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": "model_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": "model_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": "model_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": "model_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": "model_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": "model_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": "model_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": "model_stat" + }, + { + "type": "bigint", + "notNull": true, + "autoIncrement": true, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "provider_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": "provider_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": "provider_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": "provider_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": "provider_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": "provider_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": "provider_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": "provider_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": "provider_stat" + }, + { + "type": "bigint", + "notNull": true, + "autoIncrement": false, + "default": "0", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "sessions", + "entityType": "columns", + "table": "provider_stat" + }, + { + "type": "bigint", + "notNull": true, + "autoIncrement": false, + "default": "0", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "requests", + "entityType": "columns", + "table": "provider_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": "provider_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": "provider_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": "provider_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": "provider_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": "provider_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": "provider_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": "provider_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": "provider_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": "provider_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": "provider_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": "provider_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": "provider_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": "provider_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": "provider_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": "provider_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": "provider_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": "provider_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": "provider_stat" + }, + { + "type": "decimal(10,6)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "market_share_tokens", + "entityType": "columns", + "table": "provider_stat" + }, + { + "type": "decimal(10,6)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "market_share_requests", + "entityType": "columns", + "table": "provider_stat" + }, + { + "type": "decimal(10,6)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "market_share_sessions", + "entityType": "columns", + "table": "provider_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": "provider_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": "provider_stat" + }, + { + "type": "int", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "rank_by_sessions", + "entityType": "columns", + "table": "provider_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": "provider_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": "provider_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": "provider_stat" + }, + { + "columns": [ + "id" + ], + "name": "PRIMARY", + "table": "geo_stat", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "name": "PRIMARY", + "table": "model_stat", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "name": "PRIMARY", + "table": "provider_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": "country", + "isExpression": false + } + ], + "isUnique": true, + "using": null, + "algorithm": null, + "lock": null, + "nameExplicit": true, + "name": "uniq_country_period", + "entityType": "indexes", + "table": "geo_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_country_map_tokens", + "entityType": "indexes", + "table": "geo_stat" + }, + { + "columns": [ + { + "value": "grain", + "isExpression": false + }, + { + "value": "period_start", + "isExpression": false + }, + { + "value": "dataset", + "isExpression": false + }, + { + "value": "tier", + "isExpression": false + }, + { + "value": "rank_by_tokens", + "isExpression": false + } + ], + "isUnique": false, + "using": null, + "algorithm": null, + "lock": null, + "nameExplicit": true, + "name": "idx_country_rank", + "entityType": "indexes", + "table": "geo_stat" + }, + { + "columns": [ + { + "value": "country", + "isExpression": false + }, + { + "value": "grain", + "isExpression": false + }, + { + "value": "period_start", + "isExpression": false + } + ], + "isUnique": false, + "using": null, + "algorithm": null, + "lock": null, + "nameExplicit": true, + "name": "idx_country", + "entityType": "indexes", + "table": "geo_stat" + }, + { + "columns": [ + { + "value": "continent", + "isExpression": false + }, + { + "value": "grain", + "isExpression": false + }, + { + "value": "period_start", + "isExpression": false + } + ], + "isUnique": false, + "using": null, + "algorithm": null, + "lock": null, + "nameExplicit": true, + "name": "idx_continent", + "entityType": "indexes", + "table": "geo_stat" + }, + { + "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": "model_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": "model_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": "model_stat" + }, + { + "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 + } + ], + "isUnique": true, + "using": null, + "algorithm": null, + "lock": null, + "nameExplicit": true, + "name": "uniq_provider_period", + "entityType": "indexes", + "table": "provider_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_provider_leaderboard_tokens", + "entityType": "indexes", + "table": "provider_stat" + }, + { + "columns": [ + { + "value": "grain", + "isExpression": false + }, + { + "value": "period_start", + "isExpression": false + }, + { + "value": "dataset", + "isExpression": false + }, + { + "value": "tier", + "isExpression": false + }, + { + "value": "market_share_tokens", + "isExpression": false + } + ], + "isUnique": false, + "using": null, + "algorithm": null, + "lock": null, + "nameExplicit": true, + "name": "idx_provider_market_share", + "entityType": "indexes", + "table": "provider_stat" + }, + { + "columns": [ + { + "value": "grain", + "isExpression": false + }, + { + "value": "period_start", + "isExpression": false + }, + { + "value": "dataset", + "isExpression": false + }, + { + "value": "tier", + "isExpression": false + }, + { + "value": "rank_by_tokens", + "isExpression": false + } + ], + "isUnique": false, + "using": null, + "algorithm": null, + "lock": null, + "nameExplicit": true, + "name": "idx_provider_rank", + "entityType": "indexes", + "table": "provider_stat" + }, + { + "columns": [ + { + "value": "provider", + "isExpression": false + }, + { + "value": "grain", + "isExpression": false + }, + { + "value": "period_start", + "isExpression": false + } + ], + "isUnique": false, + "using": null, + "algorithm": null, + "lock": null, + "nameExplicit": true, + "name": "idx_provider", + "entityType": "indexes", + "table": "provider_stat" + } + ], + "renames": [] +} \ No newline at end of file diff --git a/packages/stats/core/package.json b/packages/stats/core/package.json new file mode 100644 index 000000000000..09ed8ca4dd5d --- /dev/null +++ b/packages/stats/core/package.json @@ -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" + } +} diff --git a/packages/stats/core/src/athena.ts b/packages/stats/core/src/athena.ts new file mode 100644 index 000000000000..a2be44ebb76f --- /dev/null +++ b/packages/stats/core/src/athena.ts @@ -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 + +export class AthenaQueryError extends Schema.TaggedErrorClass()("AthenaQueryError", { + message: Schema.String, + queryExecutionId: Schema.optional(Schema.String), + cause: Schema.optional(Schema.Defect), +}) {} + +export class AthenaQueryTimeoutError extends Schema.TaggedErrorClass()( + "AthenaQueryTimeoutError", + { + message: Schema.String, + queryExecutionId: Schema.String, + }, +) {} + +export declare namespace Athena { + export interface Service { + readonly query: (query: string) => Effect.Effect + } +} + +export class Athena extends Context.Service()("@opencode/stats/Athena") { + static readonly layer: Layer.Layer = 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 = 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 = 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]] + }), + ) +} diff --git a/packages/stats/core/src/config.ts b/packages/stats/core/src/config.ts new file mode 100644 index 000000000000..fb5a4de35290 --- /dev/null +++ b/packages/stats/core/src/config.ts @@ -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")({ + 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()("@opencode/stats/AppConfig") { + static readonly config = config + static readonly layer: Layer.Layer = Layer.effect( + AppConfig, + config.parse(ConfigProvider.fromEnv()).pipe(Effect.orDie), + ) +} diff --git a/packages/stats/core/src/database.ts b/packages/stats/core/src/database.ts new file mode 100644 index 000000000000..d265f82bf1e9 --- /dev/null +++ b/packages/stats/core/src/database.ts @@ -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")({ + 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()( + "@opencode/stats/DatabaseConfig", +) { + static readonly config = config + static readonly layer: Layer.Layer = 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 + +export class DrizzleClient extends Context.Service()("@opencode/stats/DrizzleClient") { + static readonly layer: Layer.Layer = Layer.effect( + DrizzleClient, + Effect.map(DatabaseConfig, makeDrizzle), + ) +} + +export class DatabaseError extends Schema.TaggedErrorClass()("DatabaseError", { + cause: Schema.Defect, +}) {} + +export const catchDbError = Effect.mapError((cause) => DatabaseError.make({ cause })) + +export class MigrationError extends Schema.TaggedErrorClass()("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))) diff --git a/packages/stats/core/src/database/schema.ts b/packages/stats/core/src/database/schema.ts new file mode 100644 index 000000000000..7fbfb632d7e9 --- /dev/null +++ b/packages/stats/core/src/database/schema.ts @@ -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(), + } +} diff --git a/packages/stats/core/src/domain/geo.ts b/packages/stats/core/src/domain/geo.ts new file mode 100644 index 000000000000..624f9d7c5a5a --- /dev/null +++ b/packages/stats/core/src/domain/geo.ts @@ -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 + readonly listByPeriod: (opts: { + readonly grain: string + readonly periodStart: Date + readonly dataset?: string + readonly tier?: string + readonly client?: string + readonly source?: string + }) => Effect.Effect + readonly upsert: (rows: GeoStatRow[]) => Effect.Effect + } +} + +export class GeoStatRepo extends Context.Service()("@opencode/stats/GeoStatRepo") { + static readonly layer: Layer.Layer = 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 +} diff --git a/packages/stats/core/src/domain/home.ts b/packages/stats/core/src/domain/home.ts new file mode 100644 index 000000000000..3403a86a2fd4 --- /dev/null +++ b/packages/stats/core/src/domain/home.ts @@ -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> + leaderboard: Record> + market: Record + tokenCost: Record + sessionCost: Record + country: Record +} + +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 & { + periodStart: number + periodEnd: number +} +type ProviderMetricRow = Omit & { + periodStart: number + periodEnd: number +} +type GeoMetricRow = Omit & { + 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( + 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>((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>((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>((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(value: (product: UsageProduct) => T): Record { + return { + "All Users": value("All Users"), + Zen: value("Zen"), + Go: value("Go"), + Enterprise: value("Enterprise"), + } +} + +function createTokenProductRecord(value: (product: TokenProduct) => T): Record { + return { + Zen: value("Zen"), + Go: value("Go"), + Enterprise: value("Enterprise"), + } +} + +function createRangeRecord(value: (range: UsageRange) => T): Record { + 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 = { + 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)) +} diff --git a/packages/stats/core/src/domain/inference.ts b/packages/stats/core/src/domain/inference.ts new file mode 100644 index 000000000000..c26b2b30afde --- /dev/null +++ b/packages/stats/core/src/domain/inference.ts @@ -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, "''")}'` +} diff --git a/packages/stats/core/src/domain/model.ts b/packages/stats/core/src/domain/model.ts new file mode 100644 index 000000000000..45cd2472ca61 --- /dev/null +++ b/packages/stats/core/src/domain/model.ts @@ -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 + readonly upsert: (rows: ModelStatRow[]) => Effect.Effect + } +} + +export class ModelStatRepo extends Context.Service()( + "@opencode/stats/ModelStatRepo", +) { + static readonly layer: Layer.Layer = 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>((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") +} diff --git a/packages/stats/core/src/domain/provider.ts b/packages/stats/core/src/domain/provider.ts new file mode 100644 index 000000000000..92fb82182e82 --- /dev/null +++ b/packages/stats/core/src/domain/provider.ts @@ -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 + readonly listByPeriod: (opts: { + readonly grain: string + readonly periodStart: Date + readonly dataset?: string + readonly tier?: string + readonly client?: string + readonly source?: string + }) => Effect.Effect + readonly upsert: (rows: ProviderStatRow[]) => Effect.Effect + } +} + +export class ProviderStatRepo extends Context.Service()( + "@opencode/stats/ProviderStatRepo", +) { + static readonly layer: Layer.Layer = 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 +} diff --git a/packages/stats/core/src/domain/stat.ts b/packages/stats/core/src/domain/stat.ts new file mode 100644 index 000000000000..51c1f782639a --- /dev/null +++ b/packages/stats/core/src/domain/stat.ts @@ -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(rows: T[], dimensionKey: (row: T) => string) { + return [ + ...rows, + ...Object.values( + rows.reduce>((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(rows: T[], dimensionKey: (row: T) => string) { + return Object.values( + rows.reduce>((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(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(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(rows: T[]) { + return Object.values( + rows.reduce>((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(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() +} diff --git a/packages/stats/core/src/index.ts b/packages/stats/core/src/index.ts new file mode 100644 index 000000000000..52ff565cb8cf --- /dev/null +++ b/packages/stats/core/src/index.ts @@ -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" diff --git a/packages/stats/core/src/migrate.ts b/packages/stats/core/src/migrate.ts new file mode 100644 index 000000000000..f2a7c52bca3a --- /dev/null +++ b/packages/stats/core/src/migrate.ts @@ -0,0 +1,4 @@ +import { Effect } from "effect" +import { layer, migrate } from "./database" + +await Effect.runPromise(migrate().pipe(Effect.provide(layer))) diff --git a/packages/stats/core/src/resource.d.ts b/packages/stats/core/src/resource.d.ts new file mode 100644 index 000000000000..8343f7baa63d --- /dev/null +++ b/packages/stats/core/src/resource.d.ts @@ -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 + } + } +} diff --git a/packages/stats/core/src/runtime.ts b/packages/stats/core/src/runtime.ts new file mode 100644 index 000000000000..cc1dccad24a6 --- /dev/null +++ b/packages/stats/core/src/runtime.ts @@ -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 diff --git a/packages/stats/core/src/stat-sync.ts b/packages/stats/core/src/stat-sync.ts new file mode 100644 index 000000000000..a64b3349362c --- /dev/null +++ b/packages/stats/core/src/stat-sync.ts @@ -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, + }), + ) +} diff --git a/packages/stats/core/sst-env.d.ts b/packages/stats/core/sst-env.d.ts new file mode 100644 index 000000000000..301538ccb214 --- /dev/null +++ b/packages/stats/core/sst-env.d.ts @@ -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 */ + +/// + +import "sst" +export {} \ No newline at end of file diff --git a/packages/stats/core/tsconfig.json b/packages/stats/core/tsconfig.json new file mode 100644 index 000000000000..b36744228fd9 --- /dev/null +++ b/packages/stats/core/tsconfig.json @@ -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"] + } +} diff --git a/packages/stats/server/Dockerfile b/packages/stats/server/Dockerfile new file mode 100644 index 000000000000..5e9899f89f2f --- /dev/null +++ b/packages/stats/server/Dockerfile @@ -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"] diff --git a/packages/stats/server/package.json b/packages/stats/server/package.json new file mode 100644 index 000000000000..7147e1662e54 --- /dev/null +++ b/packages/stats/server/package.json @@ -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" + } +} diff --git a/packages/stats/server/src/ingest.ts b/packages/stats/server/src/ingest.ts new file mode 100644 index 000000000000..5900a6fbdb28 --- /dev/null +++ b/packages/stats/server/src/ingest.ts @@ -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 +type RoutedEvent = IngestEvent & { _lake_database: string; _lake_table: string; _lake_operation: "insert" } +type FirehoseRecord = { Data: Uint8Array } + +export class IngestError extends Schema.TaggedErrorClass()("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()("@opencode/stats/Ingest") { + static readonly layer: Layer.Layer = 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 = 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(items: T[], size: number) { + return Array.from({ length: Math.ceil(items.length / size) }, (_, index) => + items.slice(index * size, (index + 1) * size), + ) +} diff --git a/packages/stats/server/src/resource.d.ts b/packages/stats/server/src/resource.d.ts new file mode 100644 index 000000000000..514b4d2afe81 --- /dev/null +++ b/packages/stats/server/src/resource.d.ts @@ -0,0 +1,11 @@ +import "sst/resource" + +declare module "sst/resource" { + export interface Resource { + LakeIngestConfig: { + secret: string + streamName: string + type: "sst.sst.Linkable" + } + } +} diff --git a/packages/stats/server/src/router.ts b/packages/stats/server/src/router.ts new file mode 100644 index 000000000000..8feb03988789 --- /dev/null +++ b/packages/stats/server/src/router.ts @@ -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) { + 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 { + return Boolean(item) && typeof item === "object" && !Array.isArray(item) +} + +function json(status: number, body: Record) { + return HttpServerResponse.json(body, { status }).pipe(Effect.orDie) +} diff --git a/packages/stats/server/src/server.ts b/packages/stats/server/src/server.ts new file mode 100644 index 000000000000..95104bc45eee --- /dev/null +++ b/packages/stats/server/src/server.ts @@ -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 }) diff --git a/packages/stats/server/src/shutdown.ts b/packages/stats/server/src/shutdown.ts new file mode 100644 index 000000000000..43938b32682e --- /dev/null +++ b/packages/stats/server/src/shutdown.ts @@ -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 +} diff --git a/packages/stats/server/src/stat-sync.ts b/packages/stats/server/src/stat-sync.ts new file mode 100644 index 000000000000..dcbe179483d0 --- /dev/null +++ b/packages/stats/server/src/stat-sync.ts @@ -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, +}) diff --git a/packages/stats/server/sst-env.d.ts b/packages/stats/server/sst-env.d.ts new file mode 100644 index 000000000000..301538ccb214 --- /dev/null +++ b/packages/stats/server/sst-env.d.ts @@ -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 */ + +/// + +import "sst" +export {} \ No newline at end of file diff --git a/packages/stats/server/tsconfig.json b/packages/stats/server/tsconfig.json new file mode 100644 index 000000000000..016f01c9abc6 --- /dev/null +++ b/packages/stats/server/tsconfig.json @@ -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"] +} diff --git a/sst-env.d.ts b/sst-env.d.ts index aa79ec87d38d..9f6a5db31386 100644 --- a/sst-env.d.ts +++ b/sst-env.d.ts @@ -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 diff --git a/sst.config.ts b/sst.config.ts index efef6d07250d..19080b8f1ab2 100644 --- a/sst.config.ts +++ b/sst.config.ts @@ -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 } : {}), } }, })