diff --git a/.env.build b/.env.build new file mode 100644 index 0000000..43f54ff --- /dev/null +++ b/.env.build @@ -0,0 +1,13 @@ +OPENAI_API_KEY= +ANTHROPIC_API_KEY= +COHERE_API_KEY= +MISTRAL_API_KEY= +GROQ_API_KEY= + +OLLAMA_BASE_URL= + +# Dummy database URL +DATABASE_URL=postgresql://postgres:postgres@localhost:5432/postgres + +# Build standalone +BUILD_STANDALONE=true \ No newline at end of file diff --git a/.env.example b/.env.example index 85c59e3..b228755 100644 --- a/.env.example +++ b/.env.example @@ -4,4 +4,6 @@ COHERE_API_KEY=your_cohere_api_key_here MISTRAL_API_KEY=your_mistral_api_key_here GROQ_API_KEY=your_groq_api_key_here +OLLAMA_BASE_URL= + DATABASE_URL= diff --git a/.github/workflows/docker-nightly.yml b/.github/workflows/docker-nightly.yml new file mode 100644 index 0000000..267a49e --- /dev/null +++ b/.github/workflows/docker-nightly.yml @@ -0,0 +1,60 @@ +name: Docker Nightly Build + +on: + push: + branches: [ main ] + +jobs: + build: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + + steps: + - name: Login to GitHub Container Registry + uses: docker/login-action@v2 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Set up Node.js + uses: actions/setup-node@v3 + with: + node-version: '23' + + - name: Install pnpm + run: npm install -g pnpm + + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Use .env.build instead of .env + run: cp .env.build .env + + - name: Install dependencies + run: pnpm install + + - name: Build the project + run: pnpm run build + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + + - name: Extract metadata for Docker + id: meta + uses: docker/metadata-action@v4 + with: + images: ghcr.io/${{ github.repository }} + tags: | + type=raw,value=nightly + + - name: Build and push + uses: docker/build-push-action@v4 + with: + context: . + file: run.Dockerfile + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} \ No newline at end of file diff --git a/.github/workflows/docker-release.yml b/.github/workflows/docker-release.yml new file mode 100644 index 0000000..965c699 --- /dev/null +++ b/.github/workflows/docker-release.yml @@ -0,0 +1,59 @@ +name: Docker Release Build + +on: + push: + tags: + - 'v*-alpha' + - 'v*' + +jobs: + build: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + + steps: + - name: Login to GitHub Container Registry + uses: docker/login-action@v2 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Set up Node.js + uses: actions/setup-node@v3 + with: + node-version: '23' + + - name: Install pnpm + run: npm install -g pnpm + + - name: Checkout code + uses: actions/checkout@v3 + + - name: Use .env.build instead of .env + run: cp .env.build .env + + - name: Extract Docker tags + run: | + chmod +x ./scripts/_github_action_tags_extractor.sh + ./scripts/_github_action_tags_extractor.sh "${{ github.ref_name }}" + + - name: Install dependencies + run: pnpm install + + - name: Build the project + run: pnpm run build + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + + - name: Build and push + uses: docker/build-push-action@v4 + with: + context: . + file: run.Dockerfile + push: true + tags: ${{ env.DOCKER_TAGS }} + platforms: linux/amd64,linux/arm64 \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index c8d3d0c..e075607 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,8 +2,8 @@ FROM node:18-alpine WORKDIR /app -RUN apk add --no-cache libc6-compat RUN apk update +RUN apk add --no-cache libc6-compat openssl # Install pnpm RUN npm install -g pnpm @@ -11,12 +11,14 @@ RUN npm install -g pnpm # Copy package.json and pnpm-lock.yaml COPY package.json pnpm-lock.yaml ./ -# Install dependencies -RUN pnpm install +RUN pnpm fetch # Copy the rest of the application COPY . . +# Install dependencies +RUN pnpm install + # Generate Prisma Client RUN pnpm prisma generate diff --git a/README.md b/README.md index 7cb12d3..cbc35e5 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,58 @@ We are live on ProductHunt today, please upvote us if you find this useful! 🙏 - **AI Library**: Vercel AI SDK - **Styling**: Tailwind CSS with shadcn/ui components -## Getting Started + + +## Getting Started (Using Docker Compose) + +1. Create a new file named `docker-compose.yaml` in your project directory +2. Copy and paste the YAML configuration below into the file +3. Open a terminal in the project directory +4. Run the command: + ```bash + docker compose up + ``` +(Optional: Add `-d` flag to run in detached mode) + +```yaml +services: + cursorlens: + image: ghcr.io/haouarihk/cursorlens:latest + ports: + - "3000:3000" + environment: + - DATABASE_URL=postgresql://postgres:postgres@db:5432/postgres + # restart: unless-stopped + depends_on: + db: + condition: service_healthy + + db: + image: postgres:14 + environment: + - POSTGRES_USER=postgres + - POSTGRES_PASSWORD=postgres + - POSTGRES_DB=postgres + volumes: + - postgres_data:/var/lib/postgresql/data + # restart: unless-stopped + healthcheck: + test: ["CMD-SHELL", "pg_isready -U postgres"] + interval: 5s + timeout: 5s + retries: 5 + +volumes: + postgres_data: +``` +CursorLens provides different images for various release types: + +- `nightly`: Latest development build +- `alpha`: Pre-release versions +- `stable`: Production-ready releases + + +## Getting Started (From Source) For detailed installation instructions, please refer to our [Installation Guide](https://www.cursorlens.com/docs/getting-started/installation). @@ -143,6 +194,7 @@ If you encounter any issues or have questions, please file an issue on the GitHu For more detailed information, please visit our [documentation](https://www.cursorlens.com/docs/project/introduction). + --- Happy coding with Cursor Lens! diff --git a/next.config.js b/next.config.js index e0b5fdf..0f8d6ec 100644 --- a/next.config.js +++ b/next.config.js @@ -1,8 +1,11 @@ +const IsStandalone = process.env.BUILD_STANDALONE == "true"; + /** @type {import('next').NextConfig} */ const nextConfig = { typescript: { ignoreBuildErrors: true, }, + output: IsStandalone ? "standalone" : undefined, }; module.exports = nextConfig; diff --git a/package.json b/package.json index 1876a3b..44c9c47 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,8 @@ "test:coverage": "vitest run --coverage", "test:ui": "vitest --ui", "seed": "tsx prisma/seed.ts", - "update-log-costs": "tsx scripts/update-log-costs.ts" + "update-log-costs": "tsx scripts/update-log-costs.ts", + "postinstall": "npx prisma generate" }, "prisma": { "seed": "tsx prisma/seed.ts" diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 0c94760..1a93826 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -46,7 +46,7 @@ model ModelCost { model String inputTokenCost Float outputTokenCost Float - validFrom DateTime + validFrom DateTime? validTo DateTime? @@unique([provider, model, validFrom]) diff --git a/run.Dockerfile b/run.Dockerfile new file mode 100644 index 0000000..96b1ffd --- /dev/null +++ b/run.Dockerfile @@ -0,0 +1,32 @@ +# This docker image does not build the application, it just runs it + +FROM node:18-alpine + +WORKDIR /app + +RUN apk update +RUN apk add --no-cache libc6-compat openssl + +ENV NODE_ENV=production +ENV NEXT_TELEMETRY_DISABLED=1 + +RUN addgroup --system --gid 1001 nodejs +RUN adduser --system --uid 1001 nextjs + +RUN npm install -g prisma + +USER nextjs + +COPY --chown=nextjs:nodejs ./public ./public +COPY --chown=nextjs:nodejs ./.next/standalone ./ +COPY --chown=nextjs:nodejs ./.next/static ./.next/static +COPY --chown=nextjs:nodejs ./scripts ./scripts +RUN chmod -R 777 .next + +# Copy the Prisma schema. +COPY --chown=nextjs:nodejs ./prisma ./prisma + + + + +CMD ["sh", "scripts/_docker_run.sh"] \ No newline at end of file diff --git a/scripts/_docker_run.sh b/scripts/_docker_run.sh new file mode 100644 index 0000000..a26ee0a --- /dev/null +++ b/scripts/_docker_run.sh @@ -0,0 +1,10 @@ +#!/bin/sh + +# Run migrations +npx prisma migrate deploy + +# Seed the database +npx prisma db seed + +# Start the application +node server.js \ No newline at end of file diff --git a/scripts/_github_action_tags_extractor.sh b/scripts/_github_action_tags_extractor.sh new file mode 100644 index 0000000..1e98e21 --- /dev/null +++ b/scripts/_github_action_tags_extractor.sh @@ -0,0 +1,55 @@ +#!/bin/bash + +# Exit on error +set -e + +# Check if tag name is provided +if [ -z "$1" ]; then + echo "Error: Tag name is required" + echo "Usage: $0 " + exit 1 +fi + +# Function to extract version from tag +extract_version() { + local tag=$1 + # Remove 'v' prefix and any suffix after '-' + echo "$tag" | sed -E 's/^v//' | sed -E 's/-.*$//' +} + +# Function to check if tag is alpha +is_alpha() { + local tag=$1 + [[ "$tag" == *"-alpha" ]] +} + +TAG_NAME=$1 + +# Extract version +VERSION=$(extract_version "$TAG_NAME") + +# Convert repository name to lowercase +REPO_NAME=$(echo "$GITHUB_REPOSITORY" | tr '[:upper:]' '[:lower:]') + +# Initialize tags array +TAGS=() + +# Add appropriate tags based on tag type +if is_alpha "$TAG_NAME"; then + # For alpha releases + TAGS+=("ghcr.io/$REPO_NAME:$VERSION-alpha") + TAGS+=("ghcr.io/$REPO_NAME:alpha") +else + # For regular releases + TAGS+=("ghcr.io/$REPO_NAME:$VERSION") + TAGS+=("ghcr.io/$REPO_NAME:latest") +fi + +# Convert array to comma-separated string +TAGS_STRING=$(IFS=,; echo "${TAGS[*]}") + +# Export the tags as environment variable +echo "DOCKER_TAGS=$TAGS_STRING" >> $GITHUB_ENV + +# Print for debugging +echo "Extracted tags: $TAGS_STRING" \ No newline at end of file diff --git a/scripts/alpha-release.sh b/scripts/alpha-release.sh new file mode 100755 index 0000000..b8806e9 --- /dev/null +++ b/scripts/alpha-release.sh @@ -0,0 +1,21 @@ +#!/bin/bash + +if [ -z "$1" ]; then + echo "Usage: $0 " + echo "Example: $0 1.0.0" + exit 1 +fi + +VERSION=$1 + +# Validate version format (semantic versioning) +if ! [[ $VERSION =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + echo "Error: Version must follow semantic versioning (e.g., 1.0.0)" + exit 1 +fi + +# Create and push the tag +git tag -a "v$VERSION-alpha" -m "Alpha release v$VERSION" +git push origin "v$VERSION-alpha" + +echo "Created and pushed alpha release tag v$VERSION-alpha" \ No newline at end of file diff --git a/scripts/stable-release.sh b/scripts/stable-release.sh new file mode 100755 index 0000000..f53f2ab --- /dev/null +++ b/scripts/stable-release.sh @@ -0,0 +1,21 @@ +#!/bin/bash + +if [ -z "$1" ]; then + echo "Usage: $0 " + echo "Example: $0 1.0.0" + exit 1 +fi + +VERSION=$1 + +# Validate version format (semantic versioning) +if ! [[ $VERSION =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + echo "Error: Version must follow semantic versioning (e.g., 1.0.0)" + exit 1 +fi + +# Create and push the tag +git tag -a "v$VERSION" -m "Release v$VERSION" +git push origin "v$VERSION" + +echo "Created and pushed stable release tag v$VERSION" \ No newline at end of file diff --git a/scripts/update-log-costs.ts b/scripts/update-log-costs.ts index 490f3f1..bc2084e 100644 --- a/scripts/update-log-costs.ts +++ b/scripts/update-log-costs.ts @@ -1,7 +1,6 @@ -import { PrismaClient } from "@prisma/client"; +import prisma from "@/lib/prisma"; import { getModelCost } from "../src/lib/cost-calculator"; -const prisma = new PrismaClient(); async function updateLogCosts() { const logs = await prisma.log.findMany(); diff --git a/src/app/[...openai]/route.ts b/src/app/[...openai]/route.ts index 2c023f7..8d81b78 100644 --- a/src/app/[...openai]/route.ts +++ b/src/app/[...openai]/route.ts @@ -53,7 +53,11 @@ async function getAIModelClient(provider: string, model: string) { return groqClient(model); } case "ollama": - return ollama("llama3.1"); + const olm = ollama(model || "llama3.1"); + if (process.env.OLLAMA_BASE_URL) { + olm.config.baseURL = process.env.OLLAMA_BASE_URL; + } + return olm; case "google-vertex": throw new Error("Google Vertex AI is not currently supported"); default: @@ -180,29 +184,34 @@ export async function POST( const outputTokens = usage?.completionTokens ?? 0; const totalTokens = usage?.totalTokens ?? 0; - const modelCost = await getModelCost(provider, model); - const inputCost = (inputTokens / 1000000) * modelCost.inputTokenCost; - const outputCost = - (outputTokens / 1000000) * modelCost.outputTokenCost; - const totalCost = inputCost + outputCost; - - logEntry.response = { - text, - toolCalls, - toolResults, - usage, - finishReason, - ...otherProps, - }; - logEntry.metadata = { - ...logEntry.metadata, - inputTokens, - outputTokens, - totalTokens, - inputCost, - outputCost, - totalCost, - }; + + try { + const modelCost = await getModelCost(provider, model); + const inputCost = (inputTokens / 1000000) * modelCost.inputTokenCost; + const outputCost = + (outputTokens / 1000000) * modelCost.outputTokenCost; + const totalCost = inputCost + outputCost; + + logEntry.response = { + text, + toolCalls, + toolResults, + usage, + finishReason, + ...otherProps, + }; + logEntry.metadata = { + ...logEntry.metadata, + inputTokens, + outputTokens, + totalTokens, + inputCost, + outputCost, + totalCost, + }; + } catch (err) { + console.error("Error while inserting log:", err); + } await insertLog(logEntry); }, }); @@ -250,21 +259,26 @@ export async function POST( const outputTokens = result.usage?.completionTokens ?? 0; const totalTokens = result.usage?.totalTokens ?? 0; - const modelCost = await getModelCost(provider, model); - const inputCost = inputTokens * modelCost.inputTokenCost; - const outputCost = outputTokens * modelCost.outputTokenCost; - const totalCost = inputCost + outputCost; - logEntry.response = result; - logEntry.metadata = { - ...logEntry.metadata, - inputTokens, - outputTokens, - totalTokens, - inputCost, - outputCost, - totalCost, - }; + + try { + const modelCost = await getModelCost(provider, model); + const inputCost = inputTokens * modelCost.inputTokenCost; + const outputCost = outputTokens * modelCost.outputTokenCost; + const totalCost = inputCost + outputCost; + + logEntry.metadata = { + ...logEntry.metadata, + inputTokens, + outputTokens, + totalTokens, + inputCost, + outputCost, + totalCost, + }; + } catch (err) { + console.error("Error while calculating cost:", err); + } await insertLog(logEntry); return NextResponse.json(result); diff --git a/src/lib/cost-calculator.ts b/src/lib/cost-calculator.ts index b9b129b..ceccde2 100644 --- a/src/lib/cost-calculator.ts +++ b/src/lib/cost-calculator.ts @@ -1,15 +1,29 @@ -import { PrismaClient } from "@prisma/client"; +import prisma from "@/lib/prisma"; +import { ModelCost } from "@prisma/client"; -const prisma = new PrismaClient(); - -export async function getModelCost(provider: string, model: string) { +export async function getModelCost(provider: string, model: string): Promise { const currentDate = new Date(); + + if (provider.toLowerCase() === "ollama") { + return { + id: "ollama", + provider, + model, + inputTokenCost: 0, + outputTokenCost: 0, + validFrom: null, + validTo: null, + }; + } + const modelCost = await prisma.modelCost.findFirst({ where: { provider, model, - OR: [{ validFrom: null }, { validFrom: { lte: currentDate } }], - OR: [{ validTo: null }, { validTo: { gte: currentDate } }], + AND: [ + { OR: [{ validFrom: null }, { validFrom: { lte: currentDate } }] }, + { OR: [{ validTo: null }, { validTo: { gte: currentDate } }] }, + ], }, orderBy: { validFrom: "desc" }, });