diff --git a/.env.example b/.env.example index 85c59e3..3256e50 100644 --- a/.env.example +++ b/.env.example @@ -3,5 +3,5 @@ ANTHROPIC_API_KEY=your_anthropic_api_key_here COHERE_API_KEY=your_cohere_api_key_here MISTRAL_API_KEY=your_mistral_api_key_here GROQ_API_KEY=your_groq_api_key_here - +OPENROUTER_API_KEY= DATABASE_URL= diff --git a/.gitignore b/.gitignore index b9f8d23..d6edea3 100644 --- a/.gitignore +++ b/.gitignore @@ -35,4 +35,5 @@ yarn-error.log* *.tsbuildinfo next-env.d.ts -.env \ No newline at end of file +.env +certificates \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..c8d3d0c --- /dev/null +++ b/Dockerfile @@ -0,0 +1,30 @@ +FROM node:18-alpine + +WORKDIR /app + +RUN apk add --no-cache libc6-compat +RUN apk update + +# Install pnpm +RUN npm install -g pnpm + +# Copy package.json and pnpm-lock.yaml +COPY package.json pnpm-lock.yaml ./ + +# Install dependencies +RUN pnpm install + +# Copy the rest of the application +COPY . . + +# Generate Prisma Client +RUN pnpm prisma generate + +# Build the application +RUN pnpm run build + +# Expose the port the app runs on +EXPOSE 3000 + +# Start the application +CMD ["sh", "./start.sh"] \ No newline at end of file diff --git a/README.md b/README.md index e91785a..7cb12d3 100644 --- a/README.md +++ b/README.md @@ -58,7 +58,76 @@ For full details on each step, please see the [Installation Guide](https://www.c ## Stats page -![Cursor Lens Stats](public/cl-stats.png) +![Cursor Lens Stats](public/cl-stats.jpeg) + +## Prompt caching with Anthropic (v0.1.2): + +1. Create a new config on `/configuration` page, choose `antropicCached` with Sonnet 3.5. Name it as you like. +2. Mark it as default. +3. Use Cursor with CursorLens as normal. The system and context messages in `CMD+L` and `CMD+i` chats will be cached from now on. + +> Note that TTL for the cache is 5 minutes. + +![Add a new config with Antropic Cached](public/anthropicCashedXConfig.png) +![Example Cache creation response](public/ant-cache-create.png) +![Example Cache read response](public/ant-cache-read.png) + +# Releases + +## Nightly - 2024-08-24 + +- Add new cost calculation + +To run it, make sure to run: + +- `npx prisma seed db` and then +- `pnpm run update-log-costs` to add cost info in metadata for all previous logs + +## [0.1.2-alpha] - 2024-08-22 + +### ⚠️ ALPHA RELEASE + +### Added + +- Add Anthropic Cache support for context messages +- Increase Token limit for Anthropic to 8192 tokens +- Improved statistics page: Now you can select the data points you want to see + +### Improved and fixed + +- Log details are now collapsible +- Full response is captured in the logs + +## [0.1.1-alpha] - 2024-08-18 + +### ⚠️ ALPHA RELEASE + +### Added + +- Added support for Mistral AI, Cohere, Groq, and Ollama + +## [0.1.0-alpha] - 2024-08-17 + +This is the initial alpha release of CursorLens. As an alpha version, it may contain bugs and is not yet feature-complete. Use with caution in non-production environments. + +### Added + +- Initial project setup with Next.js +- Basic proxy functionality between Cursor and AI providers (OpenAI, Anthropic) +- Simple dashboard for viewing AI interaction logs +- Token usage tracking for OpenAI and Anthropic models +- Basic cost estimation based on token usage +- Support for PostgreSQL database with Prisma ORM +- Environment variable configuration for API keys and database connection +- Basic error handling and logging + +### Known Issues + +- Limited error handling for edge cases +- Incomplete test coverage +- Basic UI with limited customization options +- Potential performance issues with large volumes of requests +- Cost calculation for cached messages in Anthropic are not correct ## Contributing diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..bac434b --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,41 @@ +services: + db: + image: postgres:14 + environment: + - POSTGRES_USER=postgres + - POSTGRES_PASSWORD=postgres + - POSTGRES_DB=postgres + volumes: + - postgres_data:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U postgres"] + interval: 5s + timeout: 5s + retries: 5 + ports: + - "5432:5432" + + app: + build: + context: . + dockerfile: Dockerfile + ports: + - "3000:3000" + environment: + - DATABASE_URL=postgresql://postgres:postgres@db:5432/postgres + depends_on: + db: + condition: service_healthy + + ngrok: + image: ngrok/ngrok:latest + environment: + NGROK_AUTHTOKEN: ${NGROK_AUTHTOKEN} + command: http app:3000 + ports: + - "4040:4040" + depends_on: + - app + +volumes: + postgres_data: diff --git a/package.json b/package.json index 8e0b734..a6fa624 100644 --- a/package.json +++ b/package.json @@ -7,14 +7,15 @@ "build": "next build", "start": "next start", "lint": "next lint", - "seed": "tsx prisma/seed.ts" + "seed": "tsx prisma/seed.ts", + "update-log-costs": "tsx scripts/update-log-costs.ts" }, "prisma": { "seed": "tsx prisma/seed.ts" }, "dependencies": { "@ai-sdk/amazon-bedrock": "^0.0.17", - "@ai-sdk/anthropic": "^0.0.41", + "@ai-sdk/anthropic": "^0.0.46", "@ai-sdk/cohere": "^0.0.17", "@ai-sdk/google-vertex": "^0.0.28", "@ai-sdk/mistral": "^0.0.34", @@ -25,13 +26,14 @@ "@radix-ui/react-icons": "^1.3.0", "@radix-ui/react-label": "^2.1.0", "@radix-ui/react-popover": "^1.1.1", + "@radix-ui/react-progress": "^1.1.0", "@radix-ui/react-scroll-area": "^1.1.0", "@radix-ui/react-select": "^2.1.1", "@radix-ui/react-slot": "^1.1.0", "@radix-ui/react-switch": "^1.1.0", "@t3-oss/env-nextjs": "^0.11.0", "@types/react-syntax-highlighter": "^15.5.13", - "ai": "^3.3.7", + "ai": "^3.3.14", "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", "date-fns": "^3.6.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5cfb507..4a0a5dd 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -12,8 +12,8 @@ importers: specifier: ^0.0.17 version: 0.0.17(zod@3.23.8) '@ai-sdk/anthropic': - specifier: ^0.0.41 - version: 0.0.41(zod@3.23.8) + specifier: ^0.0.46 + version: 0.0.46(zod@3.23.8) '@ai-sdk/cohere': specifier: ^0.0.17 version: 0.0.17(zod@3.23.8) @@ -44,6 +44,9 @@ importers: '@radix-ui/react-popover': specifier: ^1.1.1 version: 1.1.1(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-progress': + specifier: ^1.1.0 + version: 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-scroll-area': specifier: ^1.1.0 version: 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -63,8 +66,8 @@ importers: specifier: ^15.5.13 version: 15.5.13 ai: - specifier: ^3.3.7 - version: 3.3.7(openai@4.55.7(encoding@0.1.13)(zod@3.23.8))(react@18.3.1)(sswr@2.1.0(svelte@4.2.18))(svelte@4.2.18)(vue@3.4.38(typescript@5.5.4))(zod@3.23.8) + specifier: ^3.3.14 + version: 3.3.14(openai@4.55.7(encoding@0.1.13)(zod@3.23.8))(react@18.3.1)(sswr@2.1.0(svelte@4.2.18))(svelte@4.2.18)(vue@3.4.38(typescript@5.5.4))(zod@3.23.8) class-variance-authority: specifier: ^0.7.0 version: 0.7.0 @@ -177,8 +180,8 @@ packages: peerDependencies: zod: ^3.0.0 - '@ai-sdk/anthropic@0.0.41': - resolution: {integrity: sha512-ZGH0Xah9II4jEzDm/z+9G6qf0jC2vWRBURBDTWdQlVUI4COOMd8fmTrqwZrQuAl/y72vYakG5k2AKpJSnY6MeA==} + '@ai-sdk/anthropic@0.0.46': + resolution: {integrity: sha512-44tU2iXrMQmEv+UNZ7Yj9Vl8BM+emRPpDxC2ae94TEEZxyJiCzum6rZ1alc8n5Yq1t22S5JxhlGwpOT5+wV4zQ==} engines: {node: '>=18'} peerDependencies: zod: ^3.0.0 @@ -225,6 +228,15 @@ packages: zod: optional: true + '@ai-sdk/provider-utils@1.0.15': + resolution: {integrity: sha512-icZqf2kpV8XdSViei4pX9ylYcVn+pk9AnVquJJGjGQGnwZ/5OgShqnFcLYrMjQfQcSVkz0PxdQVsIhZHzlT9Og==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.0.0 + peerDependenciesMeta: + zod: + optional: true + '@ai-sdk/provider-utils@1.0.7': resolution: {integrity: sha512-xIDpinTnuInH16wBgKAVdN6pmrpjlJF+5f+f/8ahYMQlYzpe4Vj1qw8OL+rUGdCePcRSrFmjk8G/wdX5+J8dIw==} engines: {node: '>=18'} @@ -246,8 +258,12 @@ packages: resolution: {integrity: sha512-nCQZRUTi/+y+kf1ep9rujpbQEtsIwySzlQAudiFeVhzzDi9rYvWp5tOSVu8/ArT+i1xSc2tw40akxb1TX73ofQ==} engines: {node: '>=18'} - '@ai-sdk/react@0.0.43': - resolution: {integrity: sha512-maWuV9529tIVVST9iXgnxBWUoM5Z8DW0lyrMYnsaLJAZ4kostt+PbqJjhy6eAQPzmXGcu4+OdgT1v1ZNCZR4+Q==} + '@ai-sdk/provider@0.0.21': + resolution: {integrity: sha512-9j95uaPRxwYkzQdkl4XO/MmWWW5c5vcVSXtqvALpD9SMB9fzH46dO3UN4VbOJR2J3Z84CZAqgZu5tNlkptT9qQ==} + engines: {node: '>=18'} + + '@ai-sdk/react@0.0.48': + resolution: {integrity: sha512-KfW33Gj5/qDA6RWfJ42al3QsgIA2UO+x0gX1M6Kk6LY4bTFgy7+F4GLmo4eflM/9o2M7fUZrNddoOuJ15vbgZg==} engines: {node: '>=18'} peerDependencies: react: ^18 || ^19 @@ -258,8 +274,8 @@ packages: zod: optional: true - '@ai-sdk/solid@0.0.34': - resolution: {integrity: sha512-puVv9rrskWXrtaikDbpoMkGeTboa4ZY6wTmC66Xw9rhZ0zK5yN15lLJBf/LeBIV6J1V9F9bBxjRX7UQXjE3sZg==} + '@ai-sdk/solid@0.0.38': + resolution: {integrity: sha512-7pMW6leig8Y05UIL8jy/1dEDTjtfA2WG9qkVMWjnKSKiucT/Z5uOO3zWNHYq8EVwdJJnv+RR8gUASXcZLTh7og==} engines: {node: '>=18'} peerDependencies: solid-js: ^1.7.7 @@ -267,8 +283,8 @@ packages: solid-js: optional: true - '@ai-sdk/svelte@0.0.36': - resolution: {integrity: sha512-5pSaKt+UZK9+9AsbIYLs4REtAc/0HOLX4DK3nRtMcDqDLoWDoSJDKK/EjDMYVhYB1gqQmT0AeiSLo2WH0nf00w==} + '@ai-sdk/svelte@0.0.40': + resolution: {integrity: sha512-S62aB2aT7gjrVY2uDhxwZFBg9hl4wNwu+kd31zsowByC/yyZp9MRIMXkDCkj0qQLFXvfUzaUuzk8v9gvuPOFCQ==} engines: {node: '>=18'} peerDependencies: svelte: ^3.0.0 || ^4.0.0 @@ -276,8 +292,8 @@ packages: svelte: optional: true - '@ai-sdk/ui-utils@0.0.31': - resolution: {integrity: sha512-PA1mI+WC69Bc8JCTDOXwhLv9OAfocex/d+MRtQjfuWE6jTBjkBMa6davw+JjN7Vcp6zP0JLQG6gd7n6MnkFepQ==} + '@ai-sdk/ui-utils@0.0.35': + resolution: {integrity: sha512-JZWp5gbH9K0/qmmqv0JFrH97JNMB9dU1xtrR2a8uzRE0wYtNmd3KsM9x3KW/f9OGjxUHzAkrboMvxKv/3uz24w==} engines: {node: '>=18'} peerDependencies: zod: ^3.0.0 @@ -285,8 +301,8 @@ packages: zod: optional: true - '@ai-sdk/vue@0.0.35': - resolution: {integrity: sha512-7cPShsxiyxzoSB5orjCqwnFWvjpM/YX2W+a2K6lyV2Z2JAgHc+4PHhVnrKwc0c9Q7vwfpvW+3MoKM6U2xZaS+w==} + '@ai-sdk/vue@0.0.40': + resolution: {integrity: sha512-01LuQT+Cx2e19fYB4nlMlQhmpJ826S1HfGcB4BY30+/XOJebdHRPPOZ3WV9BytBD7kha/tnngBruiYzegGR+Ug==} engines: {node: '>=18'} peerDependencies: vue: ^3.3.4 @@ -985,6 +1001,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-progress@1.1.0': + resolution: {integrity: sha512-aSzvnYpP725CROcxAOEBVZZSIQVQdHgBr2QQFKySsaD14u8dNT0batuXI+AAGDdAHfXH8rbnHmjYFqVJ21KkRg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-scroll-area@1.1.0': resolution: {integrity: sha512-9ArIZ9HWhsrfqS765h+GZuLoxaRHD/j0ZWOWilsCvYTpYJp8XwCqNG7Dt9Nu/TItKOdgLGkOPCodQvDc+UMwYg==} peerDependencies: @@ -1506,8 +1535,8 @@ packages: resolution: {integrity: sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==} engines: {node: '>=8'} - ai@3.3.7: - resolution: {integrity: sha512-xMfQdOL2s0aiGozdUO0ahOAfcwkGBUye3q4wC64PPNpmE3Qeing1Tv4JSsHk0zymhCMHBDiI1Tky8BNGdu+V6A==} + ai@3.3.14: + resolution: {integrity: sha512-GF3CVS1rnOtgN68OQGlT/2quhg/D3sMFwak48OGXeqv4VRcDgGJx3UqSwT7ipFa9BncRqo7TIqDHHji3Doamaw==} engines: {node: '>=18'} peerDependencies: openai: ^4.42.0 @@ -2326,8 +2355,8 @@ packages: resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} engines: {node: '>=10'} - google-auth-library@9.13.0: - resolution: {integrity: sha512-p9Y03Uzp/Igcs36zAaB0XTSwZ8Y0/tpYiz5KIde5By+H9DCVUSYtDWZu6aFXsWTqENMb8BD/pDT3hR8NVrPkfA==} + google-auth-library@9.14.0: + resolution: {integrity: sha512-Y/eq+RWVs55Io/anIsm24sDS8X79Tq948zVLGaa7+KlJYYqaGwp1YI37w48nzrNi12RgnzMrQD4NzdmCowT90g==} engines: {node: '>=14'} gopd@1.0.1: @@ -3955,10 +3984,10 @@ snapshots: transitivePeerDependencies: - aws-crt - '@ai-sdk/anthropic@0.0.41(zod@3.23.8)': + '@ai-sdk/anthropic@0.0.46(zod@3.23.8)': dependencies: - '@ai-sdk/provider': 0.0.19 - '@ai-sdk/provider-utils': 1.0.11(zod@3.23.8) + '@ai-sdk/provider': 0.0.21 + '@ai-sdk/provider-utils': 1.0.15(zod@3.23.8) zod: 3.23.8 '@ai-sdk/cohere@0.0.17(zod@3.23.8)': @@ -4006,6 +4035,15 @@ snapshots: optionalDependencies: zod: 3.23.8 + '@ai-sdk/provider-utils@1.0.15(zod@3.23.8)': + dependencies: + '@ai-sdk/provider': 0.0.21 + eventsource-parser: 1.1.2 + nanoid: 3.3.6 + secure-json-parse: 2.7.0 + optionalDependencies: + zod: 3.23.8 + '@ai-sdk/provider-utils@1.0.7(zod@3.23.8)': dependencies: '@ai-sdk/provider': 0.0.15 @@ -4027,46 +4065,50 @@ snapshots: dependencies: json-schema: 0.4.0 - '@ai-sdk/react@0.0.43(react@18.3.1)(zod@3.23.8)': + '@ai-sdk/provider@0.0.21': dependencies: - '@ai-sdk/provider-utils': 1.0.11(zod@3.23.8) - '@ai-sdk/ui-utils': 0.0.31(zod@3.23.8) + json-schema: 0.4.0 + + '@ai-sdk/react@0.0.48(react@18.3.1)(zod@3.23.8)': + dependencies: + '@ai-sdk/provider-utils': 1.0.15(zod@3.23.8) + '@ai-sdk/ui-utils': 0.0.35(zod@3.23.8) swr: 2.2.5(react@18.3.1) optionalDependencies: react: 18.3.1 zod: 3.23.8 - '@ai-sdk/solid@0.0.34(zod@3.23.8)': + '@ai-sdk/solid@0.0.38(zod@3.23.8)': dependencies: - '@ai-sdk/provider-utils': 1.0.11(zod@3.23.8) - '@ai-sdk/ui-utils': 0.0.31(zod@3.23.8) + '@ai-sdk/provider-utils': 1.0.15(zod@3.23.8) + '@ai-sdk/ui-utils': 0.0.35(zod@3.23.8) transitivePeerDependencies: - zod - '@ai-sdk/svelte@0.0.36(svelte@4.2.18)(zod@3.23.8)': + '@ai-sdk/svelte@0.0.40(svelte@4.2.18)(zod@3.23.8)': dependencies: - '@ai-sdk/provider-utils': 1.0.11(zod@3.23.8) - '@ai-sdk/ui-utils': 0.0.31(zod@3.23.8) + '@ai-sdk/provider-utils': 1.0.15(zod@3.23.8) + '@ai-sdk/ui-utils': 0.0.35(zod@3.23.8) sswr: 2.1.0(svelte@4.2.18) optionalDependencies: svelte: 4.2.18 transitivePeerDependencies: - zod - '@ai-sdk/ui-utils@0.0.31(zod@3.23.8)': + '@ai-sdk/ui-utils@0.0.35(zod@3.23.8)': dependencies: - '@ai-sdk/provider': 0.0.19 - '@ai-sdk/provider-utils': 1.0.11(zod@3.23.8) + '@ai-sdk/provider': 0.0.21 + '@ai-sdk/provider-utils': 1.0.15(zod@3.23.8) json-schema: 0.4.0 secure-json-parse: 2.7.0 zod-to-json-schema: 3.22.5(zod@3.23.8) optionalDependencies: zod: 3.23.8 - '@ai-sdk/vue@0.0.35(vue@3.4.38(typescript@5.5.4))(zod@3.23.8)': + '@ai-sdk/vue@0.0.40(vue@3.4.38(typescript@5.5.4))(zod@3.23.8)': dependencies: - '@ai-sdk/provider-utils': 1.0.11(zod@3.23.8) - '@ai-sdk/ui-utils': 0.0.31(zod@3.23.8) + '@ai-sdk/provider-utils': 1.0.15(zod@3.23.8) + '@ai-sdk/ui-utils': 0.0.35(zod@3.23.8) swrv: 1.0.4(vue@3.4.38(typescript@5.5.4)) optionalDependencies: vue: 3.4.38(typescript@5.5.4) @@ -4602,7 +4644,7 @@ snapshots: '@google-cloud/vertexai@1.4.1(encoding@0.1.13)': dependencies: - google-auth-library: 9.13.0(encoding@0.1.13) + google-auth-library: 9.14.0(encoding@0.1.13) transitivePeerDependencies: - encoding - supports-color @@ -4933,6 +4975,16 @@ snapshots: '@types/react': 18.3.3 '@types/react-dom': 18.3.0 + '@radix-ui/react-progress@1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-context': 1.1.0(@types/react@18.3.3)(react@18.3.1) + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.3 + '@types/react-dom': 18.3.0 + '@radix-ui/react-scroll-area@1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/number': 1.1.0 @@ -5597,15 +5649,15 @@ snapshots: indent-string: 4.0.0 optional: true - ai@3.3.7(openai@4.55.7(encoding@0.1.13)(zod@3.23.8))(react@18.3.1)(sswr@2.1.0(svelte@4.2.18))(svelte@4.2.18)(vue@3.4.38(typescript@5.5.4))(zod@3.23.8): + ai@3.3.14(openai@4.55.7(encoding@0.1.13)(zod@3.23.8))(react@18.3.1)(sswr@2.1.0(svelte@4.2.18))(svelte@4.2.18)(vue@3.4.38(typescript@5.5.4))(zod@3.23.8): dependencies: - '@ai-sdk/provider': 0.0.19 - '@ai-sdk/provider-utils': 1.0.11(zod@3.23.8) - '@ai-sdk/react': 0.0.43(react@18.3.1)(zod@3.23.8) - '@ai-sdk/solid': 0.0.34(zod@3.23.8) - '@ai-sdk/svelte': 0.0.36(svelte@4.2.18)(zod@3.23.8) - '@ai-sdk/ui-utils': 0.0.31(zod@3.23.8) - '@ai-sdk/vue': 0.0.35(vue@3.4.38(typescript@5.5.4))(zod@3.23.8) + '@ai-sdk/provider': 0.0.21 + '@ai-sdk/provider-utils': 1.0.15(zod@3.23.8) + '@ai-sdk/react': 0.0.48(react@18.3.1)(zod@3.23.8) + '@ai-sdk/solid': 0.0.38(zod@3.23.8) + '@ai-sdk/svelte': 0.0.40(svelte@4.2.18)(zod@3.23.8) + '@ai-sdk/ui-utils': 0.0.35(zod@3.23.8) + '@ai-sdk/vue': 0.0.40(vue@3.4.38(typescript@5.5.4))(zod@3.23.8) '@opentelemetry/api': 1.9.0 eventsource-parser: 1.1.2 json-schema: 0.4.0 @@ -6263,8 +6315,8 @@ snapshots: '@typescript-eslint/parser': 7.2.0(eslint@8.57.0)(typescript@5.5.4) eslint: 8.57.0 eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1)(eslint@8.57.0) - eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0) + eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@8.57.0))(eslint@8.57.0) + eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0) eslint-plugin-jsx-a11y: 6.9.0(eslint@8.57.0) eslint-plugin-react: 7.35.0(eslint@8.57.0) eslint-plugin-react-hooks: 4.6.2(eslint@8.57.0) @@ -6282,13 +6334,13 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1)(eslint@8.57.0): + eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@8.57.0))(eslint@8.57.0): dependencies: debug: 4.3.6 enhanced-resolve: 5.17.1 eslint: 8.57.0 - eslint-module-utils: 2.8.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1)(eslint@8.57.0))(eslint@8.57.0) - eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0) + eslint-module-utils: 2.8.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0) + eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0) fast-glob: 3.3.2 get-tsconfig: 4.7.6 is-core-module: 2.15.0 @@ -6299,18 +6351,18 @@ snapshots: - eslint-import-resolver-webpack - supports-color - eslint-module-utils@2.8.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1)(eslint@8.57.0))(eslint@8.57.0): + eslint-module-utils@2.8.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0): dependencies: debug: 3.2.7 optionalDependencies: '@typescript-eslint/parser': 7.2.0(eslint@8.57.0)(typescript@5.5.4) eslint: 8.57.0 eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1)(eslint@8.57.0) + eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@8.57.0))(eslint@8.57.0) transitivePeerDependencies: - supports-color - eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0): + eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0): dependencies: array-includes: 3.1.8 array.prototype.findlastindex: 1.2.5 @@ -6320,7 +6372,7 @@ snapshots: doctrine: 2.1.0 eslint: 8.57.0 eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.8.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1)(eslint@8.57.0))(eslint@8.57.0) + eslint-module-utils: 2.8.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0) hasown: 2.0.2 is-core-module: 2.15.0 is-glob: 4.0.3 @@ -6671,7 +6723,7 @@ snapshots: merge2: 1.4.1 slash: 3.0.0 - google-auth-library@9.13.0(encoding@0.1.13): + google-auth-library@9.14.0(encoding@0.1.13): dependencies: base64-js: 1.5.1 ecdsa-sig-formatter: 1.0.11 diff --git a/prisma/migrations/20240824081904_add_cost_model/migration.sql b/prisma/migrations/20240824081904_add_cost_model/migration.sql new file mode 100644 index 0000000..8961b27 --- /dev/null +++ b/prisma/migrations/20240824081904_add_cost_model/migration.sql @@ -0,0 +1,32 @@ +/* + Warnings: + + - You are about to drop the column `configurationId` on the `Log` table. All the data in the column will be lost. + - Made the column `metadata` on table `Log` required. This step will fail if there are existing NULL values in that column. + +*/ +-- DropForeignKey +ALTER TABLE "Log" DROP CONSTRAINT "Log_configurationId_fkey"; + +-- AlterTable +ALTER TABLE "Log" DROP COLUMN "configurationId", +ALTER COLUMN "metadata" SET NOT NULL; + +-- CreateTable +CREATE TABLE "ModelCost" ( + "id" TEXT NOT NULL, + "provider" TEXT NOT NULL, + "model" TEXT NOT NULL, + "inputTokenCost" DOUBLE PRECISION NOT NULL, + "outputTokenCost" DOUBLE PRECISION NOT NULL, + "validFrom" TIMESTAMP(3) NOT NULL, + "validTo" TIMESTAMP(3), + + CONSTRAINT "ModelCost_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "ModelCost_provider_model_validFrom_key" ON "ModelCost"("provider", "model", "validFrom"); + +-- CreateIndex +CREATE INDEX "Log_timestamp_idx" ON "Log"("timestamp"); diff --git a/prisma/schema.prisma b/prisma/schema.prisma index e0b2642..aee2763 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -3,6 +3,7 @@ generator client { provider = "prisma-client-js" + binaryTargets = ["native", "linux-musl-openssl-3.0.x"] } datasource db { @@ -15,8 +16,8 @@ model Log { method String url String headers String - body String - response String + body Json + response Json timestamp DateTime @default(now()) metadata Json @@ -37,4 +38,16 @@ model AIConfiguration { apiKey String? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt +} + +model ModelCost { + id String @id @default(cuid()) + provider String + model String + inputTokenCost Float + outputTokenCost Float + validFrom DateTime + validTo DateTime? + + @@unique([provider, model, validFrom]) } \ No newline at end of file diff --git a/prisma/seed.ts b/prisma/seed.ts index 80ef3f4..e7bc434 100644 --- a/prisma/seed.ts +++ b/prisma/seed.ts @@ -1,15 +1,48 @@ -import { PrismaClient } from '@prisma/client'; +import { PrismaClient } from "@prisma/client"; +import { getModelConfigurations } from "../src/lib/model-config"; const prisma = new PrismaClient(); -const main = async () => { - await prisma.aIConfiguration.create({ - data: { - name: 'Default GPT-4o', - model: 'gpt-4o', - isDefault: true, - }, - }); -}; +async function main() { + const modelConfigurations = getModelConfigurations(); + + await prisma.modelCost.deleteMany({}); + + for (const [provider, models] of Object.entries(modelConfigurations)) { + for (const [model, config] of Object.entries(models)) { + if (config && "inputTokenCost" in config && "outputTokenCost" in config) { + await prisma.modelCost.upsert({ + where: { + provider_model_validFrom: { + provider, + model, + validFrom: new Date("2024-08-01"), + }, + }, + update: { + inputTokenCost: config.inputTokenCost, + outputTokenCost: config.outputTokenCost, + }, + create: { + provider, + model, + inputTokenCost: config.inputTokenCost, + outputTokenCost: config.outputTokenCost, + validFrom: new Date("2024-08-01"), + }, + }); + } + } + } -main(); + console.log("Seed data inserted successfully"); +} + +main() + .catch((e) => { + console.error(e); + process.exit(1); + }) + .finally(async () => { + await prisma.$disconnect(); + }); diff --git a/public/ant-cache-create.png b/public/ant-cache-create.png new file mode 100644 index 0000000..67921fc Binary files /dev/null and b/public/ant-cache-create.png differ diff --git a/public/ant-cache-read.png b/public/ant-cache-read.png new file mode 100644 index 0000000..f542624 Binary files /dev/null and b/public/ant-cache-read.png differ diff --git a/public/anthropicCashedXConfig.png b/public/anthropicCashedXConfig.png new file mode 100644 index 0000000..e25e84e Binary files /dev/null and b/public/anthropicCashedXConfig.png differ diff --git a/public/cl-stats.jpeg b/public/cl-stats.jpeg new file mode 100644 index 0000000..2364ce8 Binary files /dev/null and b/public/cl-stats.jpeg differ diff --git a/scripts/update-log-costs.ts b/scripts/update-log-costs.ts new file mode 100644 index 0000000..490f3f1 --- /dev/null +++ b/scripts/update-log-costs.ts @@ -0,0 +1,72 @@ +import { PrismaClient } from "@prisma/client"; +import { getModelCost } from "../src/lib/cost-calculator"; + +const prisma = new PrismaClient(); + +async function updateLogCosts() { + const logs = await prisma.log.findMany(); + + console.log(`Found ${logs.length} logs to update`); + + for (const log of logs) { + try { + const metadata = log.metadata as any; + const { provider, model } = metadata; + + if (!provider || !model) { + console.warn(`Skipping log ${log.id}: Missing provider or model`); + continue; + } + + const modelCost = await getModelCost(provider, model); + + let updatedMetadata = { ...metadata }; + let response = + typeof log.response === "string" + ? JSON.parse(log.response) + : log.response; + + // Extract token usage from response + const usage = response?.usage || {}; + const inputTokens = usage.promptTokens || metadata.inputTokens || 0; + const outputTokens = usage.completionTokens || metadata.outputTokens || 0; + const totalTokens = usage.totalTokens || inputTokens + outputTokens; + + // Calculate costs + const inputCost = (inputTokens / 1000000) * modelCost.inputTokenCost; + const outputCost = (outputTokens / 1000000) * modelCost.outputTokenCost; + const totalCost = inputCost + outputCost; + + updatedMetadata = { + ...updatedMetadata, + inputTokens, + outputTokens, + totalTokens, + inputCost, + outputCost, + totalCost, + }; + + await prisma.log.update({ + where: { id: log.id }, + data: { metadata: updatedMetadata }, + }); + + console.log( + `Updated log ${log.id}: inputTokens=${inputTokens}, outputTokens=${outputTokens}, totalCost=${totalCost}`, + ); + } catch (error) { + console.error(`Error updating log ${log.id}:`, error); + } + } + + console.log("Finished updating logs"); +} + +updateLogCosts() + .catch((error) => { + console.error("Error in updateLogCosts:", error); + }) + .finally(async () => { + await prisma.$disconnect(); + }); diff --git a/src/app/[...openai]/route.ts b/src/app/[...openai]/route.ts index 13c4e6c..e9e06a5 100644 --- a/src/app/[...openai]/route.ts +++ b/src/app/[...openai]/route.ts @@ -9,6 +9,7 @@ import { insertLog, getDefaultConfiguration } from "@/lib/db"; import { type NextRequest, NextResponse } from "next/server"; import OpenAI from "openai"; import { env } from "@/env"; +import { calculateCost, getModelCost } from "@/lib/cost-calculator"; const openaiClient = new OpenAI({ apiKey: env.OPENAI_API_KEY, @@ -27,6 +28,12 @@ async function getAIModelClient(provider: string, model: string) { }); return anthropicClient(model); } + case "anthropiccached": { + const anthropicClient = createAnthropic({ + apiKey: env.ANTHROPIC_API_KEY, + }); + return anthropicClient(model, { cacheControl: true }); + } case "cohere": { const cohereClient = createCohere({ apiKey: env.COHERE_API_KEY, @@ -49,6 +56,17 @@ async function getAIModelClient(provider: string, model: string) { return ollama("llama3.1"); case "google-vertex": throw new Error("Google Vertex AI is not currently supported"); + case "openrouter": { + const openrouterClient = createOpenAI({ + baseURL: "https://openrouter.ai/api/v1", + apiKey: env.OPENROUTER_API_KEY, + headers: { + "HTTP-Referer": "https://www.cursorlens.com", + "X-Title": "CursorLens", + }, + }); + return openrouterClient(model); + } default: throw new Error(`Unsupported provider: ${provider}`); } @@ -89,12 +107,56 @@ export async function POST( const aiModel = await getAIModelClient(provider, model); + let modifiedMessages = messages; + + if (provider.toLowerCase() === "anthropiccached") { + const hasPotentialContext = messages.some( + (message: any) => message.name === "potential_context", + ); + + modifiedMessages = messages.map((message: any) => { + if (message.name === "potential_context") { + return { + ...message, + experimental_providerMetadata: { + anthropic: { cacheControl: { type: "ephemeral" } }, + }, + }; + } + return message; + }); + + if (!hasPotentialContext && modifiedMessages.length >= 2) { + modifiedMessages[1] = { + ...modifiedMessages[1], + experimental_providerMetadata: { + anthropic: { cacheControl: { type: "ephemeral" } }, + }, + }; + } + } + + const streamTextOptions = { + model: aiModel, + messages: modifiedMessages, + maxTokens: ["anthropic", "anthropiccached"].includes( + provider.toLowerCase(), + ) + ? 8192 + : undefined, + // Add other parameters from defaultConfig if needed + }; + const logEntry = { method: "POST", url: `/api/${endpoint}`, headers: JSON.stringify(Object.fromEntries(request.headers)), - body: JSON.stringify(body), - response: "", + body: { + ...body, + ...streamTextOptions, + model: model, + }, + response: {}, timestamp: new Date(), metadata: { configId, @@ -105,21 +167,56 @@ export async function POST( topP, frequencyPenalty, presencePenalty, + inputTokens: 0, + outputTokens: 0, + totalTokens: 0, + inputCost: 0, + outputCost: 0, + totalCost: 0, }, }; if (stream) { const result = await streamText({ - model: aiModel, - messages, - async onFinish({ text, toolCalls, toolResults, usage, finishReason }) { - logEntry.response = JSON.stringify({ + ...streamTextOptions, + async onFinish({ + text, + toolCalls, + toolResults, + usage, + finishReason, + ...otherProps + }) { + const inputTokens = usage?.promptTokens ?? 0; + const outputTokens = usage?.completionTokens ?? 0; + const totalTokens = usage?.totalTokens ?? 0; + + const modelCost = (await getModelCost(provider, model)) || { + inputTokenCost: 0, + outputTokenCost: 0, + }; + const inputCost = (inputTokens * modelCost.inputTokenCost) / 1000000; + const outputCost = + (outputTokens * modelCost.outputTokenCost) / 1000000; + const totalCost = inputCost + outputCost; + + logEntry.response = { text, toolCalls, toolResults, usage, finishReason, - }); + ...otherProps, + }; + logEntry.metadata = { + ...logEntry.metadata, + inputTokens, + outputTokens, + totalTokens, + inputCost, + outputCost, + totalCost, + }; await insertLog(logEntry); }, }); @@ -163,7 +260,28 @@ export async function POST( messages, }); + const inputTokens = result.usage?.promptTokens ?? 0; + const outputTokens = result.usage?.completionTokens ?? 0; + const totalTokens = result.usage?.totalTokens ?? 0; + + const modelCost = (await getModelCost(provider, model)) || { + inputTokenCost: 0, + outputTokenCost: 0, + }; + const inputCost = inputTokens * modelCost.inputTokenCost; + const outputCost = outputTokens * modelCost.outputTokenCost; + const totalCost = inputCost + outputCost; + logEntry.response = JSON.stringify(result); + logEntry.metadata = { + ...logEntry.metadata, + inputTokens, + outputTokens, + totalTokens, + inputCost, + outputCost, + totalCost, + }; await insertLog(logEntry); return NextResponse.json(result); @@ -222,6 +340,8 @@ export async function GET( return testOpenAI(); } else if (endpoint === "test/anthropic") { return testAnthropic(); + } else if (endpoint === "test/anthropiccached") { + return testAnthropicCached(); } else if (endpoint === "test/cohere") { return testCohere(); } else if (endpoint === "test/mistral") { @@ -247,6 +367,25 @@ async function testOpenAI() { } } +async function testAnthropicCached() { + try { + const model = anthropic("claude-3-5-sonnet-20240620", { + cacheControl: true, + }); + + const result = await generateText({ + model, + messages: [ + { role: "user", content: 'Say "Hello from Anthropic and Vercel"' }, + ], + }); + return NextResponse.json({ provider: "Anthropic Cached", result }); + } catch (error) { + console.error("Error testing Anthropic:", error); + return NextResponse.json({ error: String(error) }, { status: 500 }); + } +} + async function testAnthropic() { try { const anthropicClient = createAnthropic({ diff --git a/src/app/actions.ts b/src/app/actions.ts index 2f17d91..cde8776 100644 --- a/src/app/actions.ts +++ b/src/app/actions.ts @@ -1,7 +1,8 @@ "use server"; +import { getModelConfigurations } from "@/lib/model-config"; import prisma from "@/lib/prisma"; -import { Log, AIConfiguration } from "@prisma/client"; +import type { Log, AIConfiguration, Prisma } from "@prisma/client"; // Helper function to serialize dates function serializeDates(obj: T): T { @@ -18,6 +19,10 @@ type LogMetadata = { temperature: number; presencePenalty: number; frequencyPenalty: number; + totalTokens: number; + inputTokens: number; + outputTokens: number; + totalCost: number; }; export async function getLogs({ @@ -26,40 +31,34 @@ export async function getLogs({ endDate = "", }: { provider?: string; startDate?: string; endDate?: string } = {}) { try { - let query: any = { + const query: Prisma.LogFindManyArgs = { orderBy: { timestamp: "desc", }, }; + const whereConditions: Prisma.LogWhereInput = {}; + if (provider !== "all") { - query.where = { - ...query.where, - metadata: { - path: ["provider"], - equals: provider, - }, + whereConditions.metadata = { + path: ["provider"], + equals: provider, }; } - if (startDate) { - query.where = { - ...query.where, - timestamp: { - ...query.where?.timestamp, - gte: new Date(startDate), - }, - }; + if (startDate || endDate) { + whereConditions.timestamp = {}; + if (startDate) { + whereConditions.timestamp.gte = new Date(startDate); + } + if (endDate) { + whereConditions.timestamp.lte = new Date(endDate); + } } - if (endDate) { - query.where = { - ...query.where, - timestamp: { - ...query.where?.timestamp, - lte: new Date(endDate), - }, - }; + // Only add the where clause if we have conditions + if (Object.keys(whereConditions).length > 0) { + query.where = whereConditions; } const logs = await prisma.log.findMany(query); @@ -75,18 +74,27 @@ export async function getLogs({ } } -export async function getStats(timeFilter: string = "all"): Promise<{ +export async function getStats(timeFilter = "all"): Promise<{ totalLogs: number; totalTokens: number; - perModelStats: { + totalPromptTokens: number; + totalCompletionTokens: number; + perModelProviderStats: { [key: string]: { logs: number; tokens: number; + promptTokens: number; + completionTokens: number; + cost: number; + provider: string; + model: string; }; }; tokenUsageOverTime: { date: string; tokens: number; + promptTokens: number; + completionTokens: number; }[]; }> { let startDate = new Date(0); // Default to all time @@ -114,48 +122,85 @@ export async function getStats(timeFilter: string = "all"): Promise<{ }, }); - const perModelStats: { + const perModelProviderStats: { [key: string]: { logs: number; tokens: number; + promptTokens: number; + completionTokens: number; + cost: number; + provider: string; + model: string; }; } = {}; let totalTokens = 0; - const tokenUsageOverTime: { date: string; tokens: number }[] = []; + let totalPromptTokens = 0; + let totalCompletionTokens = 0; + const tokenUsageOverTime: { + date: string; + tokens: number; + promptTokens: number; + completionTokens: number; + }[] = []; - logs.forEach((log) => { - const metadata = log.metadata as any; + for (const log of logs) { + const metadata = log.metadata as LogMetadata; const model = metadata.model || "unknown"; - if (!perModelStats[model]) { - perModelStats[model] = { logs: 0, tokens: 0 }; + const provider = metadata.provider || "unknown"; + const key = `${provider}:${model}`; + + if (!perModelProviderStats[key]) { + perModelProviderStats[key] = { + logs: 0, + tokens: 0, + promptTokens: 0, + completionTokens: 0, + cost: 0, + provider, + model, + }; } - perModelStats[model].logs += 1; - - try { - const responseObj = JSON.parse(log.response); - const tokens = responseObj.usage?.totalTokens || 0; - perModelStats[model].tokens += tokens; - totalTokens += tokens; - - const date = log.timestamp.toISOString().split("T")[0]; - const existingEntry = tokenUsageOverTime.find( - (entry) => entry.date === date, - ); - if (existingEntry) { - existingEntry.tokens += tokens; - } else { - tokenUsageOverTime.push({ date, tokens }); - } - } catch (error) { - console.error("Error parsing log response:", error); + perModelProviderStats[key].logs += 1; + + const tokens = metadata.totalTokens || 0; + const promptTokens = metadata.inputTokens || 0; + const completionTokens = metadata.outputTokens || 0; + const cost = metadata.totalCost || 0; + + perModelProviderStats[key].tokens += tokens; + perModelProviderStats[key].promptTokens += promptTokens; + perModelProviderStats[key].completionTokens += completionTokens; + perModelProviderStats[key].cost += cost; + + totalTokens += tokens; + totalPromptTokens += promptTokens; + totalCompletionTokens += completionTokens; + + const date = log.timestamp.toISOString().split("T")[0]; + const existingEntry = tokenUsageOverTime.find( + (entry) => entry.date === date, + ); + if (existingEntry) { + existingEntry.tokens += tokens; + existingEntry.promptTokens += promptTokens; + existingEntry.completionTokens += completionTokens; + } else { + tokenUsageOverTime.push({ + date, + tokens, + promptTokens, + completionTokens, + }); } - }); + } return { totalLogs: logs.length, totalTokens, - perModelStats, + totalPromptTokens, + totalCompletionTokens, + perModelProviderStats, tokenUsageOverTime, }; } @@ -178,15 +223,43 @@ export async function updateDefaultConfiguration( }); } -export async function createConfiguration( - data: Partial, -): Promise { +export async function createConfiguration(config: Partial) { + const { + name, + provider, + model, + temperature, + maxTokens, + topP, + frequencyPenalty, + presencePenalty, + isDefault, + apiKey, + } = config; + + // TODO: Consider using Zod schemas for validation and potentially integrate + // https://github.com/vantezzen/auto-form for form generation and validation + + // Guard clause to ensure required fields are present + if (!name || !provider || !model) { + throw new Error("Name, provider, and model are required fields"); + } + const newConfig = await prisma.aIConfiguration.create({ data: { - ...data, - isDefault: false, // Ensure new configurations are not default by default - } as AIConfiguration, + name, + provider, + model, + temperature: temperature, + maxTokens: maxTokens, + topP: topP, + frequencyPenalty: frequencyPenalty, + presencePenalty: presencePenalty, + isDefault: isDefault, + apiKey: apiKey, + }, }); + return serializeDates(newConfig); } @@ -207,14 +280,46 @@ export async function deleteConfiguration(id: string): Promise { }); } -export async function getConfigurationCosts(): Promise< - { provider: string; model: string; cost: number }[] -> { - // This is a placeholder function. In a real-world scenario, you would - // fetch this data from an API or database containing up-to-date pricing information. - return [ - { provider: "openai", model: "gpt-3.5-turbo", cost: 0.002 }, - { provider: "openai", model: "gpt-4", cost: 0.03 }, - { provider: "anthropic", model: "claude-2", cost: 0.01 }, - ]; +type ConfigurationCost = { + provider: string; + model: string; + inputTokenCost: number; + outputTokenCost: number; +}; + +export async function getConfigurationCosts(): Promise { + const modelConfigurations = getModelConfigurations(); + return Object.entries(modelConfigurations).flatMap(([provider, models]) => + Object.entries(models) + .filter( + (entry): entry is [string, NonNullable<(typeof entry)[1]>] => + entry[1] !== null && + "inputTokenCost" in entry[1] && + "outputTokenCost" in entry[1], + ) + .map(([model, config]) => ({ + provider, + model, + inputTokenCost: config.inputTokenCost, + outputTokenCost: config.outputTokenCost, + })), + ); +} + +export { getModelConfigurations }; + +export async function setDefaultConfiguration(configId: string): Promise { + try { + await prisma.aIConfiguration.updateMany({ + where: { isDefault: true }, + data: { isDefault: false }, + }); + await prisma.aIConfiguration.update({ + where: { id: configId }, + data: { isDefault: true }, + }); + } catch (error) { + console.error("Error setting default configuration:", error); + throw error; + } } diff --git a/src/app/api/configurations/route.ts b/src/app/api/configurations/route.ts index 3daab13..3592511 100644 --- a/src/app/api/configurations/route.ts +++ b/src/app/api/configurations/route.ts @@ -1,49 +1,70 @@ -import { NextRequest, NextResponse } from 'next/server'; -import { PrismaClient } from '@prisma/client'; - -const prisma = new PrismaClient(); +import { NextRequest, NextResponse } from "next/server"; +import { + getConfigurations, + createConfiguration, + updateConfiguration, + deleteConfiguration, +} from "@/app/actions"; export async function GET() { try { - if (!prisma) { - throw new Error('Prisma client is not initialized'); - } - const configurations = await prisma.aIConfiguration.findMany(); + const configurations = await getConfigurations(); return NextResponse.json(configurations); } catch (error) { - console.error('Error fetching configurations:', error); + console.error("Error fetching configurations:", error); return NextResponse.json( - { error: 'Internal Server Error', details: error.message }, - { status: 500 } + { error: "Error fetching configurations" }, + { status: 500 }, ); } } export async function POST(request: NextRequest) { try { - const data = await request.json(); - const { name, ...configData } = data; + const configData = await request.json(); + const newConfig = await createConfiguration(configData); + return NextResponse.json(newConfig); + } catch (error) { + console.error("Error creating configuration:", error); + return NextResponse.json( + { error: "Error creating configuration" }, + { status: 500 }, + ); + } +} - if (configData.isDefault) { - // If the new configuration is set as default, unset the current default - await prisma.aIConfiguration.updateMany({ - where: { isDefault: true }, - data: { isDefault: false }, - }); - } +export async function PUT(request: NextRequest) { + try { + const { id, ...data } = await request.json(); + const updatedConfig = await updateConfiguration(id, data); + return NextResponse.json(updatedConfig); + } catch (error) { + console.error("Error updating configuration:", error); + return NextResponse.json( + { error: "Error updating configuration" }, + { status: 500 }, + ); + } +} - const updatedConfig = await prisma.aIConfiguration.upsert({ - where: { name }, - update: configData, - create: { name, ...configData }, - }); +export async function DELETE(request: NextRequest) { + const { searchParams } = request.nextUrl; + const id = searchParams.get("id"); + if (!id) { + return NextResponse.json( + { error: "Configuration ID is required" }, + { status: 400 }, + ); + } - return NextResponse.json(updatedConfig); + try { + await deleteConfiguration(id); + return NextResponse.json({ message: "Configuration deleted successfully" }); } catch (error) { - console.error('Error updating configuration:', error); + console.error("Error deleting configuration:", error); return NextResponse.json( - { error: 'Internal Server Error' }, - { status: 500 } + { error: "Error deleting configuration" }, + { status: 500 }, ); } } diff --git a/src/app/api/logs/route.ts b/src/app/api/logs/route.ts index 748612b..5b09ac3 100644 --- a/src/app/api/logs/route.ts +++ b/src/app/api/logs/route.ts @@ -1,36 +1,32 @@ // app/api/logs/route.ts -import { type NextRequest, NextResponse } from "next/server"; -import { getLogs } from "@/lib/db"; -import prisma from "@/lib/prisma"; +import { NextRequest, NextResponse } from "next/server"; +import { getLogs } from "@/app/actions"; +import prisma from "@/lib/prisma"; // Make sure to import prisma client -export async function GET(request: NextRequest): Promise { +export async function GET(request: NextRequest) { const { searchParams } = request.nextUrl; - const filterHomepage = searchParams.get("filterHomepage") === "true"; - const logs = await getLogs(filterHomepage); - return NextResponse.json(logs); -} + const provider = searchParams.get("provider") || "all"; + const startDate = searchParams.get("startDate") || ""; + const endDate = searchParams.get("endDate") || ""; -interface LogData { - method: string; - url: string; - headers: string; - body: string; - response: string; - model: string; + try { + const logs = await getLogs({ provider, startDate, endDate }); + return NextResponse.json(logs); + } catch (error) { + console.error("Error fetching logs:", error); + return NextResponse.json({ error: "Error fetching logs" }, { status: 500 }); + } } -export async function POST(request: NextRequest): Promise { +export async function POST(request: NextRequest) { try { - const { method, url, headers, body, response, model }: LogData = - await request.json(); + const logData = await request.json(); const log = await prisma.log.create({ data: { - method, - url, - headers: JSON.stringify(headers), - body, - response, - metadata: { model }, + ...logData, + metadata: logData.metadata as any, + response: logData.response as any, // Ensure response is stored as Json + timestamp: new Date(), }, }); return NextResponse.json(log); diff --git a/src/app/api/ngrok-url/route.ts b/src/app/api/ngrok-url/route.ts new file mode 100644 index 0000000..3e302ca --- /dev/null +++ b/src/app/api/ngrok-url/route.ts @@ -0,0 +1,26 @@ +import { NextResponse } from "next/server"; + +export async function GET() { + try { + const response = await fetch("http://localhost:4040/api/tunnels"); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const data = await response.json(); + const ngrokUrl = data.tunnels[0]?.public_url; + + if (!ngrokUrl) { + throw new Error("No active ngrok tunnel found"); + } + + return NextResponse.json({ ngrokUrl }); + } catch (error) { + console.error("Failed to fetch ngrok URL:", error); + return NextResponse.json( + { error: "Failed to fetch ngrok URL" }, + { status: 500 }, + ); + } +} diff --git a/src/app/api/stats/route.ts b/src/app/api/stats/route.ts index d3aac48..e9056b2 100644 --- a/src/app/api/stats/route.ts +++ b/src/app/api/stats/route.ts @@ -1,21 +1,18 @@ -import { NextResponse } from 'next/server'; -import prisma from '@/lib/prisma'; +import { NextRequest, NextResponse } from "next/server"; +import { getStats } from "@/app/actions"; -export async function GET() { - try { - const logs = await prisma.log.findMany(); - const totalLogs = logs.length; - const totalTokens = logs.reduce((sum, log) => { - const response = JSON.parse(log.response); - return sum + (response.usage?.total_tokens || 0); - }, 0); +export async function GET(request: NextRequest) { + const { searchParams } = request.nextUrl; + const timeFilter = searchParams.get("timeFilter") || "all"; - return NextResponse.json({ totalLogs, totalTokens }); + try { + const stats = await getStats(timeFilter); + return NextResponse.json(stats); } catch (error) { - console.error('Error fetching stats:', error); + console.error("Error fetching stats:", error); return NextResponse.json( - { error: 'Error fetching stats' }, - { status: 500 } + { error: "Error fetching stats" }, + { status: 500 }, ); } } diff --git a/src/app/configurations/page.tsx b/src/app/configurations/page.tsx index 2cc28f0..2527f2d 100644 --- a/src/app/configurations/page.tsx +++ b/src/app/configurations/page.tsx @@ -1,6 +1,6 @@ "use client"; -import { useState, useEffect, useMemo } from "react"; +import { useState, useEffect, useMemo, useCallback } from "react"; import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; @@ -34,6 +34,10 @@ import { updateDefaultConfiguration, createConfiguration, } from "../actions"; +import { + getModelConfigurations, + type ModelConfigurations, +} from "@/lib/model-config"; interface AIConfiguration { id: string; @@ -51,178 +55,6 @@ interface AIConfiguration { apiKey: string | null; } -const configTemplates = [ - { - name: "OpenAI GPT-4", - provider: "openai", - model: "gpt-4-turbo", - temperature: 0.7, - maxTokens: 8192, - topP: 1, - frequencyPenalty: 0, - presencePenalty: 0, - }, - { - name: "OpenAI GPT-4 Optimized", - provider: "openai", - model: "gpt-4o", - temperature: 0.7, - maxTokens: 8192, - topP: 1, - frequencyPenalty: 0, - presencePenalty: 0, - }, - { - name: "OpenAI GPT-4 Mini", - provider: "openai", - model: "gpt-4o-mini", - temperature: 0.7, - maxTokens: 4096, - topP: 1, - frequencyPenalty: 0, - presencePenalty: 0, - }, - { - name: "Anthropic Claude 3.5 Sonnet", - provider: "anthropic", - model: "claude-3-5-sonnet-20240620", - temperature: 0.7, - maxTokens: 200000, - topP: 1, - frequencyPenalty: 0, - presencePenalty: 0, - }, - { - name: "Mistral Large", - provider: "mistral", - model: "mistral-large-latest", - temperature: 0.7, - maxTokens: 32768, - topP: 1, - frequencyPenalty: 0, - presencePenalty: 0, - }, - { - name: "Groq LLaMA 3.1", - provider: "groq", - model: "llama-3.1-70b-versatile", - temperature: 0.7, - maxTokens: 32768, - topP: 1, - frequencyPenalty: 0, - presencePenalty: 0, - }, -]; - -const providerModels = { - openai: ["gpt-4o", "gpt-4o-mini", "gpt-4-turbo"], - anthropic: [ - "claude-3-5-sonnet-20240620", - "claude-3-opus-20240229", - "claude-3-sonnet-20240229", - "claude-3-haiku-20240307", - ], - cohere: ["command-r", "command-r-plus"], - mistral: [ - "mistral-large-latest", - "mistral-medium-latest", - "mistral-small-latest", - "open-mistral-nemo", - "open-mixtral-8x22b", - "open-mixtral-8x7b", - "open-mistral-7b", - ], - groq: [ - "llama-3.1-405b-reasoning", - "llama-3.1-70b-versatile", - "llama-3.1-8b-instant", - "mixtral-8x7b-32768", - "gemma2-9b-it", - ], - ollama: [ - "llama3.1", - "codegemma", - "codegemma:2b", - "codegemma:7b", - "codellama", - "codellama:7b", - "codellama:13b", - "codellama:34b", - "codellama:70b", - "codellama:code", - "codellama:python", - "command-r", - "command-r:35b", - "command-r-plus", - "command-r-plus:104b", - "deepseek-coder-v2", - "deepseek-coder-v2:16b", - "deepseek-coder-v2:236b", - "falcon2", - "falcon2:11b", - "gemma", - "gemma:2b", - "gemma:7b", - "gemma2", - "gemma2:2b", - "gemma2:9b", - "gemma2:27b", - "llama2", - "llama2:7b", - "llama2:13b", - "llama2:70b", - "llama3", - "llama3:8b", - "llama3:70b", - "llama3-chatqa", - "llama3-chatqa:8b", - "llama3-chatqa:70b", - "llama3-gradient", - "llama3-gradient:8b", - "llama3-gradient:70b", - "llama3.1", - "llama3.1:8b", - "llama3.1:70b", - "llama3.1:405b", - "llava", - "llava:7b", - "llava:13b", - "llava:34b", - "llava-llama3", - "llava-llama3:8b", - "llava-phi3", - "llava-phi3:3.8b", - "mistral", - "mistral:7b", - "mistral-large", - "mistral-large:123b", - "mistral-nemo", - "mistral-nemo:12b", - "mixtral", - "mixtral:8x7b", - "mixtral:8x22b", - "moondream", - "moondream:1.8b", - "openhermes", - "openhermes:v2.5", - "qwen", - "qwen:7b", - "qwen:14b", - "qwen:32b", - "qwen:72b", - "qwen:110b", - "qwen2", - "qwen2:0.5b", - "qwen2:1.5b", - "qwen2:7b", - "qwen2:72b", - "phi3", - "phi3:3.8b", - "phi3:14b", - ], - other: ["Other"], -}; - export default function ConfigurationsPage() { const [configurations, setConfigurations] = useState([]); const [newConfig, setNewConfig] = useState>({}); @@ -232,6 +64,10 @@ export default function ConfigurationsPage() { const [selectedModel, setSelectedModel] = useState(""); const [customProvider, setCustomProvider] = useState(""); const [customModel, setCustomModel] = useState(""); + const modelConfigurations: ModelConfigurations = useMemo( + () => getModelConfigurations(), + [], + ); const sortedConfigurations = useMemo(() => { return [...configurations].sort((a, b) => { @@ -246,7 +82,7 @@ export default function ConfigurationsPage() { fetchConfigurations(); }, []); - const fetchConfigurations = async () => { + const fetchConfigurations = useCallback(async () => { try { const configData = await getConfigurations(); setConfigurations(configData as AIConfiguration[]); @@ -254,9 +90,14 @@ export default function ConfigurationsPage() { console.error("Error fetching configurations:", error); setError("Error loading configurations"); } - }; + }, []); + + const handleCreateConfig = useCallback(async () => { + if (!newConfig.name || !newConfig.provider || !newConfig.model) { + setError("Name, provider, and model are required fields"); + return; + } - const handleCreateConfig = async () => { try { await createConfiguration(newConfig); setIsDialogOpen(false); @@ -264,55 +105,136 @@ export default function ConfigurationsPage() { fetchConfigurations(); } catch (error) { console.error("Error creating configuration:", error); - setError("Error creating configuration"); + // TODO: Implement better error handling and user feedback + // - Handle specific errors (e.g., unique constraint violations) + // - Display error messages in the UI (e.g., using a toast notification) + // - Highlight fields with errors + // - Prevent dialog from closing on error + if ( + error instanceof Error && + error.message.includes("Unique constraint failed") + ) { + setError( + "A configuration with this name already exists. Please choose a different name.", + ); + } else { + setError("Error creating configuration. Please try again."); + } } - }; + }, [newConfig, fetchConfigurations]); - const handleToggleDefault = async (configId: string, isDefault: boolean) => { - try { - await updateDefaultConfiguration(configId); - fetchConfigurations(); - } catch (error) { - console.error("Error updating default configuration:", error); - setError("Error updating default configuration"); - } - }; + const handleToggleDefault = useCallback( + async (configId: string, isDefault: boolean) => { + try { + await updateDefaultConfiguration(configId); + fetchConfigurations(); + } catch (error) { + console.error("Error updating default configuration:", error); + setError("Error updating default configuration"); + } + }, + [fetchConfigurations], + ); - const handleTemplateSelect = (template: (typeof configTemplates)[0]) => { - const readableName = `${template.provider.charAt(0).toUpperCase() + template.provider.slice(1)} ${template.model}`; - setNewConfig({ ...template, name: readableName }); - setSelectedProvider(template.provider); - setSelectedModel(template.model); - }; + // TODO: Implement proper handling when turning off a default configuration + // - Prevent turning off the last default configuration + // - Show a toast message explaining why the action is not allowed + // - Implement logic to ensure at least one configuration is always set as default - const handleProviderChange = (value: string) => { - setSelectedProvider(value); - setSelectedModel(""); - setNewConfig({ - ...newConfig, - provider: value === "other" ? "" : value, - model: "", - }); - setCustomProvider(""); - }; + const handleTemplateSelect = useCallback( + (provider: string, model: string): void => { + const providerConfigs = modelConfigurations[provider]; - const handleModelChange = (value: string) => { - setSelectedModel(value); - setNewConfig({ ...newConfig, model: value === "Other" ? "" : value }); - setCustomModel(""); - }; + if (!providerConfigs) { + console.error(`No configurations found for provider: ${provider}`); + return; + } + + const config = providerConfigs[model]; + + if (!config || !("isTemplate" in config) || !config.isTemplate) { + console.error( + `No valid template configuration found for model: ${model}`, + ); + return; + } - const handleCustomProviderChange = ( - e: React.ChangeEvent, - ) => { - setCustomProvider(e.target.value); - setNewConfig({ ...newConfig, provider: e.target.value }); - }; + const readableName = `${provider.charAt(0).toUpperCase() + provider.slice(1)} ${model}`; + + setNewConfig({ + ...config, + name: readableName, + provider, + model, + }); + setSelectedProvider(provider); + setSelectedModel(model); + }, + [modelConfigurations], + ); + + const templateButtons = useMemo( + () => + Object.entries(modelConfigurations).flatMap(([provider, models]) => + Object.entries(models) + .filter( + ([_, config]) => + config && "isTemplate" in config && config.isTemplate, + ) + .map(([model, config]) => ( + + )), + ), + [modelConfigurations, handleTemplateSelect], + ); - const handleCustomModelChange = (e: React.ChangeEvent) => { - setCustomModel(e.target.value); - setNewConfig({ ...newConfig, model: e.target.value }); - }; + const handleProviderChange = useCallback( + (value: string) => { + setSelectedProvider(value); + setSelectedModel(""); + setNewConfig({ + ...newConfig, + provider: value === "other" ? "" : value, + model: "", + }); + setCustomProvider(""); + }, + [newConfig], + ); + + const handleModelChange = useCallback( + (value: string) => { + setSelectedModel(value); + setNewConfig({ ...newConfig, model: value === "other" ? "" : value }); + setCustomModel(""); + }, + [newConfig], + ); + + const handleCustomProviderChange = useCallback( + (e: React.ChangeEvent) => { + setCustomProvider(e.target.value); + setNewConfig({ ...newConfig, provider: e.target.value }); + }, + [newConfig], + ); + + const handleCustomModelChange = useCallback( + (e: React.ChangeEvent) => { + setCustomModel(e.target.value); + setNewConfig({ ...newConfig, model: e.target.value }); + }, + [newConfig], + ); return (
@@ -332,6 +254,7 @@ export default function ConfigurationsPage() { Temperature Max Tokens Default + {/* TODO: Add an "Actions" column for edit functionality */} @@ -350,6 +273,7 @@ export default function ConfigurationsPage() { } /> + {/* TODO: Add an edit button or icon in this cell */} ))} @@ -410,7 +334,7 @@ export default function ConfigurationsPage() { - {Object.keys(providerModels).map((provider) => ( + {Object.keys(modelConfigurations).map((provider) => ( {provider} @@ -443,16 +367,16 @@ export default function ConfigurationsPage() { {selectedProvider && - providerModels[ - selectedProvider as keyof typeof providerModels - ].map((model) => ( + Object.keys( + modelConfigurations[selectedProvider] || {}, + ).map((model) => ( {model} ))} - {selectedModel === "Other" && ( + {selectedModel === "other" && (
+ {error && ( +
{error}
+ )}
diff --git a/src/app/page.tsx b/src/app/page.tsx index 7bd3636..5b2b610 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -13,7 +13,9 @@ import LogsList from "../components/LogsList"; import Link from "next/link"; import { useRouter } from "next/navigation"; import { Label } from "@/components/ui/label"; -import { ArrowRight } from "lucide-react"; +import { ArrowRight, Copy } from "lucide-react"; +import { toast } from "sonner"; + import { getLogs, getStats, @@ -22,13 +24,14 @@ import { createConfiguration, } from "./actions"; import { Skeleton } from "@/components/ui/skeleton"; +import { Button } from "@/components/ui/button"; interface Log { id: string; method: string; url: string; - headers: string; - body: string; + headers: Record; + body: any; response: string; timestamp: Date; metadata: any; @@ -68,6 +71,7 @@ export default function Home() { const [newConfigModel, setNewConfigModel] = useState(""); const [newConfigProvider, setNewConfigProvider] = useState(""); const [isDialogOpen, setIsDialogOpen] = useState(false); + const [ngrokUrl, setNgrokUrl] = useState(null); useEffect(() => { const fetchData = async () => { @@ -94,6 +98,20 @@ export default function Home() { fetchData(); }, []); + useEffect(() => { + const fetchNgrokUrl = async () => { + try { + const response = await fetch("/api/ngrok-url"); + const data = await response.json(); + console.log(data); + setNgrokUrl(data.ngrokUrl); + } catch (error) { + console.error("Error fetching ngrok URL:", error); + } + }; + fetchNgrokUrl(); + }, []); + const handleConfigChange = async (configName: string) => { setSelectedConfig(configName); try { @@ -241,6 +259,48 @@ export default function Home() { + {ngrokUrl ? ( + + + Ngrok Public URL + + +
+

Add this as the base URL in Cursor:

+ + {`${ngrokUrl}/v1`} + + +
+
+
+ ) : ( + + + Ngrok Public URL + + +

+ Ngrok URL not available. Please run ngrok to generate a public + URL. +

+

Run the following command in your terminal:

+ + ngrok http 3000 + +
+
+ )} + Recent Logs diff --git a/src/app/stats/page.tsx b/src/app/stats/page.tsx index 3a8eac0..e726395 100644 --- a/src/app/stats/page.tsx +++ b/src/app/stats/page.tsx @@ -3,7 +3,7 @@ import { useState, useEffect } from "react"; import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card"; import { getStats, getConfigurations, getConfigurationCosts } from "../actions"; -import { AIConfiguration } from "@prisma/client"; +import type { AIConfiguration } from "@prisma/client"; import { BarChart, Bar, @@ -32,17 +32,30 @@ import { SelectValue, } from "@/components/ui/select"; import { Skeleton } from "@/components/ui/skeleton"; +import { Checkbox } from "@/components/ui/checkbox"; interface Stats { totalLogs: number; totalTokens: number; - perModelStats: { + totalPromptTokens: number; + totalCompletionTokens: number; + perModelProviderStats: { [key: string]: { logs: number; tokens: number; + promptTokens: number; + completionTokens: number; cost: number; + provider: string; + model: string; }; }; + tokenUsageOverTime: { + date: string; + tokens: number; + promptTokens: number; + completionTokens: number; + }[]; } const chartConfig = { @@ -51,12 +64,20 @@ const chartConfig = { color: "hsl(var(--chart-1))", }, tokens: { - label: "Tokens", + label: "Total Tokens", color: "hsl(var(--chart-2))", }, + promptTokens: { + label: "Input Tokens", + color: "hsl(var(--chart-3))", + }, + completionTokens: { + label: "Output Tokens", + color: "hsl(var(--chart-4))", + }, cost: { label: "Cost ($)", - color: "hsl(var(--chart-3))", + color: "hsl(var(--chart-5))", }, }; @@ -69,6 +90,11 @@ export default function StatsPage() { const [tokenUsageOverTime, setTokenUsageOverTime] = useState< { date: string; tokens: number }[] >([]); + const [selectedMetrics, setSelectedMetrics] = useState([ + "logs", + "tokens", + "cost", + ]); useEffect(() => { const fetchData = async () => { @@ -79,30 +105,44 @@ export default function StatsPage() { getConfigurationCosts(), ]); - const perModelStats: Stats["perModelStats"] = {}; - configData.forEach((config) => { - perModelStats[config.model] = { + const perModelProviderStats: Stats["perModelProviderStats"] = {}; + for (const config of configData) { + perModelProviderStats[`${config.provider}:${config.model}`] = { logs: 0, tokens: 0, + promptTokens: 0, + completionTokens: 0, cost: 0, + provider: config.provider, + model: config.model, }; - }); + } - Object.entries(statsData.perModelStats).forEach( - ([model, modelStats]) => { - perModelStats[model] = { - ...modelStats, - cost: - modelStats.tokens * - (costsData.find((c) => c.model === model)?.cost || 0), - }; - }, - ); + for (const [key, modelStats] of Object.entries( + statsData.perModelProviderStats, + )) { + const [provider, model] = key.split(":"); + const costData = costsData.find( + (c) => c.provider === provider && c.model === model, + ); + const inputTokenCost = costData?.inputTokenCost || 0; + const outputTokenCost = costData?.outputTokenCost || 0; + + perModelProviderStats[key] = { + ...modelStats, + cost: + modelStats.promptTokens * inputTokenCost + + modelStats.completionTokens * outputTokenCost, + }; + } setStats({ totalLogs: statsData.totalLogs, totalTokens: statsData.totalTokens, - perModelStats, + totalPromptTokens: statsData.totalPromptTokens, + totalCompletionTokens: statsData.totalCompletionTokens, + perModelProviderStats, + tokenUsageOverTime: statsData.tokenUsageOverTime, }); setTokenUsageOverTime(statsData.tokenUsageOverTime); setConfigurations(configData); @@ -116,13 +156,21 @@ export default function StatsPage() { fetchData(); }, [timeFilter]); + const handleMetricToggle = (metric: string) => { + setSelectedMetrics((prev) => + prev.includes(metric) + ? prev.filter((m) => m !== metric) + : [...prev, metric], + ); + }; + if (loading) { return (
- {[...Array(3)].map((_, i) => ( - + {["card1", "card2", "card3"].map((key) => ( + @@ -132,8 +180,8 @@ export default function StatsPage() { ))}
- {[...Array(3)].map((_, i) => ( - + {["card1", "card2", "card3"].map((key) => ( + @@ -163,18 +211,21 @@ export default function StatsPage() { if (!stats) return null; - const chartData = Object.entries(stats.perModelStats).map( - ([model, data]) => ({ - model, + const chartData = Object.entries(stats.perModelProviderStats).map( + ([key, data]) => ({ + provider: data.provider, + model: data.model, logs: data.logs, tokens: data.tokens, + promptTokens: data.promptTokens, + completionTokens: data.completionTokens, cost: data.cost, }), ); - const pieChartData = Object.entries(stats.perModelStats).map( - ([model, data]) => ({ - name: model, + const pieChartData = Object.entries(stats.perModelProviderStats).map( + ([key, data]) => ({ + name: key, value: data.logs, }), ); @@ -230,7 +281,7 @@ export default function StatsPage() {

$ - {Object.values(stats.perModelStats) + {Object.values(stats.perModelProviderStats) .reduce((sum, data) => sum + data.cost, 0) .toFixed(2)}

@@ -238,9 +289,27 @@ export default function StatsPage() {
+
+ {Object.entries(chartConfig).map(([key, config]) => ( +
+ handleMetricToggle(key)} + /> + +
+ ))} +
+ - Per Model Statistics + Per Model and Provider Statistics @@ -260,26 +329,64 @@ export default function StatsPage() { orientation="right" stroke="currentColor" /> - } /> - } /> - - - { + if (active && payload && payload.length) { + return ( +
+

{`${payload[0].payload.provider}: ${payload[0].payload.model}`}

+ {payload.map((entry) => ( +

+ {`${entry.name}: ${entry.value}`} +

+ ))} +
+ ); + } + return null; + }} /> + } /> + {selectedMetrics.includes("logs") && ( + + )} + {selectedMetrics.includes("tokens") && ( + + )} + {selectedMetrics.includes("promptTokens") && ( + + )} + {selectedMetrics.includes("completionTokens") && ( + + )} + {selectedMetrics.includes("cost") && ( + + )}
@@ -313,7 +420,7 @@ export default function StatsPage() { - Model Usage Distribution + Model and Provider Usage Distribution @@ -331,10 +438,10 @@ export default function StatsPage() { `${name} ${(percent * 100).toFixed(0)}%` } > - {pieChartData.map((entry, index) => ( + {pieChartData.map((entry) => ( ))} diff --git a/src/components/LogDetails.tsx b/src/components/LogDetails.tsx index 62cb4c3..6ccbc67 100644 --- a/src/components/LogDetails.tsx +++ b/src/components/LogDetails.tsx @@ -14,6 +14,14 @@ import { Copy, ChevronDown, ChevronUp } from "lucide-react"; import ReactMarkdown from "react-markdown"; import { toast } from "sonner"; import { useSearchParams } from "next/navigation"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; interface Log { id: number; @@ -23,6 +31,14 @@ interface Log { headers: string; body: string; response: string | null; + metadata: { + inputTokens: number; + outputTokens: number; + totalTokens: number; + inputCost: number; + outputCost: number; + totalCost: number; + }; } interface LogDetailsProps { @@ -36,6 +52,11 @@ export default function LogDetails({ logId }: LogDetailsProps) { null, ); const searchParams = useSearchParams(); + const [expandedSections, setExpandedSections] = useState({ + response: true, + body: true, + headers: true, + }); useEffect(() => { const fetchLog = async () => { @@ -67,6 +88,13 @@ export default function LogDetails({ logId }: LogDetailsProps) { loadTheme(); }, [logId, searchParams]); + const toggleSection = (section: "response" | "body" | "headers") => { + setExpandedSections((prev) => ({ + ...prev, + [section]: !prev[section], + })); + }; + if (error) { return ( @@ -127,7 +155,7 @@ export default function LogDetails({ logId }: LogDetailsProps) { isExpandable?: boolean; }) => { const [isExpanded, setIsExpanded] = useState(!isExpandable); - const parsedContent = parseJSON(content); + const parsedContent = content; const maskedContent = maskSensitiveInfo(parsedContent); const jsonString = JSON.stringify(maskedContent, null, 2) || "No data available"; @@ -249,26 +277,14 @@ export default function LogDetails({ logId }: LogDetailsProps) { )} )} - {parsedContent && parsedContent.text && ( -
-

AI Response

- {renderAIResponse(parsedContent)} -
- )} - {parsedContent && parsedContent.messages && ( -
-

- Messages (Most recent on top) -

- {renderMessages(parsedContent.messages)} -
- )} + {(isExpanded || !isExpandable) && ( )} + + {parsedContent && parsedContent.text && ( +
+

AI Response

+ {renderAIResponse(parsedContent)} +
+ )} + + {parsedContent && parsedContent.messages && ( +
+

+ Messages (Most recent on top) +

+ {renderMessages(parsedContent.messages)} +
+ )} ); }; + const renderUsageTable = (log: Log) => { + return ( + + + + Input Tokens + Output Tokens + Total Tokens + Input Cost + Output Cost + Total Cost + + + + + {log.metadata.inputTokens} + {log.metadata.outputTokens} + {log.metadata.totalTokens} + ${log.metadata.inputCost.toFixed(4)} + ${log.metadata.outputCost.toFixed(4)} + ${log.metadata.totalCost.toFixed(4)} + + +
+ ); + }; + return ( - + Request Details @@ -292,31 +351,75 @@ export default function LogDetails({ logId }: LogDetailsProps) {

{log.timestamp}

+ {renderUsageTable(log)} + - + toggleSection("response")} + > Response + - - - + {expandedSections.response && ( + + + + )} - + toggleSection("body")} + > Body + - - - + {expandedSections.body && ( + + + + )} - + toggleSection("headers")} + > Headers + - - - + {expandedSections.headers && ( + + + + )}
diff --git a/src/components/LogsList.tsx b/src/components/LogsList.tsx index 0bbbd5f..0d318fc 100644 --- a/src/components/LogsList.tsx +++ b/src/components/LogsList.tsx @@ -1,9 +1,13 @@ "use client"; +import { useEffect, useState } from "react"; import { useRouter } from "next/navigation"; import { Button } from "@/components/ui/button"; import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card"; import { ScrollArea } from "@/components/ui/scroll-area"; +import { Badge } from "@/components/ui/badge"; +import { DollarSign, Clock, Hash, MessageSquare } from "lucide-react"; +import { getConfigurationCosts } from "@/app/actions"; interface LogMetadata { topP: number; @@ -14,16 +18,63 @@ interface LogMetadata { temperature: number; presencePenalty: number; frequencyPenalty: number; - userMessage?: string; + totalTokens: number; + totalCost: number; +} + +interface Usage { + promptTokens: number; + completionTokens: number; + totalTokens: number; +} + +interface Message { + role: string; + content: string; + name?: string; + experimental_providerMetadata?: { + anthropic?: { + cacheControl?: { + type: string; + }; + }; + }; +} + +interface RequestBody { + messages: Message[]; + temperature: number; + user: string; + stream: boolean; +} + +interface ResponseData { + text: string; + toolCalls: any[]; + toolResults: any[]; + usage: Usage; + finishReason: string; + rawResponse: { + headers: Record; + }; + warnings: string[]; + experimental_providerMetadata?: { + anthropic?: { + cacheCreationInputTokens?: number; + cacheReadInputTokens?: number; + }; + }; } interface Log { id: string; method: string; url: string; + headers: string; + body: RequestBody; // This will be a JSON string containing RequestBody + response: ResponseData; // This will be a JSON string containing ResponseData timestamp: string; metadata: LogMetadata; - response: string; } interface LogsListProps { @@ -37,51 +88,91 @@ const LogsListComponent: React.FC = ({ onLogSelect, selectedLogId, }) => { - // Check if logs is an array and not empty + const getProviderColor = (provider: string) => { + const colors: Record = { + anthropic: "bg-purple-100 text-purple-800 border-purple-300", + anthropicCached: "bg-indigo-100 text-indigo-800 border-indigo-300", + openai: "bg-green-100 text-green-800 border-green-300", + cohere: "bg-blue-100 text-blue-800 border-blue-300", + mistral: "bg-red-100 text-red-800 border-red-300", + groq: "bg-yellow-100 text-yellow-800 border-yellow-300", + ollama: "bg-orange-100 text-orange-800 border-orange-300", + other: "bg-gray-100 text-gray-800 border-gray-300", + }; + return colors[provider] || "bg-gray-100 text-gray-800 border-gray-300"; + }; + if (!Array.isArray(logs) || logs.length === 0) { return

No logs available.

; } return ( -
    +
    {logs.map((log) => { - // Parse the response here to get totalTokens - let totalTokens: number | string = "N/A"; - try { - const responseObj = JSON.parse(log.response); - totalTokens = responseObj.usage?.totalTokens || "N/A"; - } catch (error) { - console.error("Error parsing log response:", error); - } - - const userMessage = - log.metadata.userMessage || log.url || "No message available"; + const totalTokens = log.metadata.totalTokens || 0; + const totalCost = log.metadata.totalCost || 0; + const firstUserMessage = + log.body.messages.find((m) => m.role === "user" && !("name" in m)) + ?.content || "No message available"; + const truncatedMessage = + firstUserMessage.slice(0, 100) + + (firstUserMessage.length > 100 ? "..." : ""); + const isSelected = selectedLogId === log.id; + const providerColorClass = getProviderColor(log.metadata.provider); return ( -
  • onLogSelect(log.id)} > -
    - {new Date(log.timestamp).toLocaleString()} - {log.method} -
    -
    - {userMessage.slice(0, 50)} - {userMessage.length > 50 ? "..." : ""} -
    -
    - Provider: {log.metadata.provider} | Model: {log.metadata.model} | - Tokens: {totalTokens} -
    -
  • + + + {truncatedMessage} + + + +
    +
    + + {log.metadata.provider} + + + {log.metadata.model} + +
    +
    + +
    +
    +
    + + {new Date(log.timestamp).toLocaleString()} +
    +
    + + + {totalCost.toFixed(4)} + +
    +
    + + {totalTokens} tokens +
    +
    +
    +
    + ); })} -
+ ); }; diff --git a/src/components/ui/badge.tsx b/src/components/ui/badge.tsx new file mode 100644 index 0000000..e87d62b --- /dev/null +++ b/src/components/ui/badge.tsx @@ -0,0 +1,36 @@ +import * as React from "react" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const badgeVariants = cva( + "inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", + { + variants: { + variant: { + default: + "border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80", + secondary: + "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80", + destructive: + "border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80", + outline: "text-foreground", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +export interface BadgeProps + extends React.HTMLAttributes, + VariantProps {} + +function Badge({ className, variant, ...props }: BadgeProps) { + return ( +
+ ) +} + +export { Badge, badgeVariants } diff --git a/src/components/ui/progress.tsx b/src/components/ui/progress.tsx new file mode 100644 index 0000000..4fc3b47 --- /dev/null +++ b/src/components/ui/progress.tsx @@ -0,0 +1,28 @@ +"use client" + +import * as React from "react" +import * as ProgressPrimitive from "@radix-ui/react-progress" + +import { cn } from "@/lib/utils" + +const Progress = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, value, ...props }, ref) => ( + + + +)) +Progress.displayName = ProgressPrimitive.Root.displayName + +export { Progress } diff --git a/src/env.ts b/src/env.ts index aea06e3..b78cd44 100644 --- a/src/env.ts +++ b/src/env.ts @@ -16,6 +16,7 @@ export const env = createEnv({ COHERE_API_KEY: z.string().optional(), MISTRAL_API_KEY: z.string().optional(), GROQ_API_KEY: z.string().optional(), + OPENROUTER_API_KEY: z.string().optional(), }, /** @@ -39,6 +40,7 @@ export const env = createEnv({ COHERE_API_KEY: process.env.COHERE_API_KEY, MISTRAL_API_KEY: process.env.MISTRAL_API_KEY, GROQ_API_KEY: process.env.GROQ_API_KEY, + OPENROUTER_API_KEY: process.env.OPENROUTER_API_KEY, // NEXT_PUBLIC_CLIENTVAR: process.env.NEXT_PUBLIC_CLIENTVAR, }, /** diff --git a/src/lib/cost-calculator.ts b/src/lib/cost-calculator.ts new file mode 100644 index 0000000..b9b129b --- /dev/null +++ b/src/lib/cost-calculator.ts @@ -0,0 +1,33 @@ +import { PrismaClient } from "@prisma/client"; + +const prisma = new PrismaClient(); + +export async function getModelCost(provider: string, model: string) { + const currentDate = new Date(); + const modelCost = await prisma.modelCost.findFirst({ + where: { + provider, + model, + OR: [{ validFrom: null }, { validFrom: { lte: currentDate } }], + OR: [{ validTo: null }, { validTo: { gte: currentDate } }], + }, + orderBy: { validFrom: "desc" }, + }); + + if (!modelCost) { + throw new Error(`No cost data found for ${provider} ${model}`); + } + + return modelCost; +} + +export function calculateCost( + inputTokens: number, + outputTokens: number, + modelCost: { inputTokenCost: number; outputTokenCost: number }, +) { + return ( + (inputTokens / 1000000) * modelCost.inputTokenCost + + (outputTokens / 1000000) * modelCost.outputTokenCost + ); +} diff --git a/src/lib/model-config.ts b/src/lib/model-config.ts new file mode 100644 index 0000000..986ac60 --- /dev/null +++ b/src/lib/model-config.ts @@ -0,0 +1,1214 @@ +type ModelConfig = { + name?: string; + temperature?: number; + maxTokens?: number; + topP?: number; + frequencyPenalty?: number; + presencePenalty?: number; + inputTokenCost: number; // price per million tokens + outputTokenCost: number; // price per million tokens + isTemplate: boolean; +}; + +type ProviderConfigs = { + [key: string]: ModelConfig | null; +}; + +export type ModelConfigurations = { + [key: string]: ProviderConfigs; +}; + +export const getModelConfigurations = (): ModelConfigurations => ({ + openai: { + "gpt-4o": { + name: "OpenAI GPT-4 Optimized", + temperature: 0.7, + maxTokens: 8192, + topP: 1, + frequencyPenalty: 0, + presencePenalty: 0, + inputTokenCost: 5, // updated to price per million tokens + outputTokenCost: 15, // updated to price per million tokens + isTemplate: true, + }, + "gpt-4o-mini": { + name: "OpenAI GPT-4 Mini", + temperature: 0.7, + maxTokens: 4096, + topP: 1, + frequencyPenalty: 0, + presencePenalty: 0, + inputTokenCost: 0.15, // updated to price per million tokens + outputTokenCost: 0.6, // updated to price per million tokens + isTemplate: true, + }, + "gpt-4-turbo": { + name: "OpenAI GPT-4", + temperature: 0.7, + maxTokens: 8192, + topP: 1, + frequencyPenalty: 0, + presencePenalty: 0, + inputTokenCost: 10, // updated to price per million tokens + outputTokenCost: 30, // updated to price per million tokens + isTemplate: true, + }, + "gpt-4o-2024-08-06": { + name: "GPT-4 Optimized (2024-08-06)", + inputTokenCost: 2.5, // updated to price per million tokens + outputTokenCost: 10, // updated to price per million tokens + isTemplate: false, + }, + "gpt-3.5-turbo-0125": { + inputTokenCost: 0.5, // updated to price per million tokens + outputTokenCost: 1.5, // updated to price per million tokens + isTemplate: false, + }, + "chatgpt-4o-latest": { + inputTokenCost: 5, // updated to price per million tokens + outputTokenCost: 15, // updated to price per million tokens + isTemplate: false, + }, + "gpt-4-turbo-2024-04-09": { + inputTokenCost: 10, // updated to price per million tokens + outputTokenCost: 30, // updated to price per million tokens + isTemplate: false, + }, + "gpt-4": { + inputTokenCost: 30, // updated to price per million tokens + outputTokenCost: 60, // updated to price per million tokens + isTemplate: false, + }, + "gpt-4-32k": { + inputTokenCost: 60, // updated to price per million tokens + outputTokenCost: 120, // updated to price per million tokens + isTemplate: false, + }, + "gpt-4-0125-preview": { + inputTokenCost: 10, // updated to price per million tokens + outputTokenCost: 30, // updated to price per million tokens + isTemplate: false, + }, + "gpt-4-1106-preview": { + inputTokenCost: 10, // updated to price per million tokens + outputTokenCost: 30, // updated to price per million tokens + isTemplate: false, + }, + "gpt-4-vision-preview": { + inputTokenCost: 10, // updated to price per million tokens + outputTokenCost: 30, // updated to price per million tokens + isTemplate: false, + }, + "gpt-3.5-turbo-instruct": { + inputTokenCost: 1.5, // updated to price per million tokens + outputTokenCost: 2, // updated to price per million tokens + isTemplate: false, + }, + "gpt-3.5-turbo-1106": { + inputTokenCost: 1, // updated to price per million tokens + outputTokenCost: 2, // updated to price per million tokens + isTemplate: false, + }, + "gpt-3.5-turbo-0613": { + inputTokenCost: 1.5, // updated to price per million tokens + outputTokenCost: 2, // updated to price per million tokens + isTemplate: false, + }, + "gpt-3.5-turbo-16k-0613": { + inputTokenCost: 3, // updated to price per million tokens + outputTokenCost: 4, // updated to price per million tokens + isTemplate: false, + }, + "gpt-3.5-turbo-0301": { + inputTokenCost: 1.5, // updated to price per million tokens + outputTokenCost: 2, // updated to price per million tokens + isTemplate: false, + }, + "davinci-002": { + inputTokenCost: 2, // updated to price per million tokens + outputTokenCost: 2, // updated to price per million tokens + isTemplate: false, + }, + "babbage-002": { + inputTokenCost: 0.4, // updated to price per million tokens + outputTokenCost: 0.4, // updated to price per million tokens + isTemplate: false, + }, + }, + anthropicCached: { + "claude-3-5-sonnet-20240620": { + name: "Anthropic Claude 3.5 Sonnet (Cached)", + temperature: 0.7, + maxTokens: 200000, + topP: 1, + frequencyPenalty: 0, + presencePenalty: 0, + inputTokenCost: 3, // updated to price per million tokens + outputTokenCost: 15, // updated to price per million tokens + isTemplate: true, + }, + }, + anthropic: { + "claude-3-5-sonnet-20240620": { + name: "Anthropic Claude 3.5 Sonnet", + temperature: 0.7, + maxTokens: 200000, + topP: 1, + frequencyPenalty: 0, + presencePenalty: 0, + inputTokenCost: 3, // updated to price per million tokens + outputTokenCost: 15, // updated to price per million tokens + isTemplate: true, + }, + "claude-3-opus-20240229": null, + "claude-3-sonnet-20240229": null, + "claude-3-haiku-20240307": null, + "claude-3-5-sonnet": { + inputTokenCost: 3, // updated to price per million tokens + outputTokenCost: 15, // updated to price per million tokens + isTemplate: false, + }, + "claude-3-opus": { + inputTokenCost: 15, // updated to price per million tokens + outputTokenCost: 75, // updated to price per million tokens + isTemplate: false, + }, + "claude-3-haiku": { + inputTokenCost: 0.25, // updated to price per million tokens + outputTokenCost: 1.25, // updated to price per million tokens + isTemplate: false, + }, + "claude-2-1": { + inputTokenCost: 8, // updated to price per million tokens + outputTokenCost: 24, // updated to price per million tokens + isTemplate: false, + }, + "claude-2-0": { + inputTokenCost: 8, // updated to price per million tokens + outputTokenCost: 24, // updated to price per million tokens + isTemplate: false, + }, + "claude-instant": { + inputTokenCost: 0.8, // updated to price per million tokens + outputTokenCost: 2.4, // updated to price per million tokens + isTemplate: false, + }, + }, + cohere: { + "command-r": null, + "command-r-plus": null, + }, + mistral: { + "mistral-large-latest": { + name: "Mistral Large", + temperature: 0.7, + maxTokens: 32768, + topP: 1, + frequencyPenalty: 0, + presencePenalty: 0, + inputTokenCost: 0, + outputTokenCost: 0, + isTemplate: true, + }, + "mistral-medium-latest": null, + "mistral-small-latest": null, + "open-mistral-nemo": null, + "open-mixtral-8x22b": null, + "open-mixtral-8x7b": null, + "open-mistral-7b": null, + }, + groq: { + "llama-3.1-70b-versatile": { + name: "Groq LLaMA 3.1", + temperature: 0.7, + maxTokens: 32768, + topP: 1, + frequencyPenalty: 0, + presencePenalty: 0, + inputTokenCost: 0, + outputTokenCost: 0, + isTemplate: true, + }, + "llama-3.1-405b-reasoning": null, + "llama-3.1-8b-instant": null, + "mixtral-8x7b-32768": null, + "gemma2-9b-it": null, + }, + ollama: { + codegemma: null, + "codegemma:2b": null, + "codegemma:7b": null, + codellama: null, + "codellama:7b": null, + "codellama:13b": null, + "codellama:34b": null, + "codellama:70b": null, + "codellama:code": null, + "codellama:python": null, + "command-r": null, + "command-r:35b": null, + "command-r-plus": null, + "command-r-plus:104b": null, + "deepseek-coder-v2": null, + "deepseek-coder-v2:16b": null, + "deepseek-coder-v2:236b": null, + falcon2: null, + "falcon2:11b": null, + gemma: null, + "gemma:2b": null, + "gemma:7b": null, + gemma2: null, + "gemma2:2b": null, + "gemma2:9b": null, + "gemma2:27b": null, + llama2: null, + "llama2:7b": null, + "llama2:13b": null, + "llama2:70b": null, + llama3: null, + "llama3:8b": null, + "llama3:70b": null, + "llama3-chatqa": null, + "llama3-chatqa:8b": null, + "llama3-chatqa:70b": null, + "llama3-gradient": null, + "llama3-gradient:8b": null, + "llama3-gradient:70b": null, + "llama3.1": null, + "llama3.1:8b": null, + "llama3.1:70b": null, + "llama3.1:405b": null, + llava: null, + "llava:7b": null, + "llava:13b": null, + "llava:34b": null, + "llava-llama3": null, + "llava-llama3:8b": null, + "llava-phi3": null, + "llava-phi3:3.8b": null, + mistral: null, + "mistral:7b": null, + "mistral-large": null, + "mistral-large:123b": null, + "mistral-nemo": null, + "mistral-nemo:12b": null, + mixtral: null, + "mixtral:8x7b": null, + "mixtral:8x22b": null, + moondream: null, + "moondream:1.8b": null, + openhermes: null, + "openhermes:v2.5": null, + qwen: null, + "qwen:7b": null, + "qwen:14b": null, + "qwen:32b": null, + "qwen:72b": null, + "qwen:110b": null, + qwen2: null, + "qwen2:0.5b": null, + "qwen2:1.5b": null, + "qwen2:7b": null, + "qwen2:72b": null, + phi3: null, + "phi3:3.8b": null, + "phi3:14b": null, + }, + openrouter: { + "01-ai/yi-large": { + inputTokenCost: 3, + outputTokenCost: 3, + isTemplate: false, + }, + "01-ai/yi-large-fc": { + inputTokenCost: 3, + outputTokenCost: 3, + isTemplate: false, + }, + "01-ai/yi-large-turbo": { + inputTokenCost: 0.19, + outputTokenCost: 0.19, + isTemplate: false, + }, + "01-ai/yi-34b": { + inputTokenCost: 0.72, + outputTokenCost: 0.72, + isTemplate: false, + }, + "01-ai/yi-34b-chat": { + inputTokenCost: 0.72, + outputTokenCost: 0.72, + isTemplate: false, + }, + "01-ai/yi-6b": { + inputTokenCost: 0.18, + outputTokenCost: 0.18, + isTemplate: false, + }, + "01-ai/yi-vision": { + inputTokenCost: 0.19, + outputTokenCost: 0.19, + isTemplate: false, + }, + "aetherwiing/mn-starcannon-12b": { + inputTokenCost: 2, + outputTokenCost: 2, + isTemplate: false, + }, + "ai21/jamba-instruct": { + inputTokenCost: 0.5, + outputTokenCost: 0.7, + isTemplate: false, + }, + "allenai/olmo-7b-instruct": { + inputTokenCost: 0.18, + outputTokenCost: 0.18, + isTemplate: false, + }, + "alpindale/goliath-120b": { + inputTokenCost: 9.375, + outputTokenCost: 9.375, + isTemplate: false, + }, + "alpindale/magnum-72b": { + inputTokenCost: 3.75, + outputTokenCost: 4.5, + isTemplate: false, + }, + "anthropic/claude-1": { + inputTokenCost: 8, + outputTokenCost: 24, + isTemplate: false, + }, + "anthropic/claude-1.2": { + inputTokenCost: 8, + outputTokenCost: 24, + isTemplate: false, + }, + "anthropic/claude-2": { + inputTokenCost: 8, + outputTokenCost: 24, + isTemplate: false, + }, + "anthropic/claude-2:beta": { + inputTokenCost: 8, + outputTokenCost: 24, + isTemplate: false, + }, + "anthropic/claude-2.0": { + inputTokenCost: 8, + outputTokenCost: 24, + isTemplate: false, + }, + "anthropic/claude-2.0:beta": { + inputTokenCost: 8, + outputTokenCost: 24, + isTemplate: false, + }, + "anthropic/claude-2.1": { + inputTokenCost: 8, + outputTokenCost: 24, + isTemplate: false, + }, + "anthropic/claude-2.1:beta": { + inputTokenCost: 8, + outputTokenCost: 24, + isTemplate: false, + }, + "anthropic/claude-3-haiku": { + inputTokenCost: 0.25, + outputTokenCost: 1.25, + isTemplate: false, + }, + "anthropic/claude-3-haiku:beta": { + inputTokenCost: 0.25, + outputTokenCost: 1.25, + isTemplate: false, + }, + "anthropic/claude-3-opus": { + inputTokenCost: 15, + outputTokenCost: 75, + isTemplate: false, + }, + "anthropic/claude-3-opus:beta": { + inputTokenCost: 15, + outputTokenCost: 75, + isTemplate: false, + }, + "anthropic/claude-3-sonnet": { + inputTokenCost: 3, + outputTokenCost: 15, + isTemplate: false, + }, + "anthropic/claude-3.5-sonnet": { + inputTokenCost: 3, + outputTokenCost: 15, + isTemplate: false, + }, + "anthropic/claude-3.5-sonnet:beta": { + inputTokenCost: 3, + outputTokenCost: 15, + isTemplate: false, + }, + "anthropic/claude-3-sonnet:beta": { + inputTokenCost: 3, + outputTokenCost: 15, + isTemplate: false, + }, + "anthropic/claude-instant-1": { + inputTokenCost: 0.8, + outputTokenCost: 2.4, + isTemplate: false, + }, + "anthropic/claude-instant-1:beta": { + inputTokenCost: 0.8, + outputTokenCost: 2.4, + isTemplate: false, + }, + "anthropic/claude-instant-1.0": { + inputTokenCost: 0.8, + outputTokenCost: 2.4, + isTemplate: false, + }, + "anthropic/claude-instant-1.1": { + inputTokenCost: 0.8, + outputTokenCost: 2.4, + isTemplate: false, + }, + "austism/chronos-hermes-13b": { + inputTokenCost: 0.13, + outputTokenCost: 0.13, + isTemplate: false, + }, + "cohere/command": { + inputTokenCost: 1, + outputTokenCost: 2, + isTemplate: false, + }, + "cohere/command-r": { + inputTokenCost: 0.5, + outputTokenCost: 1.5, + isTemplate: false, + }, + "cohere/command-r-plus": { + inputTokenCost: 3, + outputTokenCost: 15, + isTemplate: false, + }, + "cognitivecomputations/dolphin-llama-3-70b": { + inputTokenCost: 0.35, + outputTokenCost: 0.4, + isTemplate: false, + }, + "cognitivecomputations/dolphin-mixtral-8x22b": { + inputTokenCost: 0.9, + outputTokenCost: 0.9, + isTemplate: false, + }, + "cognitivecomputations/dolphin-mixtral-8x7b": { + inputTokenCost: 0.5, + outputTokenCost: 0.5, + isTemplate: false, + }, + "databricks/dbrx-instruct": { + inputTokenCost: 1.08, + outputTokenCost: 1.08, + isTemplate: false, + }, + "deepseek/deepseek-chat": { + inputTokenCost: 0.14, + outputTokenCost: 0.28, + isTemplate: false, + }, + "deepseek/deepseek-coder": { + inputTokenCost: 0.14, + outputTokenCost: 0.28, + isTemplate: false, + }, + "google/gemini-flash-1.5": { + inputTokenCost: 0.0375, + outputTokenCost: 0.15, + isTemplate: false, + }, + "google/gemini-pro": { + inputTokenCost: 0.125, + outputTokenCost: 0.375, + isTemplate: false, + }, + "google/gemini-pro-1.5": { + inputTokenCost: 2.5, + outputTokenCost: 7.5, + isTemplate: false, + }, + "google/gemini-pro-1.5-exp": { + inputTokenCost: 2.5, + outputTokenCost: 7.5, + isTemplate: false, + }, + "google/gemini-pro-vision": { + inputTokenCost: 0.125, + outputTokenCost: 0.375, + isTemplate: false, + }, + "google/gemma-2-27b-it": { + inputTokenCost: 0.27, + outputTokenCost: 0.27, + isTemplate: false, + }, + "google/gemma-2-9b-it": { + inputTokenCost: 0.06, + outputTokenCost: 0.06, + isTemplate: false, + }, + "google/gemma-2-9b-it:free": { + inputTokenCost: 0, + outputTokenCost: 0, + isTemplate: false, + }, + "google/gemma-7b-it": { + inputTokenCost: 0.07, + outputTokenCost: 0.07, + isTemplate: false, + }, + "google/gemma-7b-it:free": { + inputTokenCost: 0, + outputTokenCost: 0, + isTemplate: false, + }, + "google/gemma-7b-it:nitro": { + inputTokenCost: 0.07, + outputTokenCost: 0.07, + isTemplate: false, + }, + "google/palm-2-chat-bison": { + inputTokenCost: 0.25, + outputTokenCost: 0.5, + isTemplate: false, + }, + "google/palm-2-chat-bison-32k": { + inputTokenCost: 0.25, + outputTokenCost: 0.5, + isTemplate: false, + }, + "google/palm-2-codechat-bison": { + inputTokenCost: 0.25, + outputTokenCost: 0.5, + isTemplate: false, + }, + "google/palm-2-codechat-bison-32k": { + inputTokenCost: 0.25, + outputTokenCost: 0.5, + isTemplate: false, + }, + "gryphe/mythomist-7b": { + inputTokenCost: 0.375, + outputTokenCost: 0.375, + isTemplate: false, + }, + "gryphe/mythomist-7b:free": { + inputTokenCost: 0, + outputTokenCost: 0, + isTemplate: false, + }, + "gryphe/mythomax-l2-13b": { + inputTokenCost: 0.1, + outputTokenCost: 0.1, + isTemplate: false, + }, + "huggingfaceh4/zephyr-7b-beta:free": { + inputTokenCost: 0, + outputTokenCost: 0, + isTemplate: false, + }, + "jondurbin/airoboros-l2-70b": { + inputTokenCost: 0.5, + outputTokenCost: 0.5, + isTemplate: false, + }, + "lizpreciatior/lzlv-70b-fp16-hf": { + inputTokenCost: 0.35, + outputTokenCost: 0.4, + isTemplate: false, + }, + "mancer/weaver": { + inputTokenCost: 1.875, + outputTokenCost: 2.25, + isTemplate: false, + }, + "meta-llama/codellama-34b-instruct": { + inputTokenCost: 0.72, + outputTokenCost: 0.72, + isTemplate: false, + }, + "meta-llama/codellama-70b-instruct": { + inputTokenCost: 0.81, + outputTokenCost: 0.81, + isTemplate: false, + }, + "meta-llama/llama-3-70b": { + inputTokenCost: 0.81, + outputTokenCost: 0.81, + isTemplate: false, + }, + "meta-llama/llama-3-70b-instruct": { + inputTokenCost: 0.35, + outputTokenCost: 0.4, + isTemplate: false, + }, + "meta-llama/llama-3-70b-instruct:nitro": { + inputTokenCost: 0.792, + outputTokenCost: 0.792, + isTemplate: false, + }, + "meta-llama/llama-3-8b": { + inputTokenCost: 0.18, + outputTokenCost: 0.18, + isTemplate: false, + }, + "meta-llama/llama-3-8b-instruct": { + inputTokenCost: 0.055, + outputTokenCost: 0.055, + isTemplate: false, + }, + "meta-llama/llama-3-8b-instruct:extended": { + inputTokenCost: 0.1875, + outputTokenCost: 1.125, + isTemplate: false, + }, + "meta-llama/llama-3-8b-instruct:free": { + inputTokenCost: 0, + outputTokenCost: 0, + isTemplate: false, + }, + "meta-llama/llama-3-8b-instruct:nitro": { + inputTokenCost: 0.162, + outputTokenCost: 0.162, + isTemplate: false, + }, + "meta-llama/llama-3.1-405b": { + inputTokenCost: 2, + outputTokenCost: 2, + isTemplate: false, + }, + "meta-llama/llama-3.1-405b-instruct": { + inputTokenCost: 2.7, + outputTokenCost: 2.7, + isTemplate: false, + }, + "meta-llama/llama-3.1-70b-instruct": { + inputTokenCost: 0.35, + outputTokenCost: 0.4, + isTemplate: false, + }, + "meta-llama/llama-3.1-8b-instruct": { + inputTokenCost: 0.055, + outputTokenCost: 0.055, + isTemplate: false, + }, + "meta-llama/llama-3.1-8b-instruct:free": { + inputTokenCost: 0, + outputTokenCost: 0, + isTemplate: false, + }, + "meta-llama/llama-guard-2-8b": { + inputTokenCost: 0.18, + outputTokenCost: 0.18, + isTemplate: false, + }, + "microsoft/phi-3-medium-128k-instruct": { + inputTokenCost: 1, + outputTokenCost: 1, + isTemplate: false, + }, + "microsoft/phi-3-medium-128k-instruct:free": { + inputTokenCost: 0, + outputTokenCost: 0, + isTemplate: false, + }, + "microsoft/phi-3-medium-4k-instruct": { + inputTokenCost: 0.14, + outputTokenCost: 0.14, + isTemplate: false, + }, + "microsoft/phi-3-mini-128k-instruct": { + inputTokenCost: 0.1, + outputTokenCost: 0.1, + isTemplate: false, + }, + "microsoft/phi-3-mini-128k-instruct:free": { + inputTokenCost: 0, + outputTokenCost: 0, + isTemplate: false, + }, + "microsoft/wizardlm-2-7b": { + inputTokenCost: 0.055, + outputTokenCost: 0.055, + isTemplate: false, + }, + "microsoft/wizardlm-2-8x22b": { + inputTokenCost: 0.5, + outputTokenCost: 0.5, + isTemplate: false, + }, + "mistralai/codestral-mamba": { + inputTokenCost: 0.25, + outputTokenCost: 0.25, + isTemplate: false, + }, + "mistralai/mistral-7b-instruct": { + inputTokenCost: 0.055, + outputTokenCost: 0.055, + isTemplate: false, + }, + "mistralai/mistral-7b-instruct:free": { + inputTokenCost: 0, + outputTokenCost: 0, + isTemplate: false, + }, + "mistralai/mistral-7b-instruct:nitro": { + inputTokenCost: 0.07, + outputTokenCost: 0.07, + isTemplate: false, + }, + "mistralai/mistral-7b-instruct-v0.1": { + inputTokenCost: 0.055, + outputTokenCost: 0.055, + isTemplate: false, + }, + "mistralai/mistral-7b-instruct-v0.2": { + inputTokenCost: 0.055, + outputTokenCost: 0.055, + isTemplate: false, + }, + "mistralai/mistral-7b-instruct-v0.3": { + inputTokenCost: 0.055, + outputTokenCost: 0.055, + isTemplate: false, + }, + "mistralai/mistral-large": { + inputTokenCost: 3, + outputTokenCost: 9, + isTemplate: false, + }, + "mistralai/mistral-medium": { + inputTokenCost: 2.7, + outputTokenCost: 8.1, + isTemplate: false, + }, + "mistralai/mistral-nemo": { + inputTokenCost: 0.17, + outputTokenCost: 0.17, + isTemplate: false, + }, + "mistralai/mistral-small": { + inputTokenCost: 2, + outputTokenCost: 6, + isTemplate: false, + }, + "mistralai/mistral-tiny": { + inputTokenCost: 0.25, + outputTokenCost: 0.25, + isTemplate: false, + }, + "mistralai/mixtral-8x22b": { + inputTokenCost: 1.08, + outputTokenCost: 1.08, + isTemplate: false, + }, + "mistralai/mixtral-8x22b-instruct": { + inputTokenCost: 0.65, + outputTokenCost: 0.65, + isTemplate: false, + }, + "mistralai/mixtral-8x7b": { + inputTokenCost: 0.54, + outputTokenCost: 0.54, + isTemplate: false, + }, + "mistralai/mixtral-8x7b-instruct": { + inputTokenCost: 0.24, + outputTokenCost: 0.24, + isTemplate: false, + }, + "mistralai/mixtral-8x7b-instruct:nitro": { + inputTokenCost: 0.54, + outputTokenCost: 0.54, + isTemplate: false, + }, + "neversleep/llama-3-lumimaid-70b": { + inputTokenCost: 3.375, + outputTokenCost: 4.5, + isTemplate: false, + }, + "neversleep/llama-3-lumimaid-8b": { + inputTokenCost: 0.1875, + outputTokenCost: 1.125, + isTemplate: false, + }, + "neversleep/llama-3-lumimaid-8b:extended": { + inputTokenCost: 0.1875, + outputTokenCost: 1.125, + isTemplate: false, + }, + "neversleep/noromaid-20b": { + inputTokenCost: 1.5, + outputTokenCost: 2.25, + isTemplate: false, + }, + "nousresearch/hermes-2-pro-llama-3-8b": { + inputTokenCost: 0.14, + outputTokenCost: 0.14, + isTemplate: false, + }, + "nousresearch/hermes-2-theta-llama-3-8b": { + inputTokenCost: 0.1875, + outputTokenCost: 1.125, + isTemplate: false, + }, + "nousresearch/hermes-3-llama-3.1-405b": { + inputTokenCost: 0, + outputTokenCost: 0, + isTemplate: false, + }, + "nousresearch/hermes-3-llama-3.1-405b:extended": { + inputTokenCost: 0, + outputTokenCost: 0, + isTemplate: false, + }, + "nousresearch/nous-capybara-7b": { + inputTokenCost: 0.18, + outputTokenCost: 0.18, + isTemplate: false, + }, + "nousresearch/nous-capybara-7b:free": { + inputTokenCost: 0, + outputTokenCost: 0, + isTemplate: false, + }, + "nousresearch/nous-hermes-2-mixtral-8x7b-dpo": { + inputTokenCost: 0.45, + outputTokenCost: 0.45, + isTemplate: false, + }, + "nousresearch/nous-hermes-2-mixtral-8x7b-sft": { + inputTokenCost: 0.54, + outputTokenCost: 0.54, + isTemplate: false, + }, + "nousresearch/nous-hermes-2-mistral-7b-dpo": { + inputTokenCost: 0.18, + outputTokenCost: 0.18, + isTemplate: false, + }, + "nousresearch/nous-hermes-llama2-13b": { + inputTokenCost: 0.17, + outputTokenCost: 0.17, + isTemplate: false, + }, + "nousresearch/nous-hermes-yi-34b": { + inputTokenCost: 0.72, + outputTokenCost: 0.72, + isTemplate: false, + }, + "nothingiisreal/mn-celeste-12b": { + inputTokenCost: 1.5, + outputTokenCost: 1.5, + isTemplate: false, + }, + "open-orca/mistral-7b-openorca": { + inputTokenCost: 0.18, + outputTokenCost: 0.18, + isTemplate: false, + }, + "openchat/openchat-7b": { + inputTokenCost: 0.055, + outputTokenCost: 0.055, + isTemplate: false, + }, + "openchat/openchat-7b:free": { + inputTokenCost: 0, + outputTokenCost: 0, + isTemplate: false, + }, + "openchat/openchat-8b": { + inputTokenCost: 0.055, + outputTokenCost: 0.055, + isTemplate: false, + }, + "openai/gpt-3.5-turbo-0613": { + inputTokenCost: 1, + outputTokenCost: 2, + isTemplate: false, + }, + "openai/gpt-3.5-turbo-16k": { + inputTokenCost: 3, + outputTokenCost: 4, + isTemplate: false, + }, + "openai/gpt-3.5-turbo-instruct": { + inputTokenCost: 1.5, + outputTokenCost: 2, + isTemplate: false, + }, + "openai/gpt-4-32k": { + inputTokenCost: 60, + outputTokenCost: 120, + isTemplate: false, + }, + "openai/gpt-4-32k-0314": { + inputTokenCost: 60, + outputTokenCost: 120, + isTemplate: false, + }, + "openai/gpt-4-turbo": { + inputTokenCost: 10, + outputTokenCost: 30, + isTemplate: false, + }, + "openai/gpt-4-turbo-preview": { + inputTokenCost: 10, + outputTokenCost: 30, + isTemplate: false, + }, + "openai/gpt-4-vision-preview": { + inputTokenCost: 10, + outputTokenCost: 30, + isTemplate: false, + }, + "openai/gpt-4o": { + inputTokenCost: 5, + outputTokenCost: 15, + isTemplate: false, + }, + "openai/gpt-4o:extended": { + inputTokenCost: 6, + outputTokenCost: 18, + isTemplate: false, + }, + "openai/gpt-4o-2024-05-13": { + inputTokenCost: 5, + outputTokenCost: 15, + isTemplate: false, + }, + "openai/gpt-4o-2024-08-06": { + inputTokenCost: 2.5, + outputTokenCost: 10, + isTemplate: false, + }, + "openai/gpt-4o-latest": { + inputTokenCost: 5, + outputTokenCost: 15, + isTemplate: false, + }, + "openai/gpt-4o-mini": { + inputTokenCost: 0.15, + outputTokenCost: 0.6, + isTemplate: false, + }, + "openai/gpt-4o-mini-2024-07-18": { + inputTokenCost: 0.15, + outputTokenCost: 0.6, + isTemplate: false, + }, + "openrouter/auto": null, + "openrouter/flavor-of-the-week": null, + "perplexity/llama-3-sonar-large-32k-chat": { + inputTokenCost: 1, + outputTokenCost: 1, + isTemplate: false, + }, + "perplexity/llama-3-sonar-large-32k-online": { + inputTokenCost: 1, + outputTokenCost: 1, + isTemplate: false, + }, + "perplexity/llama-3-sonar-small-32k-chat": { + inputTokenCost: 0.2, + outputTokenCost: 0.2, + isTemplate: false, + }, + "perplexity/llama-3-sonar-small-32k-online": { + inputTokenCost: 0.2, + outputTokenCost: 0.2, + isTemplate: false, + }, + "perplexity/llama-3.1-sonar-huge-128k-online": { + inputTokenCost: 5, + outputTokenCost: 5, + isTemplate: false, + }, + "perplexity/llama-3.1-sonar-large-128k-chat": { + inputTokenCost: 1, + outputTokenCost: 1, + isTemplate: false, + }, + "perplexity/llama-3.1-sonar-large-128k-online": { + inputTokenCost: 1, + outputTokenCost: 1, + isTemplate: false, + }, + "perplexity/llama-3.1-sonar-small-128k-chat": { + inputTokenCost: 0.2, + outputTokenCost: 0.2, + isTemplate: false, + }, + "perplexity/llama-3.1-sonar-small-128k-online": { + inputTokenCost: 0.2, + outputTokenCost: 0.2, + isTemplate: false, + }, + "phind/phind-codellama-34b": { + inputTokenCost: 0.72, + outputTokenCost: 0.72, + isTemplate: false, + }, + "pygmalionai/mythalion-13b": { + inputTokenCost: 1.125, + outputTokenCost: 1.125, + isTemplate: false, + }, + "qwen/qwen-110b-chat": { + inputTokenCost: 1.62, + outputTokenCost: 1.62, + isTemplate: false, + }, + "qwen/qwen-14b-chat": { + inputTokenCost: 0.27, + outputTokenCost: 0.27, + isTemplate: false, + }, + "qwen/qwen-2-72b-instruct": { + inputTokenCost: 0.35, + outputTokenCost: 0.4, + isTemplate: false, + }, + "qwen/qwen-2-7b-instruct": { + inputTokenCost: 0.055, + outputTokenCost: 0.055, + isTemplate: false, + }, + "qwen/qwen-2-7b-instruct:free": { + inputTokenCost: 0, + outputTokenCost: 0, + isTemplate: false, + }, + "qwen/qwen-32b-chat": { + inputTokenCost: 0.72, + outputTokenCost: 0.72, + isTemplate: false, + }, + "qwen/qwen-4b-chat": { + inputTokenCost: 0.09, + outputTokenCost: 0.09, + isTemplate: false, + }, + "qwen/qwen-72b-chat": { + inputTokenCost: 0.81, + outputTokenCost: 0.81, + isTemplate: false, + }, + "qwen/qwen-7b-chat": { + inputTokenCost: 0.18, + outputTokenCost: 0.18, + isTemplate: false, + }, + "recursal/eagle-7b": { + inputTokenCost: 0, + outputTokenCost: 0, + isTemplate: false, + }, + "recursal/rwkv-5-3b-ai-town": { + inputTokenCost: 0, + outputTokenCost: 0, + isTemplate: false, + }, + "rwkv/rwkv-5-world-3b": { + inputTokenCost: 0, + outputTokenCost: 0, + isTemplate: false, + }, + "sao10k/fimbulvetr-11b-v2": { + inputTokenCost: 0.375, + outputTokenCost: 1.5, + isTemplate: false, + }, + "sao10k/l3-euryale-70b": { + inputTokenCost: 0.35, + outputTokenCost: 0.4, + isTemplate: false, + }, + "sao10k/l3-lunaris-8b": { + inputTokenCost: 2, + outputTokenCost: 2, + isTemplate: false, + }, + "sao10k/l3-stheno-8b": { + inputTokenCost: 0.25, + outputTokenCost: 1.5, + isTemplate: false, + }, + "snowflake/snowflake-arctic-instruct": { + inputTokenCost: 2.16, + outputTokenCost: 2.16, + isTemplate: false, + }, + "sophosympatheia/midnight-rose-70b": { + inputTokenCost: 0.8, + outputTokenCost: 0.8, + isTemplate: false, + }, + "teknium/openhermes-2-mistral-7b": { + inputTokenCost: 0.18, + outputTokenCost: 0.18, + isTemplate: false, + }, + "teknium/openhermes-2.5-mistral-7b": { + inputTokenCost: 0.17, + outputTokenCost: 0.17, + isTemplate: false, + }, + "togethercomputer/stripedhyena-hessian-7b": { + inputTokenCost: 0.18, + outputTokenCost: 0.18, + isTemplate: false, + }, + "togethercomputer/stripedhyena-nous-7b": { + inputTokenCost: 0.18, + outputTokenCost: 0.18, + isTemplate: false, + }, + "undi95/remm-slerp-l2-13b": { + inputTokenCost: 0.27, + outputTokenCost: 0.27, + isTemplate: false, + }, + "undi95/remm-slerp-l2-13b:extended": { + inputTokenCost: 1.125, + outputTokenCost: 1.125, + isTemplate: false, + }, + "undi95/toppy-m-7b": { + inputTokenCost: 0.07, + outputTokenCost: 0.07, + isTemplate: false, + }, + "undi95/toppy-m-7b:free": { + inputTokenCost: 0, + outputTokenCost: 0, + isTemplate: false, + }, + "undi95/toppy-m-7b:nitro": { + inputTokenCost: 0.07, + outputTokenCost: 0.07, + isTemplate: false, + }, + "xwin-lm/xwin-lm-70b": { + inputTokenCost: 3.75, + outputTokenCost: 3.75, + isTemplate: false, + }, + }, + other: { + other: null, + }, +}); diff --git a/start.sh b/start.sh new file mode 100644 index 0000000..356ad36 --- /dev/null +++ b/start.sh @@ -0,0 +1,10 @@ +#!/bin/sh + +# Run migrations +pnpm prisma migrate deploy + +# Seed the database +pnpm prisma db seed + +# Start the application +pnpm start \ No newline at end of file