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..bf44bd581d1d 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", }, }, @@ -207,7 +207,7 @@ "@ai-sdk/cohere": "3.0.27", "@ai-sdk/deepinfra": "2.0.41", "@ai-sdk/gateway": "3.0.104", - "@ai-sdk/google": "3.0.75", + "@ai-sdk/google": "3.0.63", "@ai-sdk/google-vertex": "4.0.131", "@ai-sdk/groq": "3.0.31", "@ai-sdk/mistral": "3.0.27", @@ -417,7 +417,7 @@ "@ai-sdk/cohere": "3.0.27", "@ai-sdk/deepinfra": "2.0.41", "@ai-sdk/gateway": "3.0.104", - "@ai-sdk/google": "3.0.75", + "@ai-sdk/google": "3.0.63", "@ai-sdk/google-vertex": "4.0.131", "@ai-sdk/groq": "3.0.31", "@ai-sdk/mistral": "3.0.27", @@ -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", @@ -834,7 +895,7 @@ "@ai-sdk/gateway": ["@ai-sdk/gateway@3.0.104", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23", "@vercel/oidc": "3.2.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ZKX5n74io8VIRlhIMSLWVlvT3sXC8Z7cZ9GHuWBWZDVi96+62AIsWuLGvMfcBA1STYuSoDrp6rIziZmvrTq0TA=="], - "@ai-sdk/google": ["@ai-sdk/google@3.0.75", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.27" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-XAm31ftiOrzlb8NjDzT7kw0xw+4lmgFdGFn1QKM73nXFFKyN1kWLESBV75UGNfjXP8X1YJ0YydnMVqO0jaPghw=="], + "@ai-sdk/google": ["@ai-sdk/google@3.0.63", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-RfOZWVMYSPu2sPRfGajrauWAZ9BSaRopSn+AszkKWQ1MFj8nhaXvCqRHB5pBQUaHTfZKagvOmMpNfa/s3gPLgQ=="], "@ai-sdk/google-vertex": ["@ai-sdk/google-vertex@4.0.131", "", { "dependencies": { "@ai-sdk/anthropic": "3.0.78", "@ai-sdk/google": "3.0.75", "@ai-sdk/openai-compatible": "2.0.47", "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.27", "google-auth-library": "^10.5.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-Oj1X8p0rVgvEoR5OOSxWi6XgzJ3QDlE/n30MZVtpKkCiToYYDyvlvVDGXz3IqhMyUev2JhlcuUk1brScKT01kA=="], @@ -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=="], @@ -5116,12 +5207,10 @@ "@ai-sdk/fireworks/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@2.0.41", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-kNAGINk71AlOXx10Dq/PXw4t/9XjdK8uxfpVElRwtSFMdeSiLVt58p9TPx4/FJD+hxZuVhvxYj9r42osxWq79g=="], - "@ai-sdk/google/@ai-sdk/provider": ["@ai-sdk/provider@3.0.10", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-Q3BZ27qfpYqnCYGvE3vt+Qi6LGOF9R5Nmzn+9JoM1lCRsD9mYaIhfJLkSunN48nfGXJ6n+XNV0J/XVpqGQl7Dw=="], - - "@ai-sdk/google/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.27", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ubkAJ+xODouwtmN1tYlvTPphH1hPOBfZaEQe8U7skGvFAnIRs9PPpsq57bC2+Ky/MB4yzhd6YOsxTAx9sGpazw=="], - "@ai-sdk/google-vertex/@ai-sdk/anthropic": ["@ai-sdk/anthropic@3.0.78", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.27" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-0OY12G20cUt6iU6htpEA1491Oz++NVxZxlmWGX4B7rSbeZ5pnDmOu6YtW9BKzdZlNx5Gn23i6WMxyZFoMKNcgA=="], + "@ai-sdk/google-vertex/@ai-sdk/google": ["@ai-sdk/google@3.0.75", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.27" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-XAm31ftiOrzlb8NjDzT7kw0xw+4lmgFdGFn1QKM73nXFFKyN1kWLESBV75UGNfjXP8X1YJ0YydnMVqO0jaPghw=="], + "@ai-sdk/google-vertex/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@2.0.47", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.27" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-Enm5UlL0zUCrW3792opk5h7hRWxZOZzDe6eQYVFqX9LUOGGCe1h8MZWAGim765nwzgnjlpeYOsuzZmLtRsTPlg=="], "@ai-sdk/google-vertex/@ai-sdk/provider": ["@ai-sdk/provider@3.0.10", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-Q3BZ27qfpYqnCYGvE3vt+Qi6LGOF9R5Nmzn+9JoM1lCRsD9mYaIhfJLkSunN48nfGXJ6n+XNV0J/XVpqGQl7Dw=="], @@ -5180,6 +5269,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 +5303,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=="], @@ -6040,10 +6149,6 @@ "@ai-sdk/google-vertex/@ai-sdk/provider-utils/eventsource-parser": ["eventsource-parser@3.0.8", "", {}, "sha512-70QWGkr4snxr0OXLRWsFLeRBIRPuQOvt4s8QYjmUlmlkyTZkRqS7EDVRZtzU3TiyDbXSzaOeF0XUKy8PchzukQ=="], - "@ai-sdk/google/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], - - "@ai-sdk/google/@ai-sdk/provider-utils/eventsource-parser": ["eventsource-parser@3.0.8", "", {}, "sha512-70QWGkr4snxr0OXLRWsFLeRBIRPuQOvt4s8QYjmUlmlkyTZkRqS7EDVRZtzU3TiyDbXSzaOeF0XUKy8PchzukQ=="], - "@ai-sdk/groq/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], "@ai-sdk/mistral/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], 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..04a3c46ba46e --- /dev/null +++ b/infra/lake.ts @@ -0,0 +1,322 @@ +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..5dc17fc85a7e 100644 --- a/infra/stage.ts +++ b/infra/stage.ts @@ -5,6 +5,9 @@ export const domain = (() => { })() export const zoneID = "430ba34c138cfb5360826c4909f99be8" +// Dev owns the shared AWS lake/stats infra for all non-production stages. +export const awsStage = $app.stage === "production" ? "production" : "dev" +export const deployAws = $app.stage === awsStage 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/nix/hashes.json b/nix/hashes.json index 8578dab16a33..c0148e83d83a 100644 --- a/nix/hashes.json +++ b/nix/hashes.json @@ -1,8 +1,8 @@ { "nodeModules": { - "x86_64-linux": "sha256-Wdo61RItv595JUIh3ElBXtj0B4y2KpsAJ2WbNNBsO1E=", - "aarch64-linux": "sha256-lVNN5fqBj+qxmxN/NiDxZF3kaJ4wDD1ZCylh7yuS1PA=", - "aarch64-darwin": "sha256-1NXdnraknHeLyzSCSKYyG4W86KLwn8hCqHkaLfhLdzM=", - "x86_64-darwin": "sha256-UVupTsKwLnJ2E0IBx9/JraBJ4U6O1obI/OR3RE64/jU=" + "x86_64-linux": "sha256-gqXxbi1OwLoDSDtlmYWcTTPT/fqhVGb53JXS/9a1vWw=", + "aarch64-linux": "sha256-+ARbWtOUDx0J5k55mmre/2kPekUsQtWWS0QZnyX6NTo=", + "aarch64-darwin": "sha256-Q6ioI6lFUhNKogbvbtGWsZcXJfbRgUAWS5X2X4mKdg4=", + "x86_64-darwin": "sha256-8K/hmcvXi9FGqprNbTKzMB80h5zZhO2TjOQbtve3ltQ=" } } 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/app/e2e/regression/session-timeline-collapse-state.spec.ts b/packages/app/e2e/regression/session-timeline-collapse-state.spec.ts index db191d2575fd..88b140a61dba 100644 --- a/packages/app/e2e/regression/session-timeline-collapse-state.spec.ts +++ b/packages/app/e2e/regression/session-timeline-collapse-state.spec.ts @@ -1,4 +1,5 @@ -import { expect, test, type Locator, type Page, type Route } from "@playwright/test" +import { expect, test, type Locator, type Page } from "@playwright/test" +import { mockOpenCodeServer } from "../utils/mock-server" const directory = "C:/OpenCode/TimelineStateRegression" const projectID = "proj_timeline_state_regression" @@ -299,39 +300,13 @@ function readExpanded(element: Element) { } async function mockServer(page: Page, events: EventPayload[]) { - await page.route("**/*", async (route) => { - const url = new URL(route.request().url()) - const targetPort = process.env.PLAYWRIGHT_SERVER_PORT ?? "4096" - if (url.port !== targetPort) return route.fallback() - - const path = url.pathname - if (path === "/global/event") return sse(route, events.splice(0)) - if ( - path === "/global/config" || - path === "/config" || - path === "/provider/auth" || - path === "/mcp" || - path === "/session/status" - ) - return json(route, {}) - if ( - ["/skill", "/command", "/lsp", "/formatter", "/permission", "/question", "/vcs/status", "/vcs/diff"].includes( - path, - ) - ) - return json(route, []) - if (path === "/provider") return json(route, provider()) - if (path === "/path") - return json(route, { state: directory, config: directory, worktree: directory, directory, home: "C:/OpenCode" }) - if (path === "/project") return json(route, [project()]) - if (path === "/project/current") return json(route, project()) - if (path === "/agent") return json(route, [{ name: "build", mode: "primary" }]) - if (path === "/vcs") return json(route, { branch: "main", default_branch: "main" }) - if (path === "/session") return json(route, [session()]) - if (path === `/session/${sessionID}`) return json(route, session()) - if (/^\/session\/[^/]+\/(children|todo|diff)$/.test(path)) return json(route, []) - if (path === `/session/${sessionID}/message`) return json(route, [userMessage, assistantMessage]) - return json(route, {}) + await mockOpenCodeServer(page, { + directory, + project: project(), + provider: provider(), + sessions: [session()], + pageMessages: () => ({ items: [userMessage, assistantMessage] }), + events: () => events.splice(0), }) } @@ -372,24 +347,6 @@ function provider() { } } -function json(route: Route, body: unknown, headers?: Record) { - return route.fulfill({ - status: 200, - contentType: "application/json", - headers: { "access-control-allow-origin": "*", "access-control-expose-headers": "x-next-cursor", ...headers }, - body: JSON.stringify(body ?? null), - }) -} - -function sse(route: Route, events: EventPayload[]) { - return route.fulfill({ - status: 200, - contentType: "text/event-stream", - headers: { "access-control-allow-origin": "*" }, - body: events.map((event) => `data: ${JSON.stringify(event)}\n\n`).join(""), - }) -} - function base64Encode(value: string) { return Buffer.from(value, "utf8").toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "") } diff --git a/packages/app/e2e/regression/session-timeline-context-resize.spec.ts b/packages/app/e2e/regression/session-timeline-context-resize.spec.ts index 98f34b2b9ca6..dc72e24f0df9 100644 --- a/packages/app/e2e/regression/session-timeline-context-resize.spec.ts +++ b/packages/app/e2e/regression/session-timeline-context-resize.spec.ts @@ -1,4 +1,5 @@ -import { expect, test, type Page, type Route } from "@playwright/test" +import { expect, test, type Page } from "@playwright/test" +import { mockOpenCodeServer } from "../utils/mock-server" const directory = "C:/OpenCode/ContextResizeRegression" const projectID = "proj_context_resize_regression" @@ -207,33 +208,12 @@ function contextTool(partID: string, messageID: string, tool: string, input: Rec } async function mockServer(page: Page) { - await page.route("**/*", async (route) => { - const url = new URL(route.request().url()) - const targetPort = process.env.PLAYWRIGHT_SERVER_PORT ?? "4096" - if (url.port !== targetPort) return route.fallback() - - const path = url.pathname - if (path === "/global/event" || path === "/event") return sse(route) - if (["/global/config", "/config", "/provider/auth", "/mcp", "/session/status"].includes(path)) - return json(route, {}) - if ( - ["/skill", "/command", "/lsp", "/formatter", "/permission", "/question", "/vcs/status", "/vcs/diff"].includes( - path, - ) - ) - return json(route, []) - if (path === "/provider") return json(route, provider()) - if (path === "/path") - return json(route, { state: directory, config: directory, worktree: directory, directory, home: "C:/OpenCode" }) - if (path === "/project") return json(route, [project()]) - if (path === "/project/current") return json(route, project()) - if (path === "/agent") return json(route, [{ name: "build", mode: "primary" }]) - if (path === "/vcs") return json(route, { branch: "main", default_branch: "main" }) - if (path === "/session") return json(route, [session()]) - if (path === `/session/${sessionID}`) return json(route, session()) - if (/^\/session\/[^/]+\/(children|todo|diff)$/.test(path)) return json(route, []) - if (path === `/session/${sessionID}/message`) return json(route, messages) - return json(route, {}) + await mockOpenCodeServer(page, { + directory, + project: project(), + provider: provider(), + sessions: [session()], + pageMessages: () => ({ items: messages }), }) } @@ -282,19 +262,6 @@ function provider() { } } -function json(route: Route, body: unknown, headers?: Record) { - return route.fulfill({ - status: 200, - contentType: "application/json", - headers: { "access-control-allow-origin": "*", "access-control-expose-headers": "x-next-cursor", ...headers }, - body: JSON.stringify(body ?? null), - }) -} - -function sse(route: Route) { - return route.fulfill({ status: 200, contentType: "text/event-stream", body: ": ok\n\n" }) -} - function base64Encode(value: string) { return Buffer.from(value, "utf8").toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "") } diff --git a/packages/app/e2e/utils/mock-server.ts b/packages/app/e2e/utils/mock-server.ts index b89f46deea61..9a03a9d5adb1 100644 --- a/packages/app/e2e/utils/mock-server.ts +++ b/packages/app/e2e/utils/mock-server.ts @@ -18,6 +18,7 @@ export interface MockServerConfig { project: unknown sessions: ({ id: string } & Record)[] pageMessages: (sessionId: string, limit: number, before?: string) => { items: unknown[]; cursor?: string } + events?: () => unknown[] } export async function mockOpenCodeServer(page: Page, config: MockServerConfig) { @@ -43,7 +44,8 @@ export async function mockOpenCodeServer(page: Page, config: MockServerConfig) { if (url.port !== targetPort) return route.fallback() const path = url.pathname - if (path === "/global/event" || path === "/event") return sse(route) + if (path === "/global/event" || path === "/event") return sse(route, config.events?.()) + if (path === "/global/health") return json(route, { healthy: true }) if (emptyObject.has(path)) return json(route, {}) if (emptyList.has(path)) return json(route, []) if (path in staticRoutes) return json(route, staticRoutes[path]) @@ -81,6 +83,10 @@ function json(route: Route, body: unknown, headers?: Record) { }) } -function sse(route: Route) { - return route.fulfill({ status: 200, contentType: "text/event-stream", body: ": ok\n\n" }) +function sse(route: Route, events?: unknown[]) { + return route.fulfill({ + status: 200, + contentType: "text/event-stream", + body: events?.map((event) => `data: ${JSON.stringify(event)}\n\n`).join("") || ": ok\n\n", + }) } diff --git a/packages/app/public/assets/Inter.ttf b/packages/app/public/assets/Inter.ttf new file mode 100644 index 000000000000..e31b51e3e938 Binary files /dev/null and b/packages/app/public/assets/Inter.ttf differ diff --git a/packages/app/src/app.tsx b/packages/app/src/app.tsx index 339cda8edfbd..c4eadbd5fbd5 100644 --- a/packages/app/src/app.tsx +++ b/packages/app/src/app.tsx @@ -46,6 +46,12 @@ import DirectoryLayout from "@/pages/directory-layout" import Layout from "@/pages/layout" import { ErrorPage } from "./pages/error" import { useCheckServerHealth } from "./utils/server-health" +import { ServersProvider } from "./context/servers" + +if (import.meta.env.VITE_OPENCODE_CHANNEL !== "prod") { + document.body.classList.remove("text-12-regular") + document.body.classList.add("font-(family-name:--font-family-text)", "text-[13px]", "font-[440]") +} const HomeRoute = lazy(() => import("@/pages/home")) const Session = lazy(() => import("@/pages/session")) @@ -296,31 +302,29 @@ export function AppInterface(props: { disableHealthCheck?: boolean }) { return ( - - - - - - - {routerProps.children}} - > - - - } /> - - - - - - - - + + + + + + + + {routerProps.children}} + > + + + } /> + + + + + + + + + ) } diff --git a/packages/app/src/components/prompt-input.tsx b/packages/app/src/components/prompt-input.tsx index a087c366e726..e26217c900fc 100644 --- a/packages/app/src/components/prompt-input.tsx +++ b/packages/app/src/components/prompt-input.tsx @@ -4,6 +4,8 @@ import { createEffect, on, Component, + splitProps, + For, Show, onCleanup, createMemo, @@ -11,7 +13,10 @@ import { createResource, Switch, Match, + type ComponentProps, + type JSX, } from "solid-js" +import { Popover as KobaltePopover } from "@kobalte/core/popover" import { createStore } from "solid-js/store" import { useLocal } from "@/context/local" import { selectionFromLines, type SelectedLineRange, useFile } from "@/context/file" @@ -26,12 +31,14 @@ import { FileAttachmentPart, } from "@/context/prompt" import { useLayout } from "@/context/layout" +import { useNavigate } from "@solidjs/router" import { useSDK } from "@/context/sdk" +import { useServer } from "@/context/server" import { useSync } from "@/context/sync" import { useComments } from "@/context/comments" import { Button } from "@opencode-ai/ui/button" import { DockShellForm, DockTray } from "@opencode-ai/ui/dock-surface" -import { Icon } from "@opencode-ai/ui/icon" +import { Icon, type IconProps } from "@opencode-ai/ui/icon" import { ProviderIcon } from "@opencode-ai/ui/provider-icon" import { Tooltip, TooltipKeybind } from "@opencode-ai/ui/tooltip" import { IconButton } from "@opencode-ai/ui/icon-button" @@ -44,6 +51,7 @@ import { Persist, persisted } from "@/utils/persist" import { usePermission } from "@/context/permission" import { useLanguage } from "@/context/language" import { usePlatform } from "@/context/platform" +import { useSettings } from "@/context/settings" import { useSessionLayout } from "@/pages/session/session-layout" import { createSessionTabs } from "@/pages/session/helpers" import { createTextFragment, getCursorPosition, setCursorPosition, setRangeEdge } from "./prompt-input/editor-dom" @@ -68,14 +76,16 @@ import { ImagePreview } from "@opencode-ai/ui/image-preview" import { useQueries } from "@tanstack/solid-query" import { useQueryOptions } from "@/context/server-sync" import { pathKey } from "@/utils/path-key" -import { getFilename } from "@opencode-ai/core/util/path" +import { base64Encode } from "@opencode-ai/core/util/encode" +import { displayName } from "@/pages/layout/helpers" + +const USE_V2_INPUT = import.meta.env.VITE_OPENCODE_CHANNEL !== "prod" interface PromptInputProps { class?: string variant?: "dock" | "new-session" ref?: (el: HTMLDivElement) => void newSessionWorktree?: string - onNewSessionWorktreeChange?: (worktree: string) => void onNewSessionWorktreeReset?: () => void edit?: { id: string; prompt: Prompt; context: FollowupDraft["context"] } onEditLoaded?: () => void @@ -113,11 +123,9 @@ const EXAMPLES = [ "prompt.example.25", ] as const -const MAIN_WORKTREE = "main" -const CREATE_WORKTREE = "create" - export const PromptInput: Component = (props) => { const sdk = useSDK() + const navigate = useNavigate() const queryOptions = useQueryOptions() const sync = useSync() @@ -125,6 +133,7 @@ export const PromptInput: Component = (props) => { const files = useFile() const prompt = usePrompt() const layout = useLayout() + const server = useServer() const comments = useComments() const dialog = useDialog() const providers = useProviders() @@ -132,11 +141,13 @@ export const PromptInput: Component = (props) => { const permission = usePermission() const language = useLanguage() const platform = usePlatform() + const settings = useSettings() const { params, tabs, view } = useSessionLayout() let editorRef!: HTMLDivElement let fileInputRef: HTMLInputElement | undefined let scrollRef!: HTMLDivElement let slashPopoverRef!: HTMLDivElement + let projectSearchRef: HTMLInputElement | undefined const mirror = { input: false } const inset = 56 @@ -277,6 +288,10 @@ export const PromptInput: Component = (props) => { mode: "normal", applyingHistory: false, }) + const [picker, setPicker] = createStore({ + projectOpen: false, + projectSearch: "", + }) const buttonsSpring = useSpring(() => (store.mode === "normal" ? 1 : 0), { visualDuration: 0.2, bounce: 0 }) const motion = (value: number) => ({ @@ -1303,91 +1318,124 @@ export const PromptInput: Component = (props) => { return "Ask anything, / for commands, @ for context..." } - const modelControl = () => ( - - 0} - fallback={ - - - - } - > - - - - - - {local.model.current()?.name ?? language.t("dialog.model.select.title")} - - - - - - ) + const modelControlState = createMemo(() => ({ + loading: providersLoading(), + paid: providers.paid().length > 0, + title: language.t("command.model.choose"), + keybind: command.keybind("model.choose"), + model: local.model, + providerID: local.model.current()?.provider?.id, + modelName: local.model.current()?.name ?? language.t("dialog.model.select.title"), + style: control(), + onClose: restoreFocus, + onUnpaidClick: () => { + void import("@/components/dialog-select-model-unpaid").then((x) => { + dialog.show(() => ) + }) + }, + })) const newSession = () => props.variant === "new-session" - const worktrees = createMemo(() => [MAIN_WORKTREE, ...(sync.project?.sandboxes ?? []), CREATE_WORKTREE]) - const currentWorktree = createMemo(() => { - if (worktrees().includes(props.newSessionWorktree ?? MAIN_WORKTREE)) - return props.newSessionWorktree ?? MAIN_WORKTREE - return MAIN_WORKTREE + const projects = createMemo(() => layout.projects.list()) + const projectForDirectory = (directory: string | undefined) => { + if (!directory) return + const key = pathKey(directory) + return projects().find( + (project) => pathKey(project.worktree) === key || project.sandboxes?.some((sandbox) => pathKey(sandbox) === key), + ) + } + const selectedProject = createMemo(() => projectForDirectory(sdk.directory)) + const projectResults = createMemo(() => { + const search = picker.projectSearch.trim().toLowerCase() + if (!search) return projects() + return projects().filter((project) => displayName(project).toLowerCase().includes(search)) }) - const worktreeLabel = (value: string) => { - if (value === MAIN_WORKTREE) return MAIN_WORKTREE - if (value === CREATE_WORKTREE) return language.t("session.new.worktree.create") - return getFilename(value) + const showAgentControl = createMemo(() => settings.general.showCustomAgents() && agentNames().length > 0) + const selectProject = (worktree: string) => { + setPicker({ + projectOpen: false, + projectSearch: "", + }) + if (pathKey(worktree) === pathKey(selectedProject()?.worktree ?? "")) { + restoreFocus() + return + } + layout.projects.open(worktree) + server.projects.touch(worktree) + navigate(`/${base64Encode(worktree)}/session`) + } + const addProject = async () => { + const select = (result: string | string[] | null) => { + const directory = Array.isArray(result) ? result[0] : result + if (!directory) return + selectProject(directory) + } + if (platform.openDirectoryPickerDialog && server.isLocal()) { + select(await platform.openDirectoryPickerDialog({ title: language.t("command.project.open") })) + return + } + void import("@/components/dialog-select-directory").then((x) => { + dialog.show( + () => , + () => select(null), + ) + }) } - const USE_V2_INPUT = import.meta.env.VITE_OPENCODE_CHANNEL !== "prod" + const projectPickerState = createMemo(() => ({ + open: picker.projectOpen, + trigger: { + action: "prompt-project", + icon: "folder", + label: selectedProject() ? displayName(selectedProject()!) : language.t("session.new.project.new"), + class: "max-w-[203px]", + style: control(), + onPress: () => setPicker("projectOpen", true), + }, + search: picker.projectSearch, + searchPlaceholder: language.t("session.new.project.search"), + clearLabel: language.t("common.clear"), + items: projectResults().map((project) => ({ + icon: "folder", + label: displayName(project), + selected: selectedProject()?.worktree === project.worktree, + onSelect: () => selectProject(project.worktree), + })), + action: { + icon: "plus", + label: language.t("session.new.project.add"), + onSelect: () => { + setPicker("projectOpen", false) + void addProject() + }, + }, + onOpenChange: (open) => { + setPicker("projectOpen", open) + if (open) requestAnimationFrame(() => projectSearchRef?.focus()) + }, + onSearchInput: (value) => setPicker("projectSearch", value), + onSearchClear: () => setPicker("projectSearch", ""), + searchRef: (el) => (projectSearchRef = el), + })) + const agentControlState = createMemo(() => ({ + title: language.t("command.agent.cycle"), + keybind: command.keybind("agent.cycle"), + options: agentNames(), + current: local.agent.current()?.name ?? "", + style: control(), + onSelect: (value) => { + local.agent.set(value) + restoreFocus() + }, + })) + const newProjectTriggerState = createMemo(() => ({ + action: "prompt-project", + icon: "folder-add-left", + label: language.t("session.new.project.new"), + class: "max-w-[160px]", + style: control(), + onPress: () => void addProject(), + })) return (
@@ -1409,154 +1457,145 @@ export const PromptInput: Component = (props) => { /> - - - { - const active = comments.active() - return !!item.commentID && item.commentID === active?.id && item.path === active?.file - }} - openComment={openComment} - remove={(item) => { - if (item.commentID) comments.remove(item.path, item.commentID) - prompt.context.remove(item.key) - }} - t={(key) => language.t(key as Parameters[0])} - /> - - dialog.show(() => ) - } - onRemove={removeAttachment} - removeLabel={language.t("prompt.attachment.remove")} - /> -
{ - const target = e.target - if (!(target instanceof HTMLElement)) return - if (target.closest('[data-action="prompt-attach"], [data-action="prompt-submit"]')) return - editorRef?.focus() +
+ -
(scrollRef = el)}> -
{ - editorRef = el - props.ref?.(el) - }} - role="textbox" - aria-multiline="true" - aria-label={designPlaceholder()} - contenteditable="true" - autocapitalize={store.mode === "normal" ? "sentences" : "off"} - autocorrect={store.mode === "normal" ? "on" : "off"} - spellcheck={store.mode === "normal"} - inputMode="text" - // @ts-expect-error - autocomplete="off" - onInput={handleInput} - onPaste={handlePaste} - onCompositionStart={handleCompositionStart} - onCompositionEnd={handleCompositionEnd} - onBlur={handleBlur} - onKeyDown={handleKeyDown} - classList={{ - "select-text": true, - "min-h-[52px] w-full px-4 pt-4 pb-2 focus:outline-none whitespace-pre-wrap leading-5 text-[13px] font-[440] text-v2-text-text-faint [font-family:Inter,var(--font-family-sans)]": true, - "[&_[data-type=file]]:text-syntax-property": true, - "[&_[data-type=agent]]:text-syntax-type": true, - "font-mono!": store.mode === "shell", - }} - /> -
- {designPlaceholder()} + + { + const active = comments.active() + return !!item.commentID && item.commentID === active?.id && item.path === active?.file + }} + openComment={openComment} + remove={(item) => { + if (item.commentID) comments.remove(item.path, item.commentID) + prompt.context.remove(item.key) + }} + t={(key) => language.t(key as Parameters[0])} + /> + + dialog.show(() => ) + } + onRemove={removeAttachment} + removeLabel={language.t("prompt.attachment.remove")} + /> +
{ + const target = e.target + if (!(target instanceof HTMLElement)) return + if (target.closest('[data-action^="prompt-"]')) return + editorRef?.focus() + }} + > +
(scrollRef = el)}> +
{ + editorRef = el + props.ref?.(el) + }} + role="textbox" + aria-multiline="true" + aria-label={designPlaceholder()} + contenteditable="true" + autocapitalize={store.mode === "normal" ? "sentences" : "off"} + autocorrect={store.mode === "normal" ? "on" : "off"} + spellcheck={store.mode === "normal"} + inputMode="text" + // @ts-expect-error + autocomplete="off" + onInput={handleInput} + onPaste={handlePaste} + onCompositionStart={handleCompositionStart} + onCompositionEnd={handleCompositionEnd} + onBlur={handleBlur} + onKeyDown={handleKeyDown} + classList={{ + "select-text": true, + "min-h-[52px] w-full px-4 pt-4 pb-2 focus:outline-none whitespace-pre-wrap leading-5 text-[13px] font-[440] text-v2-text-text-faint [font-family:Inter,var(--font-family-sans)]": true, + "[&_[data-type=file]]:text-syntax-property": true, + "[&_[data-type=agent]]:text-syntax-type": true, + "font-mono!": store.mode === "shell", + }} + /> +
+ {designPlaceholder()} +
-
-
-
- {fileAttachmentInput()} - +
+
+ {fileAttachmentInput()} + + + + + + + + + + +
+ - - -
-
- -
- props.state.onSearchInput(event.currentTarget.value)} + /> + + + +
+ {(item) => } +
+
+
+ +
+ + + + ) +} + +function ComposerAgentControl(props: { state: ComposerAgentControlState }) { + return ( +
+
+ +
+ + -
-
- -
- + + + + ) +} + +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..d1516534ada6 --- /dev/null +++ b/packages/stats/core/migrations/20260522121617_common_dust/snapshot.json @@ -0,0 +1,623 @@ +{ + "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": [] +} 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..4298716d022c --- /dev/null +++ b/packages/stats/core/migrations/20260523110335_cool_vin_gonzales/snapshot.json @@ -0,0 +1,2033 @@ +{ + "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": [] +} 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..1a89d0033629 --- /dev/null +++ b/packages/stats/core/src/domain/inference.ts @@ -0,0 +1,212 @@ +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..f2806204f752 --- /dev/null +++ b/packages/stats/server/src/ingest.ts @@ -0,0 +1,109 @@ +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..16408645c5df --- /dev/null +++ b/packages/stats/server/src/router.ts @@ -0,0 +1,63 @@ +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/packages/ui/src/v2/components/icon-button-v2.css b/packages/ui/src/v2/components/icon-button-v2.css index f5ea604e92be..e75e4c7aded1 100644 --- a/packages/ui/src/v2/components/icon-button-v2.css +++ b/packages/ui/src/v2/components/icon-button-v2.css @@ -5,7 +5,6 @@ } [data-component="icon-button-v2"] { - position: relative; display: inline-flex; align-items: center; justify-content: center; diff --git a/packages/ui/src/v2/components/icon.tsx b/packages/ui/src/v2/components/icon.tsx index 89190869ae6c..09aaea2e59b0 100644 --- a/packages/ui/src/v2/components/icon.tsx +++ b/packages/ui/src/v2/components/icon.tsx @@ -17,6 +17,18 @@ const icons = { viewBox: "0 0 20 20", body: ``, }, + "sidebar-right": { + viewBox: "0 0 20 20", + body: ``, + }, + status: { + viewBox: "0 0 20 20", + body: ``, + }, + "status-active": { + viewBox: "0 0 20 20", + body: ``, + }, "magnifying-glass": { viewBox: "0 0 16 16", body: ``, @@ -37,6 +49,14 @@ const icons = { viewBox: "0 0 16 16", body: ``, }, + "outline-chevron-down": { + viewBox: "0 0 16 16", + body: ``, + }, + "outline-dots": { + viewBox: "0 0 16 16", + body: ``, + }, } const spriteID = "opencode-v2-icon-sprite" diff --git a/packages/ui/src/v2/styles/theme.css b/packages/ui/src/v2/styles/theme.css index 2dc795d438cc..91e90d678d10 100644 --- a/packages/ui/src/v2/styles/theme.css +++ b/packages/ui/src/v2/styles/theme.css @@ -92,6 +92,8 @@ --v2-illustration-illustration-layer-01: var(--v2-grey-300); --v2-illustration-illustration-layer-02: var(--v2-grey-400); --v2-illustration-illustration-layer-03: var(--v2-grey-500); + + --font-family-text: "Inter", sans-serif; } /* OS preference fallback (no JS needed) */ 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..526fefabd41d 100644 --- a/sst.config.ts +++ b/sst.config.ts @@ -2,12 +2,28 @@ export default $config({ app(input) { + // Dev owns the shared AWS lake/stats infra for all non-production stages. + const awsStage = input.stage === "production" ? "production" : "dev" + const deployAws = input.stage === awsStage 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 +35,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 +49,8 @@ export default $config({ return { StatWorkerUrl: stat.url, + // StatsUrl: stats.app.url, + AwsStage: stage.awsStage, } }, })