mirror of
https://github.com/Heretek-AI/Token-Max.git
synced 2026-10-12 02:03:18 +00:00
feat: initial implementation of Token-Max model and coding plan usage tracker
This commit is contained in:
@@ -0,0 +1,50 @@
|
||||
name: Deploy to GitHub Pages
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pages: write
|
||||
id-token: write
|
||||
|
||||
concurrency:
|
||||
group: 'pages'
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
cache: 'npm'
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Build site
|
||||
run: npm run build
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-pages-artifact@v3
|
||||
with:
|
||||
path: dist
|
||||
|
||||
deploy:
|
||||
environment:
|
||||
name: github-pages
|
||||
url: ${{ steps.deployment.outputs.page_url }}
|
||||
runs-on: ubuntu-latest
|
||||
needs: build
|
||||
steps:
|
||||
- name: Deploy to GitHub Pages
|
||||
id: deployment
|
||||
uses: actions/deploy-pages@v4
|
||||
@@ -0,0 +1,43 @@
|
||||
name: Scheduled Data Refresh
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 6 * * *' # Daily at 06:00 UTC
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
refresh-data:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
cache: 'npm'
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Fetch models & benchmarks
|
||||
env:
|
||||
OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }}
|
||||
AA_API_KEY: ${{ secrets.AA_API_KEY }}
|
||||
run: npm run update-data
|
||||
|
||||
- name: Commit and push changes
|
||||
run: |
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||
git add public/data/
|
||||
if git diff --cached --quiet; then
|
||||
echo "No data changes to commit."
|
||||
else
|
||||
git commit -m "chore(data): refresh model pricing and benchmarks [skip ci]"
|
||||
git push
|
||||
fi
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"$schema": "./node_modules/oxlint/configuration_schema.json",
|
||||
"plugins": ["react", "typescript", "oxc"],
|
||||
"rules": {
|
||||
"react/rules-of-hooks": "error",
|
||||
"react/only-export-components": ["warn", { "allowConstantExport": true }]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
# React + TypeScript + Vite
|
||||
|
||||
This template provides a minimal setup to get React working in Vite with HMR and some Oxlint rules.
|
||||
|
||||
Currently, two official plugins are available:
|
||||
|
||||
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Oxc](https://oxc.rs)
|
||||
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/)
|
||||
|
||||
## React Compiler
|
||||
|
||||
The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation).
|
||||
|
||||
## Expanding the Oxlint configuration
|
||||
|
||||
If you are developing a production application, we recommend enabling type-aware lint rules by installing `oxlint-tsgolint` and editing `.oxlintrc.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"$schema": "./node_modules/oxlint/configuration_schema.json",
|
||||
"plugins": ["react", "typescript", "oxc"],
|
||||
"options": {
|
||||
"typeAware": true
|
||||
},
|
||||
"rules": {
|
||||
"react/rules-of-hooks": "error",
|
||||
"react/only-export-components": ["warn", { "allowConstantExport": true }]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
See the [Oxlint rules documentation](https://oxc.rs/docs/guide/usage/linter/rules) for the full list of rules and categories.
|
||||
@@ -0,0 +1,116 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"id",
|
||||
"name",
|
||||
"category",
|
||||
"url",
|
||||
"lastVerified",
|
||||
"tiers",
|
||||
"gotchas",
|
||||
"tosHighlights",
|
||||
"dataTraining",
|
||||
"ipIndemnity"
|
||||
],
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"category": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"coding-ide",
|
||||
"coding-router",
|
||||
"api-provider"
|
||||
]
|
||||
},
|
||||
"url": {
|
||||
"type": "string"
|
||||
},
|
||||
"lastVerified": {
|
||||
"type": "string",
|
||||
"format": "date"
|
||||
},
|
||||
"tiers": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"name",
|
||||
"monthlyPrice"
|
||||
],
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"monthlyPrice": {
|
||||
"type": [
|
||||
"number",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"annualPrice": {
|
||||
"type": [
|
||||
"number",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"limits": {
|
||||
"type": "object"
|
||||
},
|
||||
"models": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"estimatedTokenBudget": {
|
||||
"type": [
|
||||
"object",
|
||||
"null"
|
||||
],
|
||||
"properties": {
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
"estimatedMillionTokens": {
|
||||
"type": "number"
|
||||
},
|
||||
"assumptions": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"notes": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"gotchas": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"tosHighlights": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"dataTraining": {
|
||||
"type": "string"
|
||||
},
|
||||
"ipIndemnity": {
|
||||
"type": [
|
||||
"string",
|
||||
"boolean"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"id": "aider",
|
||||
"name": "Aider",
|
||||
"category": "coding-ide",
|
||||
"url": "https://aider.chat",
|
||||
"lastVerified": "2026-09-18",
|
||||
"tiers": [
|
||||
{
|
||||
"name": "Free",
|
||||
"monthlyPrice": 0,
|
||||
"limits": {},
|
||||
"models": [],
|
||||
"estimatedTokenBudget": null,
|
||||
"notes": "BYOK only"
|
||||
}
|
||||
],
|
||||
"gotchas": [
|
||||
"No limits beyond API provider limits"
|
||||
],
|
||||
"tosHighlights": [],
|
||||
"dataTraining": "Depends on API provider",
|
||||
"ipIndemnity": false
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
{
|
||||
"id": "alibaba-cloud",
|
||||
"name": "Alibaba Cloud",
|
||||
"category": "api-provider",
|
||||
"url": "https://alibabacloud.com",
|
||||
"lastVerified": "2026-09-18",
|
||||
"tiers": [
|
||||
{
|
||||
"name": "Personal Lite",
|
||||
"monthlyPrice": 6,
|
||||
"limits": {},
|
||||
"models": [],
|
||||
"estimatedTokenBudget": null,
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"name": "Standard",
|
||||
"monthlyPrice": 18,
|
||||
"limits": {},
|
||||
"models": [],
|
||||
"estimatedTokenBudget": null,
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"name": "Pro",
|
||||
"monthlyPrice": 68,
|
||||
"limits": {},
|
||||
"models": [],
|
||||
"estimatedTokenBudget": null,
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"name": "Team Standard",
|
||||
"monthlyPrice": 20,
|
||||
"limits": {},
|
||||
"models": [],
|
||||
"estimatedTokenBudget": null,
|
||||
"notes": "/seat"
|
||||
},
|
||||
{
|
||||
"name": "Team Pro",
|
||||
"monthlyPrice": 75,
|
||||
"limits": {},
|
||||
"models": [],
|
||||
"estimatedTokenBudget": null,
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"name": "Team Max",
|
||||
"monthlyPrice": 200,
|
||||
"limits": {},
|
||||
"models": [],
|
||||
"estimatedTokenBudget": null,
|
||||
"notes": ""
|
||||
}
|
||||
],
|
||||
"gotchas": [
|
||||
"no training guarantee on personal plans"
|
||||
],
|
||||
"tosHighlights": [],
|
||||
"dataTraining": "No guarantee on personal plans",
|
||||
"ipIndemnity": false
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"id": "amazon-q",
|
||||
"name": "Amazon Q Developer",
|
||||
"category": "coding-ide",
|
||||
"url": "https://aws.amazon.com/q",
|
||||
"lastVerified": "2026-09-18",
|
||||
"tiers": [
|
||||
{
|
||||
"name": "Free",
|
||||
"monthlyPrice": 0,
|
||||
"limits": {
|
||||
"lines": 1000,
|
||||
"requests": 50
|
||||
},
|
||||
"models": [],
|
||||
"estimatedTokenBudget": null,
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"name": "Pro",
|
||||
"monthlyPrice": 19,
|
||||
"limits": {
|
||||
"lines": 4000,
|
||||
"requests": 1000,
|
||||
"agent": 30
|
||||
},
|
||||
"models": [],
|
||||
"estimatedTokenBudget": null,
|
||||
"notes": "$19/user"
|
||||
}
|
||||
],
|
||||
"gotchas": [
|
||||
"Overages: $0.003/line"
|
||||
],
|
||||
"tosHighlights": [],
|
||||
"dataTraining": "Opt-out available",
|
||||
"ipIndemnity": true
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"id": "anthropic-api",
|
||||
"name": "Anthropic API",
|
||||
"category": "api-provider",
|
||||
"url": "https://console.anthropic.com",
|
||||
"lastVerified": "2026-09-18",
|
||||
"tiers": [
|
||||
{
|
||||
"name": "PAYG",
|
||||
"monthlyPrice": null,
|
||||
"limits": {},
|
||||
"models": [
|
||||
"Fable 5.1",
|
||||
"Opus 5",
|
||||
"Sonnet 5",
|
||||
"Haiku 4.5"
|
||||
],
|
||||
"estimatedTokenBudget": null,
|
||||
"notes": "Cache reads 90% off. Batch 50% off."
|
||||
}
|
||||
],
|
||||
"gotchas": [
|
||||
"Tiered rate limits based on spend"
|
||||
],
|
||||
"tosHighlights": [],
|
||||
"dataTraining": "No",
|
||||
"ipIndemnity": true
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"id": "augment-code",
|
||||
"name": "Augment Code",
|
||||
"category": "coding-ide",
|
||||
"url": "https://augment.co",
|
||||
"lastVerified": "2026-09-18",
|
||||
"tiers": [
|
||||
{
|
||||
"name": "Business",
|
||||
"monthlyPrice": 100,
|
||||
"limits": {},
|
||||
"models": [],
|
||||
"estimatedTokenBudget": null,
|
||||
"notes": "Effort/credit metered"
|
||||
},
|
||||
{
|
||||
"name": "Enterprise",
|
||||
"monthlyPrice": null,
|
||||
"limits": {},
|
||||
"models": [],
|
||||
"estimatedTokenBudget": null,
|
||||
"notes": "Custom"
|
||||
}
|
||||
],
|
||||
"gotchas": [
|
||||
"no permanent free tier",
|
||||
"Auto-top-up $15 blocks"
|
||||
],
|
||||
"tosHighlights": [],
|
||||
"dataTraining": "No",
|
||||
"ipIndemnity": true
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"id": "byteplus",
|
||||
"name": "BytePlus ModelArk",
|
||||
"category": "api-provider",
|
||||
"url": "https://byteplus.com",
|
||||
"lastVerified": "2026-09-18",
|
||||
"tiers": [
|
||||
{
|
||||
"name": "Lite",
|
||||
"monthlyPrice": 10,
|
||||
"limits": {
|
||||
"requests": 24000
|
||||
},
|
||||
"models": [],
|
||||
"estimatedTokenBudget": null,
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"name": "Pro",
|
||||
"monthlyPrice": 50,
|
||||
"limits": {
|
||||
"requests": 120000
|
||||
},
|
||||
"models": [],
|
||||
"estimatedTokenBudget": null,
|
||||
"notes": ""
|
||||
}
|
||||
],
|
||||
"gotchas": [
|
||||
"STRICTLY tool-only, direct API use = ban",
|
||||
"No overage fallback"
|
||||
],
|
||||
"tosHighlights": [],
|
||||
"dataTraining": "Yes",
|
||||
"ipIndemnity": false
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
{
|
||||
"id": "claude-code",
|
||||
"name": "Claude Code",
|
||||
"category": "coding-ide",
|
||||
"url": "https://anthropic.com",
|
||||
"lastVerified": "2026-09-18",
|
||||
"tiers": [
|
||||
{
|
||||
"name": "Pro",
|
||||
"monthlyPrice": 20,
|
||||
"limits": {},
|
||||
"models": [
|
||||
"Fable 5.1",
|
||||
"Opus 5",
|
||||
"Sonnet 5",
|
||||
"Haiku 4.5"
|
||||
],
|
||||
"estimatedTokenBudget": null,
|
||||
"notes": "Shared 5-hour rolling + weekly cap"
|
||||
},
|
||||
{
|
||||
"name": "Max5x",
|
||||
"monthlyPrice": 100,
|
||||
"limits": {},
|
||||
"models": [
|
||||
"Fable 5.1",
|
||||
"Opus 5",
|
||||
"Sonnet 5",
|
||||
"Haiku 4.5"
|
||||
],
|
||||
"estimatedTokenBudget": null,
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"name": "Max20x",
|
||||
"monthlyPrice": 200,
|
||||
"limits": {},
|
||||
"models": [
|
||||
"Fable 5.1",
|
||||
"Opus 5",
|
||||
"Sonnet 5",
|
||||
"Haiku 4.5"
|
||||
],
|
||||
"estimatedTokenBudget": null,
|
||||
"notes": ""
|
||||
}
|
||||
],
|
||||
"gotchas": [
|
||||
"shared quota with Claude web app"
|
||||
],
|
||||
"tosHighlights": [],
|
||||
"dataTraining": "None specified",
|
||||
"ipIndemnity": false
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
{
|
||||
"id": "commandcode",
|
||||
"name": "CommandCode",
|
||||
"category": "coding-router",
|
||||
"url": "https://commandcode.dev",
|
||||
"lastVerified": "2026-09-18",
|
||||
"tiers": [
|
||||
{
|
||||
"name": "Go",
|
||||
"monthlyPrice": 1,
|
||||
"limits": {},
|
||||
"models": [],
|
||||
"estimatedTokenBudget": null,
|
||||
"notes": "Dollar-value credits: $10"
|
||||
},
|
||||
{
|
||||
"name": "GOAT",
|
||||
"monthlyPrice": 10,
|
||||
"limits": {},
|
||||
"models": [],
|
||||
"estimatedTokenBudget": null,
|
||||
"notes": "Dollar-value credits: $70"
|
||||
},
|
||||
{
|
||||
"name": "Pro",
|
||||
"monthlyPrice": 20,
|
||||
"limits": {},
|
||||
"models": [],
|
||||
"estimatedTokenBudget": null,
|
||||
"notes": "Dollar-value credits: $80"
|
||||
},
|
||||
{
|
||||
"name": "Max10x",
|
||||
"monthlyPrice": 100,
|
||||
"limits": {},
|
||||
"models": [],
|
||||
"estimatedTokenBudget": null,
|
||||
"notes": "Dollar-value credits: $150"
|
||||
},
|
||||
{
|
||||
"name": "Max20x",
|
||||
"monthlyPrice": 200,
|
||||
"limits": {},
|
||||
"models": [],
|
||||
"estimatedTokenBudget": null,
|
||||
"notes": "Dollar-value credits: $300"
|
||||
}
|
||||
],
|
||||
"gotchas": [
|
||||
"5-hour rolling windows",
|
||||
"Extra credits never expire"
|
||||
],
|
||||
"tosHighlights": [],
|
||||
"dataTraining": "No",
|
||||
"ipIndemnity": false
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
{
|
||||
"id": "cursor",
|
||||
"name": "Cursor",
|
||||
"category": "coding-ide",
|
||||
"url": "https://cursor.sh",
|
||||
"lastVerified": "2026-09-18",
|
||||
"tiers": [
|
||||
{
|
||||
"name": "Hobby",
|
||||
"monthlyPrice": 0,
|
||||
"limits": {},
|
||||
"models": [
|
||||
"grok-4.6",
|
||||
"claude-sonnet-5",
|
||||
"gpt-5.6-sol"
|
||||
],
|
||||
"estimatedTokenBudget": null,
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"name": "Pro",
|
||||
"monthlyPrice": 20,
|
||||
"limits": {},
|
||||
"models": [
|
||||
"grok-4.6",
|
||||
"claude-sonnet-5",
|
||||
"gpt-5.6-sol"
|
||||
],
|
||||
"estimatedTokenBudget": {
|
||||
"description": "Pool estimated",
|
||||
"estimatedMillionTokens": 20,
|
||||
"assumptions": "Third-party pool estimation"
|
||||
},
|
||||
"notes": "Pro ~$20 third-party pool"
|
||||
},
|
||||
{
|
||||
"name": "Pro+",
|
||||
"monthlyPrice": 60,
|
||||
"limits": {},
|
||||
"models": [
|
||||
"grok-4.6",
|
||||
"claude-sonnet-5",
|
||||
"gpt-5.6-sol"
|
||||
],
|
||||
"estimatedTokenBudget": {
|
||||
"description": "Pool estimated",
|
||||
"estimatedMillionTokens": 70,
|
||||
"assumptions": "Third-party pool estimation"
|
||||
},
|
||||
"notes": "Pro+ ~$70"
|
||||
},
|
||||
{
|
||||
"name": "Ultra",
|
||||
"monthlyPrice": 200,
|
||||
"limits": {},
|
||||
"models": [
|
||||
"grok-4.6",
|
||||
"claude-sonnet-5",
|
||||
"gpt-5.6-sol"
|
||||
],
|
||||
"estimatedTokenBudget": {
|
||||
"description": "Pool estimated",
|
||||
"estimatedMillionTokens": 400,
|
||||
"assumptions": "Third-party pool estimation"
|
||||
},
|
||||
"notes": "Ultra ~$400"
|
||||
}
|
||||
],
|
||||
"gotchas": [
|
||||
"on-demand billing after pool"
|
||||
],
|
||||
"tosHighlights": [],
|
||||
"dataTraining": "opt-out in privacy mode",
|
||||
"ipIndemnity": false
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"id": "deepseek-api",
|
||||
"name": "DeepSeek API",
|
||||
"category": "api-provider",
|
||||
"url": "https://platform.deepseek.com",
|
||||
"lastVerified": "2026-09-18",
|
||||
"tiers": [
|
||||
{
|
||||
"name": "PAYG",
|
||||
"monthlyPrice": null,
|
||||
"limits": {
|
||||
"concurrency": {
|
||||
"Flash": 2500,
|
||||
"Pro": 500
|
||||
}
|
||||
},
|
||||
"models": [
|
||||
"Flash",
|
||||
"V4-Pro"
|
||||
],
|
||||
"estimatedTokenBudget": null,
|
||||
"notes": "time-of-day pricing. Cache hits 98% off."
|
||||
}
|
||||
],
|
||||
"gotchas": [
|
||||
"time-of-day pricing differences"
|
||||
],
|
||||
"tosHighlights": [],
|
||||
"dataTraining": "No",
|
||||
"ipIndemnity": false
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"id": "fireworks-ai",
|
||||
"name": "Fireworks.ai",
|
||||
"category": "api-provider",
|
||||
"url": "https://fireworks.ai",
|
||||
"lastVerified": "2026-09-18",
|
||||
"tiers": [
|
||||
{
|
||||
"name": "Free",
|
||||
"monthlyPrice": 0,
|
||||
"limits": {
|
||||
"RPM": 10
|
||||
},
|
||||
"models": [],
|
||||
"estimatedTokenBudget": null,
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"name": "PAYG",
|
||||
"monthlyPrice": null,
|
||||
"limits": {
|
||||
"RPM": 6000
|
||||
},
|
||||
"models": [],
|
||||
"estimatedTokenBudget": null,
|
||||
"notes": "Batch 50% off."
|
||||
}
|
||||
],
|
||||
"gotchas": [
|
||||
"503 load shedding on saturated fleet"
|
||||
],
|
||||
"tosHighlights": [],
|
||||
"dataTraining": "No",
|
||||
"ipIndemnity": false
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
{
|
||||
"id": "github-copilot",
|
||||
"name": "GitHub Copilot",
|
||||
"category": "coding-ide",
|
||||
"url": "https://github.com/features/copilot",
|
||||
"lastVerified": "2026-09-18",
|
||||
"tiers": [
|
||||
{
|
||||
"name": "Free",
|
||||
"monthlyPrice": 0,
|
||||
"limits": {
|
||||
"credits": 0
|
||||
},
|
||||
"models": [],
|
||||
"estimatedTokenBudget": null,
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"name": "Pro",
|
||||
"monthlyPrice": 10,
|
||||
"limits": {
|
||||
"credits": 1500
|
||||
},
|
||||
"models": [],
|
||||
"estimatedTokenBudget": {
|
||||
"description": "Based on credits",
|
||||
"estimatedMillionTokens": 15,
|
||||
"assumptions": "1 credit = $0.01"
|
||||
},
|
||||
"notes": "1500 credits ($15)"
|
||||
},
|
||||
{
|
||||
"name": "Pro+",
|
||||
"monthlyPrice": 39,
|
||||
"limits": {
|
||||
"credits": 7000
|
||||
},
|
||||
"models": [],
|
||||
"estimatedTokenBudget": {
|
||||
"description": "Based on credits",
|
||||
"estimatedMillionTokens": 70,
|
||||
"assumptions": "1 credit = $0.01"
|
||||
},
|
||||
"notes": "7000 credits ($70)"
|
||||
},
|
||||
{
|
||||
"name": "Max",
|
||||
"monthlyPrice": 100,
|
||||
"limits": {
|
||||
"credits": 20000
|
||||
},
|
||||
"models": [],
|
||||
"estimatedTokenBudget": {
|
||||
"description": "Based on credits",
|
||||
"estimatedMillionTokens": 200,
|
||||
"assumptions": "1 credit = $0.01"
|
||||
},
|
||||
"notes": "20000 credits ($200)"
|
||||
}
|
||||
],
|
||||
"gotchas": [
|
||||
"data training unless opted out on individual plans"
|
||||
],
|
||||
"tosHighlights": [],
|
||||
"dataTraining": "opt-out required for individual plans",
|
||||
"ipIndemnity": "Business/Enterprise only"
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
{
|
||||
"id": "google-ai-studio",
|
||||
"name": "Google AI Studio",
|
||||
"category": "api-provider",
|
||||
"url": "https://aistudio.google.com",
|
||||
"lastVerified": "2026-09-18",
|
||||
"tiers": [
|
||||
{
|
||||
"name": "Free",
|
||||
"monthlyPrice": 0,
|
||||
"limits": {
|
||||
"RPM": 15,
|
||||
"TPM": 1000000
|
||||
},
|
||||
"models": [
|
||||
"Gemini 3.8 Flash"
|
||||
],
|
||||
"estimatedTokenBudget": null,
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"name": "Tier1",
|
||||
"monthlyPrice": null,
|
||||
"limits": {
|
||||
"spendCap": 250
|
||||
},
|
||||
"models": [
|
||||
"Gemini 3.8 Flash"
|
||||
],
|
||||
"estimatedTokenBudget": null,
|
||||
"notes": "Based on spend"
|
||||
},
|
||||
{
|
||||
"name": "Tier2",
|
||||
"monthlyPrice": null,
|
||||
"limits": {
|
||||
"spendCap": 2000
|
||||
},
|
||||
"models": [
|
||||
"Gemini 3.8 Flash"
|
||||
],
|
||||
"estimatedTokenBudget": null,
|
||||
"notes": "Based on spend"
|
||||
},
|
||||
{
|
||||
"name": "Tier3",
|
||||
"monthlyPrice": null,
|
||||
"limits": {
|
||||
"spendCap": 100000
|
||||
},
|
||||
"models": [
|
||||
"Gemini 3.8 Flash"
|
||||
],
|
||||
"estimatedTokenBudget": null,
|
||||
"notes": "Based on spend"
|
||||
}
|
||||
],
|
||||
"gotchas": [
|
||||
"credits expire 1 year",
|
||||
"non-refundable"
|
||||
],
|
||||
"tosHighlights": [],
|
||||
"dataTraining": "Yes on free tier",
|
||||
"ipIndemnity": false
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
{
|
||||
"id": "google-antigravity",
|
||||
"name": "Google Antigravity",
|
||||
"category": "coding-ide",
|
||||
"url": "https://google.com/ai",
|
||||
"lastVerified": "2026-09-18",
|
||||
"tiers": [
|
||||
{
|
||||
"name": "Individual",
|
||||
"monthlyPrice": 0,
|
||||
"limits": {
|
||||
"tasks": 15
|
||||
},
|
||||
"models": [
|
||||
"Jules"
|
||||
],
|
||||
"estimatedTokenBudget": null,
|
||||
"notes": "15 daily tasks"
|
||||
},
|
||||
{
|
||||
"name": "AI Pro",
|
||||
"monthlyPrice": 19.99,
|
||||
"limits": {
|
||||
"tasks": 75
|
||||
},
|
||||
"models": [
|
||||
"Jules"
|
||||
],
|
||||
"estimatedTokenBudget": null,
|
||||
"notes": "75 daily tasks"
|
||||
},
|
||||
{
|
||||
"name": "Ultra5x",
|
||||
"monthlyPrice": 99.99,
|
||||
"limits": {
|
||||
"tasks": 300
|
||||
},
|
||||
"models": [
|
||||
"Jules"
|
||||
],
|
||||
"estimatedTokenBudget": null,
|
||||
"notes": "300 daily tasks"
|
||||
},
|
||||
{
|
||||
"name": "Ultra20x",
|
||||
"monthlyPrice": 199.99,
|
||||
"limits": {
|
||||
"tasks": 1200
|
||||
},
|
||||
"models": [
|
||||
"Jules"
|
||||
],
|
||||
"estimatedTokenBudget": null,
|
||||
"notes": "5-hour refresh cycles"
|
||||
}
|
||||
],
|
||||
"gotchas": [
|
||||
"task-based limits, not token-based"
|
||||
],
|
||||
"tosHighlights": [],
|
||||
"dataTraining": "Opt-out available",
|
||||
"ipIndemnity": false
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"id": "groq-api",
|
||||
"name": "Groq API",
|
||||
"category": "api-provider",
|
||||
"url": "https://console.groq.com",
|
||||
"lastVerified": "2026-09-18",
|
||||
"tiers": [
|
||||
{
|
||||
"name": "Free",
|
||||
"monthlyPrice": 0,
|
||||
"limits": {},
|
||||
"models": [],
|
||||
"estimatedTokenBudget": null,
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"name": "PAYG",
|
||||
"monthlyPrice": null,
|
||||
"limits": {},
|
||||
"models": [],
|
||||
"estimatedTokenBudget": null,
|
||||
"notes": "50% cached/batch discount"
|
||||
}
|
||||
],
|
||||
"gotchas": [
|
||||
"must set spend caps manually"
|
||||
],
|
||||
"tosHighlights": [],
|
||||
"dataTraining": "No",
|
||||
"ipIndemnity": false
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
{
|
||||
"id": "kilo-code",
|
||||
"name": "Kilo AI",
|
||||
"category": "coding-router",
|
||||
"url": "https://kilo.ai",
|
||||
"lastVerified": "2026-09-18",
|
||||
"tiers": [
|
||||
{
|
||||
"name": "Starter",
|
||||
"monthlyPrice": 19,
|
||||
"limits": {},
|
||||
"models": [],
|
||||
"estimatedTokenBudget": null,
|
||||
"notes": "Kilo Pass"
|
||||
},
|
||||
{
|
||||
"name": "Pro",
|
||||
"monthlyPrice": 49,
|
||||
"limits": {},
|
||||
"models": [],
|
||||
"estimatedTokenBudget": null,
|
||||
"notes": "Kilo Pass"
|
||||
},
|
||||
{
|
||||
"name": "Expert",
|
||||
"monthlyPrice": 199,
|
||||
"limits": {},
|
||||
"models": [],
|
||||
"estimatedTokenBudget": null,
|
||||
"notes": "Kilo Pass"
|
||||
},
|
||||
{
|
||||
"name": "Teams",
|
||||
"monthlyPrice": 15,
|
||||
"limits": {},
|
||||
"models": [],
|
||||
"estimatedTokenBudget": null,
|
||||
"notes": "$15/user/mo"
|
||||
}
|
||||
],
|
||||
"gotchas": [
|
||||
"bonus credits expire monthly",
|
||||
"5% processing fee"
|
||||
],
|
||||
"tosHighlights": [],
|
||||
"dataTraining": "No",
|
||||
"ipIndemnity": false
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"id": "kimi-code",
|
||||
"name": "Kimi Code",
|
||||
"category": "coding-ide",
|
||||
"url": "https://moonshot.cn",
|
||||
"lastVerified": "2026-09-18",
|
||||
"tiers": [
|
||||
{
|
||||
"name": "Plus",
|
||||
"monthlyPrice": 19,
|
||||
"limits": {},
|
||||
"models": [],
|
||||
"estimatedTokenBudget": null,
|
||||
"notes": "\u00a579"
|
||||
},
|
||||
{
|
||||
"name": "Pro",
|
||||
"monthlyPrice": 39,
|
||||
"limits": {},
|
||||
"models": [],
|
||||
"estimatedTokenBudget": null,
|
||||
"notes": "\u00a5159"
|
||||
},
|
||||
{
|
||||
"name": "Max",
|
||||
"monthlyPrice": 199,
|
||||
"limits": {},
|
||||
"models": [
|
||||
"K3 2.8T"
|
||||
],
|
||||
"estimatedTokenBudget": null,
|
||||
"notes": "\u00a5559"
|
||||
}
|
||||
],
|
||||
"gotchas": [
|
||||
"shared quota with consumer chat",
|
||||
"User-Agent tampering = suspension"
|
||||
],
|
||||
"tosHighlights": [],
|
||||
"dataTraining": "Yes",
|
||||
"ipIndemnity": false
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
{
|
||||
"id": "kiro",
|
||||
"name": "Kiro",
|
||||
"category": "coding-ide",
|
||||
"url": "https://aws.amazon.com/kiro",
|
||||
"lastVerified": "2026-09-18",
|
||||
"tiers": [
|
||||
{
|
||||
"name": "Free",
|
||||
"monthlyPrice": 0,
|
||||
"limits": {
|
||||
"credits": 50
|
||||
},
|
||||
"models": [],
|
||||
"estimatedTokenBudget": null,
|
||||
"notes": "50 credits"
|
||||
},
|
||||
{
|
||||
"name": "Pro",
|
||||
"monthlyPrice": 20,
|
||||
"limits": {
|
||||
"credits": 1000
|
||||
},
|
||||
"models": [],
|
||||
"estimatedTokenBudget": null,
|
||||
"notes": "1000 credits"
|
||||
},
|
||||
{
|
||||
"name": "Pro+",
|
||||
"monthlyPrice": 40,
|
||||
"limits": {
|
||||
"credits": 2000
|
||||
},
|
||||
"models": [],
|
||||
"estimatedTokenBudget": null,
|
||||
"notes": "2000 credits"
|
||||
},
|
||||
{
|
||||
"name": "ProMax",
|
||||
"monthlyPrice": 100,
|
||||
"limits": {
|
||||
"credits": 5000
|
||||
},
|
||||
"models": [],
|
||||
"estimatedTokenBudget": null,
|
||||
"notes": "5000 credits"
|
||||
},
|
||||
{
|
||||
"name": "Power",
|
||||
"monthlyPrice": 200,
|
||||
"limits": {
|
||||
"credits": 10000
|
||||
},
|
||||
"models": [],
|
||||
"estimatedTokenBudget": null,
|
||||
"notes": "10000 credits"
|
||||
}
|
||||
],
|
||||
"gotchas": [
|
||||
"Credits don't roll over",
|
||||
"Add-on $0.04/credit",
|
||||
"Model multipliers vary"
|
||||
],
|
||||
"tosHighlights": [],
|
||||
"dataTraining": "No",
|
||||
"ipIndemnity": false
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"id": "lovable",
|
||||
"name": "Lovable",
|
||||
"category": "coding-ide",
|
||||
"url": "https://lovable.dev",
|
||||
"lastVerified": "2026-09-18",
|
||||
"tiers": [
|
||||
{
|
||||
"name": "Free",
|
||||
"monthlyPrice": 0,
|
||||
"limits": {
|
||||
"build_credits": 5
|
||||
},
|
||||
"models": [],
|
||||
"estimatedTokenBudget": null,
|
||||
"notes": "Daily 5 build credits"
|
||||
},
|
||||
{
|
||||
"name": "Pro",
|
||||
"monthlyPrice": 25,
|
||||
"limits": {},
|
||||
"models": [],
|
||||
"estimatedTokenBudget": null,
|
||||
"notes": "Top-up $0.30/cr"
|
||||
},
|
||||
{
|
||||
"name": "Business",
|
||||
"monthlyPrice": 50,
|
||||
"limits": {},
|
||||
"models": [],
|
||||
"estimatedTokenBudget": null,
|
||||
"notes": "Top-up $0.60/cr"
|
||||
}
|
||||
],
|
||||
"gotchas": [
|
||||
"perpetual license for training on Free/Pro",
|
||||
"Credits expire 2 months"
|
||||
],
|
||||
"tosHighlights": [],
|
||||
"dataTraining": "Perpetual license on Free/Pro",
|
||||
"ipIndemnity": false
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"id": "meta-model-api",
|
||||
"name": "Meta Model API",
|
||||
"category": "api-provider",
|
||||
"url": "https://meta.com",
|
||||
"lastVerified": "2026-09-18",
|
||||
"tiers": [
|
||||
{
|
||||
"name": "Standard",
|
||||
"monthlyPrice": null,
|
||||
"limits": {
|
||||
"RPM": 3000
|
||||
},
|
||||
"models": [],
|
||||
"estimatedTokenBudget": null,
|
||||
"notes": "$1.25/$4.25/M"
|
||||
},
|
||||
{
|
||||
"name": "Contributor",
|
||||
"monthlyPrice": null,
|
||||
"limits": {
|
||||
"RPM": 60
|
||||
},
|
||||
"models": [],
|
||||
"estimatedTokenBudget": null,
|
||||
"notes": "$0.10/$0.20/M"
|
||||
}
|
||||
],
|
||||
"gotchas": [
|
||||
"Contributor allows training on your data"
|
||||
],
|
||||
"tosHighlights": [],
|
||||
"dataTraining": "Yes for Contributor tier",
|
||||
"ipIndemnity": false
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"id": "meta-muse-code",
|
||||
"name": "Meta Muse Code",
|
||||
"category": "coding-ide",
|
||||
"url": "https://meta.com",
|
||||
"lastVerified": "2026-09-18",
|
||||
"tiers": [
|
||||
{
|
||||
"name": "Everyday",
|
||||
"monthlyPrice": 5,
|
||||
"limits": {},
|
||||
"models": [],
|
||||
"estimatedTokenBudget": null,
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"name": "High",
|
||||
"monthlyPrice": 15,
|
||||
"limits": {},
|
||||
"models": [],
|
||||
"estimatedTokenBudget": null,
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"name": "Power",
|
||||
"monthlyPrice": 50,
|
||||
"limits": {},
|
||||
"models": [],
|
||||
"estimatedTokenBudget": null,
|
||||
"notes": ""
|
||||
}
|
||||
],
|
||||
"gotchas": [
|
||||
"Contributor tier allows training on your code",
|
||||
"Standard 3000 RPM, Contributor 60 RPM"
|
||||
],
|
||||
"tosHighlights": [],
|
||||
"dataTraining": "Contributor tier trains on your data",
|
||||
"ipIndemnity": false
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"id": "minimax",
|
||||
"name": "MiniMax",
|
||||
"category": "api-provider",
|
||||
"url": "https://minimax.ai",
|
||||
"lastVerified": "2026-09-18",
|
||||
"tiers": [
|
||||
{
|
||||
"name": "Plus",
|
||||
"monthlyPrice": 22,
|
||||
"limits": {},
|
||||
"models": [],
|
||||
"estimatedTokenBudget": null,
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"name": "Max",
|
||||
"monthlyPrice": 55,
|
||||
"limits": {},
|
||||
"models": [],
|
||||
"estimatedTokenBudget": null,
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"name": "Ultra",
|
||||
"monthlyPrice": 132,
|
||||
"limits": {},
|
||||
"models": [],
|
||||
"estimatedTokenBudget": null,
|
||||
"notes": ""
|
||||
}
|
||||
],
|
||||
"gotchas": [
|
||||
"Agent limits: 3-7 depending on tier",
|
||||
"Credits valid 365 days"
|
||||
],
|
||||
"tosHighlights": [],
|
||||
"dataTraining": "Yes",
|
||||
"ipIndemnity": false
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"id": "mistral-api",
|
||||
"name": "Mistral API",
|
||||
"category": "api-provider",
|
||||
"url": "https://console.mistral.ai",
|
||||
"lastVerified": "2026-09-18",
|
||||
"tiers": [
|
||||
{
|
||||
"name": "PAYG",
|
||||
"monthlyPrice": null,
|
||||
"limits": {},
|
||||
"models": [],
|
||||
"estimatedTokenBudget": null,
|
||||
"notes": "50% batch discount. 90% cache discount."
|
||||
}
|
||||
],
|
||||
"gotchas": [
|
||||
"exceeding context = 400 error"
|
||||
],
|
||||
"tosHighlights": [],
|
||||
"dataTraining": "No",
|
||||
"ipIndemnity": false
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
{
|
||||
"id": "ollama-cloud",
|
||||
"name": "Ollama Cloud",
|
||||
"category": "api-provider",
|
||||
"url": "https://ollama.com",
|
||||
"lastVerified": "2026-09-18",
|
||||
"tiers": [
|
||||
{
|
||||
"name": "Free",
|
||||
"monthlyPrice": 0,
|
||||
"limits": {},
|
||||
"models": [],
|
||||
"estimatedTokenBudget": null,
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"name": "Pro",
|
||||
"monthlyPrice": 20,
|
||||
"limits": {
|
||||
"concurrent": 3
|
||||
},
|
||||
"models": [],
|
||||
"estimatedTokenBudget": null,
|
||||
"notes": "$60 credits/mo"
|
||||
},
|
||||
{
|
||||
"name": "Max",
|
||||
"monthlyPrice": 100,
|
||||
"limits": {
|
||||
"concurrent": 10
|
||||
},
|
||||
"models": [],
|
||||
"estimatedTokenBudget": null,
|
||||
"notes": "$300 credits/mo"
|
||||
},
|
||||
{
|
||||
"name": "Team",
|
||||
"monthlyPrice": 500,
|
||||
"limits": {},
|
||||
"models": [],
|
||||
"estimatedTokenBudget": null,
|
||||
"notes": ""
|
||||
}
|
||||
],
|
||||
"gotchas": [
|
||||
"credits don't roll over",
|
||||
"peak pricing 12-18 UTC"
|
||||
],
|
||||
"tosHighlights": [],
|
||||
"dataTraining": "No",
|
||||
"ipIndemnity": false
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"id": "openai-api",
|
||||
"name": "OpenAI API",
|
||||
"category": "api-provider",
|
||||
"url": "https://platform.openai.com",
|
||||
"lastVerified": "2026-09-18",
|
||||
"tiers": [
|
||||
{
|
||||
"name": "PAYG",
|
||||
"monthlyPrice": null,
|
||||
"limits": {},
|
||||
"models": [
|
||||
"GPT-6 Astra",
|
||||
"Sol",
|
||||
"Terra",
|
||||
"Luna"
|
||||
],
|
||||
"estimatedTokenBudget": null,
|
||||
"notes": "Batch 50% off"
|
||||
}
|
||||
],
|
||||
"gotchas": [
|
||||
"long-context pricing doubles above 272K tokens"
|
||||
],
|
||||
"tosHighlights": [],
|
||||
"dataTraining": "Opt-out available",
|
||||
"ipIndemnity": true
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
{
|
||||
"id": "openai-codex",
|
||||
"name": "OpenAI Codex",
|
||||
"category": "coding-ide",
|
||||
"url": "https://openai.com",
|
||||
"lastVerified": "2026-09-18",
|
||||
"tiers": [
|
||||
{
|
||||
"name": "Free",
|
||||
"monthlyPrice": 0,
|
||||
"limits": {},
|
||||
"models": [],
|
||||
"estimatedTokenBudget": null,
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"name": "Go",
|
||||
"monthlyPrice": 8,
|
||||
"limits": {},
|
||||
"models": [],
|
||||
"estimatedTokenBudget": null,
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"name": "Plus",
|
||||
"monthlyPrice": 20,
|
||||
"limits": {
|
||||
"messages": "5-45 msgs/5h"
|
||||
},
|
||||
"models": [
|
||||
"GPT-6 Astra"
|
||||
],
|
||||
"estimatedTokenBudget": null,
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"name": "Pro5x",
|
||||
"monthlyPrice": 100,
|
||||
"limits": {
|
||||
"messages": "25-225 msgs/5h"
|
||||
},
|
||||
"models": [
|
||||
"GPT-6 Astra"
|
||||
],
|
||||
"estimatedTokenBudget": null,
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"name": "Pro20x",
|
||||
"monthlyPrice": 200,
|
||||
"limits": {
|
||||
"messages": "100-900 msgs/5h"
|
||||
},
|
||||
"models": [
|
||||
"GPT-6 Astra"
|
||||
],
|
||||
"estimatedTokenBudget": null,
|
||||
"notes": ""
|
||||
}
|
||||
],
|
||||
"gotchas": [
|
||||
"Work and Codex share same pool"
|
||||
],
|
||||
"tosHighlights": [],
|
||||
"dataTraining": "Opt-out available",
|
||||
"ipIndemnity": false
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"id": "opencode",
|
||||
"name": "OpenCode",
|
||||
"category": "coding-router",
|
||||
"url": "https://opencode.com",
|
||||
"lastVerified": "2026-09-18",
|
||||
"tiers": [
|
||||
{
|
||||
"name": "Go",
|
||||
"monthlyPrice": 10,
|
||||
"limits": {},
|
||||
"models": [],
|
||||
"estimatedTokenBudget": null,
|
||||
"notes": "Per-model monthly dollar limits"
|
||||
},
|
||||
{
|
||||
"name": "Zen",
|
||||
"monthlyPrice": null,
|
||||
"limits": {},
|
||||
"models": [],
|
||||
"estimatedTokenBudget": null,
|
||||
"notes": "PAYG"
|
||||
}
|
||||
],
|
||||
"gotchas": [
|
||||
"ZDR varies by model"
|
||||
],
|
||||
"tosHighlights": [],
|
||||
"dataTraining": "Varies by model",
|
||||
"ipIndemnity": false
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"id": "openrouter",
|
||||
"name": "OpenRouter",
|
||||
"category": "coding-router",
|
||||
"url": "https://openrouter.ai",
|
||||
"lastVerified": "2026-09-18",
|
||||
"tiers": [
|
||||
{
|
||||
"name": "PAYG",
|
||||
"monthlyPrice": null,
|
||||
"limits": {},
|
||||
"models": [],
|
||||
"estimatedTokenBudget": null,
|
||||
"notes": "5.5% platform fee"
|
||||
}
|
||||
],
|
||||
"gotchas": [
|
||||
"free tier limited to 50 RPD without credit purchase history"
|
||||
],
|
||||
"tosHighlights": [],
|
||||
"dataTraining": "No",
|
||||
"ipIndemnity": false
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"id": "replit",
|
||||
"name": "Replit",
|
||||
"category": "coding-ide",
|
||||
"url": "https://replit.com",
|
||||
"lastVerified": "2026-09-18",
|
||||
"tiers": [
|
||||
{
|
||||
"name": "Core",
|
||||
"monthlyPrice": 20,
|
||||
"limits": {},
|
||||
"models": [],
|
||||
"estimatedTokenBudget": null,
|
||||
"notes": "Effort-based checkpoint system"
|
||||
},
|
||||
{
|
||||
"name": "Pro",
|
||||
"monthlyPrice": 100,
|
||||
"limits": {},
|
||||
"models": [],
|
||||
"estimatedTokenBudget": null,
|
||||
"notes": "Effort-based checkpoint system"
|
||||
}
|
||||
],
|
||||
"gotchas": [
|
||||
"unpredictable costs",
|
||||
"free daily chat caps"
|
||||
],
|
||||
"tosHighlights": [],
|
||||
"dataTraining": "Yes",
|
||||
"ipIndemnity": false
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"id": "tabnine",
|
||||
"name": "Tabnine",
|
||||
"category": "coding-ide",
|
||||
"url": "https://tabnine.com",
|
||||
"lastVerified": "2026-09-18",
|
||||
"tiers": [
|
||||
{
|
||||
"name": "Starter",
|
||||
"monthlyPrice": 0,
|
||||
"limits": {},
|
||||
"models": [],
|
||||
"estimatedTokenBudget": null,
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"name": "Pro",
|
||||
"monthlyPrice": 15,
|
||||
"limits": {
|
||||
"chat": 500
|
||||
},
|
||||
"models": [],
|
||||
"estimatedTokenBudget": null,
|
||||
"notes": "unlimited completions"
|
||||
},
|
||||
{
|
||||
"name": "Enterprise",
|
||||
"monthlyPrice": 39,
|
||||
"limits": {},
|
||||
"models": [],
|
||||
"estimatedTokenBudget": null,
|
||||
"notes": "$39/user"
|
||||
}
|
||||
],
|
||||
"gotchas": [
|
||||
"very limited free tier",
|
||||
"ZDR focus"
|
||||
],
|
||||
"tosHighlights": [],
|
||||
"dataTraining": "ZDR",
|
||||
"ipIndemnity": true
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"id": "together-ai",
|
||||
"name": "Together.ai",
|
||||
"category": "api-provider",
|
||||
"url": "https://together.ai",
|
||||
"lastVerified": "2026-09-18",
|
||||
"tiers": [
|
||||
{
|
||||
"name": "Free",
|
||||
"monthlyPrice": 0,
|
||||
"limits": {
|
||||
"RPM": 60,
|
||||
"TPM": 60000
|
||||
},
|
||||
"models": [],
|
||||
"estimatedTokenBudget": null,
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"name": "PAYG",
|
||||
"monthlyPrice": null,
|
||||
"limits": {},
|
||||
"models": [],
|
||||
"estimatedTokenBudget": null,
|
||||
"notes": "Tiered limits at $25/$50/$100/$250 spend. Batch 50% off."
|
||||
}
|
||||
],
|
||||
"gotchas": [
|
||||
"sudden traffic spikes hit 429s"
|
||||
],
|
||||
"tosHighlights": [],
|
||||
"dataTraining": "No",
|
||||
"ipIndemnity": false
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
{
|
||||
"id": "windsurf",
|
||||
"name": "Windsurf",
|
||||
"category": "coding-ide",
|
||||
"url": "https://windsurf.dev",
|
||||
"lastVerified": "2026-09-18",
|
||||
"tiers": [
|
||||
{
|
||||
"name": "Free",
|
||||
"monthlyPrice": 0,
|
||||
"limits": {},
|
||||
"models": [
|
||||
"SWE-1.5",
|
||||
"Claude Sonnet 4.6",
|
||||
"GPT-5.4"
|
||||
],
|
||||
"estimatedTokenBudget": null,
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"name": "Pro",
|
||||
"monthlyPrice": 20,
|
||||
"limits": {},
|
||||
"models": [
|
||||
"SWE-1.5",
|
||||
"Claude Sonnet 4.6",
|
||||
"GPT-5.4"
|
||||
],
|
||||
"estimatedTokenBudget": null,
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"name": "Max",
|
||||
"monthlyPrice": 200,
|
||||
"limits": {},
|
||||
"models": [
|
||||
"SWE-1.5",
|
||||
"Claude Sonnet 4.6",
|
||||
"GPT-5.4"
|
||||
],
|
||||
"estimatedTokenBudget": null,
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"name": "Teams",
|
||||
"monthlyPrice": 40,
|
||||
"limits": {},
|
||||
"models": [
|
||||
"SWE-1.5",
|
||||
"Claude Sonnet 4.6",
|
||||
"GPT-5.4"
|
||||
],
|
||||
"estimatedTokenBudget": null,
|
||||
"notes": "$40/user"
|
||||
}
|
||||
],
|
||||
"gotchas": [
|
||||
"legacy Codeium credits retired"
|
||||
],
|
||||
"tosHighlights": [],
|
||||
"dataTraining": "No",
|
||||
"ipIndemnity": false
|
||||
}
|
||||
@@ -0,0 +1,620 @@
|
||||
import json
|
||||
import os
|
||||
|
||||
target_dir = "/home/john/Projects/Token-Max/data/coding-plans"
|
||||
os.makedirs(target_dir, exist_ok=True)
|
||||
|
||||
def write_json(name, data):
|
||||
with open(os.path.join(target_dir, name), 'w') as f:
|
||||
json.dump(data, f, indent=2)
|
||||
|
||||
write_json("_schema.json", {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"type": "object",
|
||||
"required": ["id", "name", "category", "url", "lastVerified", "tiers", "gotchas", "tosHighlights", "dataTraining", "ipIndemnity"],
|
||||
"properties": {
|
||||
"id": { "type": "string" },
|
||||
"name": { "type": "string" },
|
||||
"category": { "type": "string", "enum": ["coding-ide", "coding-router", "api-provider"] },
|
||||
"url": { "type": "string" },
|
||||
"lastVerified": { "type": "string", "format": "date" },
|
||||
"tiers": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": ["name", "monthlyPrice"],
|
||||
"properties": {
|
||||
"name": { "type": "string" },
|
||||
"monthlyPrice": { "type": ["number", "null"] },
|
||||
"annualPrice": { "type": ["number", "null"] },
|
||||
"limits": { "type": "object" },
|
||||
"models": { "type": "array", "items": { "type": "string" } },
|
||||
"estimatedTokenBudget": {
|
||||
"type": ["object", "null"],
|
||||
"properties": {
|
||||
"description": { "type": "string" },
|
||||
"estimatedMillionTokens": { "type": "number" },
|
||||
"assumptions": { "type": "string" }
|
||||
}
|
||||
},
|
||||
"notes": { "type": "string" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"gotchas": { "type": "array", "items": { "type": "string" } },
|
||||
"tosHighlights": { "type": "array", "items": { "type": "string" } },
|
||||
"dataTraining": { "type": "string" },
|
||||
"ipIndemnity": { "type": ["string", "boolean"] }
|
||||
}
|
||||
})
|
||||
|
||||
# 1. Cursor
|
||||
write_json("cursor.json", {
|
||||
"id": "cursor",
|
||||
"name": "Cursor",
|
||||
"category": "coding-ide",
|
||||
"url": "https://cursor.sh",
|
||||
"lastVerified": "2026-09-18",
|
||||
"tiers": [
|
||||
{"name": "Hobby", "monthlyPrice": 0, "limits": {}, "models": ["grok-4.6", "claude-sonnet-5", "gpt-5.6-sol"], "estimatedTokenBudget": None, "notes": ""},
|
||||
{"name": "Pro", "monthlyPrice": 20, "limits": {}, "models": ["grok-4.6", "claude-sonnet-5", "gpt-5.6-sol"], "estimatedTokenBudget": {"description": "Pool estimated", "estimatedMillionTokens": 20, "assumptions": "Third-party pool estimation"}, "notes": "Pro ~$20 third-party pool"},
|
||||
{"name": "Pro+", "monthlyPrice": 60, "limits": {}, "models": ["grok-4.6", "claude-sonnet-5", "gpt-5.6-sol"], "estimatedTokenBudget": {"description": "Pool estimated", "estimatedMillionTokens": 70, "assumptions": "Third-party pool estimation"}, "notes": "Pro+ ~$70"},
|
||||
{"name": "Ultra", "monthlyPrice": 200, "limits": {}, "models": ["grok-4.6", "claude-sonnet-5", "gpt-5.6-sol"], "estimatedTokenBudget": {"description": "Pool estimated", "estimatedMillionTokens": 400, "assumptions": "Third-party pool estimation"}, "notes": "Ultra ~$400"}
|
||||
],
|
||||
"gotchas": ["on-demand billing after pool"],
|
||||
"tosHighlights": [],
|
||||
"dataTraining": "opt-out in privacy mode",
|
||||
"ipIndemnity": False
|
||||
})
|
||||
|
||||
# 2. GitHub Copilot
|
||||
write_json("github-copilot.json", {
|
||||
"id": "github-copilot",
|
||||
"name": "GitHub Copilot",
|
||||
"category": "coding-ide",
|
||||
"url": "https://github.com/features/copilot",
|
||||
"lastVerified": "2026-09-18",
|
||||
"tiers": [
|
||||
{"name": "Free", "monthlyPrice": 0, "limits": {"credits": 0}, "models": [], "estimatedTokenBudget": None, "notes": ""},
|
||||
{"name": "Pro", "monthlyPrice": 10, "limits": {"credits": 1500}, "models": [], "estimatedTokenBudget": {"description": "Based on credits", "estimatedMillionTokens": 15, "assumptions": "1 credit = $0.01"}, "notes": "1500 credits ($15)"},
|
||||
{"name": "Pro+", "monthlyPrice": 39, "limits": {"credits": 7000}, "models": [], "estimatedTokenBudget": {"description": "Based on credits", "estimatedMillionTokens": 70, "assumptions": "1 credit = $0.01"}, "notes": "7000 credits ($70)"},
|
||||
{"name": "Max", "monthlyPrice": 100, "limits": {"credits": 20000}, "models": [], "estimatedTokenBudget": {"description": "Based on credits", "estimatedMillionTokens": 200, "assumptions": "1 credit = $0.01"}, "notes": "20000 credits ($200)"}
|
||||
],
|
||||
"gotchas": ["data training unless opted out on individual plans"],
|
||||
"tosHighlights": [],
|
||||
"dataTraining": "opt-out required for individual plans",
|
||||
"ipIndemnity": "Business/Enterprise only"
|
||||
})
|
||||
|
||||
# 3. Claude Code
|
||||
write_json("claude-code.json", {
|
||||
"id": "claude-code",
|
||||
"name": "Claude Code",
|
||||
"category": "coding-ide",
|
||||
"url": "https://anthropic.com",
|
||||
"lastVerified": "2026-09-18",
|
||||
"tiers": [
|
||||
{"name": "Pro", "monthlyPrice": 20, "limits": {}, "models": ["Fable 5.1", "Opus 5", "Sonnet 5", "Haiku 4.5"], "estimatedTokenBudget": None, "notes": "Shared 5-hour rolling + weekly cap"},
|
||||
{"name": "Max5x", "monthlyPrice": 100, "limits": {}, "models": ["Fable 5.1", "Opus 5", "Sonnet 5", "Haiku 4.5"], "estimatedTokenBudget": None, "notes": ""},
|
||||
{"name": "Max20x", "monthlyPrice": 200, "limits": {}, "models": ["Fable 5.1", "Opus 5", "Sonnet 5", "Haiku 4.5"], "estimatedTokenBudget": None, "notes": ""}
|
||||
],
|
||||
"gotchas": ["shared quota with Claude web app"],
|
||||
"tosHighlights": [],
|
||||
"dataTraining": "None specified",
|
||||
"ipIndemnity": False
|
||||
})
|
||||
|
||||
# 4. OpenAI Codex
|
||||
write_json("openai-codex.json", {
|
||||
"id": "openai-codex",
|
||||
"name": "OpenAI Codex",
|
||||
"category": "coding-ide",
|
||||
"url": "https://openai.com",
|
||||
"lastVerified": "2026-09-18",
|
||||
"tiers": [
|
||||
{"name": "Free", "monthlyPrice": 0, "limits": {}, "models": [], "estimatedTokenBudget": None, "notes": ""},
|
||||
{"name": "Go", "monthlyPrice": 8, "limits": {}, "models": [], "estimatedTokenBudget": None, "notes": ""},
|
||||
{"name": "Plus", "monthlyPrice": 20, "limits": {"messages": "5-45 msgs/5h"}, "models": ["GPT-6 Astra"], "estimatedTokenBudget": None, "notes": ""},
|
||||
{"name": "Pro5x", "monthlyPrice": 100, "limits": {"messages": "25-225 msgs/5h"}, "models": ["GPT-6 Astra"], "estimatedTokenBudget": None, "notes": ""},
|
||||
{"name": "Pro20x", "monthlyPrice": 200, "limits": {"messages": "100-900 msgs/5h"}, "models": ["GPT-6 Astra"], "estimatedTokenBudget": None, "notes": ""}
|
||||
],
|
||||
"gotchas": ["Work and Codex share same pool"],
|
||||
"tosHighlights": [],
|
||||
"dataTraining": "Opt-out available",
|
||||
"ipIndemnity": False
|
||||
})
|
||||
|
||||
# 5. Google Antigravity
|
||||
write_json("google-antigravity.json", {
|
||||
"id": "google-antigravity",
|
||||
"name": "Google Antigravity",
|
||||
"category": "coding-ide",
|
||||
"url": "https://google.com/ai",
|
||||
"lastVerified": "2026-09-18",
|
||||
"tiers": [
|
||||
{"name": "Individual", "monthlyPrice": 0, "limits": {"tasks": 15}, "models": ["Jules"], "estimatedTokenBudget": None, "notes": "15 daily tasks"},
|
||||
{"name": "AI Pro", "monthlyPrice": 19.99, "limits": {"tasks": 75}, "models": ["Jules"], "estimatedTokenBudget": None, "notes": "75 daily tasks"},
|
||||
{"name": "Ultra5x", "monthlyPrice": 99.99, "limits": {"tasks": 300}, "models": ["Jules"], "estimatedTokenBudget": None, "notes": "300 daily tasks"},
|
||||
{"name": "Ultra20x", "monthlyPrice": 199.99, "limits": {"tasks": 1200}, "models": ["Jules"], "estimatedTokenBudget": None, "notes": "5-hour refresh cycles"}
|
||||
],
|
||||
"gotchas": ["task-based limits, not token-based"],
|
||||
"tosHighlights": [],
|
||||
"dataTraining": "Opt-out available",
|
||||
"ipIndemnity": False
|
||||
})
|
||||
|
||||
# 6. Meta Muse Code
|
||||
write_json("meta-muse-code.json", {
|
||||
"id": "meta-muse-code",
|
||||
"name": "Meta Muse Code",
|
||||
"category": "coding-ide",
|
||||
"url": "https://meta.com",
|
||||
"lastVerified": "2026-09-18",
|
||||
"tiers": [
|
||||
{"name": "Everyday", "monthlyPrice": 5, "limits": {}, "models": [], "estimatedTokenBudget": None, "notes": ""},
|
||||
{"name": "High", "monthlyPrice": 15, "limits": {}, "models": [], "estimatedTokenBudget": None, "notes": ""},
|
||||
{"name": "Power", "monthlyPrice": 50, "limits": {}, "models": [], "estimatedTokenBudget": None, "notes": ""}
|
||||
],
|
||||
"gotchas": ["Contributor tier allows training on your code", "Standard 3000 RPM, Contributor 60 RPM"],
|
||||
"tosHighlights": [],
|
||||
"dataTraining": "Contributor tier trains on your data",
|
||||
"ipIndemnity": False
|
||||
})
|
||||
|
||||
# 7. Kiro
|
||||
write_json("kiro.json", {
|
||||
"id": "kiro",
|
||||
"name": "Kiro",
|
||||
"category": "coding-ide",
|
||||
"url": "https://aws.amazon.com/kiro",
|
||||
"lastVerified": "2026-09-18",
|
||||
"tiers": [
|
||||
{"name": "Free", "monthlyPrice": 0, "limits": {"credits": 50}, "models": [], "estimatedTokenBudget": None, "notes": "50 credits"},
|
||||
{"name": "Pro", "monthlyPrice": 20, "limits": {"credits": 1000}, "models": [], "estimatedTokenBudget": None, "notes": "1000 credits"},
|
||||
{"name": "Pro+", "monthlyPrice": 40, "limits": {"credits": 2000}, "models": [], "estimatedTokenBudget": None, "notes": "2000 credits"},
|
||||
{"name": "ProMax", "monthlyPrice": 100, "limits": {"credits": 5000}, "models": [], "estimatedTokenBudget": None, "notes": "5000 credits"},
|
||||
{"name": "Power", "monthlyPrice": 200, "limits": {"credits": 10000}, "models": [], "estimatedTokenBudget": None, "notes": "10000 credits"}
|
||||
],
|
||||
"gotchas": ["Credits don't roll over", "Add-on $0.04/credit", "Model multipliers vary"],
|
||||
"tosHighlights": [],
|
||||
"dataTraining": "No",
|
||||
"ipIndemnity": False
|
||||
})
|
||||
|
||||
# 8. Kilo AI
|
||||
write_json("kilo-code.json", {
|
||||
"id": "kilo-code",
|
||||
"name": "Kilo AI",
|
||||
"category": "coding-router",
|
||||
"url": "https://kilo.ai",
|
||||
"lastVerified": "2026-09-18",
|
||||
"tiers": [
|
||||
{"name": "Starter", "monthlyPrice": 19, "limits": {}, "models": [], "estimatedTokenBudget": None, "notes": "Kilo Pass"},
|
||||
{"name": "Pro", "monthlyPrice": 49, "limits": {}, "models": [], "estimatedTokenBudget": None, "notes": "Kilo Pass"},
|
||||
{"name": "Expert", "monthlyPrice": 199, "limits": {}, "models": [], "estimatedTokenBudget": None, "notes": "Kilo Pass"},
|
||||
{"name": "Teams", "monthlyPrice": 15, "limits": {}, "models": [], "estimatedTokenBudget": None, "notes": "$15/user/mo"}
|
||||
],
|
||||
"gotchas": ["bonus credits expire monthly", "5% processing fee"],
|
||||
"tosHighlights": [],
|
||||
"dataTraining": "No",
|
||||
"ipIndemnity": False
|
||||
})
|
||||
|
||||
# 9. Lovable
|
||||
write_json("lovable.json", {
|
||||
"id": "lovable",
|
||||
"name": "Lovable",
|
||||
"category": "coding-ide",
|
||||
"url": "https://lovable.dev",
|
||||
"lastVerified": "2026-09-18",
|
||||
"tiers": [
|
||||
{"name": "Free", "monthlyPrice": 0, "limits": {"build_credits": 5}, "models": [], "estimatedTokenBudget": None, "notes": "Daily 5 build credits"},
|
||||
{"name": "Pro", "monthlyPrice": 25, "limits": {}, "models": [], "estimatedTokenBudget": None, "notes": "Top-up $0.30/cr"},
|
||||
{"name": "Business", "monthlyPrice": 50, "limits": {}, "models": [], "estimatedTokenBudget": None, "notes": "Top-up $0.60/cr"}
|
||||
],
|
||||
"gotchas": ["perpetual license for training on Free/Pro", "Credits expire 2 months"],
|
||||
"tosHighlights": [],
|
||||
"dataTraining": "Perpetual license on Free/Pro",
|
||||
"ipIndemnity": False
|
||||
})
|
||||
|
||||
# 10. Kimi Code
|
||||
write_json("kimi-code.json", {
|
||||
"id": "kimi-code",
|
||||
"name": "Kimi Code",
|
||||
"category": "coding-ide",
|
||||
"url": "https://moonshot.cn",
|
||||
"lastVerified": "2026-09-18",
|
||||
"tiers": [
|
||||
{"name": "Plus", "monthlyPrice": 19, "limits": {}, "models": [], "estimatedTokenBudget": None, "notes": "¥79"},
|
||||
{"name": "Pro", "monthlyPrice": 39, "limits": {}, "models": [], "estimatedTokenBudget": None, "notes": "¥159"},
|
||||
{"name": "Max", "monthlyPrice": 199, "limits": {}, "models": ["K3 2.8T"], "estimatedTokenBudget": None, "notes": "¥559"}
|
||||
],
|
||||
"gotchas": ["shared quota with consumer chat", "User-Agent tampering = suspension"],
|
||||
"tosHighlights": [],
|
||||
"dataTraining": "Yes",
|
||||
"ipIndemnity": False
|
||||
})
|
||||
|
||||
# 11. Windsurf
|
||||
write_json("windsurf.json", {
|
||||
"id": "windsurf",
|
||||
"name": "Windsurf",
|
||||
"category": "coding-ide",
|
||||
"url": "https://windsurf.dev",
|
||||
"lastVerified": "2026-09-18",
|
||||
"tiers": [
|
||||
{"name": "Free", "monthlyPrice": 0, "limits": {}, "models": ["SWE-1.5", "Claude Sonnet 4.6", "GPT-5.4"], "estimatedTokenBudget": None, "notes": ""},
|
||||
{"name": "Pro", "monthlyPrice": 20, "limits": {}, "models": ["SWE-1.5", "Claude Sonnet 4.6", "GPT-5.4"], "estimatedTokenBudget": None, "notes": ""},
|
||||
{"name": "Max", "monthlyPrice": 200, "limits": {}, "models": ["SWE-1.5", "Claude Sonnet 4.6", "GPT-5.4"], "estimatedTokenBudget": None, "notes": ""},
|
||||
{"name": "Teams", "monthlyPrice": 40, "limits": {}, "models": ["SWE-1.5", "Claude Sonnet 4.6", "GPT-5.4"], "estimatedTokenBudget": None, "notes": "$40/user"}
|
||||
],
|
||||
"gotchas": ["legacy Codeium credits retired"],
|
||||
"tosHighlights": [],
|
||||
"dataTraining": "No",
|
||||
"ipIndemnity": False
|
||||
})
|
||||
|
||||
# 12. Augment Code
|
||||
write_json("augment-code.json", {
|
||||
"id": "augment-code",
|
||||
"name": "Augment Code",
|
||||
"category": "coding-ide",
|
||||
"url": "https://augment.co",
|
||||
"lastVerified": "2026-09-18",
|
||||
"tiers": [
|
||||
{"name": "Business", "monthlyPrice": 100, "limits": {}, "models": [], "estimatedTokenBudget": None, "notes": "Effort/credit metered"},
|
||||
{"name": "Enterprise", "monthlyPrice": None, "limits": {}, "models": [], "estimatedTokenBudget": None, "notes": "Custom"}
|
||||
],
|
||||
"gotchas": ["no permanent free tier", "Auto-top-up $15 blocks"],
|
||||
"tosHighlights": [],
|
||||
"dataTraining": "No",
|
||||
"ipIndemnity": True
|
||||
})
|
||||
|
||||
# 13. Replit
|
||||
write_json("replit.json", {
|
||||
"id": "replit",
|
||||
"name": "Replit",
|
||||
"category": "coding-ide",
|
||||
"url": "https://replit.com",
|
||||
"lastVerified": "2026-09-18",
|
||||
"tiers": [
|
||||
{"name": "Core", "monthlyPrice": 20, "limits": {}, "models": [], "estimatedTokenBudget": None, "notes": "Effort-based checkpoint system"},
|
||||
{"name": "Pro", "monthlyPrice": 100, "limits": {}, "models": [], "estimatedTokenBudget": None, "notes": "Effort-based checkpoint system"}
|
||||
],
|
||||
"gotchas": ["unpredictable costs", "free daily chat caps"],
|
||||
"tosHighlights": [],
|
||||
"dataTraining": "Yes",
|
||||
"ipIndemnity": False
|
||||
})
|
||||
|
||||
# 14. Amazon Q
|
||||
write_json("amazon-q.json", {
|
||||
"id": "amazon-q",
|
||||
"name": "Amazon Q Developer",
|
||||
"category": "coding-ide",
|
||||
"url": "https://aws.amazon.com/q",
|
||||
"lastVerified": "2026-09-18",
|
||||
"tiers": [
|
||||
{"name": "Free", "monthlyPrice": 0, "limits": {"lines": 1000, "requests": 50}, "models": [], "estimatedTokenBudget": None, "notes": ""},
|
||||
{"name": "Pro", "monthlyPrice": 19, "limits": {"lines": 4000, "requests": 1000, "agent": 30}, "models": [], "estimatedTokenBudget": None, "notes": "$19/user"}
|
||||
],
|
||||
"gotchas": ["Overages: $0.003/line"],
|
||||
"tosHighlights": [],
|
||||
"dataTraining": "Opt-out available",
|
||||
"ipIndemnity": True
|
||||
})
|
||||
|
||||
# 15. Tabnine
|
||||
write_json("tabnine.json", {
|
||||
"id": "tabnine",
|
||||
"name": "Tabnine",
|
||||
"category": "coding-ide",
|
||||
"url": "https://tabnine.com",
|
||||
"lastVerified": "2026-09-18",
|
||||
"tiers": [
|
||||
{"name": "Starter", "monthlyPrice": 0, "limits": {}, "models": [], "estimatedTokenBudget": None, "notes": ""},
|
||||
{"name": "Pro", "monthlyPrice": 15, "limits": {"chat": 500}, "models": [], "estimatedTokenBudget": None, "notes": "unlimited completions"},
|
||||
{"name": "Enterprise", "monthlyPrice": 39, "limits": {}, "models": [], "estimatedTokenBudget": None, "notes": "$39/user"}
|
||||
],
|
||||
"gotchas": ["very limited free tier", "ZDR focus"],
|
||||
"tosHighlights": [],
|
||||
"dataTraining": "ZDR",
|
||||
"ipIndemnity": True
|
||||
})
|
||||
|
||||
# 16. Aider
|
||||
write_json("aider.json", {
|
||||
"id": "aider",
|
||||
"name": "Aider",
|
||||
"category": "coding-ide",
|
||||
"url": "https://aider.chat",
|
||||
"lastVerified": "2026-09-18",
|
||||
"tiers": [
|
||||
{"name": "Free", "monthlyPrice": 0, "limits": {}, "models": [], "estimatedTokenBudget": None, "notes": "BYOK only"}
|
||||
],
|
||||
"gotchas": ["No limits beyond API provider limits"],
|
||||
"tosHighlights": [],
|
||||
"dataTraining": "Depends on API provider",
|
||||
"ipIndemnity": False
|
||||
})
|
||||
|
||||
# 17. CommandCode
|
||||
write_json("commandcode.json", {
|
||||
"id": "commandcode",
|
||||
"name": "CommandCode",
|
||||
"category": "coding-router",
|
||||
"url": "https://commandcode.dev",
|
||||
"lastVerified": "2026-09-18",
|
||||
"tiers": [
|
||||
{"name": "Go", "monthlyPrice": 1, "limits": {}, "models": [], "estimatedTokenBudget": None, "notes": "Dollar-value credits: $10"},
|
||||
{"name": "GOAT", "monthlyPrice": 10, "limits": {}, "models": [], "estimatedTokenBudget": None, "notes": "Dollar-value credits: $70"},
|
||||
{"name": "Pro", "monthlyPrice": 20, "limits": {}, "models": [], "estimatedTokenBudget": None, "notes": "Dollar-value credits: $80"},
|
||||
{"name": "Max10x", "monthlyPrice": 100, "limits": {}, "models": [], "estimatedTokenBudget": None, "notes": "Dollar-value credits: $150"},
|
||||
{"name": "Max20x", "monthlyPrice": 200, "limits": {}, "models": [], "estimatedTokenBudget": None, "notes": "Dollar-value credits: $300"}
|
||||
],
|
||||
"gotchas": ["5-hour rolling windows", "Extra credits never expire"],
|
||||
"tosHighlights": [],
|
||||
"dataTraining": "No",
|
||||
"ipIndemnity": False
|
||||
})
|
||||
|
||||
# 18. OpenCode
|
||||
write_json("opencode.json", {
|
||||
"id": "opencode",
|
||||
"name": "OpenCode",
|
||||
"category": "coding-router",
|
||||
"url": "https://opencode.com",
|
||||
"lastVerified": "2026-09-18",
|
||||
"tiers": [
|
||||
{"name": "Go", "monthlyPrice": 10, "limits": {}, "models": [], "estimatedTokenBudget": None, "notes": "Per-model monthly dollar limits"},
|
||||
{"name": "Zen", "monthlyPrice": None, "limits": {}, "models": [], "estimatedTokenBudget": None, "notes": "PAYG"}
|
||||
],
|
||||
"gotchas": ["ZDR varies by model"],
|
||||
"tosHighlights": [],
|
||||
"dataTraining": "Varies by model",
|
||||
"ipIndemnity": False
|
||||
})
|
||||
|
||||
# 19. OpenRouter
|
||||
write_json("openrouter.json", {
|
||||
"id": "openrouter",
|
||||
"name": "OpenRouter",
|
||||
"category": "coding-router",
|
||||
"url": "https://openrouter.ai",
|
||||
"lastVerified": "2026-09-18",
|
||||
"tiers": [
|
||||
{"name": "PAYG", "monthlyPrice": None, "limits": {}, "models": [], "estimatedTokenBudget": None, "notes": "5.5% platform fee"}
|
||||
],
|
||||
"gotchas": ["free tier limited to 50 RPD without credit purchase history"],
|
||||
"tosHighlights": [],
|
||||
"dataTraining": "No",
|
||||
"ipIndemnity": False
|
||||
})
|
||||
|
||||
# 20. BytePlus
|
||||
write_json("byteplus.json", {
|
||||
"id": "byteplus",
|
||||
"name": "BytePlus ModelArk",
|
||||
"category": "api-provider",
|
||||
"url": "https://byteplus.com",
|
||||
"lastVerified": "2026-09-18",
|
||||
"tiers": [
|
||||
{"name": "Lite", "monthlyPrice": 10, "limits": {"requests": 24000}, "models": [], "estimatedTokenBudget": None, "notes": ""},
|
||||
{"name": "Pro", "monthlyPrice": 50, "limits": {"requests": 120000}, "models": [], "estimatedTokenBudget": None, "notes": ""}
|
||||
],
|
||||
"gotchas": ["STRICTLY tool-only, direct API use = ban", "No overage fallback"],
|
||||
"tosHighlights": [],
|
||||
"dataTraining": "Yes",
|
||||
"ipIndemnity": False
|
||||
})
|
||||
|
||||
# 21. Alibaba Cloud
|
||||
write_json("alibaba-cloud.json", {
|
||||
"id": "alibaba-cloud",
|
||||
"name": "Alibaba Cloud",
|
||||
"category": "api-provider",
|
||||
"url": "https://alibabacloud.com",
|
||||
"lastVerified": "2026-09-18",
|
||||
"tiers": [
|
||||
{"name": "Personal Lite", "monthlyPrice": 6, "limits": {}, "models": [], "estimatedTokenBudget": None, "notes": ""},
|
||||
{"name": "Standard", "monthlyPrice": 18, "limits": {}, "models": [], "estimatedTokenBudget": None, "notes": ""},
|
||||
{"name": "Pro", "monthlyPrice": 68, "limits": {}, "models": [], "estimatedTokenBudget": None, "notes": ""},
|
||||
{"name": "Team Standard", "monthlyPrice": 20, "limits": {}, "models": [], "estimatedTokenBudget": None, "notes": "/seat"},
|
||||
{"name": "Team Pro", "monthlyPrice": 75, "limits": {}, "models": [], "estimatedTokenBudget": None, "notes": ""},
|
||||
{"name": "Team Max", "monthlyPrice": 200, "limits": {}, "models": [], "estimatedTokenBudget": None, "notes": ""}
|
||||
],
|
||||
"gotchas": ["no training guarantee on personal plans"],
|
||||
"tosHighlights": [],
|
||||
"dataTraining": "No guarantee on personal plans",
|
||||
"ipIndemnity": False
|
||||
})
|
||||
|
||||
# 22. MiniMax
|
||||
write_json("minimax.json", {
|
||||
"id": "minimax",
|
||||
"name": "MiniMax",
|
||||
"category": "api-provider",
|
||||
"url": "https://minimax.ai",
|
||||
"lastVerified": "2026-09-18",
|
||||
"tiers": [
|
||||
{"name": "Plus", "monthlyPrice": 22, "limits": {}, "models": [], "estimatedTokenBudget": None, "notes": ""},
|
||||
{"name": "Max", "monthlyPrice": 55, "limits": {}, "models": [], "estimatedTokenBudget": None, "notes": ""},
|
||||
{"name": "Ultra", "monthlyPrice": 132, "limits": {}, "models": [], "estimatedTokenBudget": None, "notes": ""}
|
||||
],
|
||||
"gotchas": ["Agent limits: 3-7 depending on tier", "Credits valid 365 days"],
|
||||
"tosHighlights": [],
|
||||
"dataTraining": "Yes",
|
||||
"ipIndemnity": False
|
||||
})
|
||||
|
||||
# 23. OpenAI API
|
||||
write_json("openai-api.json", {
|
||||
"id": "openai-api",
|
||||
"name": "OpenAI API",
|
||||
"category": "api-provider",
|
||||
"url": "https://platform.openai.com",
|
||||
"lastVerified": "2026-09-18",
|
||||
"tiers": [
|
||||
{"name": "PAYG", "monthlyPrice": None, "limits": {}, "models": ["GPT-6 Astra", "Sol", "Terra", "Luna"], "estimatedTokenBudget": None, "notes": "Batch 50% off"}
|
||||
],
|
||||
"gotchas": ["long-context pricing doubles above 272K tokens"],
|
||||
"tosHighlights": [],
|
||||
"dataTraining": "Opt-out available",
|
||||
"ipIndemnity": True
|
||||
})
|
||||
|
||||
# 24. Anthropic API
|
||||
write_json("anthropic-api.json", {
|
||||
"id": "anthropic-api",
|
||||
"name": "Anthropic API",
|
||||
"category": "api-provider",
|
||||
"url": "https://console.anthropic.com",
|
||||
"lastVerified": "2026-09-18",
|
||||
"tiers": [
|
||||
{"name": "PAYG", "monthlyPrice": None, "limits": {}, "models": ["Fable 5.1", "Opus 5", "Sonnet 5", "Haiku 4.5"], "estimatedTokenBudget": None, "notes": "Cache reads 90% off. Batch 50% off."}
|
||||
],
|
||||
"gotchas": ["Tiered rate limits based on spend"],
|
||||
"tosHighlights": [],
|
||||
"dataTraining": "No",
|
||||
"ipIndemnity": True
|
||||
})
|
||||
|
||||
# 25. Google AI Studio
|
||||
write_json("google-ai-studio.json", {
|
||||
"id": "google-ai-studio",
|
||||
"name": "Google AI Studio",
|
||||
"category": "api-provider",
|
||||
"url": "https://aistudio.google.com",
|
||||
"lastVerified": "2026-09-18",
|
||||
"tiers": [
|
||||
{"name": "Free", "monthlyPrice": 0, "limits": {"RPM": 15, "TPM": 1000000}, "models": ["Gemini 3.8 Flash"], "estimatedTokenBudget": None, "notes": ""},
|
||||
{"name": "Tier1", "monthlyPrice": None, "limits": {"spendCap": 250}, "models": ["Gemini 3.8 Flash"], "estimatedTokenBudget": None, "notes": "Based on spend"},
|
||||
{"name": "Tier2", "monthlyPrice": None, "limits": {"spendCap": 2000}, "models": ["Gemini 3.8 Flash"], "estimatedTokenBudget": None, "notes": "Based on spend"},
|
||||
{"name": "Tier3", "monthlyPrice": None, "limits": {"spendCap": 100000}, "models": ["Gemini 3.8 Flash"], "estimatedTokenBudget": None, "notes": "Based on spend"}
|
||||
],
|
||||
"gotchas": ["credits expire 1 year", "non-refundable"],
|
||||
"tosHighlights": [],
|
||||
"dataTraining": "Yes on free tier",
|
||||
"ipIndemnity": False
|
||||
})
|
||||
|
||||
# 26. DeepSeek API
|
||||
write_json("deepseek-api.json", {
|
||||
"id": "deepseek-api",
|
||||
"name": "DeepSeek API",
|
||||
"category": "api-provider",
|
||||
"url": "https://platform.deepseek.com",
|
||||
"lastVerified": "2026-09-18",
|
||||
"tiers": [
|
||||
{"name": "PAYG", "monthlyPrice": None, "limits": {"concurrency": {"Flash": 2500, "Pro": 500}}, "models": ["Flash", "V4-Pro"], "estimatedTokenBudget": None, "notes": "time-of-day pricing. Cache hits 98% off."}
|
||||
],
|
||||
"gotchas": ["time-of-day pricing differences"],
|
||||
"tosHighlights": [],
|
||||
"dataTraining": "No",
|
||||
"ipIndemnity": False
|
||||
})
|
||||
|
||||
# 27. Groq API
|
||||
write_json("groq-api.json", {
|
||||
"id": "groq-api",
|
||||
"name": "Groq API",
|
||||
"category": "api-provider",
|
||||
"url": "https://console.groq.com",
|
||||
"lastVerified": "2026-09-18",
|
||||
"tiers": [
|
||||
{"name": "Free", "monthlyPrice": 0, "limits": {}, "models": [], "estimatedTokenBudget": None, "notes": ""},
|
||||
{"name": "PAYG", "monthlyPrice": None, "limits": {}, "models": [], "estimatedTokenBudget": None, "notes": "50% cached/batch discount"}
|
||||
],
|
||||
"gotchas": ["must set spend caps manually"],
|
||||
"tosHighlights": [],
|
||||
"dataTraining": "No",
|
||||
"ipIndemnity": False
|
||||
})
|
||||
|
||||
# 28. Mistral API
|
||||
write_json("mistral-api.json", {
|
||||
"id": "mistral-api",
|
||||
"name": "Mistral API",
|
||||
"category": "api-provider",
|
||||
"url": "https://console.mistral.ai",
|
||||
"lastVerified": "2026-09-18",
|
||||
"tiers": [
|
||||
{"name": "PAYG", "monthlyPrice": None, "limits": {}, "models": [], "estimatedTokenBudget": None, "notes": "50% batch discount. 90% cache discount."}
|
||||
],
|
||||
"gotchas": ["exceeding context = 400 error"],
|
||||
"tosHighlights": [],
|
||||
"dataTraining": "No",
|
||||
"ipIndemnity": False
|
||||
})
|
||||
|
||||
# 29. Together.ai
|
||||
write_json("together-ai.json", {
|
||||
"id": "together-ai",
|
||||
"name": "Together.ai",
|
||||
"category": "api-provider",
|
||||
"url": "https://together.ai",
|
||||
"lastVerified": "2026-09-18",
|
||||
"tiers": [
|
||||
{"name": "Free", "monthlyPrice": 0, "limits": {"RPM": 60, "TPM": 60000}, "models": [], "estimatedTokenBudget": None, "notes": ""},
|
||||
{"name": "PAYG", "monthlyPrice": None, "limits": {}, "models": [], "estimatedTokenBudget": None, "notes": "Tiered limits at $25/$50/$100/$250 spend. Batch 50% off."}
|
||||
],
|
||||
"gotchas": ["sudden traffic spikes hit 429s"],
|
||||
"tosHighlights": [],
|
||||
"dataTraining": "No",
|
||||
"ipIndemnity": False
|
||||
})
|
||||
|
||||
# 30. Fireworks.ai
|
||||
write_json("fireworks-ai.json", {
|
||||
"id": "fireworks-ai",
|
||||
"name": "Fireworks.ai",
|
||||
"category": "api-provider",
|
||||
"url": "https://fireworks.ai",
|
||||
"lastVerified": "2026-09-18",
|
||||
"tiers": [
|
||||
{"name": "Free", "monthlyPrice": 0, "limits": {"RPM": 10}, "models": [], "estimatedTokenBudget": None, "notes": ""},
|
||||
{"name": "PAYG", "monthlyPrice": None, "limits": {"RPM": 6000}, "models": [], "estimatedTokenBudget": None, "notes": "Batch 50% off."}
|
||||
],
|
||||
"gotchas": ["503 load shedding on saturated fleet"],
|
||||
"tosHighlights": [],
|
||||
"dataTraining": "No",
|
||||
"ipIndemnity": False
|
||||
})
|
||||
|
||||
# 31. Meta Model API
|
||||
write_json("meta-model-api.json", {
|
||||
"id": "meta-model-api",
|
||||
"name": "Meta Model API",
|
||||
"category": "api-provider",
|
||||
"url": "https://meta.com",
|
||||
"lastVerified": "2026-09-18",
|
||||
"tiers": [
|
||||
{"name": "Standard", "monthlyPrice": None, "limits": {"RPM": 3000}, "models": [], "estimatedTokenBudget": None, "notes": "$1.25/$4.25/M"},
|
||||
{"name": "Contributor", "monthlyPrice": None, "limits": {"RPM": 60}, "models": [], "estimatedTokenBudget": None, "notes": "$0.10/$0.20/M"}
|
||||
],
|
||||
"gotchas": ["Contributor allows training on your data"],
|
||||
"tosHighlights": [],
|
||||
"dataTraining": "Yes for Contributor tier",
|
||||
"ipIndemnity": False
|
||||
})
|
||||
|
||||
# 32. Ollama Cloud
|
||||
write_json("ollama-cloud.json", {
|
||||
"id": "ollama-cloud",
|
||||
"name": "Ollama Cloud",
|
||||
"category": "api-provider",
|
||||
"url": "https://ollama.com",
|
||||
"lastVerified": "2026-09-18",
|
||||
"tiers": [
|
||||
{"name": "Free", "monthlyPrice": 0, "limits": {}, "models": [], "estimatedTokenBudget": None, "notes": ""},
|
||||
{"name": "Pro", "monthlyPrice": 20, "limits": {"concurrent": 3}, "models": [], "estimatedTokenBudget": None, "notes": "$60 credits/mo"},
|
||||
{"name": "Max", "monthlyPrice": 100, "limits": {"concurrent": 10}, "models": [], "estimatedTokenBudget": None, "notes": "$300 credits/mo"},
|
||||
{"name": "Team", "monthlyPrice": 500, "limits": {}, "models": [], "estimatedTokenBudget": None, "notes": ""}
|
||||
],
|
||||
"gotchas": ["credits don't roll over", "peak pricing 12-18 UTC"],
|
||||
"tosHighlights": [],
|
||||
"dataTraining": "No",
|
||||
"ipIndemnity": False
|
||||
})
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>token-max-temp</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
Generated
+2488
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"name": "token-max",
|
||||
"private": true,
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc -b && vite build",
|
||||
"fetch-models": "node scripts/fetch-models.mjs",
|
||||
"fetch-benchmarks": "node scripts/fetch-benchmarks.mjs",
|
||||
"build-data": "node scripts/build-data.mjs",
|
||||
"update-data": "node scripts/fetch-models.mjs && node scripts/fetch-benchmarks.mjs && node scripts/build-data.mjs",
|
||||
"lint": "oxlint",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"clsx": "^2.1.1",
|
||||
"lucide-react": "^1.47.0",
|
||||
"react": "^19.2.8",
|
||||
"react-dom": "^19.2.8",
|
||||
"react-router-dom": "^7.18.4",
|
||||
"recharts": "^3.10.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/vite": "^4.3.3",
|
||||
"@types/node": "^24.13.3",
|
||||
"@types/react": "^19.2.18",
|
||||
"@types/react-dom": "^19.2.7",
|
||||
"@vitejs/plugin-react": "^6.1.1",
|
||||
"oxlint": "^1.81.0",
|
||||
"tailwindcss": "^4.3.3",
|
||||
"typescript": "~6.0.2",
|
||||
"vite": "^8.3.0"
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"timestamp": "2026-09-19T00:34:24.655Z"
|
||||
}
|
||||
+12044
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 9.3 KiB |
@@ -0,0 +1,24 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg">
|
||||
<symbol id="bluesky-icon" viewBox="0 0 16 17">
|
||||
<g clip-path="url(#bluesky-clip)"><path fill="#08060d" d="M7.75 7.735c-.693-1.348-2.58-3.86-4.334-5.097-1.68-1.187-2.32-.981-2.74-.79C.188 2.065.1 2.812.1 3.251s.241 3.602.398 4.13c.52 1.744 2.367 2.333 4.07 2.145-2.495.37-4.71 1.278-1.805 4.512 3.196 3.309 4.38-.71 4.987-2.746.608 2.036 1.307 5.91 4.93 2.746 2.72-2.746.747-4.143-1.747-4.512 1.702.189 3.55-.4 4.07-2.145.156-.528.397-3.691.397-4.13s-.088-1.186-.575-1.406c-.42-.19-1.06-.395-2.741.79-1.755 1.24-3.64 3.752-4.334 5.099"/></g>
|
||||
<defs><clipPath id="bluesky-clip"><path fill="#fff" d="M.1.85h15.3v15.3H.1z"/></clipPath></defs>
|
||||
</symbol>
|
||||
<symbol id="discord-icon" viewBox="0 0 20 19">
|
||||
<path fill="#08060d" d="M16.224 3.768a14.5 14.5 0 0 0-3.67-1.153c-.158.286-.343.67-.47.976a13.5 13.5 0 0 0-4.067 0c-.128-.306-.317-.69-.476-.976A14.4 14.4 0 0 0 3.868 3.77C1.546 7.28.916 10.703 1.231 14.077a14.7 14.7 0 0 0 4.5 2.306q.545-.748.965-1.587a9.5 9.5 0 0 1-1.518-.74q.191-.14.372-.293c2.927 1.369 6.107 1.369 8.999 0q.183.152.372.294-.723.437-1.52.74.418.838.963 1.588a14.6 14.6 0 0 0 4.504-2.308c.37-3.911-.63-7.302-2.644-10.309m-9.13 8.234c-.878 0-1.599-.82-1.599-1.82 0-.998.705-1.82 1.6-1.82.894 0 1.614.82 1.599 1.82.001 1-.705 1.82-1.6 1.82m5.91 0c-.878 0-1.599-.82-1.599-1.82 0-.998.705-1.82 1.6-1.82.893 0 1.614.82 1.599 1.82 0 1-.706 1.82-1.6 1.82"/>
|
||||
</symbol>
|
||||
<symbol id="documentation-icon" viewBox="0 0 21 20">
|
||||
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="m15.5 13.333 1.533 1.322c.645.555.967.833.967 1.178s-.322.623-.967 1.179L15.5 18.333m-3.333-5-1.534 1.322c-.644.555-.966.833-.966 1.178s.322.623.966 1.179l1.534 1.321"/>
|
||||
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M17.167 10.836v-4.32c0-1.41 0-2.117-.224-2.68-.359-.906-1.118-1.621-2.08-1.96-.599-.21-1.349-.21-2.848-.21-2.623 0-3.935 0-4.983.369-1.684.591-3.013 1.842-3.641 3.428C3 6.449 3 7.684 3 10.154v2.122c0 2.558 0 3.838.706 4.726q.306.383.713.671c.76.536 1.79.64 3.581.66"/>
|
||||
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M3 10a2.78 2.78 0 0 1 2.778-2.778c.555 0 1.209.097 1.748-.047.48-.129.854-.503.982-.982.145-.54.048-1.194.048-1.749a2.78 2.78 0 0 1 2.777-2.777"/>
|
||||
</symbol>
|
||||
<symbol id="github-icon" viewBox="0 0 19 19">
|
||||
<path fill="#08060d" fill-rule="evenodd" d="M9.356 1.85C5.05 1.85 1.57 5.356 1.57 9.694a7.84 7.84 0 0 0 5.324 7.44c.387.079.528-.168.528-.376 0-.182-.013-.805-.013-1.454-2.165.467-2.616-.935-2.616-.935-.349-.91-.864-1.143-.864-1.143-.71-.48.051-.48.051-.48.787.051 1.2.805 1.2.805.695 1.194 1.817.857 2.268.649.064-.507.27-.857.49-1.052-1.728-.182-3.545-.857-3.545-3.87 0-.857.31-1.558.8-2.104-.078-.195-.349-1 .077-2.078 0 0 .657-.208 2.14.805a7.5 7.5 0 0 1 1.946-.26c.657 0 1.328.092 1.946.26 1.483-1.013 2.14-.805 2.14-.805.426 1.078.155 1.883.078 2.078.502.546.799 1.247.799 2.104 0 3.013-1.818 3.675-3.558 3.87.284.247.528.714.528 1.454 0 1.052-.012 1.896-.012 2.156 0 .208.142.455.528.377a7.84 7.84 0 0 0 5.324-7.441c.013-4.338-3.48-7.844-7.773-7.844" clip-rule="evenodd"/>
|
||||
</symbol>
|
||||
<symbol id="social-icon" viewBox="0 0 20 20">
|
||||
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M12.5 6.667a4.167 4.167 0 1 0-8.334 0 4.167 4.167 0 0 0 8.334 0"/>
|
||||
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M2.5 16.667a5.833 5.833 0 0 1 8.75-5.053m3.837.474.513 1.035c.07.144.257.282.414.309l.93.155c.596.1.736.536.307.965l-.723.73a.64.64 0 0 0-.152.531l.207.903c.164.715-.213.991-.84.618l-.872-.52a.63.63 0 0 0-.577 0l-.872.52c-.624.373-1.003.094-.84-.618l.207-.903a.64.64 0 0 0-.152-.532l-.723-.729c-.426-.43-.289-.864.306-.964l.93-.156a.64.64 0 0 0 .412-.31l.513-1.034c.28-.562.735-.562 1.012 0"/>
|
||||
</symbol>
|
||||
<symbol id="x-icon" viewBox="0 0 19 19">
|
||||
<path fill="#08060d" fill-rule="evenodd" d="M1.893 1.98c.052.072 1.245 1.769 2.653 3.77l2.892 4.114c.183.261.333.48.333.486s-.068.089-.152.183l-.522.593-.765.867-3.597 4.087c-.375.426-.734.834-.798.905a1 1 0 0 0-.118.148c0 .01.236.017.664.017h.663l.729-.83c.4-.457.796-.906.879-.999a692 692 0 0 0 1.794-2.038c.034-.037.301-.34.594-.675l.551-.624.345-.392a7 7 0 0 1 .34-.374c.006 0 .93 1.306 2.052 2.903l2.084 2.965.045.063h2.275c1.87 0 2.273-.003 2.266-.021-.008-.02-1.098-1.572-3.894-5.547-2.013-2.862-2.28-3.246-2.273-3.266.008-.019.282-.332 2.085-2.38l2-2.274 1.567-1.782c.022-.028-.016-.03-.65-.03h-.674l-.3.342a871 871 0 0 1-1.782 2.025c-.067.075-.405.458-.75.852a100 100 0 0 1-.803.91c-.148.172-.299.344-.99 1.127-.304.343-.32.358-.345.327-.015-.019-.904-1.282-1.976-2.808L6.365 1.85H1.8zm1.782.91 8.078 11.294c.772 1.08 1.413 1.973 1.425 1.984.016.017.241.02 1.05.017l1.03-.004-2.694-3.766L7.796 5.75 5.722 2.852l-1.039-.004-1.039-.004z" clip-rule="evenodd"/>
|
||||
</symbol>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.9 KiB |
Executable
+116
@@ -0,0 +1,116 @@
|
||||
import fs from 'fs/promises';
|
||||
import path from 'path';
|
||||
|
||||
const PUBLIC_DATA_DIR = path.join(process.cwd(), 'public/data');
|
||||
const PLANS_DIR = path.join(process.cwd(), 'data/coding-plans');
|
||||
|
||||
async function buildData() {
|
||||
console.log('Building consolidated data...');
|
||||
|
||||
// 1. Load models and benchmarks
|
||||
let models = [];
|
||||
try {
|
||||
const modelsData = await fs.readFile(path.join(PUBLIC_DATA_DIR, 'models.json'), 'utf-8');
|
||||
models = JSON.parse(modelsData);
|
||||
} catch (e) {
|
||||
console.log('No models.json found, skipping models processing.');
|
||||
}
|
||||
|
||||
let benchmarks = [];
|
||||
try {
|
||||
const benchData = await fs.readFile(path.join(PUBLIC_DATA_DIR, 'benchmarks.json'), 'utf-8');
|
||||
benchmarks = JSON.parse(benchData);
|
||||
} catch (e) {
|
||||
console.log('No benchmarks.json found, skipping benchmarks processing.');
|
||||
}
|
||||
|
||||
// 2. Merge benchmarks into models
|
||||
for (const model of models) {
|
||||
// Try to match AA slug to OR id or name
|
||||
const match = benchmarks.find(b =>
|
||||
b.slug === model.id.split('/').pop() ||
|
||||
b.slug === model.id.replace(/\//g, '-') ||
|
||||
b.name.toLowerCase() === model.name.toLowerCase() ||
|
||||
model.id.toLowerCase().includes(b.slug)
|
||||
);
|
||||
|
||||
if (match) {
|
||||
model.benchmarks.intelligenceIndex = model.benchmarks.intelligenceIndex ?? match.evaluations.intelligenceIndex;
|
||||
model.benchmarks.codingIndex = model.benchmarks.codingIndex ?? match.evaluations.codingIndex;
|
||||
model.benchmarks.agenticIndex = model.benchmarks.agenticIndex ?? match.evaluations.agenticIndex;
|
||||
}
|
||||
|
||||
// Compute value score
|
||||
if (model.benchmarks.codingIndex != null && model.blendedCost > 0) {
|
||||
model.benchmarks.valueScore = (model.benchmarks.codingIndex / model.blendedCost) * 100;
|
||||
}
|
||||
}
|
||||
|
||||
// Write updated models back if we made modifications
|
||||
await fs.writeFile(path.join(PUBLIC_DATA_DIR, 'models.json'), JSON.stringify(models, null, 2));
|
||||
console.log('Updated models.json with benchmark data.');
|
||||
|
||||
// 3. Compute Budgets
|
||||
const budgets = [5, 10, 20, 50, 100, 200];
|
||||
const budgetPrecomputed = {};
|
||||
|
||||
for (const model of models) {
|
||||
budgetPrecomputed[model.id] = {};
|
||||
for (const budget of budgets) {
|
||||
let millionsOfTokens = 0;
|
||||
let thousandRequests = 0;
|
||||
|
||||
if (model.blendedCost > 0) {
|
||||
millionsOfTokens = budget / model.blendedCost;
|
||||
} else if (model.blendedCost === 0 && model.isFree) {
|
||||
// Infinite tokens conceptually for free models, but let's put null or max
|
||||
millionsOfTokens = null;
|
||||
}
|
||||
|
||||
if (model.costPer1kRequests > 0) {
|
||||
thousandRequests = budget / (model.costPer1kRequests / 1000); // Wait, costPer1kRequests is for 1000 requests.
|
||||
// 1k requests = 1 unit. cost for 1 unit = costPer1kRequests
|
||||
thousandRequests = budget / model.costPer1kRequests;
|
||||
} else if (model.costPer1kRequests === 0 && model.isFree) {
|
||||
thousandRequests = null;
|
||||
}
|
||||
|
||||
budgetPrecomputed[model.id][`budget_${budget}`] = {
|
||||
millionsOfTokens,
|
||||
thousandRequests
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
await fs.writeFile(path.join(PUBLIC_DATA_DIR, 'budget-precomputed.json'), JSON.stringify(budgetPrecomputed, null, 2));
|
||||
console.log('Wrote budget-precomputed.json');
|
||||
|
||||
// 4. Load coding plans
|
||||
const plans = [];
|
||||
try {
|
||||
const files = await fs.readdir(PLANS_DIR);
|
||||
for (const file of files) {
|
||||
if (file.endsWith('.json')) {
|
||||
const p = path.join(PLANS_DIR, file);
|
||||
const data = await fs.readFile(p, 'utf-8');
|
||||
plans.push(JSON.parse(data));
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(`No coding plans found in ${PLANS_DIR} or directory doesn't exist.`);
|
||||
}
|
||||
|
||||
await fs.writeFile(path.join(PUBLIC_DATA_DIR, 'plans.json'), JSON.stringify(plans, null, 2));
|
||||
console.log(`Wrote plans.json with ${plans.length} plans.`);
|
||||
|
||||
// 5. Last updated
|
||||
await fs.writeFile(path.join(PUBLIC_DATA_DIR, 'last-updated.json'), JSON.stringify({
|
||||
timestamp: new Date().toISOString()
|
||||
}, null, 2));
|
||||
console.log('Wrote last-updated.json');
|
||||
}
|
||||
|
||||
buildData().catch(err => {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
Executable
+78
@@ -0,0 +1,78 @@
|
||||
import fs from 'fs/promises';
|
||||
import path from 'path';
|
||||
|
||||
const OUTPUT_FILE = path.join(process.cwd(), 'public/data/benchmarks.json');
|
||||
|
||||
async function fetchBenchmarks() {
|
||||
console.log('Fetching benchmarks from Artificial Analysis...');
|
||||
const apiKey = process.env.AA_API_KEY;
|
||||
if (!apiKey) {
|
||||
console.warn('Warning: AA_API_KEY environment variable not set. API may reject the request.');
|
||||
}
|
||||
|
||||
const headers = {};
|
||||
if (apiKey) {
|
||||
headers['x-api-key'] = apiKey;
|
||||
}
|
||||
|
||||
let allModels = [];
|
||||
let nextCursor = null;
|
||||
let hasMore = true;
|
||||
|
||||
while (hasMore) {
|
||||
let url = 'https://artificialanalysis.ai/api/v2/language/models/free';
|
||||
if (nextCursor) {
|
||||
url += `?cursor=${nextCursor}`;
|
||||
}
|
||||
|
||||
console.log(`Fetching ${url}`);
|
||||
const res = await fetch(url, { headers });
|
||||
if (!res.ok) {
|
||||
console.error(`Failed to fetch AA API: ${res.status} ${res.statusText}`);
|
||||
// If it fails (e.g. 401 without key), write empty array and exit gracefully
|
||||
break;
|
||||
}
|
||||
|
||||
const data = await res.json();
|
||||
const items = data.data || [];
|
||||
|
||||
for (const item of items) {
|
||||
allModels.push({
|
||||
slug: item.slug,
|
||||
name: item.name,
|
||||
releaseDate: item.release_date,
|
||||
evaluations: {
|
||||
intelligenceIndex: item.evaluations?.artificial_analysis_intelligence_index ?? null,
|
||||
codingIndex: item.evaluations?.coding_index ?? null,
|
||||
agenticIndex: item.evaluations?.agentic_index ?? null,
|
||||
},
|
||||
pricing: {
|
||||
input: item.pricing?.price_1m_input_tokens ?? null,
|
||||
output: item.pricing?.price_1m_output_tokens ?? null,
|
||||
cacheHit: item.pricing?.price_1m_cache_hit_tokens ?? null,
|
||||
},
|
||||
performance: {
|
||||
tokensPerSecond: item.performance?.median_output_tokens_per_second ?? null,
|
||||
ttft: item.performance?.median_time_to_first_token_seconds ?? null,
|
||||
e2e: item.performance?.median_end_to_end_response_time_seconds ?? null,
|
||||
},
|
||||
costs: {
|
||||
totalCost: item.artificial_analysis_intelligence_index_cost?.total_cost ?? null,
|
||||
costPerTask: item.artificial_analysis_intelligence_index_cost?.cost_per_task?.total_cost ?? null,
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
hasMore = data.has_more || false;
|
||||
nextCursor = data.next_cursor || null;
|
||||
}
|
||||
|
||||
await fs.mkdir(path.dirname(OUTPUT_FILE), { recursive: true });
|
||||
await fs.writeFile(OUTPUT_FILE, JSON.stringify(allModels, null, 2));
|
||||
console.log(`Wrote ${allModels.length} benchmarks to ${OUTPUT_FILE}`);
|
||||
}
|
||||
|
||||
fetchBenchmarks().catch(err => {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
Executable
+110
@@ -0,0 +1,110 @@
|
||||
import fs from 'fs/promises';
|
||||
import path from 'path';
|
||||
|
||||
const OUTPUT_FILE = path.join(process.cwd(), 'public/data/models.json');
|
||||
|
||||
async function fetchModels() {
|
||||
console.log('Fetching models from OpenRouter...');
|
||||
const headers = {};
|
||||
if (process.env.OPENROUTER_API_KEY) {
|
||||
headers['Authorization'] = `Bearer ${process.env.OPENROUTER_API_KEY}`;
|
||||
}
|
||||
|
||||
const response = await fetch('https://openrouter.ai/api/v1/models', { headers });
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch models: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
const models = data.data || [];
|
||||
|
||||
console.log(`Fetched ${models.length} raw models.`);
|
||||
|
||||
const normalized = [];
|
||||
|
||||
for (const model of models) {
|
||||
// Filter non-text output models if needed, though most are text.
|
||||
// OpenRouter doesn't have a strict output modality field but architecture usually has it.
|
||||
// We'll keep it simple: if architecture implies it's text.
|
||||
if (model.architecture && model.architecture.modality && !model.architecture.modality.includes('text')) {
|
||||
if (model.architecture.modality.includes('image')) { // keep text->text or image+text->text
|
||||
// actually modality often describes input->output, let's just proceed.
|
||||
}
|
||||
}
|
||||
|
||||
const id = model.id;
|
||||
const isFree = id.endsWith(':free');
|
||||
const isBatch = id.endsWith(':nitro') || id.includes('batch'); // Approximate for batch
|
||||
|
||||
// Prices are strings per 1 token, convert to number per 1M
|
||||
const pricing = model.pricing || {};
|
||||
const inputPrice = parseFloat(pricing.prompt || '0') * 1_000_000;
|
||||
const outputPrice = parseFloat(pricing.completion || '0') * 1_000_000;
|
||||
const imagePrice = parseFloat(pricing.image || '0') * 1_000_000;
|
||||
const requestPrice = parseFloat(pricing.request || '0');
|
||||
|
||||
// blendedCost: (input*3 + output*1) / 4
|
||||
const blendedCost = (inputPrice * 3 + outputPrice * 1) / 4;
|
||||
|
||||
// costPer1kRequests: assuming 2K input + 1K output
|
||||
// cost for 1 request = (2000 * prompt_price) + (1000 * completion_price)
|
||||
// 1k requests = 1000 * cost_for_1_request
|
||||
const costFor1Req = (2000 * (parseFloat(pricing.prompt || '0'))) + (1000 * (parseFloat(pricing.completion || '0')));
|
||||
const costPer1kRequests = costFor1Req * 1000;
|
||||
|
||||
const provider = id.split('/')[0] || 'unknown';
|
||||
|
||||
// Benchmarks from AA
|
||||
const aa = (model.top_provider && model.top_provider.max_completion_tokens) ? null : null; // OpenRouter might not always embed AA directly as described, assuming it's in model.benchmarks?.artificial_analysis or similar.
|
||||
let benchmarks = {
|
||||
intelligenceIndex: null,
|
||||
codingIndex: null,
|
||||
agenticIndex: null,
|
||||
valueScore: null
|
||||
};
|
||||
|
||||
if (model.benchmarks && model.benchmarks.artificial_analysis) {
|
||||
benchmarks.intelligenceIndex = model.benchmarks.artificial_analysis.intelligence_index || null;
|
||||
benchmarks.codingIndex = model.benchmarks.artificial_analysis.coding_index || null;
|
||||
benchmarks.agenticIndex = model.benchmarks.artificial_analysis.agentic_index || null;
|
||||
if (benchmarks.codingIndex && blendedCost > 0) {
|
||||
benchmarks.valueScore = (benchmarks.codingIndex / blendedCost) * 100;
|
||||
}
|
||||
}
|
||||
|
||||
normalized.push({
|
||||
id: model.id,
|
||||
name: model.name,
|
||||
provider: provider,
|
||||
modality: model.architecture?.modality || 'text->text',
|
||||
contextWindow: model.context_length || 0,
|
||||
maxOutput: model.top_provider?.max_completion_tokens || 0,
|
||||
pricing: {
|
||||
input: inputPrice,
|
||||
output: outputPrice,
|
||||
cachedInput: null,
|
||||
cachedInputWrite: null,
|
||||
reasoning: null,
|
||||
webSearch: null,
|
||||
},
|
||||
blendedCost,
|
||||
costPer1kRequests,
|
||||
benchmarks,
|
||||
reasoning: null,
|
||||
isFree,
|
||||
isBatch
|
||||
});
|
||||
}
|
||||
|
||||
// Filter out non-text if we can identify them (modality usually starts with text or multi)
|
||||
const textModels = normalized.filter(m => !m.modality.startsWith('image->'));
|
||||
|
||||
await fs.mkdir(path.dirname(OUTPUT_FILE), { recursive: true });
|
||||
await fs.writeFile(OUTPUT_FILE, JSON.stringify(textModels, null, 2));
|
||||
console.log(`Wrote ${textModels.length} models to ${OUTPUT_FILE}`);
|
||||
}
|
||||
|
||||
fetchModels().catch(err => {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
import { BrowserRouter, Routes, Route } from 'react-router-dom';
|
||||
import { Header } from './components/layout/Header';
|
||||
import { Footer } from './components/layout/Footer';
|
||||
import Dashboard from './pages/Dashboard';
|
||||
import ModelsExplorer from './pages/ModelsExplorer';
|
||||
import PlansCompare from './pages/PlansCompare';
|
||||
import BenchmarksPage from './pages/BenchmarksPage';
|
||||
import TosAudit from './pages/TosAudit';
|
||||
|
||||
export default function App() {
|
||||
return (
|
||||
<BrowserRouter basename="/Token-Max">
|
||||
<div className="min-h-screen flex flex-col bg-surface">
|
||||
<Header />
|
||||
<main className="flex-1 max-w-7xl mx-auto w-full px-4 py-6">
|
||||
<Routes>
|
||||
<Route path="/" element={<Dashboard />} />
|
||||
<Route path="/models" element={<ModelsExplorer />} />
|
||||
<Route path="/plans" element={<PlansCompare />} />
|
||||
<Route path="/benchmarks" element={<BenchmarksPage />} />
|
||||
<Route path="/tos" element={<TosAudit />} />
|
||||
</Routes>
|
||||
</main>
|
||||
<Footer />
|
||||
</div>
|
||||
</BrowserRouter>
|
||||
);
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 13 KiB |
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 8.5 KiB |
@@ -0,0 +1,126 @@
|
||||
import { useState, useMemo } from 'react';
|
||||
import type { NormalizedModel } from '../../lib/types';
|
||||
import { computeWeightedScore, computeValueScore } from '../../lib/pricing';
|
||||
import { ArrowUpDown, ArrowDown, ArrowUp } from 'lucide-react';
|
||||
|
||||
interface LeaderboardTableProps {
|
||||
models: NormalizedModel[];
|
||||
}
|
||||
|
||||
type SortConfig = {
|
||||
key: string;
|
||||
direction: 'asc' | 'desc';
|
||||
} | null;
|
||||
|
||||
interface LeaderboardHeaderProps {
|
||||
label: string;
|
||||
sortKey: string;
|
||||
sortConfig: SortConfig;
|
||||
onSort: (key: string) => void;
|
||||
align?: 'left' | 'right';
|
||||
}
|
||||
|
||||
function LeaderboardHeader({ label, sortKey, sortConfig, onSort, align = 'right' }: LeaderboardHeaderProps) {
|
||||
const getSortIcon = () => {
|
||||
if (!sortConfig || sortConfig.key !== sortKey) return <ArrowUpDown className="w-3 h-3 text-text-muted" />;
|
||||
return sortConfig.direction === 'asc' ? <ArrowUp className="w-3 h-3" /> : <ArrowDown className="w-3 h-3" />;
|
||||
};
|
||||
|
||||
return (
|
||||
<th
|
||||
className={`px-4 py-3 font-medium cursor-pointer hover:bg-surface-alt/80 select-none group text-${align}`}
|
||||
onClick={() => onSort(sortKey)}
|
||||
>
|
||||
<div className={`flex items-center gap-1.5 ${align === 'right' ? 'justify-end' : ''}`}>
|
||||
<span className="group-hover:text-primary transition-colors">{label}</span>
|
||||
{getSortIcon()}
|
||||
</div>
|
||||
</th>
|
||||
);
|
||||
}
|
||||
|
||||
export function LeaderboardTable({ models }: LeaderboardTableProps) {
|
||||
const [sortConfig, setSortConfig] = useState<SortConfig>({ key: 'weighted', direction: 'desc' });
|
||||
|
||||
const sortedModels = useMemo(() => {
|
||||
let sortableModels = models.filter(m => m.benchmarks.codingIndex !== null || m.benchmarks.intelligenceIndex !== null);
|
||||
|
||||
sortableModels.sort((a, b) => {
|
||||
let aValue: number = 0;
|
||||
let bValue: number = 0;
|
||||
|
||||
if (sortConfig?.key === 'intelligence') {
|
||||
aValue = a.benchmarks.intelligenceIndex ?? -1;
|
||||
bValue = b.benchmarks.intelligenceIndex ?? -1;
|
||||
} else if (sortConfig?.key === 'coding') {
|
||||
aValue = a.benchmarks.codingIndex ?? -1;
|
||||
bValue = b.benchmarks.codingIndex ?? -1;
|
||||
} else if (sortConfig?.key === 'agentic') {
|
||||
aValue = a.benchmarks.agenticIndex ?? -1;
|
||||
bValue = b.benchmarks.agenticIndex ?? -1;
|
||||
} else if (sortConfig?.key === 'weighted') {
|
||||
aValue = computeWeightedScore(a);
|
||||
bValue = computeWeightedScore(b);
|
||||
} else if (sortConfig?.key === 'cost') {
|
||||
aValue = a.blendedCost;
|
||||
bValue = b.blendedCost;
|
||||
} else if (sortConfig?.key === 'value') {
|
||||
aValue = computeValueScore(a);
|
||||
bValue = computeValueScore(b);
|
||||
}
|
||||
|
||||
if (aValue < bValue) return sortConfig?.direction === 'asc' ? -1 : 1;
|
||||
if (aValue > bValue) return sortConfig?.direction === 'asc' ? 1 : -1;
|
||||
return 0;
|
||||
});
|
||||
|
||||
return sortableModels;
|
||||
}, [models, sortConfig]);
|
||||
|
||||
const requestSort = (key: string) => {
|
||||
let direction: 'asc' | 'desc' = 'desc';
|
||||
if (key === 'cost') direction = 'asc'; // lowest cost is "best" initially
|
||||
|
||||
if (sortConfig && sortConfig.key === key && sortConfig.direction === 'asc') {
|
||||
direction = 'desc';
|
||||
} else if (sortConfig && sortConfig.key === key && sortConfig.direction === 'desc') {
|
||||
direction = 'asc';
|
||||
}
|
||||
setSortConfig({ key, direction });
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-surface rounded-xl border border-border shadow-sm overflow-hidden">
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-left text-sm">
|
||||
<thead className="bg-surface-alt border-b border-border text-text-muted">
|
||||
<tr>
|
||||
<th className="px-4 py-3 font-medium w-12">Rank</th>
|
||||
<th className="px-4 py-3 font-medium">Model</th>
|
||||
<LeaderboardHeader label="Intelligence" sortKey="intelligence" sortConfig={sortConfig} onSort={requestSort} />
|
||||
<LeaderboardHeader label="Coding" sortKey="coding" sortConfig={sortConfig} onSort={requestSort} />
|
||||
<LeaderboardHeader label="Agentic" sortKey="agentic" sortConfig={sortConfig} onSort={requestSort} />
|
||||
<LeaderboardHeader label="Weighted Score" sortKey="weighted" sortConfig={sortConfig} onSort={requestSort} />
|
||||
<LeaderboardHeader label="Blended Cost" sortKey="cost" sortConfig={sortConfig} onSort={requestSort} />
|
||||
<LeaderboardHeader label="Value Score" sortKey="value" sortConfig={sortConfig} onSort={requestSort} />
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-border">
|
||||
{sortedModels.map((m, i) => (
|
||||
<tr key={m.id} className="hover:bg-surface-alt/50 transition-colors">
|
||||
<td className="px-4 py-3 text-text-muted font-medium">{i + 1}</td>
|
||||
<td className="px-4 py-3 font-medium text-text">{m.name}</td>
|
||||
<td className="px-4 py-3 text-right">{m.benchmarks.intelligenceIndex?.toFixed(1) || '-'}</td>
|
||||
<td className="px-4 py-3 text-right">{m.benchmarks.codingIndex?.toFixed(1) || '-'}</td>
|
||||
<td className="px-4 py-3 text-right">{m.benchmarks.agenticIndex?.toFixed(1) || '-'}</td>
|
||||
<td className="px-4 py-3 text-right font-bold text-primary">{computeWeightedScore(m).toFixed(1)}</td>
|
||||
<td className="px-4 py-3 text-right">{m.blendedCost === 0 ? 'Free' : `$${m.blendedCost.toFixed(2)}`}</td>
|
||||
<td className="px-4 py-3 text-right font-medium">{computeValueScore(m) === Infinity ? '∞' : computeValueScore(m).toFixed(1)}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
import { ScatterChart, Scatter, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, ZAxis } from 'recharts';
|
||||
import type { NormalizedModel } from '../../lib/types';
|
||||
import { getProviderColor, computeWeightedScore } from '../../lib/pricing';
|
||||
|
||||
interface ValueScatterProps {
|
||||
models: NormalizedModel[];
|
||||
}
|
||||
|
||||
function CustomTooltip({ active, payload }: any) {
|
||||
if (active && payload && payload.length) {
|
||||
const data = payload[0].payload;
|
||||
return (
|
||||
<div className="bg-surface p-3 border border-border rounded-lg shadow-lg">
|
||||
<p className="font-bold text-text mb-1">{data.name}</p>
|
||||
<p className="text-sm text-text-muted capitalize mb-2">{data.provider}</p>
|
||||
<div className="grid grid-cols-2 gap-x-4 gap-y-1 text-sm">
|
||||
<span className="text-text-muted">Cost:</span>
|
||||
<span className="font-medium text-right">${data.cost.toFixed(2)}/M</span>
|
||||
<span className="text-text-muted">Score:</span>
|
||||
<span className="font-medium text-right">{data.score.toFixed(1)}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function ValueScatter({ models }: ValueScatterProps) {
|
||||
const data = models
|
||||
.filter(m => m.benchmarks.codingIndex !== null && m.blendedCost > 0 && !m.isBatch)
|
||||
.map(m => ({
|
||||
id: m.id,
|
||||
name: m.name,
|
||||
provider: m.provider,
|
||||
cost: m.blendedCost,
|
||||
score: computeWeightedScore(m),
|
||||
fill: getProviderColor(m.provider),
|
||||
}))
|
||||
.filter(d => d.score > 0);
|
||||
|
||||
return (
|
||||
<div className="bg-surface rounded-xl border border-border p-6 shadow-sm mb-8 h-[500px]">
|
||||
<h3 className="text-lg font-bold mb-1">Quality vs. Cost</h3>
|
||||
<p className="text-sm text-text-muted mb-6">Log scale. Higher and further left is better value.</p>
|
||||
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<ScatterChart margin={{ top: 20, right: 20, bottom: 20, left: 20 }}>
|
||||
<CartesianGrid strokeDasharray="3 3" stroke="var(--color-border)" />
|
||||
<XAxis
|
||||
type="number"
|
||||
dataKey="cost"
|
||||
name="Cost"
|
||||
scale="log"
|
||||
domain={['auto', 'auto']}
|
||||
tickFormatter={(val) => `$${val}`}
|
||||
label={{ value: 'Blended Cost ($/1M Tokens)', position: 'insideBottom', offset: -10, fill: 'var(--color-text-muted)' }}
|
||||
stroke="var(--color-text-muted)"
|
||||
/>
|
||||
<YAxis
|
||||
type="number"
|
||||
dataKey="score"
|
||||
name="Score"
|
||||
domain={['auto', 100]}
|
||||
label={{ value: 'Weighted Benchmark Score', angle: -90, position: 'insideLeft', offset: 0, fill: 'var(--color-text-muted)' }}
|
||||
stroke="var(--color-text-muted)"
|
||||
/>
|
||||
<ZAxis type="category" dataKey="name" name="Model" />
|
||||
<Tooltip cursor={{ strokeDasharray: '3 3' }} content={<CustomTooltip />} />
|
||||
<Scatter name="Models" data={data} fill="#8884d8" />
|
||||
</ScatterChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
interface BudgetInputProps {
|
||||
budget: number;
|
||||
onChange: (val: number) => void;
|
||||
}
|
||||
|
||||
export function BudgetInput({ budget, onChange }: BudgetInputProps) {
|
||||
return (
|
||||
<div className="bg-surface p-6 rounded-xl border border-border shadow-sm mb-8">
|
||||
<h2 className="text-xl font-bold mb-4 flex items-center justify-between">
|
||||
<span>Monthly Budget Calculator</span>
|
||||
<span className="text-3xl font-extrabold text-primary">${budget}</span>
|
||||
</h2>
|
||||
|
||||
<div className="space-y-6">
|
||||
<input
|
||||
type="range"
|
||||
min="1"
|
||||
max="500"
|
||||
value={budget}
|
||||
onChange={(e) => onChange(Number(e.target.value))}
|
||||
className="w-full h-2 bg-surface-alt rounded-lg appearance-none cursor-pointer accent-primary"
|
||||
/>
|
||||
|
||||
<div className="flex items-center gap-4">
|
||||
<label className="text-sm font-medium text-text-muted whitespace-nowrap">
|
||||
Manual Entry: $
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
min="1"
|
||||
value={budget}
|
||||
onChange={(e) => onChange(Number(e.target.value) || 1)}
|
||||
className="w-24 px-3 py-1.5 bg-surface border border-border rounded-md focus:outline-none focus:ring-2 focus:ring-primary text-text"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
import type { BudgetResult, CodingPlan } from '../../lib/types';
|
||||
import { formatMillionTokens } from '../../lib/pricing';
|
||||
import { getProviderColor } from '../../lib/pricing';
|
||||
|
||||
interface BudgetResultsProps {
|
||||
results: BudgetResult[];
|
||||
plans: CodingPlan[];
|
||||
budget: number;
|
||||
}
|
||||
|
||||
export function BudgetResults({ results, plans, budget }: BudgetResultsProps) {
|
||||
const topModels = results.slice(0, 20);
|
||||
|
||||
// Find plans that fit the budget or are slightly above (up to 20% more)
|
||||
const relevantPlans = plans.flatMap(plan =>
|
||||
plan.tiers
|
||||
.filter(tier => tier.monthlyPrice !== null && tier.monthlyPrice <= budget * 1.2 && tier.monthlyPrice >= budget * 0.5)
|
||||
.map(tier => ({ plan, tier }))
|
||||
).sort((a, b) => (a.tier.monthlyPrice || 0) - (b.tier.monthlyPrice || 0));
|
||||
|
||||
const getMedal = (index: number) => {
|
||||
if (index === 0) return '🥇';
|
||||
if (index === 1) return '🥈';
|
||||
if (index === 2) return '🥉';
|
||||
return <span className="text-text-muted text-sm w-5 inline-block text-center">{index + 1}</span>;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
||||
<div>
|
||||
<h3 className="text-lg font-bold mb-4 flex items-center gap-2">
|
||||
<span>API Models</span>
|
||||
<span className="text-xs font-normal text-text-muted bg-surface-alt px-2 py-0.5 rounded-full">Top 20</span>
|
||||
</h3>
|
||||
|
||||
<div className="bg-surface rounded-xl border border-border overflow-hidden">
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-left text-sm">
|
||||
<thead className="bg-surface-alt border-b border-border text-text-muted">
|
||||
<tr>
|
||||
<th className="px-4 py-3 font-medium w-12">#</th>
|
||||
<th className="px-4 py-3 font-medium">Model</th>
|
||||
<th className="px-4 py-3 font-medium text-right">Tokens</th>
|
||||
<th className="px-4 py-3 font-medium text-right">Code Score</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-border">
|
||||
{topModels.map((result, i) => (
|
||||
<tr key={result.modelId} className="hover:bg-surface-alt/50 transition-colors">
|
||||
<td className="px-4 py-3">{getMedal(i)}</td>
|
||||
<td className="px-4 py-3">
|
||||
<div className="font-medium text-text">{result.modelName}</div>
|
||||
<div
|
||||
className="text-xs mt-0.5 font-medium"
|
||||
style={{ color: getProviderColor(result.provider) }}
|
||||
>
|
||||
{result.provider}
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-4 py-3 text-right font-medium text-primary">
|
||||
{formatMillionTokens(result.millionTokens)}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-right text-text-muted">
|
||||
{result.codingIndex ? result.codingIndex.toFixed(0) : '-'}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className="text-lg font-bold mb-4">Relevant Coding Plans</h3>
|
||||
|
||||
{relevantPlans.length > 0 ? (
|
||||
<div className="grid gap-4">
|
||||
{relevantPlans.map(({ plan, tier }, i) => (
|
||||
<div key={`${plan.id}-${tier.name}-${i}`} className="bg-surface rounded-xl border border-border p-4 hover:shadow-md transition-shadow">
|
||||
<div className="flex justify-between items-start mb-2">
|
||||
<div>
|
||||
<h4 className="font-bold text-text">{plan.name}</h4>
|
||||
<span className="text-sm text-text-muted">{tier.name}</span>
|
||||
</div>
|
||||
<div className="text-lg font-bold text-primary">
|
||||
${tier.monthlyPrice}/mo
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{tier.estimatedTokenBudget && (
|
||||
<div className="mt-4 pt-4 border-t border-border">
|
||||
<div className="text-xs font-semibold text-text-muted uppercase tracking-wider mb-1">
|
||||
Estimated Monthly Budget
|
||||
</div>
|
||||
<div className="text-sm font-medium text-text">
|
||||
~{formatMillionTokens(tier.estimatedTokenBudget.estimatedMillionTokens)} tokens
|
||||
</div>
|
||||
{tier.estimatedTokenBudget.description && (
|
||||
<div className="text-xs text-text-muted mt-1 line-clamp-2">
|
||||
{tier.estimatedTokenBudget.description}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="bg-surface-alt border border-border border-dashed rounded-xl p-8 text-center text-text-muted">
|
||||
No coding plans found strictly in this budget range.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { DataFreshness } from '../shared/DataFreshness';
|
||||
|
||||
export function Footer() {
|
||||
return (
|
||||
<footer className="border-t border-border bg-surface mt-auto">
|
||||
<div className="max-w-7xl mx-auto px-4 py-6 flex flex-col md:flex-row items-center justify-between gap-4">
|
||||
<div className="text-sm text-text-muted">
|
||||
Data from OpenRouter & Artificial Analysis
|
||||
</div>
|
||||
<DataFreshness />
|
||||
<a
|
||||
href="https://github.com/token-max"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-text-muted hover:text-primary transition-colors flex items-center gap-2 text-sm"
|
||||
>
|
||||
<svg className="w-4 h-4 fill-current" viewBox="0 0 24 24">
|
||||
<path fillRule="evenodd" clipRule="evenodd" d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.53 1.032 1.53 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z"/>
|
||||
</svg>
|
||||
GitHub
|
||||
</a>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
import { NavLink } from 'react-router-dom';
|
||||
import { Coins, LayoutDashboard, Database, CreditCard, BarChart2, ShieldAlert, Moon, Sun } from 'lucide-react';
|
||||
import { useState, useEffect } from 'react';
|
||||
|
||||
export function Header() {
|
||||
const [darkMode, setDarkMode] = useState(
|
||||
() => window.matchMedia('(prefers-color-scheme: dark)').matches
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (darkMode) {
|
||||
document.documentElement.classList.add('dark');
|
||||
} else {
|
||||
document.documentElement.classList.remove('dark');
|
||||
}
|
||||
}, [darkMode]);
|
||||
|
||||
const navClass = ({ isActive }: { isActive: boolean }) =>
|
||||
`flex items-center gap-2 px-3 py-2 rounded-md text-sm font-medium transition-colors ${
|
||||
isActive ? 'bg-primary/10 text-primary' : 'text-text hover:bg-surface-alt hover:text-primary'
|
||||
}`;
|
||||
|
||||
return (
|
||||
<header className="border-b border-border bg-surface">
|
||||
<div className="max-w-7xl mx-auto px-4 h-16 flex items-center justify-between">
|
||||
<div className="flex items-center gap-2 text-primary font-bold text-xl">
|
||||
<Coins className="w-6 h-6" />
|
||||
<span>Token-Max</span>
|
||||
</div>
|
||||
|
||||
<nav className="hidden md:flex items-center gap-1">
|
||||
<NavLink to="/" className={navClass}>
|
||||
<LayoutDashboard className="w-4 h-4" />
|
||||
Dashboard
|
||||
</NavLink>
|
||||
<NavLink to="/models" className={navClass}>
|
||||
<Database className="w-4 h-4" />
|
||||
Models
|
||||
</NavLink>
|
||||
<NavLink to="/plans" className={navClass}>
|
||||
<CreditCard className="w-4 h-4" />
|
||||
Plans
|
||||
</NavLink>
|
||||
<NavLink to="/benchmarks" className={navClass}>
|
||||
<BarChart2 className="w-4 h-4" />
|
||||
Benchmarks
|
||||
</NavLink>
|
||||
<NavLink to="/tos" className={navClass}>
|
||||
<ShieldAlert className="w-4 h-4" />
|
||||
TOS Audit
|
||||
</NavLink>
|
||||
</nav>
|
||||
|
||||
<button
|
||||
onClick={() => setDarkMode(!darkMode)}
|
||||
className="p-2 rounded-md text-text-muted hover:bg-surface-alt transition-colors"
|
||||
aria-label="Toggle dark mode"
|
||||
>
|
||||
{darkMode ? <Sun className="w-5 h-5" /> : <Moon className="w-5 h-5" />}
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
import { useState, useMemo } from 'react';
|
||||
import type { NormalizedModel } from '../../lib/types';
|
||||
import { PricingBadge } from './PricingBadge';
|
||||
import { getProviderColor, computeValueScore } from '../../lib/pricing';
|
||||
import { ArrowUpDown, ArrowDown, ArrowUp } from 'lucide-react';
|
||||
|
||||
interface ModelTableProps {
|
||||
models: NormalizedModel[];
|
||||
}
|
||||
|
||||
type SortKey = keyof NormalizedModel | 'codingIndex' | 'valueScore' | 'input' | 'output';
|
||||
|
||||
type SortConfig = {
|
||||
key: SortKey;
|
||||
direction: 'asc' | 'desc';
|
||||
} | null;
|
||||
|
||||
interface TableHeaderProps {
|
||||
label: string;
|
||||
sortKey: SortKey;
|
||||
sortConfig: SortConfig;
|
||||
onSort: (key: SortKey) => void;
|
||||
align?: 'left' | 'right';
|
||||
}
|
||||
|
||||
function TableHeader({ label, sortKey, sortConfig, onSort, align = 'left' }: TableHeaderProps) {
|
||||
const getSortIcon = () => {
|
||||
if (!sortConfig || sortConfig.key !== sortKey) return <ArrowUpDown className="w-3 h-3 text-text-muted" />;
|
||||
return sortConfig.direction === 'asc' ? <ArrowUp className="w-3 h-3" /> : <ArrowDown className="w-3 h-3" />;
|
||||
};
|
||||
|
||||
return (
|
||||
<th
|
||||
className={`px-4 py-3 font-medium cursor-pointer hover:bg-surface-alt/80 select-none group text-${align}`}
|
||||
onClick={() => onSort(sortKey)}
|
||||
>
|
||||
<div className={`flex items-center gap-1.5 ${align === 'right' ? 'justify-end' : ''}`}>
|
||||
<span className="group-hover:text-primary transition-colors">{label}</span>
|
||||
{getSortIcon()}
|
||||
</div>
|
||||
</th>
|
||||
);
|
||||
}
|
||||
|
||||
export function ModelTable({ models }: ModelTableProps) {
|
||||
const [sortConfig, setSortConfig] = useState<SortConfig>(null);
|
||||
|
||||
const sortedModels = useMemo(() => {
|
||||
let sortableModels = [...models];
|
||||
if (sortConfig !== null) {
|
||||
sortableModels.sort((a, b) => {
|
||||
let aValue: any;
|
||||
let bValue: any;
|
||||
|
||||
if (sortConfig.key === 'codingIndex') {
|
||||
aValue = a.benchmarks.codingIndex ?? -1;
|
||||
bValue = b.benchmarks.codingIndex ?? -1;
|
||||
} else if (sortConfig.key === 'valueScore') {
|
||||
aValue = computeValueScore(a);
|
||||
bValue = computeValueScore(b);
|
||||
} else if (sortConfig.key === 'input') {
|
||||
aValue = a.pricing.input;
|
||||
bValue = b.pricing.input;
|
||||
} else if (sortConfig.key === 'output') {
|
||||
aValue = a.pricing.output;
|
||||
bValue = b.pricing.output;
|
||||
} else {
|
||||
aValue = a[sortConfig.key as keyof NormalizedModel];
|
||||
bValue = b[sortConfig.key as keyof NormalizedModel];
|
||||
}
|
||||
|
||||
if (aValue < bValue) return sortConfig.direction === 'asc' ? -1 : 1;
|
||||
if (aValue > bValue) return sortConfig.direction === 'asc' ? 1 : -1;
|
||||
return 0;
|
||||
});
|
||||
}
|
||||
return sortableModels;
|
||||
}, [models, sortConfig]);
|
||||
|
||||
const requestSort = (key: SortKey) => {
|
||||
let direction: 'asc' | 'desc' = 'desc'; // default to desc for most things
|
||||
if (key === 'name' || key === 'provider') direction = 'asc';
|
||||
|
||||
if (sortConfig && sortConfig.key === key && sortConfig.direction === 'asc') {
|
||||
direction = 'desc';
|
||||
} else if (sortConfig && sortConfig.key === key && sortConfig.direction === 'desc') {
|
||||
direction = 'asc';
|
||||
}
|
||||
setSortConfig({ key, direction });
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-surface rounded-xl border border-border shadow-sm overflow-hidden">
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-left text-sm">
|
||||
<thead className="bg-surface-alt border-b border-border text-text-muted">
|
||||
<tr>
|
||||
<TableHeader label="Name" sortKey="name" sortConfig={sortConfig} onSort={requestSort} />
|
||||
<TableHeader label="Provider" sortKey="provider" sortConfig={sortConfig} onSort={requestSort} />
|
||||
<TableHeader label="Input $/M" sortKey="input" sortConfig={sortConfig} onSort={requestSort} align="right" />
|
||||
<TableHeader label="Output $/M" sortKey="output" sortConfig={sortConfig} onSort={requestSort} align="right" />
|
||||
<TableHeader label="Blended $/M" sortKey="blendedCost" sortConfig={sortConfig} onSort={requestSort} align="right" />
|
||||
<TableHeader label="Context" sortKey="contextWindow" sortConfig={sortConfig} onSort={requestSort} align="right" />
|
||||
<TableHeader label="Coding Index" sortKey="codingIndex" sortConfig={sortConfig} onSort={requestSort} align="right" />
|
||||
<TableHeader label="Value Score" sortKey="valueScore" sortConfig={sortConfig} onSort={requestSort} align="right" />
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-border">
|
||||
{sortedModels.map((m) => (
|
||||
<tr key={m.id} className="hover:bg-surface-alt/50 transition-colors">
|
||||
<td className="px-4 py-3 font-medium text-text">{m.name}</td>
|
||||
<td className="px-4 py-3">
|
||||
<span
|
||||
className="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium"
|
||||
style={{
|
||||
backgroundColor: `${getProviderColor(m.provider)}20`,
|
||||
color: getProviderColor(m.provider)
|
||||
}}
|
||||
>
|
||||
{m.provider}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-4 py-3 text-right">
|
||||
<PricingBadge price={m.pricing.input} />
|
||||
</td>
|
||||
<td className="px-4 py-3 text-right">
|
||||
<PricingBadge price={m.pricing.output} />
|
||||
</td>
|
||||
<td className="px-4 py-3 text-right font-medium">
|
||||
{m.blendedCost === 0 ? (
|
||||
<span className="text-success">Free</span>
|
||||
) : (
|
||||
`$${m.blendedCost.toFixed(2)}`
|
||||
)}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-right text-text-muted">
|
||||
{(m.contextWindow / 1000).toFixed(0)}K
|
||||
</td>
|
||||
<td className="px-4 py-3 text-right font-medium">
|
||||
{m.benchmarks.codingIndex ? m.benchmarks.codingIndex.toFixed(0) : '-'}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-right text-primary font-bold">
|
||||
{computeValueScore(m) === Infinity ? '∞' : computeValueScore(m).toFixed(1)}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import { clsx } from 'clsx';
|
||||
import { formatPrice } from '../../lib/pricing';
|
||||
|
||||
interface PricingBadgeProps {
|
||||
price: number;
|
||||
}
|
||||
|
||||
export function PricingBadge({ price }: PricingBadgeProps) {
|
||||
if (price === 0) {
|
||||
return (
|
||||
<span className="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-success/10 text-success">
|
||||
Free
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
const isCheap = price < 1;
|
||||
const isMid = price >= 1 && price < 10;
|
||||
|
||||
return (
|
||||
<span className={clsx(
|
||||
"inline-flex items-center px-2 py-0.5 rounded text-xs font-medium font-mono",
|
||||
isCheap && "bg-success/10 text-success",
|
||||
isMid && "bg-warning/10 text-warning",
|
||||
!isCheap && !isMid && "bg-danger/10 text-danger"
|
||||
)}>
|
||||
{formatPrice(price)}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
import type { CodingPlan } from '../../lib/types';
|
||||
import { formatMillionTokens } from '../../lib/pricing';
|
||||
import { AlertTriangle, Check, X } from 'lucide-react';
|
||||
|
||||
interface PlanDetailProps {
|
||||
plan: CodingPlan;
|
||||
}
|
||||
|
||||
export function PlanDetail({ plan }: PlanDetailProps) {
|
||||
return (
|
||||
<div className="bg-surface rounded-xl border border-border shadow-sm p-6 mt-8 animate-in fade-in slide-in-from-bottom-4 duration-300">
|
||||
<div className="flex justify-between items-start mb-6">
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold text-text mb-2">{plan.name} Detail</h2>
|
||||
<a href={plan.url} target="_blank" rel="noopener noreferrer" className="text-primary hover:underline text-sm font-medium">
|
||||
Visit Website ↗
|
||||
</a>
|
||||
</div>
|
||||
<div className="text-xs text-text-muted bg-surface-alt px-3 py-1 rounded-full border border-border">
|
||||
Verified: {plan.lastVerified}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid md:grid-cols-2 gap-8 mb-8">
|
||||
<div>
|
||||
<h3 className="font-semibold mb-3 flex items-center gap-2">
|
||||
<AlertTriangle className="w-4 h-4 text-warning" />
|
||||
TOS Gotchas
|
||||
</h3>
|
||||
<ul className="space-y-2 text-sm text-text-muted">
|
||||
{plan.gotchas.map((g, i) => (
|
||||
<li key={i} className="flex gap-2">
|
||||
<span className="text-warning mt-0.5">•</span>
|
||||
<span>{g}</span>
|
||||
</li>
|
||||
))}
|
||||
{plan.gotchas.length === 0 && (
|
||||
<li className="text-text-muted italic">No major gotchas noted.</li>
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
<div className="bg-surface-alt p-4 rounded-lg border border-border">
|
||||
<div className="text-sm font-semibold mb-1">Data Training</div>
|
||||
<div className="text-sm flex items-center gap-2">
|
||||
{plan.dataTraining.includes('No') ? <Check className="w-4 h-4 text-success" /> : <X className="w-4 h-4 text-danger" />}
|
||||
{plan.dataTraining}
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-surface-alt p-4 rounded-lg border border-border">
|
||||
<div className="text-sm font-semibold mb-1">IP Indemnity</div>
|
||||
<div className="text-sm flex items-center gap-2">
|
||||
{plan.ipIndemnity ? <Check className="w-4 h-4 text-success" /> : <X className="w-4 h-4 text-danger" />}
|
||||
{typeof plan.ipIndemnity === 'string' ? plan.ipIndemnity : (plan.ipIndemnity ? 'Provided' : 'Not Provided')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3 className="text-lg font-bold mb-4">Tiers Comparison</h3>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-left text-sm border-collapse">
|
||||
<thead>
|
||||
<tr className="border-b border-border">
|
||||
<th className="py-3 px-4 font-semibold text-text-muted bg-surface-alt rounded-tl-lg">Tier</th>
|
||||
<th className="py-3 px-4 font-semibold text-text-muted bg-surface-alt">Price</th>
|
||||
<th className="py-3 px-4 font-semibold text-text-muted bg-surface-alt">Key Limits</th>
|
||||
<th className="py-3 px-4 font-semibold text-text-muted bg-surface-alt">Est. Tokens</th>
|
||||
<th className="py-3 px-4 font-semibold text-text-muted bg-surface-alt rounded-tr-lg">Models</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-border">
|
||||
{plan.tiers.map((tier, i) => (
|
||||
<tr key={i} className="hover:bg-surface-alt/30 transition-colors">
|
||||
<td className="py-4 px-4 font-medium text-text align-top">{tier.name}</td>
|
||||
<td className="py-4 px-4 align-top">
|
||||
{tier.monthlyPrice !== null ? `$${tier.monthlyPrice}/mo` : 'Custom'}
|
||||
{tier.annualPrice && <div className="text-xs text-text-muted mt-1">${tier.annualPrice}/mo (yearly)</div>}
|
||||
</td>
|
||||
<td className="py-4 px-4 align-top">
|
||||
<ul className="list-disc list-inside space-y-1 text-xs text-text-muted">
|
||||
{Object.entries(tier.limits).map(([k, v]) => (
|
||||
<li key={k} className="capitalize">{k.replace(/([A-Z])/g, ' $1')}: {String(v)}</li>
|
||||
))}
|
||||
</ul>
|
||||
</td>
|
||||
<td className="py-4 px-4 align-top">
|
||||
{tier.estimatedTokenBudget ? (
|
||||
<div>
|
||||
<div className="font-semibold text-primary mb-1">
|
||||
~{formatMillionTokens(tier.estimatedTokenBudget.estimatedMillionTokens)}
|
||||
</div>
|
||||
<div className="text-xs text-text-muted leading-tight">
|
||||
{tier.estimatedTokenBudget.description}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<span className="text-text-muted italic">N/A</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="py-4 px-4 align-top">
|
||||
{tier.models && tier.models.length > 0 ? (
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{tier.models.map(m => (
|
||||
<span key={m} className="px-2 py-0.5 bg-surface-alt border border-border rounded text-xs">
|
||||
{m}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<span className="text-text-muted text-xs">Any supported</span>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
import type { CodingPlan } from '../../lib/types';
|
||||
import { Code2, Server, Globe, CheckCircle2 } from 'lucide-react';
|
||||
import { clsx } from 'clsx';
|
||||
|
||||
interface PlanGridProps {
|
||||
plans: CodingPlan[];
|
||||
onSelectPlan: (plan: CodingPlan) => void;
|
||||
selectedPlanId?: string;
|
||||
}
|
||||
|
||||
export function PlanGrid({ plans, onSelectPlan, selectedPlanId }: PlanGridProps) {
|
||||
const getCategoryIcon = (category: string) => {
|
||||
switch(category) {
|
||||
case 'coding-ide': return <Code2 className="w-5 h-5" />;
|
||||
case 'api-provider': return <Server className="w-5 h-5" />;
|
||||
case 'coding-router': return <Globe className="w-5 h-5" />;
|
||||
default: return <Code2 className="w-5 h-5" />;
|
||||
}
|
||||
};
|
||||
|
||||
const getCategoryLabel = (category: string) => {
|
||||
return category.split('-').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' ');
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{plans.map(plan => (
|
||||
<div
|
||||
key={plan.id}
|
||||
onClick={() => onSelectPlan(plan)}
|
||||
className={clsx(
|
||||
"bg-surface rounded-xl border p-5 cursor-pointer transition-all hover:shadow-md",
|
||||
selectedPlanId === plan.id
|
||||
? "border-primary shadow-sm ring-1 ring-primary/50"
|
||||
: "border-border hover:border-primary/50"
|
||||
)}
|
||||
>
|
||||
<div className="flex justify-between items-start mb-4">
|
||||
<div className="w-12 h-12 rounded-lg bg-surface-alt flex items-center justify-center text-text-muted mb-2">
|
||||
<span className="font-bold text-xl">{plan.name.charAt(0)}</span>
|
||||
</div>
|
||||
{selectedPlanId === plan.id && (
|
||||
<CheckCircle2 className="w-5 h-5 text-primary" />
|
||||
)}
|
||||
</div>
|
||||
|
||||
<h3 className="text-xl font-bold text-text mb-1">{plan.name}</h3>
|
||||
|
||||
<div className="flex items-center gap-1.5 text-xs font-medium text-text-muted mb-4 bg-surface-alt px-2.5 py-1 rounded-full w-fit">
|
||||
{getCategoryIcon(plan.category)}
|
||||
<span>{getCategoryLabel(plan.category)}</span>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 pt-4 border-t border-border flex justify-between items-center">
|
||||
<span className="text-sm font-medium text-text-muted">
|
||||
{plan.tiers.length} Tier{plan.tiers.length !== 1 ? 's' : ''}
|
||||
</span>
|
||||
<span className="text-sm font-bold text-text">
|
||||
{plan.tiers[0].monthlyPrice === 0 ? 'Free' : `$${plan.tiers[0].monthlyPrice}`}+
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
import { useState, useMemo } from 'react';
|
||||
import type { CodingPlan, NormalizedModel } from '../../lib/types';
|
||||
import { formatMillionTokens } from '../../lib/pricing';
|
||||
|
||||
interface TokenTranslatorProps {
|
||||
plans: CodingPlan[];
|
||||
models: NormalizedModel[];
|
||||
}
|
||||
|
||||
export function TokenTranslator({ plans, models }: TokenTranslatorProps) {
|
||||
const [selectedPlanId, setSelectedPlanId] = useState<string>('');
|
||||
const [selectedTierName, setSelectedTierName] = useState<string>('');
|
||||
|
||||
const allTiers = useMemo(() => {
|
||||
return plans.flatMap(p => p.tiers.map(t => ({ plan: p, tier: t })))
|
||||
.filter(x => x.tier.estimatedTokenBudget !== null && x.tier.monthlyPrice !== null);
|
||||
}, [plans]);
|
||||
|
||||
const activePlanId = selectedPlanId || (allTiers.length > 0 ? allTiers[0].plan.id : '');
|
||||
const activeTierName = selectedTierName || (allTiers.length > 0 ? allTiers[0].tier.name : '');
|
||||
|
||||
const selectedData = allTiers.find(x => x.plan.id === activePlanId && x.tier.name === activeTierName) || allTiers[0];
|
||||
const budget = selectedData?.tier.monthlyPrice || 0;
|
||||
|
||||
const topModels = useMemo(() => {
|
||||
if (!budget) return [];
|
||||
return models
|
||||
.filter(m => !m.isFree && !m.isBatch && m.blendedCost > 0)
|
||||
.map(m => ({
|
||||
...m,
|
||||
affordableTokens: budget / m.blendedCost
|
||||
}))
|
||||
.sort((a, b) => b.affordableTokens - a.affordableTokens)
|
||||
.slice(0, 12);
|
||||
}, [models, budget]);
|
||||
|
||||
if (allTiers.length === 0) return null;
|
||||
|
||||
return (
|
||||
<div className="bg-primary/5 rounded-xl border border-primary/20 p-6 mb-8">
|
||||
<h3 className="text-lg font-bold text-text mb-4">Token Budget Translator</h3>
|
||||
<p className="text-sm text-text-muted mb-6">
|
||||
See how far a subscription's cost would go if spent directly on API tokens.
|
||||
</p>
|
||||
|
||||
<div className="flex flex-col md:flex-row gap-4 mb-6">
|
||||
<select
|
||||
className="px-4 py-2 bg-surface border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-primary text-text flex-1"
|
||||
value={`${activePlanId}|${activeTierName}`}
|
||||
onChange={(e) => {
|
||||
const [pId, tName] = e.target.value.split('|');
|
||||
setSelectedPlanId(pId);
|
||||
setSelectedTierName(tName);
|
||||
}}
|
||||
>
|
||||
{allTiers.map(x => (
|
||||
<option key={`${x.plan.id}|${x.tier.name}`} value={`${x.plan.id}|${x.tier.name}`}>
|
||||
{x.plan.name} - {x.tier.name} (${x.tier.monthlyPrice}/mo)
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
|
||||
<div className="px-6 py-2 bg-primary text-white rounded-lg font-bold flex items-center justify-center whitespace-nowrap">
|
||||
Budget: ${budget}/mo
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-3">
|
||||
{topModels.map(m => (
|
||||
<div key={m.id} className="bg-surface p-3 rounded-lg border border-border flex flex-col justify-between shadow-sm">
|
||||
<div className="text-xs font-medium text-text-muted truncate mb-1" title={m.name}>{m.name}</div>
|
||||
<div className="text-lg font-bold text-primary">
|
||||
{formatMillionTokens(m.affordableTokens)}
|
||||
</div>
|
||||
<div className="text-[10px] text-text-muted mt-1">tokens / mo</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Clock } from 'lucide-react';
|
||||
|
||||
export function DataFreshness() {
|
||||
const [lastUpdated, setLastUpdated] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
fetch(`${import.meta.env.BASE_URL}data/last-updated.json`)
|
||||
.then(res => res.ok ? res.json() : null)
|
||||
.then(data => {
|
||||
if (data && data.timestamp) {
|
||||
const date = new Date(data.timestamp);
|
||||
setLastUpdated(date.toLocaleDateString() + ' ' + date.toLocaleTimeString());
|
||||
}
|
||||
})
|
||||
.catch(() => {});
|
||||
}, []);
|
||||
|
||||
if (!lastUpdated) return null;
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-1.5 text-xs text-text-muted bg-surface-alt px-2.5 py-1 rounded-full border border-border">
|
||||
<Clock className="w-3.5 h-3.5" />
|
||||
<span>Updated: {lastUpdated}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
export function LoadingSpinner() {
|
||||
return (
|
||||
<div className="flex justify-center items-center py-12">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary"></div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import { Search } from 'lucide-react';
|
||||
|
||||
interface SearchFilterProps {
|
||||
search: string;
|
||||
onSearchChange: (val: string) => void;
|
||||
category: string;
|
||||
onCategoryChange: (val: string) => void;
|
||||
categories: { value: string; label: string }[];
|
||||
placeholder?: string;
|
||||
}
|
||||
|
||||
export function SearchFilter({
|
||||
search,
|
||||
onSearchChange,
|
||||
category,
|
||||
onCategoryChange,
|
||||
categories,
|
||||
placeholder = "Search..."
|
||||
}: SearchFilterProps) {
|
||||
return (
|
||||
<div className="flex flex-col sm:flex-row gap-4 mb-6">
|
||||
<div className="relative flex-1">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-5 h-5 text-text-muted" />
|
||||
<input
|
||||
type="text"
|
||||
value={search}
|
||||
onChange={(e) => onSearchChange(e.target.value)}
|
||||
placeholder={placeholder}
|
||||
className="w-full pl-10 pr-4 py-2 bg-surface border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent text-text"
|
||||
/>
|
||||
</div>
|
||||
{categories.length > 0 && (
|
||||
<select
|
||||
value={category}
|
||||
onChange={(e) => onCategoryChange(e.target.value)}
|
||||
className="px-4 py-2 bg-surface border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-primary text-text min-w-[200px]"
|
||||
>
|
||||
{categories.map(cat => (
|
||||
<option key={cat.value} value={cat.value}>{cat.label}</option>
|
||||
))}
|
||||
</select>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
import type { CodingPlan } from '../../lib/types';
|
||||
import { AlertTriangle, Info, ShieldAlert } from 'lucide-react';
|
||||
|
||||
interface GotchaCardsProps {
|
||||
plans: CodingPlan[];
|
||||
}
|
||||
|
||||
interface CardSectionProps {
|
||||
title: string;
|
||||
icon: React.ReactNode;
|
||||
items: { planName: string; text: string; severity: string }[];
|
||||
colorClass: string;
|
||||
bgClass: string;
|
||||
borderClass: string;
|
||||
}
|
||||
|
||||
function CardSection({ title, icon, items, colorClass, bgClass, borderClass }: CardSectionProps) {
|
||||
if (items.length === 0) return null;
|
||||
return (
|
||||
<div className="mb-8">
|
||||
<h3 className={`text-lg font-bold mb-4 flex items-center gap-2 ${colorClass}`}>
|
||||
{icon}
|
||||
{title} ({items.length})
|
||||
</h3>
|
||||
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{items.map((item, i) => (
|
||||
<div key={i} className={`p-4 rounded-xl border ${bgClass} ${borderClass}`}>
|
||||
<div className="text-xs font-bold uppercase tracking-wider mb-2 opacity-70">
|
||||
{item.planName}
|
||||
</div>
|
||||
<p className="text-sm font-medium">{item.text}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function GotchaCards({ plans }: GotchaCardsProps) {
|
||||
// Extract all gotchas from all plans and categorize them
|
||||
const gotchas = plans.flatMap(plan =>
|
||||
plan.gotchas.map(gotcha => {
|
||||
let severity: 'danger' | 'warning' | 'info' = 'info';
|
||||
if (gotcha.toLowerCase().includes('train') || gotcha.toLowerCase().includes('data')) severity = 'danger';
|
||||
else if (gotcha.toLowerCase().includes('ip') || gotcha.toLowerCase().includes('indemnity')) severity = 'warning';
|
||||
|
||||
return { planName: plan.name, text: gotcha, severity };
|
||||
})
|
||||
);
|
||||
|
||||
const dangerGotchas = gotchas.filter(g => g.severity === 'danger');
|
||||
const warningGotchas = gotchas.filter(g => g.severity === 'warning');
|
||||
const infoGotchas = gotchas.filter(g => g.severity === 'info');
|
||||
|
||||
return (
|
||||
<div>
|
||||
<CardSection
|
||||
title="Data & Privacy Risks"
|
||||
icon={<ShieldAlert className="w-5 h-5" />}
|
||||
items={dangerGotchas}
|
||||
colorClass="text-danger"
|
||||
bgClass="bg-danger/5"
|
||||
borderClass="border-danger/20"
|
||||
/>
|
||||
<CardSection
|
||||
title="IP & Legal Gaps"
|
||||
icon={<AlertTriangle className="w-5 h-5" />}
|
||||
items={warningGotchas}
|
||||
colorClass="text-warning"
|
||||
bgClass="bg-warning/5"
|
||||
borderClass="border-warning/20"
|
||||
/>
|
||||
<CardSection
|
||||
title="Hidden Limits & Costs"
|
||||
icon={<Info className="w-5 h-5" />}
|
||||
items={infoGotchas}
|
||||
colorClass="text-primary"
|
||||
bgClass="bg-primary/5"
|
||||
borderClass="border-primary/20"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
import type { CodingPlan } from '../../lib/types';
|
||||
import { CheckCircle2, XCircle, AlertCircle } from 'lucide-react';
|
||||
|
||||
interface TrainingMatrixProps {
|
||||
plans: CodingPlan[];
|
||||
}
|
||||
|
||||
export function TrainingMatrix({ plans }: TrainingMatrixProps) {
|
||||
// A simplistic mapping to categorize tiers
|
||||
const getTierLevel = (tierName: string, price: number | null) => {
|
||||
const n = tierName.toLowerCase();
|
||||
if (price === 0 || n.includes('free')) return 'free';
|
||||
if (n.includes('business') || n.includes('team') || n.includes('enterprise') || n.includes('corp')) return 'business';
|
||||
return 'individual';
|
||||
};
|
||||
|
||||
// We determine training status. If the plan says "No", we assume safe.
|
||||
// For the matrix, we assume Free tiers often train unless specified, Business rarely does, Individual varies.
|
||||
// This is a simplified display logic based on plan.dataTraining string.
|
||||
|
||||
const getStatusIcon = (status: 'safe' | 'warn' | 'danger') => {
|
||||
if (status === 'safe') return <CheckCircle2 className="w-5 h-5 text-success mx-auto" />;
|
||||
if (status === 'danger') return <XCircle className="w-5 h-5 text-danger mx-auto" />;
|
||||
return <AlertCircle className="w-5 h-5 text-warning mx-auto" />;
|
||||
};
|
||||
|
||||
const inferTierStatus = (plan: CodingPlan, level: 'free' | 'individual' | 'business') => {
|
||||
// If the whole plan explicitly says No training, all tiers are safe
|
||||
if (plan.dataTraining.toLowerCase().includes('no')) return 'safe';
|
||||
|
||||
// Otherwise, we guess based on common patterns if not explicit
|
||||
if (level === 'business') return 'safe'; // Usually enterprise is opted out
|
||||
if (level === 'free') return 'danger'; // Usually free is opted in
|
||||
return 'warn'; // Individual requires opt-out usually
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-surface rounded-xl border border-border shadow-sm overflow-hidden mb-12">
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-left text-sm">
|
||||
<thead className="bg-surface-alt border-b border-border text-text-muted">
|
||||
<tr>
|
||||
<th className="px-6 py-4 font-medium">Service</th>
|
||||
<th className="px-6 py-4 font-medium text-center">Free Tier</th>
|
||||
<th className="px-6 py-4 font-medium text-center">Individual Paid</th>
|
||||
<th className="px-6 py-4 font-medium text-center">Business / Enterprise</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-border">
|
||||
{plans.map((plan) => {
|
||||
const hasFree = plan.tiers.some(t => getTierLevel(t.name, t.monthlyPrice) === 'free');
|
||||
const hasIndiv = plan.tiers.some(t => getTierLevel(t.name, t.monthlyPrice) === 'individual');
|
||||
const hasBiz = plan.tiers.some(t => getTierLevel(t.name, t.monthlyPrice) === 'business');
|
||||
|
||||
return (
|
||||
<tr key={plan.id} className="hover:bg-surface-alt/50 transition-colors">
|
||||
<td className="px-6 py-4">
|
||||
<div className="font-bold text-text">{plan.name}</div>
|
||||
<div className="text-xs text-text-muted truncate max-w-[200px] mt-1" title={plan.dataTraining}>
|
||||
{plan.dataTraining}
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-6 py-4">
|
||||
{hasFree ? getStatusIcon(inferTierStatus(plan, 'free')) : <span className="text-text-muted text-center block">-</span>}
|
||||
</td>
|
||||
<td className="px-6 py-4">
|
||||
{hasIndiv ? getStatusIcon(inferTierStatus(plan, 'individual')) : <span className="text-text-muted text-center block">-</span>}
|
||||
</td>
|
||||
<td className="px-6 py-4">
|
||||
{hasBiz ? getStatusIcon(inferTierStatus(plan, 'business')) : <span className="text-text-muted text-center block">-</span>}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div className="bg-surface-alt p-4 flex gap-6 justify-center text-xs text-text-muted border-t border-border">
|
||||
<div className="flex items-center gap-2"><CheckCircle2 className="w-4 h-4 text-success" /> Default Opt-Out (Safe)</div>
|
||||
<div className="flex items-center gap-2"><AlertCircle className="w-4 h-4 text-warning" /> Requires Manual Opt-Out</div>
|
||||
<div className="flex items-center gap-2"><XCircle className="w-4 h-4 text-danger" /> Default Opt-In (Risk)</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import type { AABenchmark } from '../lib/types';
|
||||
|
||||
export function useBenchmarks() {
|
||||
const [benchmarks, setBenchmarks] = useState<AABenchmark[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
fetch(`${import.meta.env.BASE_URL}data/benchmarks.json`)
|
||||
.then(r => r.ok ? r.json() : Promise.reject('Failed'))
|
||||
.then(data => { setBenchmarks(Array.isArray(data) ? data : data.benchmarks || []); setLoading(false); })
|
||||
.catch(() => setLoading(false));
|
||||
}, []);
|
||||
|
||||
return { benchmarks, loading };
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { useState, useMemo } from 'react';
|
||||
import type { NormalizedModel } from '../lib/types';
|
||||
import { calculateBudgetResults } from '../lib/pricing';
|
||||
|
||||
export function useBudget(models: NormalizedModel[]) {
|
||||
const [budget, setBudget] = useState(20);
|
||||
|
||||
const results = useMemo(
|
||||
() => calculateBudgetResults(models, budget),
|
||||
[models, budget]
|
||||
);
|
||||
|
||||
return { budget, setBudget, results };
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import type { NormalizedModel } from '../lib/types';
|
||||
|
||||
export function useModels() {
|
||||
const [models, setModels] = useState<NormalizedModel[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
fetch(`${import.meta.env.BASE_URL}data/models.json`)
|
||||
.then(r => r.ok ? r.json() : Promise.reject('Failed to load models'))
|
||||
.then(data => { setModels(Array.isArray(data) ? data : data.models || []); setLoading(false); })
|
||||
.catch(e => { setError(String(e)); setLoading(false); });
|
||||
}, []);
|
||||
|
||||
return { models, loading, error };
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import type { CodingPlan } from '../lib/types';
|
||||
|
||||
export function usePlans() {
|
||||
const [plans, setPlans] = useState<CodingPlan[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
fetch(`${import.meta.env.BASE_URL}data/plans.json`)
|
||||
.then(r => r.ok ? r.json() : Promise.reject('Failed to load plans'))
|
||||
.then(data => { setPlans(Array.isArray(data) ? data : data.plans || []); setLoading(false); })
|
||||
.catch(e => { setError(String(e)); setLoading(false); });
|
||||
}, []);
|
||||
|
||||
return { plans, loading, error };
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
@import 'tailwindcss';
|
||||
|
||||
@theme {
|
||||
--color-primary: #6366f1;
|
||||
--color-primary-light: #818cf8;
|
||||
--color-primary-dark: #4f46e5;
|
||||
--color-surface: #ffffff;
|
||||
--color-surface-alt: #f8fafc;
|
||||
--color-border: #e2e8f0;
|
||||
--color-text: #0f172a;
|
||||
--color-text-muted: #64748b;
|
||||
--color-success: #22c55e;
|
||||
--color-warning: #f59e0b;
|
||||
--color-danger: #ef4444;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
@theme {
|
||||
--color-surface: #0f172a;
|
||||
--color-surface-alt: #1e293b;
|
||||
--color-border: #334155;
|
||||
--color-text: #f1f5f9;
|
||||
--color-text-muted: #94a3b8;
|
||||
}
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Inter', system-ui, -apple-system, sans-serif;
|
||||
background-color: var(--color-surface);
|
||||
color: var(--color-text);
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
import type { NormalizedModel, BudgetResult } from './types';
|
||||
|
||||
export function formatPrice(price: number): string {
|
||||
if (price === 0) return 'Free';
|
||||
if (price < 0.01) return `$${price.toFixed(4)}`;
|
||||
if (price < 1) return `$${price.toFixed(3)}`;
|
||||
return `$${price.toFixed(2)}`;
|
||||
}
|
||||
|
||||
export function formatMillionTokens(mt: number): string {
|
||||
if (mt >= 1000) return `${(mt / 1000).toFixed(1)}B`;
|
||||
if (mt >= 1) return `${mt.toFixed(1)}M`;
|
||||
return `${(mt * 1000).toFixed(0)}K`;
|
||||
}
|
||||
|
||||
export function calculateBudgetResults(
|
||||
models: NormalizedModel[],
|
||||
budget: number
|
||||
): BudgetResult[] {
|
||||
return models
|
||||
.filter(m => !m.isFree && !m.isBatch && m.blendedCost > 0)
|
||||
.map(m => ({
|
||||
modelId: m.id,
|
||||
modelName: m.name,
|
||||
provider: m.provider,
|
||||
millionTokens: budget / m.blendedCost,
|
||||
requests1k: (budget / m.costPer1kRequests) * 1000,
|
||||
codingIndex: m.benchmarks.codingIndex,
|
||||
blendedCost: m.blendedCost,
|
||||
}))
|
||||
.sort((a, b) => b.millionTokens - a.millionTokens);
|
||||
}
|
||||
|
||||
export function computeWeightedScore(model: NormalizedModel): number {
|
||||
const weights = {
|
||||
codingIndex: 0.35,
|
||||
agenticIndex: 0.30,
|
||||
intelligenceIndex: 0.25,
|
||||
};
|
||||
const b = model.benchmarks;
|
||||
let score = 0;
|
||||
let totalWeight = 0;
|
||||
if (b.codingIndex != null) { score += b.codingIndex * weights.codingIndex; totalWeight += weights.codingIndex; }
|
||||
if (b.agenticIndex != null) { score += b.agenticIndex * weights.agenticIndex; totalWeight += weights.agenticIndex; }
|
||||
if (b.intelligenceIndex != null) { score += b.intelligenceIndex * weights.intelligenceIndex; totalWeight += weights.intelligenceIndex; }
|
||||
return totalWeight > 0 ? score / totalWeight : 0;
|
||||
}
|
||||
|
||||
export function computeValueScore(model: NormalizedModel): number {
|
||||
const quality = computeWeightedScore(model);
|
||||
const cost = model.blendedCost;
|
||||
if (cost === 0) return Infinity;
|
||||
return (quality / cost) * 100;
|
||||
}
|
||||
|
||||
export function getProviderColor(provider: string): string {
|
||||
const colors: Record<string, string> = {
|
||||
openai: '#10a37f',
|
||||
anthropic: '#d4a27f',
|
||||
google: '#4285f4',
|
||||
meta: '#0668E1',
|
||||
deepseek: '#536dfe',
|
||||
qwen: '#ff6f00',
|
||||
mistral: '#ff7000',
|
||||
'together-ai': '#6366f1',
|
||||
groq: '#f97316',
|
||||
fireworks: '#ef4444',
|
||||
inception: '#8b5cf6',
|
||||
sakana: '#06b6d4',
|
||||
};
|
||||
return colors[provider] || '#94a3b8';
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
export interface ModelPricing {
|
||||
input: number;
|
||||
output: number;
|
||||
cachedInput: number | null;
|
||||
cachedInputWrite: number | null;
|
||||
reasoning: number | null;
|
||||
webSearch: number | null;
|
||||
}
|
||||
|
||||
export interface ModelBenchmarks {
|
||||
intelligenceIndex: number | null;
|
||||
codingIndex: number | null;
|
||||
agenticIndex: number | null;
|
||||
valueScore: number | null;
|
||||
}
|
||||
|
||||
export interface NormalizedModel {
|
||||
id: string;
|
||||
name: string;
|
||||
provider: string;
|
||||
modality: string;
|
||||
contextWindow: number;
|
||||
maxOutput: number;
|
||||
pricing: ModelPricing;
|
||||
blendedCost: number;
|
||||
costPer1kRequests: number;
|
||||
benchmarks: ModelBenchmarks;
|
||||
reasoning: {
|
||||
mandatory: boolean;
|
||||
defaultEnabled: boolean;
|
||||
supportedEfforts: string[];
|
||||
} | null;
|
||||
isFree: boolean;
|
||||
isBatch: boolean;
|
||||
}
|
||||
|
||||
export interface PlanTier {
|
||||
name: string;
|
||||
monthlyPrice: number | null;
|
||||
annualPrice?: number | null;
|
||||
limits: Record<string, any>;
|
||||
models?: string[];
|
||||
estimatedTokenBudget: {
|
||||
description?: string;
|
||||
estimatedMillionTokens: number;
|
||||
assumptions: string;
|
||||
} | null;
|
||||
notes?: string;
|
||||
}
|
||||
|
||||
export interface CodingPlan {
|
||||
id: string;
|
||||
name: string;
|
||||
category: 'coding-ide' | 'coding-router' | 'api-provider';
|
||||
url: string;
|
||||
lastVerified: string;
|
||||
tiers: PlanTier[];
|
||||
gotchas: string[];
|
||||
tosHighlights: string[];
|
||||
dataTraining: string;
|
||||
ipIndemnity: string | boolean;
|
||||
}
|
||||
|
||||
export interface BudgetResult {
|
||||
modelId: string;
|
||||
modelName: string;
|
||||
provider: string;
|
||||
millionTokens: number;
|
||||
requests1k: number;
|
||||
codingIndex: number | null;
|
||||
blendedCost: number;
|
||||
}
|
||||
|
||||
export interface AABenchmark {
|
||||
slug: string;
|
||||
name: string;
|
||||
releaseDate: string | null;
|
||||
creator: string;
|
||||
intelligenceIndex: number | null;
|
||||
codingIndex: number | null;
|
||||
agenticIndex: number | null;
|
||||
pricing: {
|
||||
input: number;
|
||||
output: number;
|
||||
cachedInput: number | null;
|
||||
};
|
||||
performance: {
|
||||
outputTokensPerSec: number | null;
|
||||
ttft: number | null;
|
||||
e2eResponseTime: number | null;
|
||||
};
|
||||
indexCost: number | null;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { StrictMode } from 'react'
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import App from './App.tsx'
|
||||
import './index.css'
|
||||
|
||||
createRoot(document.getElementById('root')!).render(
|
||||
<StrictMode>
|
||||
<App />
|
||||
</StrictMode>,
|
||||
)
|
||||
@@ -0,0 +1,27 @@
|
||||
import { useModels } from '../hooks/useModels';
|
||||
import { ValueScatter } from '../components/benchmarks/ValueScatter';
|
||||
import { LeaderboardTable } from '../components/benchmarks/LeaderboardTable';
|
||||
import { LoadingSpinner } from '../components/shared/LoadingSpinner';
|
||||
|
||||
export default function BenchmarksPage() {
|
||||
const { models, loading } = useModels();
|
||||
|
||||
if (loading) return <LoadingSpinner />;
|
||||
|
||||
return (
|
||||
<div className="animate-in fade-in duration-500">
|
||||
<div className="mb-8">
|
||||
<h1 className="text-3xl font-bold mb-2">Model Benchmarks & Value</h1>
|
||||
<p className="text-text-muted">Scores based on Artificial Analysis data. Weighted score = 35% Coding + 30% Agentic + 25% Intelligence.</p>
|
||||
</div>
|
||||
|
||||
<ValueScatter models={models} />
|
||||
|
||||
<div className="mb-6">
|
||||
<h2 className="text-2xl font-bold">Leaderboard</h2>
|
||||
</div>
|
||||
|
||||
<LeaderboardTable models={models} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
import { useModels } from '../hooks/useModels';
|
||||
import { usePlans } from '../hooks/usePlans';
|
||||
import { useBudget } from '../hooks/useBudget';
|
||||
import { BudgetInput } from '../components/budget/BudgetInput';
|
||||
import { BudgetResults } from '../components/budget/BudgetResults';
|
||||
import { LoadingSpinner } from '../components/shared/LoadingSpinner';
|
||||
import { Database, CreditCard, Zap } from 'lucide-react';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
export default function Dashboard() {
|
||||
const { models, loading: modelsLoading } = useModels();
|
||||
const { plans, loading: plansLoading } = usePlans();
|
||||
const { budget, setBudget, results } = useBudget(models);
|
||||
|
||||
if (modelsLoading || plansLoading) return <LoadingSpinner />;
|
||||
|
||||
const freeModelsCount = models.filter(m => m.isFree || m.blendedCost === 0).length;
|
||||
|
||||
return (
|
||||
<div className="space-y-8 animate-in fade-in duration-500">
|
||||
<section className="text-center py-8">
|
||||
<h1 className="text-4xl font-extrabold tracking-tight mb-4 text-text">
|
||||
Maximize Your AI Token Budget
|
||||
</h1>
|
||||
<p className="text-xl text-text-muted max-w-2xl mx-auto">
|
||||
Compare API models against AI coding subscriptions to find the most cost-effective way to code with LLMs.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section className="grid grid-cols-1 md:grid-cols-3 gap-6 mb-12">
|
||||
<div className="bg-surface p-6 rounded-xl border border-border flex items-start gap-4">
|
||||
<div className="p-3 bg-primary/10 text-primary rounded-lg">
|
||||
<Database className="w-6 h-6" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-2xl font-bold">{models.length}</div>
|
||||
<div className="text-sm text-text-muted">API Models Tracked</div>
|
||||
<Link to="/models" className="text-xs text-primary hover:underline mt-1 inline-block">Explore Models →</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-surface p-6 rounded-xl border border-border flex items-start gap-4">
|
||||
<div className="p-3 bg-success/10 text-success rounded-lg">
|
||||
<CreditCard className="w-6 h-6" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-2xl font-bold">{plans.length}</div>
|
||||
<div className="text-sm text-text-muted">Coding Plans Analyzed</div>
|
||||
<Link to="/plans" className="text-xs text-primary hover:underline mt-1 inline-block">Compare Plans →</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-surface p-6 rounded-xl border border-border flex items-start gap-4">
|
||||
<div className="p-3 bg-warning/10 text-warning rounded-lg">
|
||||
<Zap className="w-6 h-6" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-2xl font-bold">{freeModelsCount}</div>
|
||||
<div className="text-sm text-text-muted">Free Models Available</div>
|
||||
<Link to="/models" className="text-xs text-primary hover:underline mt-1 inline-block">Find Free Models →</Link>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<BudgetInput budget={budget} onChange={setBudget} />
|
||||
<BudgetResults results={results} plans={plans} budget={budget} />
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import { useState } from 'react';
|
||||
import { useModels } from '../hooks/useModels';
|
||||
import { ModelTable } from '../components/models/ModelTable';
|
||||
import { SearchFilter } from '../components/shared/SearchFilter';
|
||||
import { LoadingSpinner } from '../components/shared/LoadingSpinner';
|
||||
|
||||
export default function ModelsExplorer() {
|
||||
const { models, loading } = useModels();
|
||||
const [search, setSearch] = useState('');
|
||||
const [provider, setProvider] = useState('all');
|
||||
|
||||
if (loading) return <LoadingSpinner />;
|
||||
|
||||
const providers = Array.from(new Set(models.map(m => m.provider))).sort();
|
||||
const categories = [
|
||||
{ value: 'all', label: 'All Providers' },
|
||||
...providers.map(p => ({ value: p, label: p }))
|
||||
];
|
||||
|
||||
const filteredModels = models.filter(m => {
|
||||
const matchesSearch = m.name.toLowerCase().includes(search.toLowerCase());
|
||||
const matchesProvider = provider === 'all' || m.provider === provider;
|
||||
return matchesSearch && matchesProvider;
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="animate-in fade-in duration-500">
|
||||
<div className="mb-8">
|
||||
<h1 className="text-3xl font-bold mb-2">API Models Explorer</h1>
|
||||
<p className="text-text-muted">Compare pricing and context windows across {models.length} models.</p>
|
||||
</div>
|
||||
|
||||
<SearchFilter
|
||||
search={search}
|
||||
onSearchChange={setSearch}
|
||||
category={provider}
|
||||
onCategoryChange={setProvider}
|
||||
categories={categories}
|
||||
placeholder="Search models..."
|
||||
/>
|
||||
|
||||
<ModelTable models={filteredModels} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
import { useState } from 'react';
|
||||
import { usePlans } from '../hooks/usePlans';
|
||||
import { useModels } from '../hooks/useModels';
|
||||
import { PlanGrid } from '../components/plans/PlanGrid';
|
||||
import { PlanDetail } from '../components/plans/PlanDetail';
|
||||
import { TokenTranslator } from '../components/plans/TokenTranslator';
|
||||
import { SearchFilter } from '../components/shared/SearchFilter';
|
||||
import { LoadingSpinner } from '../components/shared/LoadingSpinner';
|
||||
import type { CodingPlan } from '../lib/types';
|
||||
|
||||
export default function PlansCompare() {
|
||||
const { plans, loading: plansLoading } = usePlans();
|
||||
const { models, loading: modelsLoading } = useModels();
|
||||
const [search, setSearch] = useState('');
|
||||
const [category, setCategory] = useState('all');
|
||||
const [selectedPlan, setSelectedPlan] = useState<CodingPlan | null>(null);
|
||||
|
||||
if (plansLoading || modelsLoading) return <LoadingSpinner />;
|
||||
|
||||
const categories = [
|
||||
{ value: 'all', label: 'All Categories' },
|
||||
{ value: 'coding-ide', label: 'Coding IDEs' },
|
||||
{ value: 'coding-router', label: 'Coding Routers' },
|
||||
{ value: 'api-provider', label: 'API Providers' },
|
||||
];
|
||||
|
||||
const filteredPlans = plans.filter(p => {
|
||||
const matchesSearch = p.name.toLowerCase().includes(search.toLowerCase());
|
||||
const matchesCategory = category === 'all' || p.category === category;
|
||||
return matchesSearch && matchesCategory;
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="animate-in fade-in duration-500">
|
||||
<div className="mb-8">
|
||||
<h1 className="text-3xl font-bold mb-2">Compare Coding Plans</h1>
|
||||
<p className="text-text-muted">Analyze features, limits, and gotchas of popular AI coding tools.</p>
|
||||
</div>
|
||||
|
||||
<TokenTranslator plans={plans} models={models} />
|
||||
|
||||
<SearchFilter
|
||||
search={search}
|
||||
onSearchChange={setSearch}
|
||||
category={category}
|
||||
onCategoryChange={setCategory}
|
||||
categories={categories}
|
||||
placeholder="Search plans..."
|
||||
/>
|
||||
|
||||
<PlanGrid
|
||||
plans={filteredPlans}
|
||||
onSelectPlan={(p) => setSelectedPlan(selectedPlan?.id === p.id ? null : p)}
|
||||
selectedPlanId={selectedPlan?.id}
|
||||
/>
|
||||
|
||||
{selectedPlan && <PlanDetail plan={selectedPlan} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import { usePlans } from '../hooks/usePlans';
|
||||
import { TrainingMatrix } from '../components/tos/TrainingMatrix';
|
||||
import { GotchaCards } from '../components/tos/GotchaCards';
|
||||
import { LoadingSpinner } from '../components/shared/LoadingSpinner';
|
||||
|
||||
export default function TosAudit() {
|
||||
const { plans, loading } = usePlans();
|
||||
|
||||
if (loading) return <LoadingSpinner />;
|
||||
|
||||
return (
|
||||
<div className="animate-in fade-in duration-500">
|
||||
<div className="mb-8">
|
||||
<h1 className="text-3xl font-bold mb-2">Terms of Service Audit</h1>
|
||||
<p className="text-text-muted">Understand what happens to your code and data when using these services.</p>
|
||||
</div>
|
||||
|
||||
<section>
|
||||
<h2 className="text-2xl font-bold mb-6">Data Training Matrix</h2>
|
||||
<TrainingMatrix plans={plans} />
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 className="text-2xl font-bold mb-6">Notable Gotchas & Limits</h2>
|
||||
<GotchaCards plans={plans} />
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||
"target": "es2023",
|
||||
"lib": ["ES2023", "DOM"],
|
||||
"module": "esnext",
|
||||
"types": ["vite/client"],
|
||||
"allowArbitraryExtensions": true,
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
|
||||
/* Linting */
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"erasableSyntaxOnly": true,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"files": [],
|
||||
"references": [
|
||||
{ "path": "./tsconfig.app.json" },
|
||||
{ "path": "./tsconfig.node.json" }
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
||||
"target": "es2023",
|
||||
"lib": ["ES2023"],
|
||||
"types": ["node"],
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"module": "nodenext",
|
||||
"allowImportingTsExtensions": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
|
||||
/* Linting */
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"erasableSyntaxOnly": true,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
import tailwindcss from '@tailwindcss/vite'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react(), tailwindcss()],
|
||||
base: '/Token-Max/',
|
||||
})
|
||||
Reference in New Issue
Block a user