From 53a1e07e562430d9e66a22c9dd83493ca200e007 Mon Sep 17 00:00:00 2001 From: cte Date: Wed, 11 Jun 2025 08:55:11 -0700 Subject: [PATCH 01/19] Cloud agents PoC --- apps/cloud-agents/.gitignore | 4 + apps/cloud-agents/ARCHITECTURE.md | 299 ++++++++++++++++++ apps/cloud-agents/README.md | 252 +++++++++++++++ apps/cloud-agents/docker-compose.yml | 88 ++++++ apps/cloud-agents/docker/Dockerfile.agent | 88 ++++++ apps/cloud-agents/docker/Dockerfile.app | 50 +++ apps/cloud-agents/drizzle.config.ts | 10 + apps/cloud-agents/eslint.config.mjs | 17 + apps/cloud-agents/next-env.d.ts | 5 + apps/cloud-agents/next.config.ts | 7 + apps/cloud-agents/package.json | 42 +++ apps/cloud-agents/src/app/api/health/route.ts | 47 +++ .../src/app/api/jobs/[id]/route.ts | 35 ++ apps/cloud-agents/src/app/api/jobs/route.ts | 73 +++++ .../src/app/api/webhooks/github/route.ts | 100 ++++++ apps/cloud-agents/src/app/layout.tsx | 14 + apps/cloud-agents/src/app/page.tsx | 31 ++ apps/cloud-agents/src/lib/db/index.ts | 11 + apps/cloud-agents/src/lib/db/schema.ts | 85 +++++ apps/cloud-agents/src/lib/docker/runner.ts | 97 ++++++ apps/cloud-agents/src/lib/queue/index.ts | 51 +++ apps/cloud-agents/src/lib/queue/processor.ts | 138 ++++++++ apps/cloud-agents/src/lib/queue/worker.ts | 19 ++ apps/cloud-agents/tsconfig.json | 8 + pnpm-lock.yaml | 248 ++++++++++++++- 25 files changed, 1804 insertions(+), 15 deletions(-) create mode 100644 apps/cloud-agents/.gitignore create mode 100644 apps/cloud-agents/ARCHITECTURE.md create mode 100644 apps/cloud-agents/README.md create mode 100644 apps/cloud-agents/docker-compose.yml create mode 100644 apps/cloud-agents/docker/Dockerfile.agent create mode 100644 apps/cloud-agents/docker/Dockerfile.app create mode 100644 apps/cloud-agents/drizzle.config.ts create mode 100644 apps/cloud-agents/eslint.config.mjs create mode 100644 apps/cloud-agents/next-env.d.ts create mode 100644 apps/cloud-agents/next.config.ts create mode 100644 apps/cloud-agents/package.json create mode 100644 apps/cloud-agents/src/app/api/health/route.ts create mode 100644 apps/cloud-agents/src/app/api/jobs/[id]/route.ts create mode 100644 apps/cloud-agents/src/app/api/jobs/route.ts create mode 100644 apps/cloud-agents/src/app/api/webhooks/github/route.ts create mode 100644 apps/cloud-agents/src/app/layout.tsx create mode 100644 apps/cloud-agents/src/app/page.tsx create mode 100644 apps/cloud-agents/src/lib/db/index.ts create mode 100644 apps/cloud-agents/src/lib/db/schema.ts create mode 100644 apps/cloud-agents/src/lib/docker/runner.ts create mode 100644 apps/cloud-agents/src/lib/queue/index.ts create mode 100644 apps/cloud-agents/src/lib/queue/processor.ts create mode 100644 apps/cloud-agents/src/lib/queue/worker.ts create mode 100644 apps/cloud-agents/tsconfig.json diff --git a/apps/cloud-agents/.gitignore b/apps/cloud-agents/.gitignore new file mode 100644 index 0000000000..efacd67d11 --- /dev/null +++ b/apps/cloud-agents/.gitignore @@ -0,0 +1,4 @@ +.next + +docker/postgres-data +docker/redis-data diff --git a/apps/cloud-agents/ARCHITECTURE.md b/apps/cloud-agents/ARCHITECTURE.md new file mode 100644 index 0000000000..f4d2c02892 --- /dev/null +++ b/apps/cloud-agents/ARCHITECTURE.md @@ -0,0 +1,299 @@ +# Cloud Agents Architecture + +## Overview + +Cloud Agents is a Next.js application that exposes an API for enqueueing Roo Code tasks via BullMQ, processes them in Docker containers, and integrates with GitHub webhooks to automatically fix issues. + +## System Architecture + +```mermaid +flowchart TB + GH[GitHub Webhook] --> API[Next.js API Routes] + API --> BQ[BullMQ Queue] + BQ --> JH[Job Handler] + JH --> DC[Docker Container] + DC --> RC[Roo Code Task Runner] + + subgraph Infrastructure + PG[(PostgreSQL)] + RD[(Redis)] + end + + API --> PG + BQ --> RD + JH --> PG +``` + +## Directory Structure + +``` +apps/cloud-agents/ +├── src/ +│ ├── app/ +│ │ ├── api/ +│ │ │ ├── webhooks/ +│ │ │ │ └── github/ +│ │ │ │ └── route.ts # GitHub webhook handler +│ │ │ ├── jobs/ +│ │ │ │ ├── route.ts # Create job endpoint +│ │ │ │ └── [id]/ +│ │ │ │ └── route.ts # Get job status +│ │ │ └── health/ +│ │ │ └── route.ts # Health check +│ │ ├── layout.tsx +│ │ └── page.tsx # Simple dashboard +│ ├── lib/ +│ │ ├── db/ +│ │ │ ├── schema.ts # Cloud agents schema +│ │ │ └── index.ts +│ │ ├── queue/ +│ │ │ ├── processor.ts # BullMQ processor +│ │ │ ├── jobs.ts # Job definitions +│ │ │ └── index.ts +│ │ ├── docker/ +│ │ │ ├── runner.ts # Docker container runner +│ │ │ └── config.ts +│ │ └── github/ +│ │ ├── webhooks.ts # Webhook handlers +│ │ └── types.ts +│ └── types/ +│ └── index.ts +├── docker/ +│ ├── Dockerfile.agent # Simplified runner +│ └── docker-compose.yml +├── package.json +├── tsconfig.json +├── next.config.ts +└── .env.example +``` + +## Key Components + +### 1. Database Schema (Drizzle ORM) + +The cloud agents database extends the existing evals database with additional tables: + +```typescript +// Cloud agent specific tables +- cloudJobs: Track job requests + - id: integer (primary key) + - type: text (e.g., 'github.issue.fix', 'task.execute') + - status: text ('pending', 'processing', 'completed', 'failed') + - payload: jsonb (job-specific data) + - result: jsonb (job output) + - error: text (error message if failed) + - createdAt: timestamp + - startedAt: timestamp + - completedAt: timestamp + +- cloudTasks: Link cloud jobs to Roo Code tasks + - id: integer (primary key) + - jobId: integer (references cloudJobs) + - taskId: integer (references tasks from evals) + - containerId: text (Docker container ID) + - createdAt: timestamp +``` + +### 2. BullMQ Job Types + +```typescript +interface JobTypes { + "github.issue.fix": { + repo: string // e.g., "RooCodeInc/Roo-Code" + issue: number // Issue number + title: string // Issue title + body: string // Issue description + labels?: string[] // Issue labels + } + + "task.execute": { + prompt: string // Task prompt + workspace?: string // Optional workspace path + settings?: RooCodeSettings // Optional Roo Code settings override + } +} +``` + +### 3. Simplified Docker Runner + +The cloud agents Docker image is based on the existing `Dockerfile.runner` but simplified: + +**Remove:** + +- Language-specific VS Code extensions (Go, Java, Python, Rust) +- Eval-specific dependencies and test infrastructure +- UV/Python sync steps + +**Keep:** + +- Base Node.js environment +- VS Code installation +- Roo Code extension build and installation +- Basic utilities (git, curl, etc.) +- Docker CLI for nested container support + +### 4. API Endpoints + +#### `POST /api/webhooks/github` + +Handles GitHub webhook events, specifically for issue events. + +- Verifies webhook signature +- Parses issue data +- Creates appropriate job in queue + +#### `POST /api/jobs` + +Creates a new job in the queue. + +```typescript +Request: { + type: keyof JobTypes + payload: JobTypes[type] +} +Response: { + id: string + status: string +} +``` + +#### `GET /api/jobs/:id` + +Retrieves job status and results. + +```typescript +Response: { + id: string + type: string + status: string + payload: object + result?: object + error?: string + createdAt: string + startedAt?: string + completedAt?: string +} +``` + +#### `GET /api/health` + +Health check endpoint for monitoring. + +```typescript +Response: { + status: "ok" | "error" + services: { + database: boolean + redis: boolean + docker: boolean + } +} +``` + +## Implementation Phases + +### Phase 1: Core Infrastructure + +1. Create Next.js app structure in `apps/cloud-agents` +2. Set up database schema using Drizzle ORM +3. Configure Docker compose with PostgreSQL & Redis +4. Create simplified Dockerfile.agent + +### Phase 2: Job Queue + +1. Implement BullMQ setup and configuration +2. Create job processor with proper error handling +3. Add Docker container spawning logic +4. Implement job status tracking in database + +### Phase 3: API & Webhooks + +1. Create all API route handlers +2. Implement GitHub webhook signature verification +3. Add issue parsing and automatic job creation +4. Create simple status dashboard + +### Phase 4: Testing & Deployment + +1. Add integration tests for API endpoints +2. Create production docker-compose configuration +3. Add monitoring and structured logging +4. Write comprehensive documentation + +## Configuration + +### Environment Variables + +```env +# Database +DATABASE_URL=postgresql://postgres:password@localhost:5432/cloud_agents + +# Redis +REDIS_URL=redis://localhost:6379 + +# GitHub Integration +GITHUB_WEBHOOK_SECRET=your-webhook-secret +GITHUB_APP_ID=your-app-id +GITHUB_PRIVATE_KEY=your-private-key-base64 + +# API Keys for Roo Code +OPENROUTER_API_KEY=your-openrouter-key +ANTHROPIC_API_KEY=your-anthropic-key + +# Docker Configuration +DOCKER_NETWORK=cloud-agents_default +DOCKER_IMAGE=cloud-agents-runner:latest +MAX_CONCURRENT_CONTAINERS=5 + +# Application +PORT=3001 +NODE_ENV=development +``` + +### Docker Compose Services + +```yaml +services: + app: + build: . + ports: + - "3001:3001" + environment: + - DATABASE_URL + - REDIS_URL + volumes: + - /var/run/docker.sock:/var/run/docker.sock + depends_on: + - db + - redis + + db: + image: postgres:17 + environment: + - POSTGRES_PASSWORD=password + - POSTGRES_DB=cloud_agents + + redis: + image: redis:7-alpine +``` + +## Error Handling & Retry Logic + +1. **Job Retries**: Failed jobs will be retried up to 3 times with exponential backoff +2. **Container Timeouts**: Tasks have a 30-minute timeout by default +3. **Resource Cleanup**: Containers are always cleaned up, even on failure +4. **Dead Letter Queue**: Failed jobs after all retries go to DLQ for manual review + +## Security Considerations + +1. **Webhook Verification**: All GitHub webhooks are verified using HMAC +2. **Container Isolation**: Each task runs in an isolated container +3. **Resource Limits**: CPU and memory limits on containers +4. **API Authentication**: Consider adding API key authentication for job creation + +## Monitoring & Observability + +1. **Metrics**: Job queue depth, processing time, success rate +2. **Logging**: Structured logs for all job processing steps +3. **Health Checks**: Regular checks on all dependent services +4. **Alerts**: Notifications for failed jobs and system issues diff --git a/apps/cloud-agents/README.md b/apps/cloud-agents/README.md new file mode 100644 index 0000000000..c18997804b --- /dev/null +++ b/apps/cloud-agents/README.md @@ -0,0 +1,252 @@ +# Cloud Agents + +A Next.js application that exposes an API for enqueueing Roo Code tasks via BullMQ, processes them in Docker containers, and integrates with GitHub webhooks to automatically fix issues. + +## Features + +- **Job Queue**: BullMQ-powered job processing with Redis +- **GitHub Integration**: Webhook handler for automatic issue fixing +- **Docker Containers**: Isolated task execution using Roo Code +- **REST API**: Simple endpoints for job management +- **Database**: PostgreSQL with Drizzle ORM + +## Quick Start + +### Prerequisites + +- Node.js 20+ +- pnpm +- Docker & Docker Compose + +### Development Setup + +1. **Install dependencies**: + + ```bash + cd apps/cloud-agents + pnpm install + ``` + +2. **Set up environment**: + + ```bash + cp .env.example .env.local + # Edit .env.local with your configuration + ``` + +3. **Start services**: + + ```bash + pnpm services:start + ``` + +4. **Run database migrations**: + + ```bash + pnpm db:push + ``` + +5. **Start the development server**: + + ```bash + pnpm dev + ``` + +6. **Start the worker** (in another terminal): + ```bash + pnpm worker:dev + ``` + +The API will be available at http://localhost:3001 + +### Production Deployment + +1. **Build Docker images**: + + ```bash + docker compose build + ``` + +2. **Start all services**: + ```bash + docker compose --profile server up -d + ``` + +## API Endpoints + +### Health Check + +```http +GET /api/health +``` + +### Create Job + +```http +POST /api/jobs +Content-Type: application/json + +{ + "type": "github.issue.fix", + "payload": { + "repo": "owner/repo", + "issue": 123, + "title": "Issue title", + "body": "Issue description" + } +} +``` + +### Get Job Status + +```http +GET /api/jobs/:id +``` + +### GitHub Webhook + +```http +POST /api/webhooks/github +``` + +## Job Types + +### GitHub Issue Fix + +Automatically creates tasks to fix GitHub issues when they are opened. + +```typescript +{ + "type": "github.issue.fix", + "payload": { + "repo": "owner/repo", + "issue": 123, + "title": "Issue title", + "body": "Issue description", + "labels": ["bug", "priority:high"] + } +} +``` + +### Custom Task + +Execute arbitrary Roo Code tasks. + +```typescript +{ + "type": "task.execute", + "payload": { + "prompt": "Your task description", + "workspace": "/path/to/workspace", + "settings": { + "model": "claude-3-sonnet", + "temperature": 0.1 + } + } +} +``` + +## Configuration + +### Environment Variables + +See `.env.example` for all available configuration options. + +### GitHub Webhook Setup + +1. Create a webhook in your GitHub repository +2. Set the payload URL to `https://your-domain.com/api/webhooks/github` +3. Set the content type to `application/json` +4. Set the secret to match `GITHUB_WEBHOOK_SECRET` in your environment +5. Subscribe to "Issues" events + +## Architecture + +See [ARCHITECTURE.md](./ARCHITECTURE.md) for detailed system design and component descriptions. + +## Development + +### Database Management + +```bash +# Generate migration +pnpm db:generate + +# Apply migration +pnpm db:migrate + +# Push schema changes (development) +pnpm db:push + +# Open database studio +pnpm db:studio +``` + +### Docker Commands + +```bash +# Build agent container +docker compose build runner + +# Start a shell in the agent container +docker compose run --rm runner bash + +# View logs +docker compose logs -f worker +``` + +### Testing + +Test the API with curl: + +```bash +# Health check +curl http://localhost:3001/api/health + +# Create a job +curl -X POST http://localhost:3001/api/jobs \ + -H "Content-Type: application/json" \ + -d '{ + "type": "task.execute", + "payload": { + "prompt": "Create a simple hello world program" + } + }' + +# Check job status +curl http://localhost:3001/api/jobs/1 +``` + +## Troubleshooting + +### Common Issues + +1. **Database connection failed**: Make sure PostgreSQL is running and the connection string is correct +2. **Redis connection failed**: Ensure Redis is running on the specified port +3. **Docker permission denied**: Make sure your user is in the docker group +4. **Port already in use**: Check if another service is using ports 3001, 5433, or 6380 + +### Logs + +```bash +# Application logs +docker compose logs app + +# Worker logs +docker compose logs worker + +# Database logs +docker compose logs db +``` + +## Contributing + +1. Fork the repository +2. Create a feature branch +3. Make your changes +4. Add tests if applicable +5. Submit a pull request + +## License + +See the main repository license. diff --git a/apps/cloud-agents/docker-compose.yml b/apps/cloud-agents/docker-compose.yml new file mode 100644 index 0000000000..97662b281c --- /dev/null +++ b/apps/cloud-agents/docker-compose.yml @@ -0,0 +1,88 @@ +services: + app: + build: + context: ../../ + dockerfile: apps/cloud-agents/docker/Dockerfile.app + ports: + - "${CLOUD_AGENTS_PORT:-3001}:3001" + environment: + - DATABASE_URL=postgresql://postgres:password@db:5432/cloud_agents + - REDIS_URL=redis://redis:6379 + - NODE_ENV=production + volumes: + - /var/run/docker.sock:/var/run/docker.sock + depends_on: + db: + condition: service_healthy + redis: + condition: service_started + profiles: + - server + + worker: + build: + context: ../../ + dockerfile: apps/cloud-agents/docker/Dockerfile.app + command: pnpm --filter @roo-code/cloud-agents worker:dev + environment: + - DATABASE_URL=postgresql://postgres:password@db:5432/cloud_agents + - REDIS_URL=redis://redis:6379 + - NODE_ENV=production + volumes: + - /var/run/docker.sock:/var/run/docker.sock + - /tmp/cloud-agents:/var/log/cloud-agents + depends_on: + - db + - redis + profiles: + - server + + db: + container_name: cloud-agents-db + image: postgres:17.5 + ports: + - "${CLOUD_AGENTS_DB_PORT:-5433}:5432" + volumes: + - ./docker/postgres-data:/var/lib/postgresql/data + environment: + - POSTGRES_USER=postgres + - POSTGRES_PASSWORD=password + - POSTGRES_DB=cloud_agents + healthcheck: + test: ["CMD-SHELL", "pg_isready -U postgres -d cloud_agents"] + interval: 5s + timeout: 5s + retries: 5 + start_period: 30s + profiles: + - server + + redis: + container_name: cloud-agents-redis + image: redis:7-alpine + ports: + - "${CLOUD_AGENTS_REDIS_PORT:-6380}:6379" + volumes: + - ./docker/redis-data:/data + command: redis-server --appendonly yes + profiles: + - server + + runner: + build: + context: ../../ + dockerfile: apps/cloud-agents/docker/Dockerfile.agent + environment: + - HOST_EXECUTION_METHOD=docker + volumes: + - /var/run/docker.sock:/var/run/docker.sock + - /tmp/cloud-agents:/var/log/cloud-agents + stdin_open: true + tty: true + profiles: + - runner + +networks: + default: + name: cloud-agents_default + driver: bridge \ No newline at end of file diff --git a/apps/cloud-agents/docker/Dockerfile.agent b/apps/cloud-agents/docker/Dockerfile.agent new file mode 100644 index 0000000000..e6cdb0fc81 --- /dev/null +++ b/apps/cloud-agents/docker/Dockerfile.agent @@ -0,0 +1,88 @@ +FROM node:20-slim AS base + +# Install pnpm +ENV PNPM_HOME="/pnpm" +ENV PATH="$PNPM_HOME:$PATH" +RUN corepack enable + +# Install system packages +RUN apt update && \ + apt install -y \ + curl \ + git \ + vim \ + jq \ + netcat-openbsd \ + apt-transport-https \ + ca-certificates \ + gnupg \ + lsb-release \ + wget \ + gpg \ + xvfb \ + && rm -rf /var/lib/apt/lists/* + +# Install Docker cli +RUN curl -fsSL https://download.docker.com/linux/debian/gpg | gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg \ + && echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/debian $(lsb_release -cs) stable" | tee /etc/apt/sources.list.d/docker.list > /dev/null \ + && apt update && apt install -y docker-ce-cli \ + && rm -rf /var/lib/apt/lists/* + +# Install VS Code +RUN wget -qO- https://packages.microsoft.com/keys/microsoft.asc | gpg --dearmor > packages.microsoft.gpg \ + && install -D -o root -g root -m 644 packages.microsoft.gpg /etc/apt/keyrings/packages.microsoft.gpg \ + && echo "deb [arch=amd64,arm64,armhf signed-by=/etc/apt/keyrings/packages.microsoft.gpg] https://packages.microsoft.com/repos/code stable main" | tee /etc/apt/sources.list.d/vscode.list > /dev/null \ + && rm -f packages.microsoft.gpg \ + && apt update && apt install -y code \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /roo + +# Create VS Code template directory and install basic extensions +RUN mkdir -p /roo/.vscode-template \ + && code --no-sandbox --user-data-dir /roo/.vscode-template --install-extension ms-vscode.vscode-eslint + +WORKDIR /roo/repo + +# Install npm packages +RUN mkdir -p \ + scripts \ + packages/build \ + packages/config-eslint \ + packages/config-typescript \ + packages/ipc \ + packages/types \ + src \ + webview-ui + +# Copy package files for dependency installation +COPY ./package.json ./ +COPY ./pnpm-lock.yaml ./ +COPY ./pnpm-workspace.yaml ./ +COPY ./scripts/bootstrap.mjs ./scripts/ +COPY ./packages/build/package.json ./packages/build/ +COPY ./packages/config-eslint/package.json ./packages/config-eslint/ +COPY ./packages/config-typescript/package.json ./packages/config-typescript/ +COPY ./packages/ipc/package.json ./packages/ipc/ +COPY ./packages/types/package.json ./packages/types/ +COPY ./src/package.json ./src/ +COPY ./webview-ui/package.json ./webview-ui/ + +RUN pnpm install + +# Copy source code +COPY . ./ + +# Copy the pre-installed VS Code extensions +RUN cp -r /roo/.vscode-template /roo/.vscode + +# Build the Roo Code extension +RUN pnpm vsix -- --out ../bin/roo-code.vsix \ + && code --no-sandbox --user-data-dir /roo/.vscode --install-extension bin/roo-code.vsix + +# Set environment variables +ENV DATABASE_URL=postgresql://postgres:password@db:5432/cloud_agents +ENV REDIS_URL=redis://redis:6379 + +# Default command +CMD ["bash"] \ No newline at end of file diff --git a/apps/cloud-agents/docker/Dockerfile.app b/apps/cloud-agents/docker/Dockerfile.app new file mode 100644 index 0000000000..1269e4e25d --- /dev/null +++ b/apps/cloud-agents/docker/Dockerfile.app @@ -0,0 +1,50 @@ +FROM node:20-slim AS base + +# Install pnpm +ENV PNPM_HOME="/pnpm" +ENV PATH="$PNPM_HOME:$PATH" +RUN corepack enable + +# Install Docker CLI for container management +RUN apt update && \ + apt install -y \ + curl \ + apt-transport-https \ + ca-certificates \ + gnupg \ + lsb-release \ + && curl -fsSL https://download.docker.com/linux/debian/gpg | gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg \ + && echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/debian $(lsb_release -cs) stable" | tee /etc/apt/sources.list.d/docker.list > /dev/null \ + && apt update && apt install -y docker-ce-cli \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /app + +# Copy package files +COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./ +COPY scripts/bootstrap.mjs ./scripts/ +COPY packages/types/package.json ./packages/types/ +COPY packages/config-eslint/package.json ./packages/config-eslint/ +COPY packages/config-typescript/package.json ./packages/config-typescript/ +COPY apps/cloud-agents/package.json ./apps/cloud-agents/ + +# Install dependencies +RUN pnpm install --filter @roo-code/cloud-agents + +# Copy source code +COPY packages/types ./packages/types/ +COPY packages/config-eslint ./packages/config-eslint/ +COPY packages/config-typescript ./packages/config-typescript/ +COPY apps/cloud-agents ./apps/cloud-agents/ + +# Set working directory to the app +WORKDIR /app/apps/cloud-agents + +# Build the application +RUN pnpm build + +# Expose port +EXPOSE 3001 + +# Start the application +CMD ["pnpm", "start"] \ No newline at end of file diff --git a/apps/cloud-agents/drizzle.config.ts b/apps/cloud-agents/drizzle.config.ts new file mode 100644 index 0000000000..446af92fa4 --- /dev/null +++ b/apps/cloud-agents/drizzle.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from "drizzle-kit" + +export default defineConfig({ + schema: "./src/lib/db/schema.ts", + out: "./drizzle", + dialect: "postgresql", + dbCredentials: { + url: process.env.DATABASE_URL!, + }, +}) diff --git a/apps/cloud-agents/eslint.config.mjs b/apps/cloud-agents/eslint.config.mjs new file mode 100644 index 0000000000..024d6157d4 --- /dev/null +++ b/apps/cloud-agents/eslint.config.mjs @@ -0,0 +1,17 @@ +import { nextJsConfig } from "@roo-code/config-eslint/next-js" + +/** @type {import("eslint").Linter.Config} */ +export default [ + ...nextJsConfig, + { + rules: { + "no-unused-vars": "off", + "@typescript-eslint/no-unused-vars": [ + "error", + { + caughtErrorsIgnorePattern: "^_", + }, + ], + }, + }, +] diff --git a/apps/cloud-agents/next-env.d.ts b/apps/cloud-agents/next-env.d.ts new file mode 100644 index 0000000000..1b3be0840f --- /dev/null +++ b/apps/cloud-agents/next-env.d.ts @@ -0,0 +1,5 @@ +/// +/// + +// NOTE: This file should not be edited +// see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/apps/cloud-agents/next.config.ts b/apps/cloud-agents/next.config.ts new file mode 100644 index 0000000000..dd960b3d85 --- /dev/null +++ b/apps/cloud-agents/next.config.ts @@ -0,0 +1,7 @@ +import type { NextConfig } from "next" + +const nextConfig: NextConfig = { + serverExternalPackages: ["postgres", "ioredis", "bullmq"], +} + +export default nextConfig diff --git a/apps/cloud-agents/package.json b/apps/cloud-agents/package.json new file mode 100644 index 0000000000..dbcc7285b5 --- /dev/null +++ b/apps/cloud-agents/package.json @@ -0,0 +1,42 @@ +{ + "name": "@roo-code/cloud-agents", + "version": "0.0.0", + "type": "module", + "scripts": { + "lint": "next lint", + "check-types": "tsc --noEmit", + "dev": "next dev --port 3001", + "build": "next build", + "start": "next start --port 3001", + "drizzle-kit": "dotenvx run -f .env.local -- tsx node_modules/drizzle-kit/bin.cjs", + "db:generate": "pnpm drizzle-kit generate", + "db:migrate": "pnpm drizzle-kit migrate", + "db:push": "pnpm drizzle-kit push", + "db:check": "pnpm drizzle-kit check", + "db:studio": "pnpm drizzle-kit studio", + "services:start": "docker compose up -d db redis", + "services:stop": "docker compose down", + "worker:dev": "tsx src/lib/queue/worker.ts" + }, + "dependencies": { + "@roo-code/types": "workspace:^", + "bullmq": "^5.37.0", + "drizzle-orm": "^0.44.1", + "execa": "^9.6.0", + "ioredis": "^5.4.3", + "next": "^15.2.5", + "postgres": "^3.4.7", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "zod": "^3.25.41" + }, + "devDependencies": { + "@roo-code/config-eslint": "workspace:^", + "@roo-code/config-typescript": "workspace:^", + "@types/node": "^22.15.20", + "@types/react": "^18.3.23", + "@types/react-dom": "^18.3.7", + "drizzle-kit": "^0.31.1", + "tsx": "^4.19.3" + } +} diff --git a/apps/cloud-agents/src/app/api/health/route.ts b/apps/cloud-agents/src/app/api/health/route.ts new file mode 100644 index 0000000000..aae951b021 --- /dev/null +++ b/apps/cloud-agents/src/app/api/health/route.ts @@ -0,0 +1,47 @@ +import { NextResponse } from "next/server" +import { redis } from "@/lib/queue" +import { db } from "@/lib/db" + +export async function GET() { + try { + const services = { + database: false, + redis: false, + } + + // Check database connection + try { + await db.execute("SELECT 1") + services.database = true + } catch (error) { + console.error("Database health check failed:", error) + } + + // Check Redis connection + try { + await redis.ping() + services.redis = true + } catch (error) { + console.error("Redis health check failed:", error) + } + + const allHealthy = Object.values(services).every(Boolean) + + return NextResponse.json( + { + status: allHealthy ? "ok" : "error", + services, + }, + { status: allHealthy ? 200 : 500 }, + ) + } catch (error) { + console.error("Health check error:", error) + return NextResponse.json( + { + status: "error", + error: "Internal server error", + }, + { status: 500 }, + ) + } +} diff --git a/apps/cloud-agents/src/app/api/jobs/[id]/route.ts b/apps/cloud-agents/src/app/api/jobs/[id]/route.ts new file mode 100644 index 0000000000..42e50f8086 --- /dev/null +++ b/apps/cloud-agents/src/app/api/jobs/[id]/route.ts @@ -0,0 +1,35 @@ +import { NextRequest, NextResponse } from "next/server" +import { db, cloudJobs } from "@/lib/db" +import { eq } from "drizzle-orm" + +export async function GET(request: NextRequest, { params }: { params: Promise<{ id: string }> }) { + try { + const { id } = await params + const jobId = parseInt(id, 10) + + if (isNaN(jobId)) { + return NextResponse.json({ error: "Invalid job ID" }, { status: 400 }) + } + + const [job] = await db.select().from(cloudJobs).where(eq(cloudJobs.id, jobId)).limit(1) + + if (!job) { + return NextResponse.json({ error: "Job not found" }, { status: 404 }) + } + + return NextResponse.json({ + id: job.id, + type: job.type, + status: job.status, + payload: job.payload, + result: job.result, + error: job.error, + createdAt: job.createdAt, + startedAt: job.startedAt, + completedAt: job.completedAt, + }) + } catch (error) { + console.error("Error fetching job:", error) + return NextResponse.json({ error: "Internal server error" }, { status: 500 }) + } +} diff --git a/apps/cloud-agents/src/app/api/jobs/route.ts b/apps/cloud-agents/src/app/api/jobs/route.ts new file mode 100644 index 0000000000..8e0932fbcf --- /dev/null +++ b/apps/cloud-agents/src/app/api/jobs/route.ts @@ -0,0 +1,73 @@ +import { NextRequest, NextResponse } from "next/server" +import { z } from "zod" +import { db, cloudJobs, JobTypes } from "@/lib/db" +import { addJob } from "@/lib/queue" + +const githubIssueFixSchema = z.object({ + repo: z.string(), + issue: z.number(), + title: z.string(), + body: z.string(), + labels: z.array(z.string()).optional(), +}) + +const taskExecuteSchema = z.object({ + prompt: z.string(), + workspace: z.string().optional(), + settings: z.record(z.unknown()).optional(), +}) + +const createJobSchema = z.discriminatedUnion("type", [ + z.object({ + type: z.literal("github.issue.fix"), + payload: githubIssueFixSchema, + }), + z.object({ + type: z.literal("task.execute"), + payload: taskExecuteSchema, + }), +]) + +export async function POST(request: NextRequest) { + try { + const body = await request.json() + const parsed = createJobSchema.parse(body) + + // Create job record in database + const jobs = await db + .insert(cloudJobs) + .values({ + type: parsed.type, + status: "pending", + payload: parsed.payload, + }) + .returning() + + const job = jobs[0] + if (!job) { + throw new Error("Failed to create job") + } + + // Add job to queue with proper typing + if (parsed.type === "github.issue.fix") { + await addJob("github.issue.fix", parsed.payload as JobTypes["github.issue.fix"], job.id) + } else if (parsed.type === "task.execute") { + await addJob("task.execute", parsed.payload as JobTypes["task.execute"], job.id) + } + + return NextResponse.json({ + id: job.id, + type: job.type, + status: job.status, + createdAt: job.createdAt, + }) + } catch (error) { + console.error("Error creating job:", error) + + if (error instanceof z.ZodError) { + return NextResponse.json({ error: "Invalid request body", details: error.errors }, { status: 400 }) + } + + return NextResponse.json({ error: "Internal server error" }, { status: 500 }) + } +} diff --git a/apps/cloud-agents/src/app/api/webhooks/github/route.ts b/apps/cloud-agents/src/app/api/webhooks/github/route.ts new file mode 100644 index 0000000000..1a81b5a92e --- /dev/null +++ b/apps/cloud-agents/src/app/api/webhooks/github/route.ts @@ -0,0 +1,100 @@ +import { NextRequest, NextResponse } from "next/server" +import { createHmac } from "crypto" +import { z } from "zod" +import { db, cloudJobs } from "@/lib/db" +import { addJob } from "@/lib/queue" + +const githubWebhookSchema = z.object({ + action: z.string(), + issue: z.object({ + number: z.number(), + title: z.string(), + body: z.string().nullable(), + labels: z.array( + z.object({ + name: z.string(), + }), + ), + }), + repository: z.object({ + full_name: z.string(), + }), +}) + +function verifySignature(payload: string, signature: string, secret: string): boolean { + const expectedSignature = createHmac("sha256", secret).update(payload, "utf8").digest("hex") + + const receivedSignature = signature.replace("sha256=", "") + return expectedSignature === receivedSignature +} + +export async function POST(request: NextRequest) { + try { + const signature = request.headers.get("x-hub-signature-256") + const event = request.headers.get("x-github-event") + + if (!signature) { + return NextResponse.json({ error: "Missing signature" }, { status: 400 }) + } + + const payload = await request.text() + + // Verify webhook signature + const webhookSecret = process.env.GITHUB_WEBHOOK_SECRET + if (webhookSecret && !verifySignature(payload, signature, webhookSecret)) { + return NextResponse.json({ error: "Invalid signature" }, { status: 401 }) + } + + // Only handle issue events + if (event !== "issues") { + return NextResponse.json({ message: "Event ignored" }) + } + + const data = githubWebhookSchema.parse(JSON.parse(payload)) + + // Only handle opened issues + if (data.action !== "opened") { + return NextResponse.json({ message: "Action ignored" }) + } + + // Create job for issue fix + const jobPayload = { + repo: data.repository.full_name, + issue: data.issue.number, + title: data.issue.title, + body: data.issue.body || "", + labels: data.issue.labels.map((label) => label.name), + } + + // Create job record in database + const jobs = await db + .insert(cloudJobs) + .values({ + type: "github.issue.fix", + status: "pending", + payload: jobPayload, + }) + .returning() + + const job = jobs[0] + if (!job) { + throw new Error("Failed to create job") + } + + // Add job to queue + await addJob("github.issue.fix", jobPayload, job.id) + + return NextResponse.json({ + message: "Job created successfully", + jobId: job.id, + }) + } catch (error) { + console.error("GitHub webhook error:", error) + + if (error instanceof z.ZodError) { + return NextResponse.json({ error: "Invalid webhook payload", details: error.errors }, { status: 400 }) + } + + return NextResponse.json({ error: "Internal server error" }, { status: 500 }) + } +} diff --git a/apps/cloud-agents/src/app/layout.tsx b/apps/cloud-agents/src/app/layout.tsx new file mode 100644 index 0000000000..03b9311e65 --- /dev/null +++ b/apps/cloud-agents/src/app/layout.tsx @@ -0,0 +1,14 @@ +import type { Metadata } from "next" + +export const metadata: Metadata = { + title: "Cloud Agents", + description: "Roo Code task execution service", +} + +export default function RootLayout({ children }: { children: React.ReactNode }) { + return ( + + {children} + + ) +} diff --git a/apps/cloud-agents/src/app/page.tsx b/apps/cloud-agents/src/app/page.tsx new file mode 100644 index 0000000000..0af14a042b --- /dev/null +++ b/apps/cloud-agents/src/app/page.tsx @@ -0,0 +1,31 @@ +export default function HomePage() { + return ( +
+

Cloud Agents

+

Roo Code task execution service

+ +
+

Status

+

Service is running

+
+ +
+

API Endpoints

+
    +
  • + POST /api/jobs - Create a new job +
  • +
  • + GET /api/jobs/:id - Get job status +
  • +
  • + POST /api/webhooks/github - GitHub webhook handler +
  • +
  • + GET /api/health - Health check +
  • +
+
+
+ ) +} diff --git a/apps/cloud-agents/src/lib/db/index.ts b/apps/cloud-agents/src/lib/db/index.ts new file mode 100644 index 0000000000..d8ef609c3e --- /dev/null +++ b/apps/cloud-agents/src/lib/db/index.ts @@ -0,0 +1,11 @@ +import { drizzle } from "drizzle-orm/postgres-js" +import postgres from "postgres" +import { schema } from "./schema" + +const connectionString = process.env.DATABASE_URL! + +// Disable prefetch as it is not supported for "Transaction" pool mode +const client = postgres(connectionString, { prepare: false }) +export const db = drizzle(client, { schema }) + +export * from "./schema" diff --git a/apps/cloud-agents/src/lib/db/schema.ts b/apps/cloud-agents/src/lib/db/schema.ts new file mode 100644 index 0000000000..ed6689376d --- /dev/null +++ b/apps/cloud-agents/src/lib/db/schema.ts @@ -0,0 +1,85 @@ +import { pgTable, text, timestamp, integer, jsonb } from "drizzle-orm/pg-core" +import { relations } from "drizzle-orm" + +/** + * Cloud Jobs - Track job requests + */ +export const cloudJobs = pgTable("cloud_jobs", { + id: integer().primaryKey().generatedAlwaysAsIdentity(), + type: text().notNull(), // e.g., 'github.issue.fix', 'task.execute' + status: text().notNull().default("pending"), // 'pending', 'processing', 'completed', 'failed' + payload: jsonb().notNull(), // job-specific data + result: jsonb(), // job output + error: text(), // error message if failed + createdAt: timestamp("created_at").notNull().defaultNow(), + startedAt: timestamp("started_at"), + completedAt: timestamp("completed_at"), +}) + +/** + * Cloud Tasks - Link cloud jobs to Roo Code tasks + */ +export const cloudTasks = pgTable("cloud_tasks", { + id: integer().primaryKey().generatedAlwaysAsIdentity(), + jobId: integer("job_id") + .references(() => cloudJobs.id) + .notNull(), + taskId: integer("task_id"), // references tasks from evals database + containerId: text("container_id"), // Docker container ID + createdAt: timestamp("created_at").notNull().defaultNow(), +}) + +/** + * Relations + */ +export const cloudJobsRelations = relations(cloudJobs, ({ many }) => ({ + tasks: many(cloudTasks), +})) + +export const cloudTasksRelations = relations(cloudTasks, ({ one }) => ({ + job: one(cloudJobs, { fields: [cloudTasks.jobId], references: [cloudJobs.id] }), +})) + +/** + * Types + */ +export type CloudJob = typeof cloudJobs.$inferSelect +export type InsertCloudJob = typeof cloudJobs.$inferInsert +export type UpdateCloudJob = Partial> + +export type CloudTask = typeof cloudTasks.$inferSelect +export type InsertCloudTask = typeof cloudTasks.$inferInsert +export type UpdateCloudTask = Partial> + +/** + * Job Types + */ +export interface JobTypes { + "github.issue.fix": { + repo: string // e.g., "RooCodeInc/Roo-Code" + issue: number // Issue number + title: string // Issue title + body: string // Issue description + labels?: string[] // Issue labels + } + "task.execute": { + prompt: string // Task prompt + workspace?: string // Optional workspace path + settings?: Record // Optional Roo Code settings override + } +} + +/** + * Job Status + */ +export type JobStatus = "pending" | "processing" | "completed" | "failed" + +/** + * Schema export + */ +export const schema = { + cloudJobs, + cloudTasks, + cloudJobsRelations, + cloudTasksRelations, +} diff --git a/apps/cloud-agents/src/lib/docker/runner.ts b/apps/cloud-agents/src/lib/docker/runner.ts new file mode 100644 index 0000000000..2c8f76d32b --- /dev/null +++ b/apps/cloud-agents/src/lib/docker/runner.ts @@ -0,0 +1,97 @@ +import { execa } from "execa" +import { db, cloudTasks } from "../db/index.js" + +interface RunTaskOptions { + prompt: string + workspace?: string + settings?: Record + jobId: number + type: string +} + +export async function runTaskInContainer(options: RunTaskOptions): Promise<{ + success: boolean + message: string + type: string + timestamp: string +}> { + const { prompt, jobId, type } = options + + console.log(`Starting container for job ${jobId}`) + + // Create a cloud task record to track this container + await db + .insert(cloudTasks) + .values({ + jobId, + taskId: null, // Will be updated when Roo Code task starts + containerId: null, // Will be updated when container starts + }) + .returning() + + try { + // For now, create a simple mock implementation + // TODO: Implement actual Docker container spawning + + // Simulate some processing time + await new Promise((resolve) => setTimeout(resolve, 2000)) + + // Mock result + const result = { + success: true, + message: `Task processed: ${prompt.substring(0, 100)}...`, + type, + timestamp: new Date().toISOString(), + } + + console.log(`Container for job ${jobId} completed successfully`) + return result + } catch (error) { + console.error(`Container for job ${jobId} failed:`, error) + throw error + } +} + +// TODO: Implement actual Docker container logic based on packages/evals/src/cli/runTask.ts +export async function runTaskInDockerContainer(options: RunTaskOptions): Promise<{ + success: boolean + exitCode: number + stdout: string + stderr: string +}> { + const { jobId } = options + + // This will be similar to processTaskInContainer from the evals package + // but simplified for cloud agents use case + + const baseArgs = [ + "--rm", + "--network cloud-agents_default", + "-v /var/run/docker.sock:/var/run/docker.sock", + "-v /tmp/cloud-agents:/var/log/cloud-agents", + "-e HOST_EXECUTION_METHOD=docker", + ] + + const containerName = `cloud-agent-task-${jobId}` + const args = [`--name ${containerName}`, ...baseArgs] + + const command = `echo "Running Roo Code task for job ${jobId}"` + console.log(`Executing: docker run ${args.join(" ")} cloud-agents-runner sh -c "${command}"`) + + try { + const subprocess = execa(`docker run ${args.join(" ")} cloud-agents-runner sh -c "${command}"`, { shell: true }) + + const result = await subprocess + console.log(`Container process completed with exit code: ${result.exitCode}`) + + return { + success: true, + exitCode: result.exitCode ?? 0, + stdout: result.stdout, + stderr: result.stderr, + } + } catch (error) { + console.error(`Container process failed:`, error) + throw error + } +} diff --git a/apps/cloud-agents/src/lib/queue/index.ts b/apps/cloud-agents/src/lib/queue/index.ts new file mode 100644 index 0000000000..b55c118d49 --- /dev/null +++ b/apps/cloud-agents/src/lib/queue/index.ts @@ -0,0 +1,51 @@ +import { Queue, Job } from "bullmq" +import IORedis from "ioredis" +import { JobTypes } from "../db/schema.js" + +// Redis connection +const redis = new IORedis(process.env.REDIS_URL || "redis://localhost:6379", { + maxRetriesPerRequest: null, +}) + +// Job queue +export const cloudQueue = new Queue("cloud-agents", { + connection: redis, + defaultJobOptions: { + removeOnComplete: 100, + removeOnFail: 50, + attempts: 3, + backoff: { + type: "exponential", + delay: 2000, + }, + }, +}) + +// Job types +export type CloudJobData = { + type: T + payload: JobTypes[T] + jobId: number +} + +// Add job to queue +export async function addJob( + type: T, + payload: JobTypes[T], + jobId: number, +): Promise>> { + return cloudQueue.add( + type, + { + type, + payload, + jobId, + }, + { + jobId: `${type}-${jobId}`, + }, + ) +} + +// Export redis connection for other uses +export { redis } diff --git a/apps/cloud-agents/src/lib/queue/processor.ts b/apps/cloud-agents/src/lib/queue/processor.ts new file mode 100644 index 0000000000..e23b6339e4 --- /dev/null +++ b/apps/cloud-agents/src/lib/queue/processor.ts @@ -0,0 +1,138 @@ +import { Worker, Job } from "bullmq" +import { redis } from "./index.js" +import { db, cloudJobs, JobTypes } from "../db/index.js" +import { eq } from "drizzle-orm" +import { runTaskInContainer } from "../docker/runner.js" + +export type CloudJobData = { + type: T + payload: JobTypes[T] + jobId: number +} + +async function updateJobStatus( + jobId: number, + status: "processing" | "completed" | "failed", + result?: unknown, + error?: string, +) { + const updates: Record = { status } + + if (status === "processing") { + updates.startedAt = new Date() + } else if (status === "completed" || status === "failed") { + updates.completedAt = new Date() + if (result) updates.result = result + if (error) updates.error = error + } + + await db.update(cloudJobs).set(updates).where(eq(cloudJobs.id, jobId)) +} + +async function processGitHubIssueFix( + jobId: number, + payload: JobTypes["github.issue.fix"], +): Promise<{ + repo: string + issue: number + result: unknown +}> { + // Generate a prompt for fixing the GitHub issue + const prompt = ` +Fix the following GitHub issue: + +Repository: ${payload.repo} +Issue #${payload.issue}: ${payload.title} + +Description: +${payload.body} + +${payload.labels && payload.labels.length > 0 ? `Labels: ${payload.labels.join(", ")}` : ""} + +Please analyze the issue, understand what needs to be fixed, and implement a solution. +`.trim() + + // Run the task in a container + const result = await runTaskInContainer({ + prompt, + jobId, + type: "github.issue.fix", + }) + + return { + repo: payload.repo, + issue: payload.issue, + result, + } +} + +async function processTaskExecute(jobId: number, payload: JobTypes["task.execute"]): Promise { + // Run the custom task in a container + const result = await runTaskInContainer({ + prompt: payload.prompt, + workspace: payload.workspace, + settings: payload.settings, + jobId, + type: "task.execute", + }) + + return result +} + +async function processJob(job: Job) { + const { type, payload, jobId } = job.data + + console.log(`Processing job ${jobId} of type ${type}`) + + try { + // Update job status to processing + await updateJobStatus(jobId, "processing") + + let result: unknown + + switch (type) { + case "github.issue.fix": + result = await processGitHubIssueFix(jobId, payload as JobTypes["github.issue.fix"]) + break + case "task.execute": + result = await processTaskExecute(jobId, payload as JobTypes["task.execute"]) + break + default: + throw new Error(`Unknown job type: ${type}`) + } + + // Update job status to completed + await updateJobStatus(jobId, "completed", result) + + console.log(`Job ${jobId} completed successfully`) + } catch (error) { + console.error(`Job ${jobId} failed:`, error) + + const errorMessage = error instanceof Error ? error.message : String(error) + await updateJobStatus(jobId, "failed", undefined, errorMessage) + + throw error // Re-throw to mark job as failed in BullMQ + } +} + +// Create and export the worker +export const worker = new Worker("cloud-agents", processJob, { + connection: redis, + concurrency: 2, // Process up to 2 jobs concurrently + removeOnComplete: { count: 100 }, + removeOnFail: { count: 50 }, +}) + +worker.on("completed", (job) => { + console.log(`Job ${job.id} completed`) +}) + +worker.on("failed", (job, err) => { + console.error(`Job ${job?.id} failed:`, err) +}) + +worker.on("error", (err) => { + console.error("Worker error:", err) +}) + +console.log("Cloud agents worker started") diff --git a/apps/cloud-agents/src/lib/queue/worker.ts b/apps/cloud-agents/src/lib/queue/worker.ts new file mode 100644 index 0000000000..64ad8a7e91 --- /dev/null +++ b/apps/cloud-agents/src/lib/queue/worker.ts @@ -0,0 +1,19 @@ +#!/usr/bin/env node + +import { worker } from "./processor.js" + +// Handle graceful shutdown +process.on("SIGTERM", async () => { + console.log("Received SIGTERM, shutting down gracefully...") + await worker.close() + process.exit(0) +}) + +process.on("SIGINT", async () => { + console.log("Received SIGINT, shutting down gracefully...") + await worker.close() + process.exit(0) +}) + +// Keep the process running +console.log("Worker process started") diff --git a/apps/cloud-agents/tsconfig.json b/apps/cloud-agents/tsconfig.json new file mode 100644 index 0000000000..75ccc2980d --- /dev/null +++ b/apps/cloud-agents/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "@roo-code/config-typescript/nextjs.json", + "compilerOptions": { + "paths": { "@/*": ["./src/*"] } + }, + "include": ["next-env.d.ts", "src/**/*.ts", "src/**/*.tsx", ".next/types/**/*.ts", "drizzle.config.ts"], + "exclude": ["node_modules"] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ed849dcb6d..e8e8d3185e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -54,6 +54,61 @@ importers: specifier: ^5.4.5 version: 5.8.3 + apps/cloud-agents: + dependencies: + '@roo-code/types': + specifier: workspace:^ + version: link:../../packages/types + bullmq: + specifier: ^5.37.0 + version: 5.53.2 + drizzle-orm: + specifier: ^0.44.1 + version: 0.44.1(@libsql/client@0.15.8)(better-sqlite3@11.10.0)(gel@2.1.0)(postgres@3.4.7) + execa: + specifier: ^9.6.0 + version: 9.6.0 + ioredis: + specifier: ^5.4.3 + version: 5.6.1 + next: + specifier: ^15.2.5 + version: 15.2.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + postgres: + specifier: ^3.4.7 + version: 3.4.7 + react: + specifier: ^18.3.1 + version: 18.3.1 + react-dom: + specifier: ^18.3.1 + version: 18.3.1(react@18.3.1) + zod: + specifier: ^3.25.41 + version: 3.25.57 + devDependencies: + '@roo-code/config-eslint': + specifier: workspace:^ + version: link:../../packages/config-eslint + '@roo-code/config-typescript': + specifier: workspace:^ + version: link:../../packages/config-typescript + '@types/node': + specifier: ^22.15.20 + version: 22.15.29 + '@types/react': + specifier: ^18.3.23 + version: 18.3.23 + '@types/react-dom': + specifier: ^18.3.7 + version: 18.3.7(@types/react@18.3.23) + drizzle-kit: + specifier: ^0.31.1 + version: 0.31.1 + tsx: + specifier: ^4.19.3 + version: 4.19.4 + apps/vscode-e2e: devDependencies: '@roo-code/config-eslint': @@ -2270,6 +2325,9 @@ packages: cpu: [x64] os: [win32] + '@ioredis/commands@1.2.0': + resolution: {integrity: sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg==} + '@isaacs/cliui@8.0.2': resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} engines: {node: '>=12'} @@ -2476,6 +2534,36 @@ packages: resolution: {integrity: sha512-m//7RlINx1F3sz3KqwY1WWzVgTcYX52HYk4bJ1hkBXV3zccAEth+jRvG8DBRrdaQuRsPAJOx2MH3zaHNCKL7Zg==} engines: {node: '>=18'} + '@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.3': + resolution: {integrity: sha512-QZHtlVgbAdy2zAqNA9Gu1UpIuI8Xvsd1v8ic6B2pZmeFnFcMWiPLfWXh7TVw4eGEZ/C9TH281KwhVoeQUKbyjw==} + cpu: [arm64] + os: [darwin] + + '@msgpackr-extract/msgpackr-extract-darwin-x64@3.0.3': + resolution: {integrity: sha512-mdzd3AVzYKuUmiWOQ8GNhl64/IoFGol569zNRdkLReh6LRLHOXxU4U8eq0JwaD8iFHdVGqSy4IjFL4reoWCDFw==} + cpu: [x64] + os: [darwin] + + '@msgpackr-extract/msgpackr-extract-linux-arm64@3.0.3': + resolution: {integrity: sha512-YxQL+ax0XqBJDZiKimS2XQaf+2wDGVa1enVRGzEvLLVFeqa5kx2bWbtcSXgsxjQB7nRqqIGFIcLteF/sHeVtQg==} + cpu: [arm64] + os: [linux] + + '@msgpackr-extract/msgpackr-extract-linux-arm@3.0.3': + resolution: {integrity: sha512-fg0uy/dG/nZEXfYilKoRe7yALaNmHoYeIoJuJ7KJ+YyU2bvY8vPv27f7UKhGRpY6euFYqEVhxCFZgAUNQBM3nw==} + cpu: [arm] + os: [linux] + + '@msgpackr-extract/msgpackr-extract-linux-x64@3.0.3': + resolution: {integrity: sha512-cvwNfbP07pKUfq1uH+S6KJ7dT9K8WOE4ZiAcsrSes+UY55E/0jLYc+vq+DO7jlmqRb5zAggExKm0H7O/CBaesg==} + cpu: [x64] + os: [linux] + + '@msgpackr-extract/msgpackr-extract-win32-x64@3.0.3': + resolution: {integrity: sha512-x0fWaQtYp4E6sktbsdAqnehxDgEc/VwM7uLsRCYWaiGu0ykYdZPiS8zCWdnjHwyiumousxfBm4SO31eXqwEZhQ==} + cpu: [x64] + os: [win32] + '@mswjs/interceptors@0.38.6': resolution: {integrity: sha512-qFlpmObPqeUs4u3oFYv/OM/xyX+pNa5TRAjqjvMhbGYlyMhzSrE5UfncL2rUcEeVfD9Gebgff73hPwqcOwJQNA==} engines: {node: '>=18'} @@ -4890,6 +4978,9 @@ packages: buffer@5.7.1: resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} + bullmq@5.53.2: + resolution: {integrity: sha512-xHgxrP/yNJHD7VCw1h+eRBh+2TCPBCM39uC9gCyksYc6ufcJP+HTZ/A2lzB2x7qMFWrvsX7tM40AT2BmdkYL/Q==} + bundle-name@4.1.0: resolution: {integrity: sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==} engines: {node: '>=18'} @@ -5244,6 +5335,10 @@ packages: engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} hasBin: true + cron-parser@4.9.0: + resolution: {integrity: sha512-p0SaNjrHOnQeR8/VnfGbmg9te2kfyYSQ7Sc/j/6DtPL3JQvKxmjO9TSjNFpujqV3vEYYBvNNvXSxzyksBWAx1Q==} + engines: {node: '>=12.0.0'} + cross-fetch@4.0.0: resolution: {integrity: sha512-e4a5N8lVvuLgAWgnCrLr2PP0YyDOTHa9H/Rj54dirp61qXnNq46m82bRhNqIA5VccJtWBvPTFRV3TtvHUKPB1g==} @@ -5579,6 +5674,10 @@ packages: resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} engines: {node: '>=0.4.0'} + denque@2.1.0: + resolution: {integrity: sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==} + engines: {node: '>=0.10'} + depd@2.0.0: resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} engines: {node: '>= 0.8'} @@ -6764,6 +6863,10 @@ packages: resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==} engines: {node: '>=12'} + ioredis@5.6.1: + resolution: {integrity: sha512-UxC0Yv1Y4WRJiGQxQkP0hfdL0/5/6YvdfOOClRgJ0qppSarkhneSa6UvkMkms0AkdGimSH3Ikqm+6mkMmX7vGA==} + engines: {node: '>=12.22.0'} + ip-address@9.0.5: resolution: {integrity: sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==} engines: {node: '>= 12'} @@ -7548,9 +7651,15 @@ packages: lodash.debounce@4.0.8: resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==} + lodash.defaults@4.2.0: + resolution: {integrity: sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==} + lodash.includes@4.3.0: resolution: {integrity: sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==} + lodash.isarguments@3.1.0: + resolution: {integrity: sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==} + lodash.isboolean@3.0.3: resolution: {integrity: sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==} @@ -7635,6 +7744,10 @@ packages: peerDependencies: react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0 + luxon@3.6.1: + resolution: {integrity: sha512-tJLxrKJhO2ukZ5z0gyjY1zPh3Rh88Ej9P7jNrZiHMUXHae1yvI2imgOZtL1TO8TW6biMMKfTtAOoEJANgtWBMQ==} + engines: {node: '>=12'} + lz-string@1.5.0: resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==} hasBin: true @@ -7969,6 +8082,13 @@ packages: ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + msgpackr-extract@3.0.3: + resolution: {integrity: sha512-P0efT1C9jIdVRefqjzOQ9Xml57zpOXnIuS+csaB4MdZbTdmGDLo8XhzBG1N7aO11gKDDkJvBLULeFTo46wwreA==} + hasBin: true + + msgpackr@1.11.4: + resolution: {integrity: sha512-uaff7RG9VIC4jacFW9xzL3jc0iM32DNHe4jYVycBcjUePT/Klnfj7pqtWJt9khvDFizmjN2TlYniYmSS2LIaZg==} + mute-stream@0.0.8: resolution: {integrity: sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==} @@ -8039,6 +8159,9 @@ packages: resolution: {integrity: sha512-OhYaY5sDsIka7H7AtijtI9jwGYLyl29eQn/W623DiN/MIv5sUqc4g7BIDThX+gb7di9f6xK02nkp8sdfFWZLTg==} engines: {node: '>=10'} + node-abort-controller@3.1.1: + resolution: {integrity: sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==} + node-addon-api@4.3.0: resolution: {integrity: sha512-73sE9+3UaLYYFmDsFZnqCInzPyh3MqIwZO9cw58yIqAZhONrrabrYyYe3TuIqtIiOuTXVhsGau8hcrhhwSsDIQ==} @@ -8067,6 +8190,10 @@ packages: resolution: {integrity: sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + node-gyp-build-optional-packages@5.2.2: + resolution: {integrity: sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw==} + hasBin: true + node-int64@0.4.0: resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==} @@ -8865,6 +8992,14 @@ packages: resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==} engines: {node: '>=8'} + redis-errors@1.2.0: + resolution: {integrity: sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==} + engines: {node: '>=4'} + + redis-parser@3.0.0: + resolution: {integrity: sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==} + engines: {node: '>=4'} + redis@5.5.5: resolution: {integrity: sha512-x7vpciikEY7nptGzQrE5I+/pvwFZJDadPk/uEoyGSg/pZ2m/CX2n5EhSgUh+S5T7Gz3uKM6YzWcXEu3ioAsdFQ==} engines: {node: '>= 18'} @@ -9265,6 +9400,9 @@ packages: stacktrace-js@2.0.2: resolution: {integrity: sha512-Je5vBeY4S1r/RnLydLl0TBTi3F2qdfWmYsGvtfZgEI+SCprPppaIhQf5nGcal4gI4cGpCV/duLcAzT1np6sQqg==} + standard-as-callback@2.1.0: + resolution: {integrity: sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==} + statuses@2.0.1: resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} engines: {node: '>= 0.8'} @@ -10475,9 +10613,6 @@ packages: zod@3.25.49: resolution: {integrity: sha512-JMMPMy9ZBk3XFEdbM3iL1brx4NUSejd6xr3ELrrGEfGb355gjhiAWtG3K5o+AViV/3ZfkIrCzXsZn6SbLwTR8Q==} - zod@3.25.51: - resolution: {integrity: sha512-TQSnBldh+XSGL+opiSIq0575wvDPqu09AqWe1F7JhUMKY+M91/aGlK4MhpVNO7MgYfHcVCB1ffwAUTJzllKJqg==} - zod@3.25.57: resolution: {integrity: sha512-6tgzLuwVST5oLUxXTmBqoinKMd3JeesgbgseXeFasKKj8Q1FCZrHnbqJOyiEvr4cVAlbug+CgIsmJ8cl/pU5FA==} @@ -11804,8 +11939,8 @@ snapshots: '@modelcontextprotocol/sdk': 1.12.0 google-auth-library: 9.15.1 ws: 8.18.2 - zod: 3.25.51 - zod-to-json-schema: 3.24.5(zod@3.25.51) + zod: 3.25.57 + zod-to-json-schema: 3.24.5(zod@3.25.57) transitivePeerDependencies: - bufferutil - encoding @@ -11920,6 +12055,8 @@ snapshots: '@img/sharp-win32-x64@0.33.5': optional: true + '@ioredis/commands@1.2.0': {} + '@isaacs/cliui@8.0.2': dependencies: string-width: 5.1.2 @@ -12253,11 +12390,29 @@ snapshots: express-rate-limit: 7.5.0(express@5.1.0) pkce-challenge: 5.0.0 raw-body: 3.0.0 - zod: 3.25.49 - zod-to-json-schema: 3.24.5(zod@3.25.49) + zod: 3.25.57 + zod-to-json-schema: 3.24.5(zod@3.25.57) transitivePeerDependencies: - supports-color + '@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.3': + optional: true + + '@msgpackr-extract/msgpackr-extract-darwin-x64@3.0.3': + optional: true + + '@msgpackr-extract/msgpackr-extract-linux-arm64@3.0.3': + optional: true + + '@msgpackr-extract/msgpackr-extract-linux-arm@3.0.3': + optional: true + + '@msgpackr-extract/msgpackr-extract-linux-x64@3.0.3': + optional: true + + '@msgpackr-extract/msgpackr-extract-win32-x64@3.0.3': + optional: true + '@mswjs/interceptors@0.38.6': dependencies: '@open-draft/deferred-promise': 2.2.0 @@ -14937,6 +15092,18 @@ snapshots: base64-js: 1.5.1 ieee754: 1.2.1 + bullmq@5.53.2: + dependencies: + cron-parser: 4.9.0 + ioredis: 5.6.1 + msgpackr: 1.11.4 + node-abort-controller: 3.1.1 + semver: 7.7.2 + tslib: 2.8.1 + uuid: 9.0.1 + transitivePeerDependencies: + - supports-color + bundle-name@4.1.0: dependencies: run-applescript: 7.0.0 @@ -15324,6 +15491,10 @@ snapshots: - supports-color - ts-node + cron-parser@4.9.0: + dependencies: + luxon: 3.6.1 + cross-fetch@4.0.0: dependencies: node-fetch: 2.7.0 @@ -15668,6 +15839,8 @@ snapshots: delayed-stream@1.0.0: {} + denque@2.1.0: {} + depd@2.0.0: {} dequal@2.0.3: {} @@ -17055,6 +17228,20 @@ snapshots: internmap@2.0.3: {} + ioredis@5.6.1: + dependencies: + '@ioredis/commands': 1.2.0 + cluster-key-slot: 1.1.2 + debug: 4.4.1(supports-color@8.1.1) + denque: 2.1.0 + lodash.defaults: 4.2.0 + lodash.isarguments: 3.1.0 + redis-errors: 1.2.0 + redis-parser: 3.0.0 + standard-as-callback: 2.1.0 + transitivePeerDependencies: + - supports-color + ip-address@9.0.5: dependencies: jsbn: 1.1.0 @@ -18123,8 +18310,12 @@ snapshots: lodash.debounce@4.0.8: {} + lodash.defaults@4.2.0: {} + lodash.includes@4.3.0: {} + lodash.isarguments@3.1.0: {} + lodash.isboolean@3.0.3: {} lodash.isinteger@4.0.4: {} @@ -18203,6 +18394,8 @@ snapshots: dependencies: react: 18.3.1 + luxon@3.6.1: {} + lz-string@1.5.0: {} macos-release@3.3.0: {} @@ -18777,6 +18970,22 @@ snapshots: ms@2.1.3: {} + msgpackr-extract@3.0.3: + dependencies: + node-gyp-build-optional-packages: 5.2.2 + optionalDependencies: + '@msgpackr-extract/msgpackr-extract-darwin-arm64': 3.0.3 + '@msgpackr-extract/msgpackr-extract-darwin-x64': 3.0.3 + '@msgpackr-extract/msgpackr-extract-linux-arm': 3.0.3 + '@msgpackr-extract/msgpackr-extract-linux-arm64': 3.0.3 + '@msgpackr-extract/msgpackr-extract-linux-x64': 3.0.3 + '@msgpackr-extract/msgpackr-extract-win32-x64': 3.0.3 + optional: true + + msgpackr@1.11.4: + optionalDependencies: + msgpackr-extract: 3.0.3 + mute-stream@0.0.8: {} mz@2.7.0: @@ -18850,6 +19059,8 @@ snapshots: dependencies: semver: 7.7.2 + node-abort-controller@3.1.1: {} + node-addon-api@4.3.0: optional: true @@ -18871,6 +19082,11 @@ snapshots: fetch-blob: 3.2.0 formdata-polyfill: 4.0.10 + node-gyp-build-optional-packages@5.2.2: + dependencies: + detect-libc: 2.0.4 + optional: true + node-int64@0.4.0: {} node-ipc@12.0.0: @@ -19766,6 +19982,12 @@ snapshots: indent-string: 4.0.0 strip-indent: 3.0.0 + redis-errors@1.2.0: {} + + redis-parser@3.0.0: + dependencies: + redis-errors: 1.2.0 + redis@5.5.5: dependencies: '@redis/bloom': 5.5.5(@redis/client@5.5.5) @@ -20290,6 +20512,8 @@ snapshots: stack-generator: 2.0.10 stacktrace-gps: 3.1.2 + standard-as-callback@2.1.0: {} + statuses@2.0.1: {} std-env@3.9.0: {} @@ -21716,13 +21940,9 @@ snapshots: dependencies: zod: 3.24.4 - zod-to-json-schema@3.24.5(zod@3.25.49): - dependencies: - zod: 3.25.49 - - zod-to-json-schema@3.24.5(zod@3.25.51): + zod-to-json-schema@3.24.5(zod@3.25.57): dependencies: - zod: 3.25.51 + zod: 3.25.57 zod-to-ts@1.2.0(typescript@5.8.3)(zod@3.24.4): dependencies: @@ -21739,8 +21959,6 @@ snapshots: zod@3.25.49: {} - zod@3.25.51: {} - zod@3.25.57: {} zwitch@2.0.4: {} From 0f4f0465a10fd056229123e2c2027de7d703d54f Mon Sep 17 00:00:00 2001 From: cte Date: Wed, 11 Jun 2025 08:59:20 -0700 Subject: [PATCH 02/19] Cloud agents PoC --- apps/cloud-agents/package.json | 1 + apps/web-evals/package.json | 3 ++- pnpm-lock.yaml | 1 + 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/apps/cloud-agents/package.json b/apps/cloud-agents/package.json index dbcc7285b5..63057573fa 100644 --- a/apps/cloud-agents/package.json +++ b/apps/cloud-agents/package.json @@ -8,6 +8,7 @@ "dev": "next dev --port 3001", "build": "next build", "start": "next start --port 3001", + "clean": "rimraf .next .turbo", "drizzle-kit": "dotenvx run -f .env.local -- tsx node_modules/drizzle-kit/bin.cjs", "db:generate": "pnpm drizzle-kit generate", "db:migrate": "pnpm drizzle-kit migrate", diff --git a/apps/web-evals/package.json b/apps/web-evals/package.json index 99fa68829c..995cfcf741 100644 --- a/apps/web-evals/package.json +++ b/apps/web-evals/package.json @@ -8,7 +8,8 @@ "dev": "scripts/check-services.sh && next dev", "format": "prettier --write src", "build": "next build", - "start": "next start" + "start": "next start", + "clean": "rimraf .next .turbo" }, "dependencies": { "@hookform/resolvers": "^4.1.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e8e8d3185e..6fcf637e28 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -7474,6 +7474,7 @@ packages: libsql@0.5.12: resolution: {integrity: sha512-TikiQZ1j4TwFEqVdJdTM9ZTti28is/ytGEvn0S2MocOj69UKQetWACe/qd8KAD5VeNnQSVd6Nlm2AJx0DFW9Ag==} + cpu: [x64, arm64, wasm32, arm] os: [darwin, linux, win32] lie@3.3.0: From a898dd03d3bc3b907d271194bfcca65e6e5c7046 Mon Sep 17 00:00:00 2001 From: cte Date: Wed, 11 Jun 2025 14:55:10 -0700 Subject: [PATCH 03/19] Forgot this --- pnpm-lock.yaml | 91 ++++++++++++++++++++------------------------------ 1 file changed, 37 insertions(+), 54 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f9d6747520..319d0b5b66 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -257,7 +257,7 @@ importers: version: 1.1.2(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) zod: specifier: ^3.25.61 - version: 3.25.61 + version: 3.25.62 devDependencies: '@roo-code/config-eslint': specifier: workspace:^ @@ -351,7 +351,7 @@ importers: version: 1.0.7(tailwindcss@3.4.17) zod: specifier: ^3.25.61 - version: 3.25.61 + version: 3.25.62 devDependencies: '@roo-code/config-eslint': specifier: workspace:^ @@ -385,7 +385,7 @@ importers: dependencies: zod: specifier: ^3.25.61 - version: 3.25.61 + version: 3.25.62 devDependencies: '@roo-code/config-eslint': specifier: workspace:^ @@ -413,7 +413,7 @@ importers: version: 1.9.0 zod: specifier: ^3.25.61 - version: 3.25.61 + version: 3.25.62 devDependencies: '@roo-code/config-eslint': specifier: workspace:^ @@ -506,7 +506,7 @@ importers: version: 5.5.5 zod: specifier: ^3.25.61 - version: 3.25.61 + version: 3.25.62 devDependencies: '@roo-code/config-eslint': specifier: workspace:^ @@ -568,7 +568,7 @@ importers: version: 4.17.2 zod: specifier: ^3.25.61 - version: 3.25.61 + version: 3.25.62 devDependencies: '@roo-code/config-eslint': specifier: workspace:^ @@ -590,7 +590,7 @@ importers: dependencies: zod: specifier: ^3.25.61 - version: 3.25.61 + version: 3.25.62 devDependencies: '@roo-code/config-eslint': specifier: workspace:^ @@ -630,7 +630,7 @@ importers: version: 1.3.0(@modelcontextprotocol/sdk@1.12.0) '@mistralai/mistralai': specifier: ^1.3.6 - version: 1.6.1(zod@3.25.61) + version: 1.6.1(zod@3.25.62) '@modelcontextprotocol/sdk': specifier: ^1.9.0 version: 1.12.0 @@ -726,7 +726,7 @@ importers: version: 12.0.0 openai: specifier: ^4.78.1 - version: 4.103.0(ws@8.18.2)(zod@3.25.61) + version: 4.103.0(ws@8.18.2)(zod@3.25.62) os-name: specifier: ^6.0.0 version: 6.1.0 @@ -810,7 +810,7 @@ importers: version: 2.8.0 zod: specifier: ^3.25.61 - version: 3.25.61 + version: 3.25.62 devDependencies: '@jest/globals': specifier: ^29.7.0 @@ -922,7 +922,7 @@ importers: version: 3.1.3(@types/debug@4.1.12)(@types/node@20.17.50)(jiti@2.4.2)(jsdom@20.0.3)(lightningcss@1.30.1)(tsx@4.19.4)(yaml@2.8.0) zod-to-ts: specifier: ^1.2.0 - version: 1.2.0(typescript@5.8.3)(zod@3.25.61) + version: 1.2.0(typescript@5.8.3)(zod@3.25.62) webview-ui: dependencies: @@ -1099,7 +1099,7 @@ importers: version: 0.2.2(@types/react@18.3.23)(react@18.3.1) zod: specifier: ^3.25.61 - version: 3.25.61 + version: 3.25.62 devDependencies: '@jest/globals': specifier: ^29.7.0 @@ -4331,9 +4331,6 @@ packages: '@types/node@20.17.57': resolution: {integrity: sha512-f3T4y6VU4fVQDKVqJV4Uppy8c1p/sVvS3peyqxyWnzkqXFJLRU7Y1Bl7rMS1Qe9z0v4M6McY0Fp9yBsgHJUsWQ==} - '@types/node@20.19.0': - resolution: {integrity: sha512-hfrc+1tud1xcdVTABC2JiomZJEklMcXYNTVtZLAeqTVWD+qL5jkHKT+1lOtqDdGxt+mB53DTtiz673vfjU8D1Q==} - '@types/node@22.15.29': resolution: {integrity: sha512-LNdjOkUDlU1RZb8e1kOIUpN1qQUlzGkEtbVNo53vbrwDg5om6oduhm4SiUaPW5ASTXhAiP0jInWG8Qx9fVlOeQ==} @@ -7454,8 +7451,8 @@ packages: resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} engines: {node: '>= 0.8.0'} - libsql@0.5.12: - resolution: {integrity: sha512-TikiQZ1j4TwFEqVdJdTM9ZTti28is/ytGEvn0S2MocOj69UKQetWACe/qd8KAD5VeNnQSVd6Nlm2AJx0DFW9Ag==} + libsql@0.5.13: + resolution: {integrity: sha512-5Bwoa/CqzgkTwySgqHA5TsaUDRrdLIbdM4egdPcaAnqO3aC+qAgS6BwdzuZwARA5digXwiskogZ8H7Yy4XfdOg==} cpu: [x64, arm64, wasm32, arm] os: [darwin, linux, win32] @@ -10590,15 +10587,12 @@ packages: zod@3.23.8: resolution: {integrity: sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==} - zod@3.24.4: - resolution: {integrity: sha512-OdqJE9UDRPwWsrHjLN2F8bPxvwJBK22EHLWtanu0LSYr5YqzsaaW3RMgmjwr8Rypg5k+meEJdSPXJZXE/yqOMg==} - - zod@3.25.49: - resolution: {integrity: sha512-JMMPMy9ZBk3XFEdbM3iL1brx4NUSejd6xr3ELrrGEfGb355gjhiAWtG3K5o+AViV/3ZfkIrCzXsZn6SbLwTR8Q==} - zod@3.25.57: resolution: {integrity: sha512-6tgzLuwVST5oLUxXTmBqoinKMd3JeesgbgseXeFasKKj8Q1FCZrHnbqJOyiEvr4cVAlbug+CgIsmJ8cl/pU5FA==} + zod@3.25.62: + resolution: {integrity: sha512-YCxsr4DmhPcrKPC9R1oBHQNlQzlJEyPAId//qTau/vBee9uO8K6prmRq4eMkOyxvBfH4wDPIPdLx9HVMWIY3xA==} + zwitch@2.0.4: resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} @@ -11922,8 +11916,8 @@ snapshots: '@modelcontextprotocol/sdk': 1.12.0 google-auth-library: 9.15.1 ws: 8.18.2 - zod: 3.25.57 - zod-to-json-schema: 3.24.5(zod@3.25.57) + zod: 3.25.62 + zod-to-json-schema: 3.24.5(zod@3.25.62) transitivePeerDependencies: - bufferutil - encoding @@ -12360,10 +12354,10 @@ snapshots: dependencies: exenv-es6: 1.1.1 - '@mistralai/mistralai@1.6.1(zod@3.25.61)': + '@mistralai/mistralai@1.6.1(zod@3.25.62)': dependencies: - zod: 3.25.61 - zod-to-json-schema: 3.24.5(zod@3.25.61) + zod: 3.25.62 + zod-to-json-schema: 3.24.5(zod@3.25.62) '@mixmark-io/domino@2.2.0': {} @@ -12378,8 +12372,8 @@ snapshots: express-rate-limit: 7.5.0(express@5.1.0) pkce-challenge: 5.0.0 raw-body: 3.0.0 - zod: 3.25.57 - zod-to-json-schema: 3.24.5(zod@3.25.57) + zod: 3.25.62 + zod-to-json-schema: 3.24.5(zod@3.25.62) transitivePeerDependencies: - supports-color @@ -14290,11 +14284,6 @@ snapshots: dependencies: undici-types: 6.19.8 - '@types/node@20.19.0': - dependencies: - undici-types: 6.21.0 - optional: true - '@types/node@22.15.29': dependencies: undici-types: 6.21.0 @@ -14348,7 +14337,7 @@ snapshots: '@types/ws@8.18.1': dependencies: - '@types/node': 20.19.0 + '@types/node': 22.15.29 optional: true '@types/yargs-parser@21.0.3': {} @@ -18091,8 +18080,8 @@ snapshots: smol-toml: 1.3.4 strip-json-comments: 5.0.2 typescript: 5.8.3 - zod: 3.25.61 - zod-validation-error: 3.4.1(zod@3.25.61) + zod: 3.25.62 + zod-validation-error: 3.4.1(zod@3.25.62) knuth-shuffle-seeded@1.0.6: dependencies: @@ -19198,7 +19187,7 @@ snapshots: is-inside-container: 1.0.0 is-wsl: 3.1.0 - openai@4.103.0(ws@8.18.2)(zod@3.25.61): + openai@4.103.0(ws@8.18.2)(zod@3.25.62): dependencies: '@types/node': 18.19.100 '@types/node-fetch': 2.6.12 @@ -19209,7 +19198,7 @@ snapshots: node-fetch: 2.7.0 optionalDependencies: ws: 8.18.2 - zod: 3.25.61 + zod: 3.25.62 transitivePeerDependencies: - encoding @@ -21910,29 +21899,23 @@ snapshots: yoctocolors@2.1.1: {} - zod-to-json-schema@3.24.5(zod@3.25.61): - dependencies: - zod: 3.25.61 - - zod-to-json-schema@3.24.5(zod@3.25.57): + zod-to-json-schema@3.24.5(zod@3.25.62): dependencies: - zod: 3.25.57 + zod: 3.25.62 - zod-to-ts@1.2.0(typescript@5.8.3)(zod@3.24.4): + zod-to-ts@1.2.0(typescript@5.8.3)(zod@3.25.62): dependencies: typescript: 5.8.3 - zod: 3.25.61 + zod: 3.25.62 - zod-validation-error@3.4.1(zod@3.25.61): + zod-validation-error@3.4.1(zod@3.25.62): dependencies: - zod: 3.25.61 + zod: 3.25.62 zod@3.23.8: {} - zod@3.24.4: {} - - zod@3.25.49: {} - zod@3.25.57: {} + zod@3.25.62: {} + zwitch@2.0.4: {} From 496d5ed445c29e1a64407c56f677f32cc63abba5 Mon Sep 17 00:00:00 2001 From: cte Date: Thu, 12 Jun 2025 08:36:12 -0700 Subject: [PATCH 04/19] Add BullBoard --- .dockerignore | 1 + apps/cloud-agents/docker-compose.yml | 16 ++ apps/cloud-agents/docker/Dockerfile.dashboard | 27 +++ .../drizzle/0000_cuddly_luke_cage.sql | 21 +++ .../drizzle/meta/0000_snapshot.json | 164 ++++++++++++++++++ apps/cloud-agents/drizzle/meta/_journal.json | 13 ++ apps/cloud-agents/package.json | 10 +- apps/cloud-agents/scripts/dashboard.ts | 31 ++++ pnpm-lock.yaml | 122 +++++++++++++ 9 files changed, 403 insertions(+), 2 deletions(-) create mode 100644 apps/cloud-agents/docker/Dockerfile.dashboard create mode 100644 apps/cloud-agents/drizzle/0000_cuddly_luke_cage.sql create mode 100644 apps/cloud-agents/drizzle/meta/0000_snapshot.json create mode 100644 apps/cloud-agents/drizzle/meta/_journal.json create mode 100644 apps/cloud-agents/scripts/dashboard.ts diff --git a/.dockerignore b/.dockerignore index 11d03a1c54..fd3b383272 100644 --- a/.dockerignore +++ b/.dockerignore @@ -77,6 +77,7 @@ src/node_modules !pnpm-workspace.yaml !scripts/bootstrap.mjs !apps/web-evals/ +!apps/cloud-agents/ !src/ !webview-ui/ !packages/evals/.docker/entrypoints/runner.sh diff --git a/apps/cloud-agents/docker-compose.yml b/apps/cloud-agents/docker-compose.yml index 97662b281c..5de9f71fa7 100644 --- a/apps/cloud-agents/docker-compose.yml +++ b/apps/cloud-agents/docker-compose.yml @@ -37,6 +37,22 @@ services: profiles: - server + bullmq-dashboard: + container_name: cloud-agents-bullmq-dashboard + build: + context: ../../ + dockerfile: apps/cloud-agents/docker/Dockerfile.dashboard + ports: + - "${BULL_BOARD_PORT:-3002}:3002" + environment: + - REDIS_URL=redis://redis:6379 + - BULL_BOARD_PORT=3002 + - NODE_ENV=production + depends_on: + - redis + profiles: + - server + db: container_name: cloud-agents-db image: postgres:17.5 diff --git a/apps/cloud-agents/docker/Dockerfile.dashboard b/apps/cloud-agents/docker/Dockerfile.dashboard new file mode 100644 index 0000000000..f5bbd57a03 --- /dev/null +++ b/apps/cloud-agents/docker/Dockerfile.dashboard @@ -0,0 +1,27 @@ +FROM node:20-slim AS base + +ENV PNPM_HOME="/pnpm" +ENV PATH="$PNPM_HOME:$PATH" +RUN corepack enable + +WORKDIR /app + +COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./ +COPY scripts/bootstrap.mjs ./scripts/ +COPY packages/types/package.json ./packages/types/ +COPY packages/config-eslint/package.json ./packages/config-eslint/ +COPY packages/config-typescript/package.json ./packages/config-typescript/ +COPY apps/cloud-agents/package.json ./apps/cloud-agents/ + +RUN pnpm install --filter @roo-code/cloud-agents + +COPY packages/types ./packages/types/ +COPY packages/config-eslint ./packages/config-eslint/ +COPY packages/config-typescript ./packages/config-typescript/ +COPY apps/cloud-agents ./apps/cloud-agents/ + +WORKDIR /app/apps/cloud-agents + +EXPOSE 3002 + +CMD ["pnpm", "dashboard"] diff --git a/apps/cloud-agents/drizzle/0000_cuddly_luke_cage.sql b/apps/cloud-agents/drizzle/0000_cuddly_luke_cage.sql new file mode 100644 index 0000000000..2535f219e2 --- /dev/null +++ b/apps/cloud-agents/drizzle/0000_cuddly_luke_cage.sql @@ -0,0 +1,21 @@ +CREATE TABLE "cloud_jobs" ( + "id" integer PRIMARY KEY GENERATED ALWAYS AS IDENTITY (sequence name "cloud_jobs_id_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 2147483647 START WITH 1 CACHE 1), + "type" text NOT NULL, + "status" text DEFAULT 'pending' NOT NULL, + "payload" jsonb NOT NULL, + "result" jsonb, + "error" text, + "created_at" timestamp DEFAULT now() NOT NULL, + "started_at" timestamp, + "completed_at" timestamp +); +--> statement-breakpoint +CREATE TABLE "cloud_tasks" ( + "id" integer PRIMARY KEY GENERATED ALWAYS AS IDENTITY (sequence name "cloud_tasks_id_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 2147483647 START WITH 1 CACHE 1), + "job_id" integer NOT NULL, + "task_id" integer, + "container_id" text, + "created_at" timestamp DEFAULT now() NOT NULL +); +--> statement-breakpoint +ALTER TABLE "cloud_tasks" ADD CONSTRAINT "cloud_tasks_job_id_cloud_jobs_id_fk" FOREIGN KEY ("job_id") REFERENCES "public"."cloud_jobs"("id") ON DELETE no action ON UPDATE no action; \ No newline at end of file diff --git a/apps/cloud-agents/drizzle/meta/0000_snapshot.json b/apps/cloud-agents/drizzle/meta/0000_snapshot.json new file mode 100644 index 0000000000..8815e6a299 --- /dev/null +++ b/apps/cloud-agents/drizzle/meta/0000_snapshot.json @@ -0,0 +1,164 @@ +{ + "id": "8f65bfed-78de-4e22-a15f-36de8afe5f2e", + "prevId": "00000000-0000-0000-0000-000000000000", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.cloud_jobs": { + "name": "cloud_jobs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "cloud_jobs_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "2147483647", + "cache": "1", + "cycle": false + } + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "result": { + "name": "result", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.cloud_tasks": { + "name": "cloud_tasks", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "cloud_tasks_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "2147483647", + "cache": "1", + "cycle": false + } + }, + "job_id": { + "name": "job_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "task_id": { + "name": "task_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "container_id": { + "name": "container_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "cloud_tasks_job_id_cloud_jobs_id_fk": { + "name": "cloud_tasks_job_id_cloud_jobs_id_fk", + "tableFrom": "cloud_tasks", + "tableTo": "cloud_jobs", + "columnsFrom": ["job_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} diff --git a/apps/cloud-agents/drizzle/meta/_journal.json b/apps/cloud-agents/drizzle/meta/_journal.json new file mode 100644 index 0000000000..4c1055b8e6 --- /dev/null +++ b/apps/cloud-agents/drizzle/meta/_journal.json @@ -0,0 +1,13 @@ +{ + "version": "7", + "dialect": "postgresql", + "entries": [ + { + "idx": 0, + "version": "7", + "when": 1749740498648, + "tag": "0000_cuddly_luke_cage", + "breakpoints": true + } + ] +} diff --git a/apps/cloud-agents/package.json b/apps/cloud-agents/package.json index 63057573fa..f091fe4f47 100644 --- a/apps/cloud-agents/package.json +++ b/apps/cloud-agents/package.json @@ -15,15 +15,20 @@ "db:push": "pnpm drizzle-kit push", "db:check": "pnpm drizzle-kit check", "db:studio": "pnpm drizzle-kit studio", - "services:start": "docker compose up -d db redis", + "services:start": "docker compose up -d db redis bullmq-dashboard", "services:stop": "docker compose down", - "worker:dev": "tsx src/lib/queue/worker.ts" + "worker:dev": "tsx src/lib/queue/worker.ts", + "dashboard": "tsx scripts/dashboard.ts" }, "dependencies": { + "@bull-board/api": "^6.10.1", + "@bull-board/express": "^6.10.1", + "@bull-board/ui": "^6.10.1", "@roo-code/types": "workspace:^", "bullmq": "^5.37.0", "drizzle-orm": "^0.44.1", "execa": "^9.6.0", + "express": "^5.1.0", "ioredis": "^5.4.3", "next": "^15.2.5", "postgres": "^3.4.7", @@ -34,6 +39,7 @@ "devDependencies": { "@roo-code/config-eslint": "workspace:^", "@roo-code/config-typescript": "workspace:^", + "@types/express": "^5.0.3", "@types/node": "^22.15.20", "@types/react": "^18.3.23", "@types/react-dom": "^18.3.7", diff --git a/apps/cloud-agents/scripts/dashboard.ts b/apps/cloud-agents/scripts/dashboard.ts new file mode 100644 index 0000000000..6d77be5cff --- /dev/null +++ b/apps/cloud-agents/scripts/dashboard.ts @@ -0,0 +1,31 @@ +import { createBullBoard } from "@bull-board/api" +import { BullMQAdapter } from "@bull-board/api/bullMQAdapter" +import { ExpressAdapter } from "@bull-board/express" +import { Queue } from "bullmq" +import IORedis from "ioredis" +import express, { type Express, type Request, type Response } from "express" + +const redis = new IORedis(process.env.REDIS_URL || "redis://localhost:6380", { + maxRetriesPerRequest: null, +}) + +const cloudQueue = new Queue("cloud-agents", { connection: redis }) + +const serverAdapter = new ExpressAdapter() +serverAdapter.setBasePath("/admin/queues") + +createBullBoard({ queues: [new BullMQAdapter(cloudQueue)], serverAdapter: serverAdapter }) + +const app: Express = express() + +app.use("/admin/queues", serverAdapter.getRouter()) + +app.use("/", (req: Request, res: Response) => { + res.redirect("/admin/queues") +}) + +const port = process.env.BULL_BOARD_PORT || 3002 + +app.listen(port, () => { + console.log(`Bull Board running on: http://localhost:${port}/admin/queues`) +}) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 319d0b5b66..eb9f7da5ab 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -56,6 +56,15 @@ importers: apps/cloud-agents: dependencies: + '@bull-board/api': + specifier: ^6.10.1 + version: 6.10.1(@bull-board/ui@6.10.1) + '@bull-board/express': + specifier: ^6.10.1 + version: 6.10.1 + '@bull-board/ui': + specifier: ^6.10.1 + version: 6.10.1 '@roo-code/types': specifier: workspace:^ version: link:../../packages/types @@ -68,6 +77,9 @@ importers: execa: specifier: ^9.6.0 version: 9.6.0 + express: + specifier: ^5.1.0 + version: 5.1.0 ioredis: specifier: ^5.4.3 version: 5.6.1 @@ -93,6 +105,9 @@ importers: '@roo-code/config-typescript': specifier: workspace:^ version: link:../../packages/config-typescript + '@types/express': + specifier: ^5.0.3 + version: 5.0.3 '@types/node': specifier: ^22.15.20 version: 22.15.29 @@ -1569,6 +1584,17 @@ packages: '@braintree/sanitize-url@7.1.1': resolution: {integrity: sha512-i1L7noDNxtFyL5DmZafWy1wRVhGehQmzZaz1HiN5e7iylJMSZR7ekOV7NsIqa5qBldlLrsKv4HbgFUVlQrz8Mw==} + '@bull-board/api@6.10.1': + resolution: {integrity: sha512-VPkZa2XZI2Wk2MqK1XyiiS+tOhNan54mnm2fpv2KA0fdZ92mQqNjhKkOpsykhQv9XUEc8cCRlZqGxf67YCMJbQ==} + peerDependencies: + '@bull-board/ui': 6.10.1 + + '@bull-board/express@6.10.1': + resolution: {integrity: sha512-IZl+t6B8bGCHqd/8Mbvit9RXqndXBe2Kx7pYHq7PZRrEKjK9b643Uizlc+OVqmWpLSjpQMP96K1SO5/Nq24o+A==} + + '@bull-board/ui@6.10.1': + resolution: {integrity: sha512-b6z6MBid/0DEShAMFPjPVZoPSoWqRBHCvTknyaxr/m8gL2/C+QP7jlCXut+L7uTFbCj9qs+CreAP0x/VdLI/Ig==} + '@changesets/apply-release-plan@7.0.12': resolution: {integrity: sha512-EaET7As5CeuhTzvXTQCRZeBUcisoYPDDcXvgTE/2jmmypKp0RC7LxKj/yzqeh/1qFTZI7oDGFcL1PHRuQuketQ==} @@ -4135,12 +4161,18 @@ packages: '@types/babel__traverse@7.20.7': resolution: {integrity: sha512-dkO5fhS7+/oos4ciWxyEyjWe48zmG6wbCheo/G2ZnHx4fs3EU6YC6UM8rk56gAjNJ9P3MTH2jo5jb92/K6wbng==} + '@types/body-parser@1.19.6': + resolution: {integrity: sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==} + '@types/chai@5.2.2': resolution: {integrity: sha512-8kB30R7Hwqf40JPiKhVzodJs2Qc1ZJ5zuT3uzw5Hq/dhNCl3G3l83jfpdI1e20BP348+fV7VIL/+FxaXkqBmWg==} '@types/clone-deep@4.0.4': resolution: {integrity: sha512-vXh6JuuaAha6sqEbJueYdh5zNBPPgG1OYumuz2UvLvriN6ABHDSW8ludREGWJb1MLIzbwZn4q4zUbUCerJTJfA==} + '@types/connect@3.4.38': + resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==} + '@types/d3-array@3.2.1': resolution: {integrity: sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==} @@ -4255,6 +4287,12 @@ packages: '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + '@types/express-serve-static-core@5.0.6': + resolution: {integrity: sha512-3xhRnjJPkULekpSzgtoNYYcTWgEZkp4myc+Saevii5JPnHNvHMRlBSHDbs7Bh1iPPoVTERHEZXyhyLbMEsExsA==} + + '@types/express@5.0.3': + resolution: {integrity: sha512-wGA0NX93b19/dZC1J18tKWVIYWyyF2ZjT9vin/NRu0qzzvfVzWjs04iq2rQ3H65vCTQYlRqs3YHfY7zjdV+9Kw==} + '@types/geojson@7946.0.16': resolution: {integrity: sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==} @@ -4267,6 +4305,9 @@ packages: '@types/hast@3.0.4': resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==} + '@types/http-errors@2.0.5': + resolution: {integrity: sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==} + '@types/istanbul-lib-coverage@2.0.6': resolution: {integrity: sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==} @@ -4300,6 +4341,9 @@ packages: '@types/mdast@4.0.4': resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==} + '@types/mime@1.3.5': + resolution: {integrity: sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==} + '@types/minimatch@5.1.2': resolution: {integrity: sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==} @@ -4343,6 +4387,12 @@ packages: '@types/ps-tree@1.1.6': resolution: {integrity: sha512-PtrlVaOaI44/3pl3cvnlK+GxOM3re2526TJvPvh7W+keHIXdV4TE0ylpPBAcvFQCbGitaTXwL9u+RF7qtVeazQ==} + '@types/qs@6.14.0': + resolution: {integrity: sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==} + + '@types/range-parser@1.2.7': + resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==} + '@types/react-dom@18.3.7': resolution: {integrity: sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==} peerDependencies: @@ -4351,6 +4401,12 @@ packages: '@types/react@18.3.23': resolution: {integrity: sha512-/LDXMQh55EzZQ0uVAZmKKhfENivEvWz6E+EYzh+/MCjMhNsotd+ZHhBGIjFDTi6+fz0OhQQQLbTgdQIxxCsC0w==} + '@types/send@0.17.5': + resolution: {integrity: sha512-z6F2D3cOStZvuk2SaP6YrwkNO65iTZcwA2ZkSABegdkAh/lf+Aa/YQndZVfmEXT5vgAp6zv06VQ3ejSVjAny4w==} + + '@types/serve-static@1.15.8': + resolution: {integrity: sha512-roei0UY3LhpOJvjbIP6ZZFngyLKl5dskOtDhxY5THRSpO+ZI+nzJ+m5yUMzGrp89YRa7lvknKkMYjqQFGwA7Sg==} + '@types/shell-quote@1.7.5': resolution: {integrity: sha512-+UE8GAGRPbJVQDdxi16dgadcBfQ+KG2vgZhV1+3A1XmHbmwcdwhCUwIdy+d3pAGrbvgRoVSjeI9vOWyq376Yzw==} @@ -8976,6 +9032,9 @@ packages: resolution: {integrity: sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==} engines: {node: '>=4'} + redis-info@3.1.0: + resolution: {integrity: sha512-ER4L9Sh/vm63DkIE0bkSjxluQlioBiBgf5w1UuldaW/3vPcecdljVDisZhmnCMvsxHNiARTTDDHGg9cGwTfrKg==} + redis-parser@3.0.0: resolution: {integrity: sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==} engines: {node: '>=4'} @@ -11414,6 +11473,24 @@ snapshots: '@braintree/sanitize-url@7.1.1': {} + '@bull-board/api@6.10.1(@bull-board/ui@6.10.1)': + dependencies: + '@bull-board/ui': 6.10.1 + redis-info: 3.1.0 + + '@bull-board/express@6.10.1': + dependencies: + '@bull-board/api': 6.10.1(@bull-board/ui@6.10.1) + '@bull-board/ui': 6.10.1 + ejs: 3.1.10 + express: 5.1.0 + transitivePeerDependencies: + - supports-color + + '@bull-board/ui@6.10.1': + dependencies: + '@bull-board/api': 6.10.1(@bull-board/ui@6.10.1) + '@changesets/apply-release-plan@7.0.12': dependencies: '@changesets/config': 3.1.1 @@ -14056,12 +14133,21 @@ snapshots: dependencies: '@babel/types': 7.27.1 + '@types/body-parser@1.19.6': + dependencies: + '@types/connect': 3.4.38 + '@types/node': 22.15.29 + '@types/chai@5.2.2': dependencies: '@types/deep-eql': 4.0.2 '@types/clone-deep@4.0.4': {} + '@types/connect@3.4.38': + dependencies: + '@types/node': 22.15.29 + '@types/d3-array@3.2.1': {} '@types/d3-axis@3.0.6': @@ -14197,6 +14283,19 @@ snapshots: '@types/estree@1.0.8': {} + '@types/express-serve-static-core@5.0.6': + dependencies: + '@types/node': 22.15.29 + '@types/qs': 6.14.0 + '@types/range-parser': 1.2.7 + '@types/send': 0.17.5 + + '@types/express@5.0.3': + dependencies: + '@types/body-parser': 1.19.6 + '@types/express-serve-static-core': 5.0.6 + '@types/serve-static': 1.15.8 + '@types/geojson@7946.0.16': {} '@types/glob@8.1.0': @@ -14212,6 +14311,8 @@ snapshots: dependencies: '@types/unist': 3.0.3 + '@types/http-errors@2.0.5': {} + '@types/istanbul-lib-coverage@2.0.6': {} '@types/istanbul-lib-report@3.0.3': @@ -14251,6 +14352,8 @@ snapshots: dependencies: '@types/unist': 3.0.3 + '@types/mime@1.3.5': {} + '@types/minimatch@5.1.2': {} '@types/mocha@10.0.10': {} @@ -14295,6 +14398,10 @@ snapshots: '@types/ps-tree@1.1.6': {} + '@types/qs@6.14.0': {} + + '@types/range-parser@1.2.7': {} + '@types/react-dom@18.3.7(@types/react@18.3.23)': dependencies: '@types/react': 18.3.23 @@ -14304,6 +14411,17 @@ snapshots: '@types/prop-types': 15.7.14 csstype: 3.1.3 + '@types/send@0.17.5': + dependencies: + '@types/mime': 1.3.5 + '@types/node': 22.15.29 + + '@types/serve-static@1.15.8': + dependencies: + '@types/http-errors': 2.0.5 + '@types/node': 22.15.29 + '@types/send': 0.17.5 + '@types/shell-quote@1.7.5': {} '@types/stack-utils@2.0.3': {} @@ -19955,6 +20073,10 @@ snapshots: redis-errors@1.2.0: {} + redis-info@3.1.0: + dependencies: + lodash: 4.17.21 + redis-parser@3.0.0: dependencies: redis-errors: 1.2.0 From 1e229259aba170448a55a513f2bd768c813952fe Mon Sep 17 00:00:00 2001 From: cte Date: Thu, 12 Jun 2025 22:01:12 -0700 Subject: [PATCH 05/19] More progress --- .roo/rules/rules.md | 13 +- apps/cloud-agents/.env.example | 4 + apps/cloud-agents/.gitignore | 2 + apps/cloud-agents/ARCHITECTURE.md | 299 ------------------ apps/cloud-agents/README.md | 252 --------------- apps/cloud-agents/docker-compose.yml | 191 ++++++----- .../{Dockerfile.agent => Dockerfile.runner} | 0 apps/cloud-agents/package.json | 8 +- apps/cloud-agents/scripts/dashboard.ts | 35 +- apps/cloud-agents/scripts/enqueue-test-job.sh | 18 ++ apps/cloud-agents/src/app/api/health/route.ts | 29 +- .../src/app/api/jobs/[id]/route.ts | 7 +- apps/cloud-agents/src/app/api/jobs/route.ts | 60 +--- .../src/app/api/webhooks/github/route.ts | 38 +-- apps/cloud-agents/src/app/page.tsx | 32 +- apps/cloud-agents/src/db/index.ts | 8 + apps/cloud-agents/src/{lib => }/db/schema.ts | 54 +--- apps/cloud-agents/src/lib/db/index.ts | 11 - apps/cloud-agents/src/lib/docker/runner.ts | 97 ------ apps/cloud-agents/src/lib/index.ts | 2 + apps/cloud-agents/src/lib/job.ts | 58 ++++ .../src/lib/jobs/fixGitHubIssue.ts | 42 +++ apps/cloud-agents/src/lib/logger.ts | 86 +++++ apps/cloud-agents/src/lib/queue.ts | 23 ++ apps/cloud-agents/src/lib/queue/index.ts | 51 --- apps/cloud-agents/src/lib/queue/processor.ts | 138 -------- apps/cloud-agents/src/lib/queue/worker.ts | 19 -- apps/cloud-agents/src/lib/redis.ts | 5 + apps/cloud-agents/src/lib/runTask.ts | 204 ++++++++++++ apps/cloud-agents/src/lib/utils.ts | 37 +++ apps/cloud-agents/src/lib/worker.ts | 29 ++ apps/cloud-agents/src/types/index.ts | 34 ++ pnpm-lock.yaml | 6 + roo-code-messages.log | 118 +++++++ 34 files changed, 842 insertions(+), 1168 deletions(-) create mode 100644 apps/cloud-agents/.env.example delete mode 100644 apps/cloud-agents/ARCHITECTURE.md delete mode 100644 apps/cloud-agents/README.md rename apps/cloud-agents/docker/{Dockerfile.agent => Dockerfile.runner} (100%) create mode 100755 apps/cloud-agents/scripts/enqueue-test-job.sh create mode 100644 apps/cloud-agents/src/db/index.ts rename apps/cloud-agents/src/{lib => }/db/schema.ts (69%) delete mode 100644 apps/cloud-agents/src/lib/db/index.ts delete mode 100644 apps/cloud-agents/src/lib/docker/runner.ts create mode 100644 apps/cloud-agents/src/lib/index.ts create mode 100644 apps/cloud-agents/src/lib/job.ts create mode 100644 apps/cloud-agents/src/lib/jobs/fixGitHubIssue.ts create mode 100644 apps/cloud-agents/src/lib/logger.ts create mode 100644 apps/cloud-agents/src/lib/queue.ts delete mode 100644 apps/cloud-agents/src/lib/queue/index.ts delete mode 100644 apps/cloud-agents/src/lib/queue/processor.ts delete mode 100644 apps/cloud-agents/src/lib/queue/worker.ts create mode 100644 apps/cloud-agents/src/lib/redis.ts create mode 100644 apps/cloud-agents/src/lib/runTask.ts create mode 100644 apps/cloud-agents/src/lib/utils.ts create mode 100644 apps/cloud-agents/src/lib/worker.ts create mode 100644 apps/cloud-agents/src/types/index.ts create mode 100644 roo-code-messages.log diff --git a/.roo/rules/rules.md b/.roo/rules/rules.md index bf3f863a0b..90308f6dec 100644 --- a/.roo/rules/rules.md +++ b/.roo/rules/rules.md @@ -1,19 +1,16 @@ +# Repository Rules + +1. This repository is a monorepo that uses pnpm workspaces. Prefer `pnpm` over `npm` when executing commands. +2. Tests should be written with vitest instead of jest. + # Code Quality Rules 1. Test Coverage: - - Before attempting completion, always make sure that any code changes have test coverage - Ensure all tests pass before submitting changes - 2. Lint Rules: - - Never disable any lint rules without explicit user approval - 3. Styling Guidelines: - Use Tailwind CSS classes instead of inline style objects for new markup - VSCode CSS variables must be added to webview-ui/src/index.css before using them in Tailwind classes - Example: `
` instead of style objects - -# Adding a New Setting - -To add a new setting that persists its state, follow the steps in docs/settings.md diff --git a/apps/cloud-agents/.env.example b/apps/cloud-agents/.env.example new file mode 100644 index 0000000000..95fc37b06d --- /dev/null +++ b/apps/cloud-agents/.env.example @@ -0,0 +1,4 @@ +DATABASE_URL=postgresql://postgres:password@localhost:5433/cloud_agents +REDIS_URL=redis://localhost:6380 +GITHUB_WEBHOOK_SECRET=your-webhook-secret-here +OPENROUTER_API_KEY=your-openrouter-api-key diff --git a/apps/cloud-agents/.gitignore b/apps/cloud-agents/.gitignore index efacd67d11..63fb868ec2 100644 --- a/apps/cloud-agents/.gitignore +++ b/apps/cloud-agents/.gitignore @@ -2,3 +2,5 @@ docker/postgres-data docker/redis-data + +!.env.example diff --git a/apps/cloud-agents/ARCHITECTURE.md b/apps/cloud-agents/ARCHITECTURE.md deleted file mode 100644 index f4d2c02892..0000000000 --- a/apps/cloud-agents/ARCHITECTURE.md +++ /dev/null @@ -1,299 +0,0 @@ -# Cloud Agents Architecture - -## Overview - -Cloud Agents is a Next.js application that exposes an API for enqueueing Roo Code tasks via BullMQ, processes them in Docker containers, and integrates with GitHub webhooks to automatically fix issues. - -## System Architecture - -```mermaid -flowchart TB - GH[GitHub Webhook] --> API[Next.js API Routes] - API --> BQ[BullMQ Queue] - BQ --> JH[Job Handler] - JH --> DC[Docker Container] - DC --> RC[Roo Code Task Runner] - - subgraph Infrastructure - PG[(PostgreSQL)] - RD[(Redis)] - end - - API --> PG - BQ --> RD - JH --> PG -``` - -## Directory Structure - -``` -apps/cloud-agents/ -├── src/ -│ ├── app/ -│ │ ├── api/ -│ │ │ ├── webhooks/ -│ │ │ │ └── github/ -│ │ │ │ └── route.ts # GitHub webhook handler -│ │ │ ├── jobs/ -│ │ │ │ ├── route.ts # Create job endpoint -│ │ │ │ └── [id]/ -│ │ │ │ └── route.ts # Get job status -│ │ │ └── health/ -│ │ │ └── route.ts # Health check -│ │ ├── layout.tsx -│ │ └── page.tsx # Simple dashboard -│ ├── lib/ -│ │ ├── db/ -│ │ │ ├── schema.ts # Cloud agents schema -│ │ │ └── index.ts -│ │ ├── queue/ -│ │ │ ├── processor.ts # BullMQ processor -│ │ │ ├── jobs.ts # Job definitions -│ │ │ └── index.ts -│ │ ├── docker/ -│ │ │ ├── runner.ts # Docker container runner -│ │ │ └── config.ts -│ │ └── github/ -│ │ ├── webhooks.ts # Webhook handlers -│ │ └── types.ts -│ └── types/ -│ └── index.ts -├── docker/ -│ ├── Dockerfile.agent # Simplified runner -│ └── docker-compose.yml -├── package.json -├── tsconfig.json -├── next.config.ts -└── .env.example -``` - -## Key Components - -### 1. Database Schema (Drizzle ORM) - -The cloud agents database extends the existing evals database with additional tables: - -```typescript -// Cloud agent specific tables -- cloudJobs: Track job requests - - id: integer (primary key) - - type: text (e.g., 'github.issue.fix', 'task.execute') - - status: text ('pending', 'processing', 'completed', 'failed') - - payload: jsonb (job-specific data) - - result: jsonb (job output) - - error: text (error message if failed) - - createdAt: timestamp - - startedAt: timestamp - - completedAt: timestamp - -- cloudTasks: Link cloud jobs to Roo Code tasks - - id: integer (primary key) - - jobId: integer (references cloudJobs) - - taskId: integer (references tasks from evals) - - containerId: text (Docker container ID) - - createdAt: timestamp -``` - -### 2. BullMQ Job Types - -```typescript -interface JobTypes { - "github.issue.fix": { - repo: string // e.g., "RooCodeInc/Roo-Code" - issue: number // Issue number - title: string // Issue title - body: string // Issue description - labels?: string[] // Issue labels - } - - "task.execute": { - prompt: string // Task prompt - workspace?: string // Optional workspace path - settings?: RooCodeSettings // Optional Roo Code settings override - } -} -``` - -### 3. Simplified Docker Runner - -The cloud agents Docker image is based on the existing `Dockerfile.runner` but simplified: - -**Remove:** - -- Language-specific VS Code extensions (Go, Java, Python, Rust) -- Eval-specific dependencies and test infrastructure -- UV/Python sync steps - -**Keep:** - -- Base Node.js environment -- VS Code installation -- Roo Code extension build and installation -- Basic utilities (git, curl, etc.) -- Docker CLI for nested container support - -### 4. API Endpoints - -#### `POST /api/webhooks/github` - -Handles GitHub webhook events, specifically for issue events. - -- Verifies webhook signature -- Parses issue data -- Creates appropriate job in queue - -#### `POST /api/jobs` - -Creates a new job in the queue. - -```typescript -Request: { - type: keyof JobTypes - payload: JobTypes[type] -} -Response: { - id: string - status: string -} -``` - -#### `GET /api/jobs/:id` - -Retrieves job status and results. - -```typescript -Response: { - id: string - type: string - status: string - payload: object - result?: object - error?: string - createdAt: string - startedAt?: string - completedAt?: string -} -``` - -#### `GET /api/health` - -Health check endpoint for monitoring. - -```typescript -Response: { - status: "ok" | "error" - services: { - database: boolean - redis: boolean - docker: boolean - } -} -``` - -## Implementation Phases - -### Phase 1: Core Infrastructure - -1. Create Next.js app structure in `apps/cloud-agents` -2. Set up database schema using Drizzle ORM -3. Configure Docker compose with PostgreSQL & Redis -4. Create simplified Dockerfile.agent - -### Phase 2: Job Queue - -1. Implement BullMQ setup and configuration -2. Create job processor with proper error handling -3. Add Docker container spawning logic -4. Implement job status tracking in database - -### Phase 3: API & Webhooks - -1. Create all API route handlers -2. Implement GitHub webhook signature verification -3. Add issue parsing and automatic job creation -4. Create simple status dashboard - -### Phase 4: Testing & Deployment - -1. Add integration tests for API endpoints -2. Create production docker-compose configuration -3. Add monitoring and structured logging -4. Write comprehensive documentation - -## Configuration - -### Environment Variables - -```env -# Database -DATABASE_URL=postgresql://postgres:password@localhost:5432/cloud_agents - -# Redis -REDIS_URL=redis://localhost:6379 - -# GitHub Integration -GITHUB_WEBHOOK_SECRET=your-webhook-secret -GITHUB_APP_ID=your-app-id -GITHUB_PRIVATE_KEY=your-private-key-base64 - -# API Keys for Roo Code -OPENROUTER_API_KEY=your-openrouter-key -ANTHROPIC_API_KEY=your-anthropic-key - -# Docker Configuration -DOCKER_NETWORK=cloud-agents_default -DOCKER_IMAGE=cloud-agents-runner:latest -MAX_CONCURRENT_CONTAINERS=5 - -# Application -PORT=3001 -NODE_ENV=development -``` - -### Docker Compose Services - -```yaml -services: - app: - build: . - ports: - - "3001:3001" - environment: - - DATABASE_URL - - REDIS_URL - volumes: - - /var/run/docker.sock:/var/run/docker.sock - depends_on: - - db - - redis - - db: - image: postgres:17 - environment: - - POSTGRES_PASSWORD=password - - POSTGRES_DB=cloud_agents - - redis: - image: redis:7-alpine -``` - -## Error Handling & Retry Logic - -1. **Job Retries**: Failed jobs will be retried up to 3 times with exponential backoff -2. **Container Timeouts**: Tasks have a 30-minute timeout by default -3. **Resource Cleanup**: Containers are always cleaned up, even on failure -4. **Dead Letter Queue**: Failed jobs after all retries go to DLQ for manual review - -## Security Considerations - -1. **Webhook Verification**: All GitHub webhooks are verified using HMAC -2. **Container Isolation**: Each task runs in an isolated container -3. **Resource Limits**: CPU and memory limits on containers -4. **API Authentication**: Consider adding API key authentication for job creation - -## Monitoring & Observability - -1. **Metrics**: Job queue depth, processing time, success rate -2. **Logging**: Structured logs for all job processing steps -3. **Health Checks**: Regular checks on all dependent services -4. **Alerts**: Notifications for failed jobs and system issues diff --git a/apps/cloud-agents/README.md b/apps/cloud-agents/README.md deleted file mode 100644 index c18997804b..0000000000 --- a/apps/cloud-agents/README.md +++ /dev/null @@ -1,252 +0,0 @@ -# Cloud Agents - -A Next.js application that exposes an API for enqueueing Roo Code tasks via BullMQ, processes them in Docker containers, and integrates with GitHub webhooks to automatically fix issues. - -## Features - -- **Job Queue**: BullMQ-powered job processing with Redis -- **GitHub Integration**: Webhook handler for automatic issue fixing -- **Docker Containers**: Isolated task execution using Roo Code -- **REST API**: Simple endpoints for job management -- **Database**: PostgreSQL with Drizzle ORM - -## Quick Start - -### Prerequisites - -- Node.js 20+ -- pnpm -- Docker & Docker Compose - -### Development Setup - -1. **Install dependencies**: - - ```bash - cd apps/cloud-agents - pnpm install - ``` - -2. **Set up environment**: - - ```bash - cp .env.example .env.local - # Edit .env.local with your configuration - ``` - -3. **Start services**: - - ```bash - pnpm services:start - ``` - -4. **Run database migrations**: - - ```bash - pnpm db:push - ``` - -5. **Start the development server**: - - ```bash - pnpm dev - ``` - -6. **Start the worker** (in another terminal): - ```bash - pnpm worker:dev - ``` - -The API will be available at http://localhost:3001 - -### Production Deployment - -1. **Build Docker images**: - - ```bash - docker compose build - ``` - -2. **Start all services**: - ```bash - docker compose --profile server up -d - ``` - -## API Endpoints - -### Health Check - -```http -GET /api/health -``` - -### Create Job - -```http -POST /api/jobs -Content-Type: application/json - -{ - "type": "github.issue.fix", - "payload": { - "repo": "owner/repo", - "issue": 123, - "title": "Issue title", - "body": "Issue description" - } -} -``` - -### Get Job Status - -```http -GET /api/jobs/:id -``` - -### GitHub Webhook - -```http -POST /api/webhooks/github -``` - -## Job Types - -### GitHub Issue Fix - -Automatically creates tasks to fix GitHub issues when they are opened. - -```typescript -{ - "type": "github.issue.fix", - "payload": { - "repo": "owner/repo", - "issue": 123, - "title": "Issue title", - "body": "Issue description", - "labels": ["bug", "priority:high"] - } -} -``` - -### Custom Task - -Execute arbitrary Roo Code tasks. - -```typescript -{ - "type": "task.execute", - "payload": { - "prompt": "Your task description", - "workspace": "/path/to/workspace", - "settings": { - "model": "claude-3-sonnet", - "temperature": 0.1 - } - } -} -``` - -## Configuration - -### Environment Variables - -See `.env.example` for all available configuration options. - -### GitHub Webhook Setup - -1. Create a webhook in your GitHub repository -2. Set the payload URL to `https://your-domain.com/api/webhooks/github` -3. Set the content type to `application/json` -4. Set the secret to match `GITHUB_WEBHOOK_SECRET` in your environment -5. Subscribe to "Issues" events - -## Architecture - -See [ARCHITECTURE.md](./ARCHITECTURE.md) for detailed system design and component descriptions. - -## Development - -### Database Management - -```bash -# Generate migration -pnpm db:generate - -# Apply migration -pnpm db:migrate - -# Push schema changes (development) -pnpm db:push - -# Open database studio -pnpm db:studio -``` - -### Docker Commands - -```bash -# Build agent container -docker compose build runner - -# Start a shell in the agent container -docker compose run --rm runner bash - -# View logs -docker compose logs -f worker -``` - -### Testing - -Test the API with curl: - -```bash -# Health check -curl http://localhost:3001/api/health - -# Create a job -curl -X POST http://localhost:3001/api/jobs \ - -H "Content-Type: application/json" \ - -d '{ - "type": "task.execute", - "payload": { - "prompt": "Create a simple hello world program" - } - }' - -# Check job status -curl http://localhost:3001/api/jobs/1 -``` - -## Troubleshooting - -### Common Issues - -1. **Database connection failed**: Make sure PostgreSQL is running and the connection string is correct -2. **Redis connection failed**: Ensure Redis is running on the specified port -3. **Docker permission denied**: Make sure your user is in the docker group -4. **Port already in use**: Check if another service is using ports 3001, 5433, or 6380 - -### Logs - -```bash -# Application logs -docker compose logs app - -# Worker logs -docker compose logs worker - -# Database logs -docker compose logs db -``` - -## Contributing - -1. Fork the repository -2. Create a feature branch -3. Make your changes -4. Add tests if applicable -5. Submit a pull request - -## License - -See the main repository license. diff --git a/apps/cloud-agents/docker-compose.yml b/apps/cloud-agents/docker-compose.yml index 5de9f71fa7..9cdce01568 100644 --- a/apps/cloud-agents/docker-compose.yml +++ b/apps/cloud-agents/docker-compose.yml @@ -1,104 +1,103 @@ services: - app: - build: - context: ../../ - dockerfile: apps/cloud-agents/docker/Dockerfile.app - ports: - - "${CLOUD_AGENTS_PORT:-3001}:3001" - environment: - - DATABASE_URL=postgresql://postgres:password@db:5432/cloud_agents - - REDIS_URL=redis://redis:6379 - - NODE_ENV=production - volumes: - - /var/run/docker.sock:/var/run/docker.sock - depends_on: - db: - condition: service_healthy - redis: - condition: service_started - profiles: - - server + db: + container_name: cloud-agents-db + image: postgres:17.5 + ports: + - "5433:5432" + volumes: + - ./docker/postgres-data:/var/lib/postgresql/data + environment: + - POSTGRES_USER=postgres + - POSTGRES_PASSWORD=password + - POSTGRES_DB=cloud_agents + healthcheck: + test: ["CMD-SHELL", "pg_isready -U postgres -d cloud_agents"] + interval: 5s + timeout: 5s + retries: 5 + start_period: 30s + profiles: + - server - worker: - build: - context: ../../ - dockerfile: apps/cloud-agents/docker/Dockerfile.app - command: pnpm --filter @roo-code/cloud-agents worker:dev - environment: - - DATABASE_URL=postgresql://postgres:password@db:5432/cloud_agents - - REDIS_URL=redis://redis:6379 - - NODE_ENV=production - volumes: - - /var/run/docker.sock:/var/run/docker.sock - - /tmp/cloud-agents:/var/log/cloud-agents - depends_on: - - db - - redis - profiles: - - server + redis: + container_name: cloud-agents-redis + image: redis:7-alpine + ports: + - "6380:6379" + volumes: + - ./docker/redis-data:/data + command: redis-server --appendonly yes + profiles: + - server - bullmq-dashboard: - container_name: cloud-agents-bullmq-dashboard - build: - context: ../../ - dockerfile: apps/cloud-agents/docker/Dockerfile.dashboard - ports: - - "${BULL_BOARD_PORT:-3002}:3002" - environment: - - REDIS_URL=redis://redis:6379 - - BULL_BOARD_PORT=3002 - - NODE_ENV=production - depends_on: - - redis - profiles: - - server + dashboard: + container_name: cloud-agents-dashboard + build: + context: ../../ + dockerfile: apps/cloud-agents/docker/Dockerfile.dashboard + ports: + - "3002:3002" + environment: + - REDIS_URL=redis://redis:6379 + - NODE_ENV=production + depends_on: + - redis + profiles: + - server - db: - container_name: cloud-agents-db - image: postgres:17.5 - ports: - - "${CLOUD_AGENTS_DB_PORT:-5433}:5432" - volumes: - - ./docker/postgres-data:/var/lib/postgresql/data - environment: - - POSTGRES_USER=postgres - - POSTGRES_PASSWORD=password - - POSTGRES_DB=cloud_agents - healthcheck: - test: ["CMD-SHELL", "pg_isready -U postgres -d cloud_agents"] - interval: 5s - timeout: 5s - retries: 5 - start_period: 30s - profiles: - - server + api: + build: + context: ../../ + dockerfile: apps/cloud-agents/docker/Dockerfile.app + ports: + - "3001:3001" + environment: + - DATABASE_URL=postgresql://postgres:password@db:5432/cloud_agents + - REDIS_URL=redis://redis:6379 + - NODE_ENV=production + volumes: + - /var/run/docker.sock:/var/run/docker.sock + depends_on: + db: + condition: service_healthy + redis: + condition: service_started + profiles: + - server - redis: - container_name: cloud-agents-redis - image: redis:7-alpine - ports: - - "${CLOUD_AGENTS_REDIS_PORT:-6380}:6379" - volumes: - - ./docker/redis-data:/data - command: redis-server --appendonly yes - profiles: - - server + worker: + build: + context: ../../ + dockerfile: apps/cloud-agents/docker/Dockerfile.app + command: pnpm --filter @roo-code/cloud-agents worker:dev + environment: + - DATABASE_URL=postgresql://postgres:password@db:5432/cloud_agents + - REDIS_URL=redis://redis:6379 + - NODE_ENV=production + volumes: + - /var/run/docker.sock:/var/run/docker.sock + - /tmp/cloud-agents:/var/log/cloud-agents + depends_on: + - db + - redis + profiles: + - server - runner: - build: - context: ../../ - dockerfile: apps/cloud-agents/docker/Dockerfile.agent - environment: - - HOST_EXECUTION_METHOD=docker - volumes: - - /var/run/docker.sock:/var/run/docker.sock - - /tmp/cloud-agents:/var/log/cloud-agents - stdin_open: true - tty: true - profiles: - - runner + runner: + build: + context: ../../ + dockerfile: apps/cloud-agents/docker/Dockerfile.runner + environment: + - HOST_EXECUTION_METHOD=docker + volumes: + - /var/run/docker.sock:/var/run/docker.sock + - /tmp/cloud-agents:/var/log/cloud-agents + stdin_open: true + tty: true + profiles: + - runner networks: - default: - name: cloud-agents_default - driver: bridge \ No newline at end of file + default: + name: cloud-agents_default + driver: bridge diff --git a/apps/cloud-agents/docker/Dockerfile.agent b/apps/cloud-agents/docker/Dockerfile.runner similarity index 100% rename from apps/cloud-agents/docker/Dockerfile.agent rename to apps/cloud-agents/docker/Dockerfile.runner diff --git a/apps/cloud-agents/package.json b/apps/cloud-agents/package.json index f091fe4f47..011e717596 100644 --- a/apps/cloud-agents/package.json +++ b/apps/cloud-agents/package.json @@ -9,21 +9,22 @@ "build": "next build", "start": "next start --port 3001", "clean": "rimraf .next .turbo", - "drizzle-kit": "dotenvx run -f .env.local -- tsx node_modules/drizzle-kit/bin.cjs", + "drizzle-kit": "dotenvx run -f .env -- tsx node_modules/drizzle-kit/bin.cjs", "db:generate": "pnpm drizzle-kit generate", "db:migrate": "pnpm drizzle-kit migrate", "db:push": "pnpm drizzle-kit push", "db:check": "pnpm drizzle-kit check", "db:studio": "pnpm drizzle-kit studio", - "services:start": "docker compose up -d db redis bullmq-dashboard", + "services:start": "docker compose up -d db redis dashboard", "services:stop": "docker compose down", - "worker:dev": "tsx src/lib/queue/worker.ts", + "worker": "dotenvx run -f .env -- tsx src/lib/worker.ts", "dashboard": "tsx scripts/dashboard.ts" }, "dependencies": { "@bull-board/api": "^6.10.1", "@bull-board/express": "^6.10.1", "@bull-board/ui": "^6.10.1", + "@roo-code/ipc": "workspace:^", "@roo-code/types": "workspace:^", "bullmq": "^5.37.0", "drizzle-orm": "^0.44.1", @@ -31,6 +32,7 @@ "express": "^5.1.0", "ioredis": "^5.4.3", "next": "^15.2.5", + "p-wait-for": "^5.0.2", "postgres": "^3.4.7", "react": "^18.3.1", "react-dom": "^18.3.1", diff --git a/apps/cloud-agents/scripts/dashboard.ts b/apps/cloud-agents/scripts/dashboard.ts index 6d77be5cff..24acc39f96 100644 --- a/apps/cloud-agents/scripts/dashboard.ts +++ b/apps/cloud-agents/scripts/dashboard.ts @@ -1,31 +1,20 @@ -import { createBullBoard } from "@bull-board/api" -import { BullMQAdapter } from "@bull-board/api/bullMQAdapter" -import { ExpressAdapter } from "@bull-board/express" -import { Queue } from "bullmq" import IORedis from "ioredis" -import express, { type Express, type Request, type Response } from "express" - -const redis = new IORedis(process.env.REDIS_URL || "redis://localhost:6380", { - maxRetriesPerRequest: null, -}) +import { Queue } from "bullmq" +import { ExpressAdapter } from "@bull-board/express" +import { BullMQAdapter } from "@bull-board/api/bullMQAdapter" +import { createBullBoard } from "@bull-board/api" +import express from "express" +import type { Express, Request, Response } from "express" -const cloudQueue = new Queue("cloud-agents", { connection: redis }) +const redis = new IORedis(process.env.REDIS_URL || "redis://localhost:6380", { maxRetriesPerRequest: null }) +const queue = new Queue("cloud-agents", { connection: redis }) const serverAdapter = new ExpressAdapter() serverAdapter.setBasePath("/admin/queues") +createBullBoard({ queues: [new BullMQAdapter(queue)], serverAdapter }) -createBullBoard({ queues: [new BullMQAdapter(cloudQueue)], serverAdapter: serverAdapter }) - +const port = 3002 const app: Express = express() - app.use("/admin/queues", serverAdapter.getRouter()) - -app.use("/", (req: Request, res: Response) => { - res.redirect("/admin/queues") -}) - -const port = process.env.BULL_BOARD_PORT || 3002 - -app.listen(port, () => { - console.log(`Bull Board running on: http://localhost:${port}/admin/queues`) -}) +app.use("/", (req: Request, res: Response) => res.redirect("/admin/queues")) +app.listen(port, () => console.log(`Bull Board running on: http://localhost:${port}/admin/queues`)) diff --git a/apps/cloud-agents/scripts/enqueue-test-job.sh b/apps/cloud-agents/scripts/enqueue-test-job.sh new file mode 100755 index 0000000000..a39ded3a2c --- /dev/null +++ b/apps/cloud-agents/scripts/enqueue-test-job.sh @@ -0,0 +1,18 @@ +#!/bin/bash + +BASE_URL="http://localhost:3001" +JOBS_ENDPOINT="$BASE_URL/api/jobs" + +curl -X POST "$JOBS_ENDPOINT" \ + -H "Content-Type: application/json" \ + -d '{ + "type": "github.issue.fix", + "payload": { + "repo": "RooCodeInc/Roo-Code", + "issue": 123, + "title": "Fix authentication bug in login flow", + "body": "Users are experiencing issues logging in with OAuth providers. The token refresh mechanism seems to be failing intermittently, causing users to be logged out unexpectedly.\n\nSteps to reproduce:\n1. Login with GitHub OAuth\n2. Wait for token to expire\n3. Try to make an authenticated request\n4. User gets logged out instead of token being refreshed", + "labels": ["bug", "authentication", "oauth"] + } + }' \ + -w "\nStatus: %{http_code}\n\n" diff --git a/apps/cloud-agents/src/app/api/health/route.ts b/apps/cloud-agents/src/app/api/health/route.ts index aae951b021..26afc264a8 100644 --- a/apps/cloud-agents/src/app/api/health/route.ts +++ b/apps/cloud-agents/src/app/api/health/route.ts @@ -1,15 +1,12 @@ import { NextResponse } from "next/server" -import { redis } from "@/lib/queue" -import { db } from "@/lib/db" + +import { db } from "@/db" +import { redis } from "@/lib" export async function GET() { try { - const services = { - database: false, - redis: false, - } + const services = { database: false, redis: false } - // Check database connection try { await db.execute("SELECT 1") services.database = true @@ -17,7 +14,6 @@ export async function GET() { console.error("Database health check failed:", error) } - // Check Redis connection try { await redis.ping() services.redis = true @@ -26,22 +22,9 @@ export async function GET() { } const allHealthy = Object.values(services).every(Boolean) - - return NextResponse.json( - { - status: allHealthy ? "ok" : "error", - services, - }, - { status: allHealthy ? 200 : 500 }, - ) + return NextResponse.json({ status: allHealthy ? "ok" : "error", services }, { status: allHealthy ? 200 : 500 }) } catch (error) { console.error("Health check error:", error) - return NextResponse.json( - { - status: "error", - error: "Internal server error", - }, - { status: 500 }, - ) + return NextResponse.json({ status: "error", error: "Internal server error" }, { status: 500 }) } } diff --git a/apps/cloud-agents/src/app/api/jobs/[id]/route.ts b/apps/cloud-agents/src/app/api/jobs/[id]/route.ts index 42e50f8086..1299312c41 100644 --- a/apps/cloud-agents/src/app/api/jobs/[id]/route.ts +++ b/apps/cloud-agents/src/app/api/jobs/[id]/route.ts @@ -1,8 +1,11 @@ import { NextRequest, NextResponse } from "next/server" -import { db, cloudJobs } from "@/lib/db" import { eq } from "drizzle-orm" -export async function GET(request: NextRequest, { params }: { params: Promise<{ id: string }> }) { +import { db, cloudJobs } from "@/db" + +type Params = Promise<{ id: string }> + +export async function GET(request: NextRequest, { params }: { params: Params }) { try { const { id } = await params const jobId = parseInt(id, 10) diff --git a/apps/cloud-agents/src/app/api/jobs/route.ts b/apps/cloud-agents/src/app/api/jobs/route.ts index 8e0932fbcf..28186042ee 100644 --- a/apps/cloud-agents/src/app/api/jobs/route.ts +++ b/apps/cloud-agents/src/app/api/jobs/route.ts @@ -1,66 +1,32 @@ import { NextRequest, NextResponse } from "next/server" import { z } from "zod" -import { db, cloudJobs, JobTypes } from "@/lib/db" -import { addJob } from "@/lib/queue" -const githubIssueFixSchema = z.object({ - repo: z.string(), - issue: z.number(), - title: z.string(), - body: z.string(), - labels: z.array(z.string()).optional(), -}) - -const taskExecuteSchema = z.object({ - prompt: z.string(), - workspace: z.string().optional(), - settings: z.record(z.unknown()).optional(), -}) - -const createJobSchema = z.discriminatedUnion("type", [ - z.object({ - type: z.literal("github.issue.fix"), - payload: githubIssueFixSchema, - }), - z.object({ - type: z.literal("task.execute"), - payload: taskExecuteSchema, - }), -]) +import { createJobSchema } from "@/types" +import { db, cloudJobs } from "@/db" +import { enqueue } from "@/lib" export async function POST(request: NextRequest) { try { const body = await request.json() - const parsed = createJobSchema.parse(body) - - // Create job record in database - const jobs = await db + const values = createJobSchema.parse(body) + const [job] = await db .insert(cloudJobs) - .values({ - type: parsed.type, - status: "pending", - payload: parsed.payload, - }) + .values({ ...values, status: "pending" }) .returning() - const job = jobs[0] if (!job) { throw new Error("Failed to create job") } - // Add job to queue with proper typing - if (parsed.type === "github.issue.fix") { - await addJob("github.issue.fix", parsed.payload as JobTypes["github.issue.fix"], job.id) - } else if (parsed.type === "task.execute") { - await addJob("task.execute", parsed.payload as JobTypes["task.execute"], job.id) + switch (values.type) { + case "github.issue.fix": + await enqueue("github.issue.fix", values.payload, job.id) + break + default: + throw new Error(`Unknown job type: ${values.type}`) } - return NextResponse.json({ - id: job.id, - type: job.type, - status: job.status, - createdAt: job.createdAt, - }) + return NextResponse.json(job) } catch (error) { console.error("Error creating job:", error) diff --git a/apps/cloud-agents/src/app/api/webhooks/github/route.ts b/apps/cloud-agents/src/app/api/webhooks/github/route.ts index 1a81b5a92e..81eab584ad 100644 --- a/apps/cloud-agents/src/app/api/webhooks/github/route.ts +++ b/apps/cloud-agents/src/app/api/webhooks/github/route.ts @@ -1,8 +1,9 @@ import { NextRequest, NextResponse } from "next/server" import { createHmac } from "crypto" import { z } from "zod" -import { db, cloudJobs } from "@/lib/db" -import { addJob } from "@/lib/queue" + +import { db, cloudJobs } from "@/db" +import { enqueue } from "@/lib" const githubWebhookSchema = z.object({ action: z.string(), @@ -10,11 +11,7 @@ const githubWebhookSchema = z.object({ number: z.number(), title: z.string(), body: z.string().nullable(), - labels: z.array( - z.object({ - name: z.string(), - }), - ), + labels: z.array(z.object({ name: z.string() })), }), repository: z.object({ full_name: z.string(), @@ -23,7 +20,6 @@ const githubWebhookSchema = z.object({ function verifySignature(payload: string, signature: string, secret: string): boolean { const expectedSignature = createHmac("sha256", secret).update(payload, "utf8").digest("hex") - const receivedSignature = signature.replace("sha256=", "") return expectedSignature === receivedSignature } @@ -39,25 +35,20 @@ export async function POST(request: NextRequest) { const payload = await request.text() - // Verify webhook signature - const webhookSecret = process.env.GITHUB_WEBHOOK_SECRET - if (webhookSecret && !verifySignature(payload, signature, webhookSecret)) { + if (!verifySignature(payload, signature, process.env.GITHUB_WEBHOOK_SECRET!)) { return NextResponse.json({ error: "Invalid signature" }, { status: 401 }) } - // Only handle issue events if (event !== "issues") { return NextResponse.json({ message: "Event ignored" }) } const data = githubWebhookSchema.parse(JSON.parse(payload)) - // Only handle opened issues if (data.action !== "opened") { return NextResponse.json({ message: "Action ignored" }) } - // Create job for issue fix const jobPayload = { repo: data.repository.full_name, issue: data.issue.number, @@ -66,28 +57,17 @@ export async function POST(request: NextRequest) { labels: data.issue.labels.map((label) => label.name), } - // Create job record in database - const jobs = await db + const [job] = await db .insert(cloudJobs) - .values({ - type: "github.issue.fix", - status: "pending", - payload: jobPayload, - }) + .values({ type: "github.issue.fix", status: "pending", payload: jobPayload }) .returning() - const job = jobs[0] if (!job) { throw new Error("Failed to create job") } - // Add job to queue - await addJob("github.issue.fix", jobPayload, job.id) - - return NextResponse.json({ - message: "Job created successfully", - jobId: job.id, - }) + await enqueue("github.issue.fix", jobPayload, job.id) + return NextResponse.json({ message: "Job created successfully", jobId: job.id }) } catch (error) { console.error("GitHub webhook error:", error) diff --git a/apps/cloud-agents/src/app/page.tsx b/apps/cloud-agents/src/app/page.tsx index 0af14a042b..0cbb883e46 100644 --- a/apps/cloud-agents/src/app/page.tsx +++ b/apps/cloud-agents/src/app/page.tsx @@ -1,31 +1,3 @@ -export default function HomePage() { - return ( -
-

Cloud Agents

-

Roo Code task execution service

- -
-

Status

-

Service is running

-
- -
-

API Endpoints

-
    -
  • - POST /api/jobs - Create a new job -
  • -
  • - GET /api/jobs/:id - Get job status -
  • -
  • - POST /api/webhooks/github - GitHub webhook handler -
  • -
  • - GET /api/health - Health check -
  • -
-
-
- ) +export default function Page() { + return
Hello, World!
} diff --git a/apps/cloud-agents/src/db/index.ts b/apps/cloud-agents/src/db/index.ts new file mode 100644 index 0000000000..57fcedc8ec --- /dev/null +++ b/apps/cloud-agents/src/db/index.ts @@ -0,0 +1,8 @@ +import { drizzle } from "drizzle-orm/postgres-js" +import postgres from "postgres" + +import { schema } from "./schema" + +export const db = drizzle(postgres(process.env.DATABASE_URL!, { prepare: false }), { schema }) + +export * from "./schema" diff --git a/apps/cloud-agents/src/lib/db/schema.ts b/apps/cloud-agents/src/db/schema.ts similarity index 69% rename from apps/cloud-agents/src/lib/db/schema.ts rename to apps/cloud-agents/src/db/schema.ts index ed6689376d..d8962575c7 100644 --- a/apps/cloud-agents/src/lib/db/schema.ts +++ b/apps/cloud-agents/src/db/schema.ts @@ -2,11 +2,12 @@ import { pgTable, text, timestamp, integer, jsonb } from "drizzle-orm/pg-core" import { relations } from "drizzle-orm" /** - * Cloud Jobs - Track job requests + * cloudJobs */ + export const cloudJobs = pgTable("cloud_jobs", { id: integer().primaryKey().generatedAlwaysAsIdentity(), - type: text().notNull(), // e.g., 'github.issue.fix', 'task.execute' + type: text().notNull(), // e.g., 'github.issue.fix' status: text().notNull().default("pending"), // 'pending', 'processing', 'completed', 'failed' payload: jsonb().notNull(), // job-specific data result: jsonb(), // job output @@ -16,9 +17,18 @@ export const cloudJobs = pgTable("cloud_jobs", { completedAt: timestamp("completed_at"), }) +export const cloudJobsRelations = relations(cloudJobs, ({ many }) => ({ + tasks: many(cloudTasks), +})) + +export type CloudJob = typeof cloudJobs.$inferSelect +export type InsertCloudJob = typeof cloudJobs.$inferInsert +export type UpdateCloudJob = Partial> + /** - * Cloud Tasks - Link cloud jobs to Roo Code tasks + * cloudTasks */ + export const cloudTasks = pgTable("cloud_tasks", { id: integer().primaryKey().generatedAlwaysAsIdentity(), jobId: integer("job_id") @@ -29,54 +39,18 @@ export const cloudTasks = pgTable("cloud_tasks", { createdAt: timestamp("created_at").notNull().defaultNow(), }) -/** - * Relations - */ -export const cloudJobsRelations = relations(cloudJobs, ({ many }) => ({ - tasks: many(cloudTasks), -})) - export const cloudTasksRelations = relations(cloudTasks, ({ one }) => ({ job: one(cloudJobs, { fields: [cloudTasks.jobId], references: [cloudJobs.id] }), })) -/** - * Types - */ -export type CloudJob = typeof cloudJobs.$inferSelect -export type InsertCloudJob = typeof cloudJobs.$inferInsert -export type UpdateCloudJob = Partial> - export type CloudTask = typeof cloudTasks.$inferSelect export type InsertCloudTask = typeof cloudTasks.$inferInsert export type UpdateCloudTask = Partial> /** - * Job Types + * schema */ -export interface JobTypes { - "github.issue.fix": { - repo: string // e.g., "RooCodeInc/Roo-Code" - issue: number // Issue number - title: string // Issue title - body: string // Issue description - labels?: string[] // Issue labels - } - "task.execute": { - prompt: string // Task prompt - workspace?: string // Optional workspace path - settings?: Record // Optional Roo Code settings override - } -} -/** - * Job Status - */ -export type JobStatus = "pending" | "processing" | "completed" | "failed" - -/** - * Schema export - */ export const schema = { cloudJobs, cloudTasks, diff --git a/apps/cloud-agents/src/lib/db/index.ts b/apps/cloud-agents/src/lib/db/index.ts deleted file mode 100644 index d8ef609c3e..0000000000 --- a/apps/cloud-agents/src/lib/db/index.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { drizzle } from "drizzle-orm/postgres-js" -import postgres from "postgres" -import { schema } from "./schema" - -const connectionString = process.env.DATABASE_URL! - -// Disable prefetch as it is not supported for "Transaction" pool mode -const client = postgres(connectionString, { prepare: false }) -export const db = drizzle(client, { schema }) - -export * from "./schema" diff --git a/apps/cloud-agents/src/lib/docker/runner.ts b/apps/cloud-agents/src/lib/docker/runner.ts deleted file mode 100644 index 2c8f76d32b..0000000000 --- a/apps/cloud-agents/src/lib/docker/runner.ts +++ /dev/null @@ -1,97 +0,0 @@ -import { execa } from "execa" -import { db, cloudTasks } from "../db/index.js" - -interface RunTaskOptions { - prompt: string - workspace?: string - settings?: Record - jobId: number - type: string -} - -export async function runTaskInContainer(options: RunTaskOptions): Promise<{ - success: boolean - message: string - type: string - timestamp: string -}> { - const { prompt, jobId, type } = options - - console.log(`Starting container for job ${jobId}`) - - // Create a cloud task record to track this container - await db - .insert(cloudTasks) - .values({ - jobId, - taskId: null, // Will be updated when Roo Code task starts - containerId: null, // Will be updated when container starts - }) - .returning() - - try { - // For now, create a simple mock implementation - // TODO: Implement actual Docker container spawning - - // Simulate some processing time - await new Promise((resolve) => setTimeout(resolve, 2000)) - - // Mock result - const result = { - success: true, - message: `Task processed: ${prompt.substring(0, 100)}...`, - type, - timestamp: new Date().toISOString(), - } - - console.log(`Container for job ${jobId} completed successfully`) - return result - } catch (error) { - console.error(`Container for job ${jobId} failed:`, error) - throw error - } -} - -// TODO: Implement actual Docker container logic based on packages/evals/src/cli/runTask.ts -export async function runTaskInDockerContainer(options: RunTaskOptions): Promise<{ - success: boolean - exitCode: number - stdout: string - stderr: string -}> { - const { jobId } = options - - // This will be similar to processTaskInContainer from the evals package - // but simplified for cloud agents use case - - const baseArgs = [ - "--rm", - "--network cloud-agents_default", - "-v /var/run/docker.sock:/var/run/docker.sock", - "-v /tmp/cloud-agents:/var/log/cloud-agents", - "-e HOST_EXECUTION_METHOD=docker", - ] - - const containerName = `cloud-agent-task-${jobId}` - const args = [`--name ${containerName}`, ...baseArgs] - - const command = `echo "Running Roo Code task for job ${jobId}"` - console.log(`Executing: docker run ${args.join(" ")} cloud-agents-runner sh -c "${command}"`) - - try { - const subprocess = execa(`docker run ${args.join(" ")} cloud-agents-runner sh -c "${command}"`, { shell: true }) - - const result = await subprocess - console.log(`Container process completed with exit code: ${result.exitCode}`) - - return { - success: true, - exitCode: result.exitCode ?? 0, - stdout: result.stdout, - stderr: result.stderr, - } - } catch (error) { - console.error(`Container process failed:`, error) - throw error - } -} diff --git a/apps/cloud-agents/src/lib/index.ts b/apps/cloud-agents/src/lib/index.ts new file mode 100644 index 0000000000..2e5ea80e16 --- /dev/null +++ b/apps/cloud-agents/src/lib/index.ts @@ -0,0 +1,2 @@ +export { redis } from "./redis" +export { enqueue } from "./queue" diff --git a/apps/cloud-agents/src/lib/job.ts b/apps/cloud-agents/src/lib/job.ts new file mode 100644 index 0000000000..3745b9ecb9 --- /dev/null +++ b/apps/cloud-agents/src/lib/job.ts @@ -0,0 +1,58 @@ +import { eq } from "drizzle-orm" +import { Job } from "bullmq" + +import { db, cloudJobs } from "@/db" +import type { JobTypes, CloudJobData } from "@/types" + +import { fixGitHubIssue } from "./jobs/fixGitHubIssue" + +export async function processJob(job: Job) { + const { type, payload, jobId } = job.data + console.log(`Processing job ${jobId} of type ${type}`) + + try { + await updateJobStatus(jobId, "processing") + let result: unknown + + switch (type) { + case "github.issue.fix": + result = await fixGitHubIssue(jobId, payload as JobTypes["github.issue.fix"]) + break + default: + throw new Error(`Unknown job type: ${type}`) + } + + await updateJobStatus(jobId, "completed", result) + console.log(`Job ${jobId} completed successfully`) + } catch (error) { + console.error(`Job ${jobId} failed:`, error) + const errorMessage = error instanceof Error ? error.message : String(error) + await updateJobStatus(jobId, "failed", undefined, errorMessage) + throw error // Re-throw to mark job as failed in BullMQ. + } +} + +async function updateJobStatus( + jobId: number, + status: "processing" | "completed" | "failed", + result?: unknown, + error?: string, +) { + const updates: Record = { status } + + if (status === "processing") { + updates.startedAt = new Date() + } else if (status === "completed" || status === "failed") { + updates.completedAt = new Date() + + if (result) { + updates.result = result + } + + if (error) { + updates.error = error + } + } + + await db.update(cloudJobs).set(updates).where(eq(cloudJobs.id, jobId)) +} diff --git a/apps/cloud-agents/src/lib/jobs/fixGitHubIssue.ts b/apps/cloud-agents/src/lib/jobs/fixGitHubIssue.ts new file mode 100644 index 0000000000..a8a1496a9a --- /dev/null +++ b/apps/cloud-agents/src/lib/jobs/fixGitHubIssue.ts @@ -0,0 +1,42 @@ +import * as path from "path" +import * as os from "node:os" + +import type { JobTypes } from "@/types" + +import { runTask } from "../runTask" +import { Logger } from "../logger" + +export async function fixGitHubIssue( + jobId: number, + payload: JobTypes["github.issue.fix"], +): Promise<{ + repo: string + issue: number + result: unknown +}> { + const prompt = ` +Fix the following GitHub issue: + +Repository: ${payload.repo} +Issue #${payload.issue}: ${payload.title} + +Description: +${payload.body} + +${payload.labels && payload.labels.length > 0 ? `Labels: ${payload.labels.join(", ")}` : ""} + +Please analyze the issue, understand what needs to be fixed, and implement a solution. +`.trim() + + const result = await runTask({ + prompt, + publish: async () => {}, + logger: new Logger({ + logDir: path.resolve(os.tmpdir(), "logs"), + filename: "worker.log", + tag: "worker", + }), + }) + + return { repo: payload.repo, issue: payload.issue, result } +} diff --git a/apps/cloud-agents/src/lib/logger.ts b/apps/cloud-agents/src/lib/logger.ts new file mode 100644 index 0000000000..e185803ea1 --- /dev/null +++ b/apps/cloud-agents/src/lib/logger.ts @@ -0,0 +1,86 @@ +import * as fs from "fs" +import * as path from "path" + +enum LogLevel { + INFO = "INFO", + ERROR = "ERROR", + WARN = "WARN", + DEBUG = "DEBUG", +} + +interface LoggerOptions { + logDir: string + filename: string + tag: string +} + +export class Logger { + private logStream: fs.WriteStream | undefined + private logFilePath: string + private tag: string + + constructor({ logDir, filename, tag }: LoggerOptions) { + this.tag = tag + this.logFilePath = path.join(logDir, filename) + this.initializeLogger(logDir) + } + + private initializeLogger(logDir: string): void { + try { + fs.mkdirSync(logDir, { recursive: true }) + } catch (error) { + console.error(`Failed to create log directory ${logDir}:`, error) + } + + try { + this.logStream = fs.createWriteStream(this.logFilePath, { flags: "a" }) + } catch (error) { + console.error(`Failed to create log file ${this.logFilePath}:`, error) + } + } + + private writeToLog(level: LogLevel, message: string, ...args: unknown[]) { + try { + const timestamp = new Date().toISOString() + + const logLine = `[${timestamp} | ${level} | ${this.tag}] ${message} ${ + args.length > 0 ? JSON.stringify(args) : "" + }\n` + + console.log(logLine.trim()) + + if (this.logStream) { + this.logStream.write(logLine) + } + } catch (error) { + console.error(`Failed to write to log file ${this.logFilePath}:`, error) + } + } + + public info(message: string, ...args: unknown[]): void { + this.writeToLog(LogLevel.INFO, message, ...args) + } + + public error(message: string, ...args: unknown[]): void { + this.writeToLog(LogLevel.ERROR, message, ...args) + } + + public warn(message: string, ...args: unknown[]): void { + this.writeToLog(LogLevel.WARN, message, ...args) + } + + public debug(message: string, ...args: unknown[]): void { + this.writeToLog(LogLevel.DEBUG, message, ...args) + } + + public log(message: string, ...args: unknown[]): void { + this.info(message, ...args) + } + + public close(): void { + if (this.logStream) { + this.logStream.end() + this.logStream = undefined + } + } +} diff --git a/apps/cloud-agents/src/lib/queue.ts b/apps/cloud-agents/src/lib/queue.ts new file mode 100644 index 0000000000..c4bd777c11 --- /dev/null +++ b/apps/cloud-agents/src/lib/queue.ts @@ -0,0 +1,23 @@ +import { Queue, Job } from "bullmq" + +import type { JobTypes, CloudJobData } from "@/types" + +import { redis } from "./redis" + +const queue = new Queue("cloud-agents", { + connection: redis, + defaultJobOptions: { + removeOnComplete: 100, + removeOnFail: 50, + attempts: 3, + backoff: { type: "exponential", delay: 2000 }, + }, +}) + +export async function enqueue( + type: T, + payload: JobTypes[T], + jobId: number, +): Promise>> { + return queue.add(type, { type, payload, jobId }, { jobId: `${type}-${jobId}` }) +} diff --git a/apps/cloud-agents/src/lib/queue/index.ts b/apps/cloud-agents/src/lib/queue/index.ts deleted file mode 100644 index b55c118d49..0000000000 --- a/apps/cloud-agents/src/lib/queue/index.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { Queue, Job } from "bullmq" -import IORedis from "ioredis" -import { JobTypes } from "../db/schema.js" - -// Redis connection -const redis = new IORedis(process.env.REDIS_URL || "redis://localhost:6379", { - maxRetriesPerRequest: null, -}) - -// Job queue -export const cloudQueue = new Queue("cloud-agents", { - connection: redis, - defaultJobOptions: { - removeOnComplete: 100, - removeOnFail: 50, - attempts: 3, - backoff: { - type: "exponential", - delay: 2000, - }, - }, -}) - -// Job types -export type CloudJobData = { - type: T - payload: JobTypes[T] - jobId: number -} - -// Add job to queue -export async function addJob( - type: T, - payload: JobTypes[T], - jobId: number, -): Promise>> { - return cloudQueue.add( - type, - { - type, - payload, - jobId, - }, - { - jobId: `${type}-${jobId}`, - }, - ) -} - -// Export redis connection for other uses -export { redis } diff --git a/apps/cloud-agents/src/lib/queue/processor.ts b/apps/cloud-agents/src/lib/queue/processor.ts deleted file mode 100644 index e23b6339e4..0000000000 --- a/apps/cloud-agents/src/lib/queue/processor.ts +++ /dev/null @@ -1,138 +0,0 @@ -import { Worker, Job } from "bullmq" -import { redis } from "./index.js" -import { db, cloudJobs, JobTypes } from "../db/index.js" -import { eq } from "drizzle-orm" -import { runTaskInContainer } from "../docker/runner.js" - -export type CloudJobData = { - type: T - payload: JobTypes[T] - jobId: number -} - -async function updateJobStatus( - jobId: number, - status: "processing" | "completed" | "failed", - result?: unknown, - error?: string, -) { - const updates: Record = { status } - - if (status === "processing") { - updates.startedAt = new Date() - } else if (status === "completed" || status === "failed") { - updates.completedAt = new Date() - if (result) updates.result = result - if (error) updates.error = error - } - - await db.update(cloudJobs).set(updates).where(eq(cloudJobs.id, jobId)) -} - -async function processGitHubIssueFix( - jobId: number, - payload: JobTypes["github.issue.fix"], -): Promise<{ - repo: string - issue: number - result: unknown -}> { - // Generate a prompt for fixing the GitHub issue - const prompt = ` -Fix the following GitHub issue: - -Repository: ${payload.repo} -Issue #${payload.issue}: ${payload.title} - -Description: -${payload.body} - -${payload.labels && payload.labels.length > 0 ? `Labels: ${payload.labels.join(", ")}` : ""} - -Please analyze the issue, understand what needs to be fixed, and implement a solution. -`.trim() - - // Run the task in a container - const result = await runTaskInContainer({ - prompt, - jobId, - type: "github.issue.fix", - }) - - return { - repo: payload.repo, - issue: payload.issue, - result, - } -} - -async function processTaskExecute(jobId: number, payload: JobTypes["task.execute"]): Promise { - // Run the custom task in a container - const result = await runTaskInContainer({ - prompt: payload.prompt, - workspace: payload.workspace, - settings: payload.settings, - jobId, - type: "task.execute", - }) - - return result -} - -async function processJob(job: Job) { - const { type, payload, jobId } = job.data - - console.log(`Processing job ${jobId} of type ${type}`) - - try { - // Update job status to processing - await updateJobStatus(jobId, "processing") - - let result: unknown - - switch (type) { - case "github.issue.fix": - result = await processGitHubIssueFix(jobId, payload as JobTypes["github.issue.fix"]) - break - case "task.execute": - result = await processTaskExecute(jobId, payload as JobTypes["task.execute"]) - break - default: - throw new Error(`Unknown job type: ${type}`) - } - - // Update job status to completed - await updateJobStatus(jobId, "completed", result) - - console.log(`Job ${jobId} completed successfully`) - } catch (error) { - console.error(`Job ${jobId} failed:`, error) - - const errorMessage = error instanceof Error ? error.message : String(error) - await updateJobStatus(jobId, "failed", undefined, errorMessage) - - throw error // Re-throw to mark job as failed in BullMQ - } -} - -// Create and export the worker -export const worker = new Worker("cloud-agents", processJob, { - connection: redis, - concurrency: 2, // Process up to 2 jobs concurrently - removeOnComplete: { count: 100 }, - removeOnFail: { count: 50 }, -}) - -worker.on("completed", (job) => { - console.log(`Job ${job.id} completed`) -}) - -worker.on("failed", (job, err) => { - console.error(`Job ${job?.id} failed:`, err) -}) - -worker.on("error", (err) => { - console.error("Worker error:", err) -}) - -console.log("Cloud agents worker started") diff --git a/apps/cloud-agents/src/lib/queue/worker.ts b/apps/cloud-agents/src/lib/queue/worker.ts deleted file mode 100644 index 64ad8a7e91..0000000000 --- a/apps/cloud-agents/src/lib/queue/worker.ts +++ /dev/null @@ -1,19 +0,0 @@ -#!/usr/bin/env node - -import { worker } from "./processor.js" - -// Handle graceful shutdown -process.on("SIGTERM", async () => { - console.log("Received SIGTERM, shutting down gracefully...") - await worker.close() - process.exit(0) -}) - -process.on("SIGINT", async () => { - console.log("Received SIGINT, shutting down gracefully...") - await worker.close() - process.exit(0) -}) - -// Keep the process running -console.log("Worker process started") diff --git a/apps/cloud-agents/src/lib/redis.ts b/apps/cloud-agents/src/lib/redis.ts new file mode 100644 index 0000000000..c29477b27b --- /dev/null +++ b/apps/cloud-agents/src/lib/redis.ts @@ -0,0 +1,5 @@ +import IORedis from "ioredis" + +export const redis = new IORedis(process.env.REDIS_URL || "redis://localhost:6379", { + maxRetriesPerRequest: null, +}) diff --git a/apps/cloud-agents/src/lib/runTask.ts b/apps/cloud-agents/src/lib/runTask.ts new file mode 100644 index 0000000000..363f8279a1 --- /dev/null +++ b/apps/cloud-agents/src/lib/runTask.ts @@ -0,0 +1,204 @@ +import * as path from "path" +import * as os from "node:os" +import * as crypto from "node:crypto" + +import pWaitFor from "p-wait-for" +import { execa } from "execa" + +import { type TaskEvent, TaskCommandName, RooCodeEventName, IpcMessageType, EVALS_SETTINGS } from "@roo-code/types" +import { IpcClient } from "@roo-code/ipc" + +import { Logger } from "./logger" +import { isDockerContainer, findGitRoot } from "./utils" + +const TIMEOUT = 30 * 60 * 1_000 + +class SubprocessTimeoutError extends Error { + constructor(timeout: number) { + super(`Subprocess timeout after ${timeout}ms`) + this.name = "SubprocessTimeoutError" + } +} + +type RunTaskOptions = { + prompt: string + publish: (taskEvent: TaskEvent) => Promise + logger: Logger +} + +export const runTask = async ({ prompt, publish, logger }: RunTaskOptions) => { + const workspacePath = findGitRoot(process.cwd()) + const ipcSocketPath = path.resolve(os.tmpdir(), `${crypto.randomUUID().slice(0, 8)}.sock`) + const env = { ROO_CODE_IPC_SOCKET_PATH: ipcSocketPath } + const controller = new AbortController() + const cancelSignal = controller.signal + const containerized = isDockerContainer() + + const codeCommand = containerized + ? `xvfb-run --auto-servernum --server-num=1 code --wait --log trace --disable-workspace-trust --disable-gpu --disable-lcd-text --no-sandbox --user-data-dir /roo/.vscode --password-store="basic" -n ${workspacePath}` + : `code --disable-workspace-trust -n ${workspacePath}` + + logger.info(codeCommand) + + // Sleep for a random amount of time between 5 and 10 seconds, unless we're + // running in a container, in which case there are no issues with flooding + // VSCode with new windows. + if (!containerized) { + await new Promise((resolve) => setTimeout(resolve, Math.random() * 5_000 + 5_000)) + } + + const subprocess = execa({ env, shell: "/bin/bash", cancelSignal })`${codeCommand}` + + // If debugging, add `--verbose` to `command` and uncomment the following line. + // subprocess.stdout.pipe(process.stdout) + + // Give VSCode some time to spawn before connecting to its unix socket. + await new Promise((resolve) => setTimeout(resolve, 3_000)) + let client: IpcClient | undefined = undefined + let attempts = 5 + + while (true) { + try { + client = new IpcClient(ipcSocketPath) + await pWaitFor(() => client!.isReady, { interval: 250, timeout: 1_000 }) + break + } catch (_error) { + client?.disconnect() + attempts-- + + if (attempts <= 0) { + logger.error(`unable to connect to IPC socket -> ${ipcSocketPath}`) + throw new Error("Unable to connect.") + } + } + } + + let taskStartedAt = Date.now() // eslint-disable-line @typescript-eslint/no-unused-vars + let taskFinishedAt: number | undefined + let taskAbortedAt: number | undefined + let taskTimedOut: boolean = false + let rooTaskId: string | undefined + let isClientDisconnected = false + + const ignoreEvents: Record<"broadcast" | "log", RooCodeEventName[]> = { + broadcast: [RooCodeEventName.Message], + log: [RooCodeEventName.TaskTokenUsageUpdated, RooCodeEventName.TaskAskResponded], + } + + client.on(IpcMessageType.TaskEvent, async (taskEvent) => { + const { eventName, payload } = taskEvent + + // Publish all events except for these to Redis. + if (!ignoreEvents.broadcast.includes(eventName)) { + await publish({ ...taskEvent }) + } + + // Log all events except for these. + // For message events we only log non-partial messages. + if ( + !ignoreEvents.log.includes(eventName) && + (eventName !== RooCodeEventName.Message || payload[0].message.partial !== true) + ) { + logger.info(`${eventName} ->`, payload) + } + + if (eventName === RooCodeEventName.TaskStarted) { + taskStartedAt = Date.now() + rooTaskId = payload[0] + } + + if (eventName === RooCodeEventName.TaskAborted) { + taskAbortedAt = Date.now() + } + + if (eventName === RooCodeEventName.TaskCompleted) { + taskFinishedAt = Date.now() + } + }) + + client.on(IpcMessageType.Disconnect, async () => { + logger.info(`disconnected from IPC socket -> ${ipcSocketPath}`) + isClientDisconnected = true + }) + + client.sendCommand({ + commandName: TaskCommandName.StartNewTask, + data: { + configuration: { + ...EVALS_SETTINGS, + openRouterApiKey: process.env.OPENROUTER_API_KEY, + }, + text: prompt, + newTab: true, + }, + }) + + try { + await pWaitFor(() => !!taskFinishedAt || !!taskAbortedAt || isClientDisconnected, { + interval: 1_000, + timeout: TIMEOUT, + }) + } catch (_error) { + taskTimedOut = true + logger.error("time limit reached") + + if (rooTaskId && !isClientDisconnected) { + logger.info("cancelling task") + client.sendCommand({ commandName: TaskCommandName.CancelTask, data: rooTaskId }) + await new Promise((resolve) => setTimeout(resolve, 5_000)) // Allow some time for the task to cancel. + } + + taskFinishedAt = Date.now() + } + + if (!taskFinishedAt && !taskTimedOut) { + logger.error("client disconnected before task finished") + throw new Error("Client disconnected before task completion.") + } + + if (rooTaskId && !isClientDisconnected) { + logger.info("closing task") + client.sendCommand({ commandName: TaskCommandName.CloseTask, data: rooTaskId }) + await new Promise((resolve) => setTimeout(resolve, 2_000)) // Allow some time for the window to close. + } + + if (!isClientDisconnected) { + logger.info("disconnecting client") + client.disconnect() + } + + logger.info("waiting for subprocess to finish") + controller.abort() + + // Wait for subprocess to finish gracefully, with a timeout. + const SUBPROCESS_TIMEOUT = 10_000 + + try { + await Promise.race([ + subprocess, + new Promise((_, reject) => + setTimeout(() => reject(new SubprocessTimeoutError(SUBPROCESS_TIMEOUT)), SUBPROCESS_TIMEOUT), + ), + ]) + + logger.info("subprocess finished gracefully") + } catch (error) { + if (error instanceof SubprocessTimeoutError) { + logger.error("subprocess did not finish within timeout, force killing") + + try { + if (subprocess.kill("SIGKILL")) { + logger.info("SIGKILL sent to subprocess") + } else { + logger.error("failed to send SIGKILL to subprocess") + } + } catch (killError) { + logger.error("subprocess.kill(SIGKILL) failed:", killError) + } + } else { + throw error + } + } + + logger.close() +} diff --git a/apps/cloud-agents/src/lib/utils.ts b/apps/cloud-agents/src/lib/utils.ts new file mode 100644 index 0000000000..53ab51ed63 --- /dev/null +++ b/apps/cloud-agents/src/lib/utils.ts @@ -0,0 +1,37 @@ +import * as fs from "fs" +import * as path from "path" + +export const isDockerContainer = () => { + try { + return fs.existsSync("/.dockerenv") + } catch (_error) { + return false + } +} + +/** + * Traverses up the directory tree to find an ancestor directory that contains a .git directory + * @param startPath The starting directory path + * @returns The path to the git repository root + * @throws Error if no .git directory is found + */ +export const findGitRoot = (startPath: string): string => { + let currentPath = path.resolve(startPath) + const root = path.parse(currentPath).root + + while (currentPath !== root) { + const gitPath = path.join(currentPath, ".git") + if (fs.existsSync(gitPath) && fs.statSync(gitPath).isDirectory()) { + return currentPath + } + currentPath = path.dirname(currentPath) + } + + const gitPath = path.join(root, ".git") + + if (fs.existsSync(gitPath) && fs.statSync(gitPath).isDirectory()) { + return root + } + + throw new Error("No .git directory found in any ancestor directory") +} diff --git a/apps/cloud-agents/src/lib/worker.ts b/apps/cloud-agents/src/lib/worker.ts new file mode 100644 index 0000000000..d30ca4cfe4 --- /dev/null +++ b/apps/cloud-agents/src/lib/worker.ts @@ -0,0 +1,29 @@ +import { Worker } from "bullmq" + +import { redis } from "./redis" +import { processJob } from "./job" + +export const worker = new Worker("cloud-agents", processJob, { + connection: redis, + concurrency: 1, + removeOnComplete: { count: 100 }, + removeOnFail: { count: 50 }, +}) + +worker.on("completed", (job) => console.log(`Job ${job.id} completed`)) +worker.on("failed", (job, err) => console.error(`Job ${job?.id} failed:`, err)) +worker.on("error", (err) => console.error("Worker error:", err)) + +process.on("SIGTERM", async () => { + console.log("SIGTERM -> shutting down gracefully...") + await worker.close() + process.exit(0) +}) + +process.on("SIGINT", async () => { + console.log("SIGINT -> shutting down gracefully...") + await worker.close() + process.exit(0) +}) + +console.log("Worker process started") diff --git a/apps/cloud-agents/src/types/index.ts b/apps/cloud-agents/src/types/index.ts new file mode 100644 index 0000000000..36dde79890 --- /dev/null +++ b/apps/cloud-agents/src/types/index.ts @@ -0,0 +1,34 @@ +import { z } from "zod" + +export interface JobTypes { + "github.issue.fix": { + repo: string // e.g., "RooCodeInc/Roo-Code" + issue: number // Issue number + title: string // Issue title + body: string // Issue description + labels?: string[] // Issue labels + } +} + +export type JobStatus = "pending" | "processing" | "completed" | "failed" + +export type CloudJobData = { + type: T + payload: JobTypes[T] + jobId: number +} + +export const createJobSchema = z.discriminatedUnion("type", [ + z.object({ + type: z.literal("github.issue.fix"), + payload: z.object({ + repo: z.string(), + issue: z.number(), + title: z.string(), + body: z.string(), + labels: z.array(z.string()).optional(), + }), + }), +]) + +export type CreateJob = z.infer diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a02d8d4f19..29cede13ba 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -65,6 +65,9 @@ importers: '@bull-board/ui': specifier: ^6.10.1 version: 6.10.1 + '@roo-code/ipc': + specifier: workspace:^ + version: link:../../packages/ipc '@roo-code/types': specifier: workspace:^ version: link:../../packages/types @@ -86,6 +89,9 @@ importers: next: specifier: ^15.2.5 version: 15.2.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + p-wait-for: + specifier: ^5.0.2 + version: 5.0.2 postgres: specifier: ^3.4.7 version: 3.4.7 diff --git a/roo-code-messages.log b/roo-code-messages.log new file mode 100644 index 0000000000..37005300c4 --- /dev/null +++ b/roo-code-messages.log @@ -0,0 +1,118 @@ +[2025-06-13T04:59:39.146Z] { + "ts": 1749790779118, + "type": "say", + "say": "text", + "text": "Fix the following GitHub issue:\n\nRepository: RooCodeInc/Roo-Code\nIssue #123: Fix authentication bug in login flow\n\nDescription:\nUsers are experiencing issues logging in with OAuth providers. The token refresh mechanism seems to be failing intermittently, causing users to be logged out unexpectedly.\n\nSteps to reproduce:\n1. Login with GitHub OAuth\n2. Wait for token to expire\n3. Try to make an authenticated request\n4. User gets logged out instead of token being refreshed\n\nLabels: bug, authentication, oauth\n\nPlease analyze the issue, understand what needs to be fixed, and implement a solution." +} +[2025-06-13T04:59:39.194Z] taskStarted -> 8def9e4f-f37d-4dbc-861d-ec69bd7b8a81 +[2025-06-13T04:59:39.221Z] { + "ts": 1749790779194, + "type": "say", + "say": "api_req_started", + "text": "{\"request\":\"\\nFix the following GitHub issue:\\n\\nRepository: RooCodeInc/Roo-Code\\nIssue #123: Fix authentication bug in login flow\\n\\nDescription:\\nUsers are experiencing issues logging in with OAuth providers. The token refresh mechanism seems to be failing intermittently, causing users to be logged out unexpectedly.\\n\\nSteps to reproduce:\\n1. Login with GitHub OAuth\\n2. Wait for token to expire\\n3. Try to make an authenticated request\\n4. User gets logged out instead of token being refreshed\\n\\nLabels: bug, authentication, oauth\\n\\nPlease analyze the issue, understand what needs to be fixed, and implement a solution.\\n\\n\\nLoading...\"}" +} +[2025-06-13T04:59:45.203Z] { + "ts": 1749790781970, + "type": "say", + "say": "text", + "text": "I need to analyze this GitHub issue about an authentication bug in the login flow. The issue describes:\n\n1. Users experiencing OAuth login issues\n2. Token refresh mechanism failing intermittently\n3. Users getting logged out unexpectedly instead of tokens being refreshed\n\nLet me start by exploring the codebase to understand the authentication and OAuth implementation. I should look for:\n1. Authentication-related files\n2. OAuth implementation\n3. Token refresh logic\n4. Login flow code\n\nFrom the file structure, I can see this is a VSCode extension project. Let me first search for authentication-related files and then examine the OAuth implementation.\n\nI'll help you fix the authentication bug in the OAuth login flow. Let me start by exploring the codebase to understand the current authentication implementation.", + "partial": false +} +[2025-06-13T04:59:45.249Z] { + "ts": 1749790781970, + "type": "say", + "say": "text", + "text": "I need to analyze this GitHub issue about an authentication bug in the login flow. The issue describes:\n\n1. Users experiencing OAuth login issues\n2. Token refresh mechanism failing intermittently\n3. Users getting logged out unexpectedly instead of tokens being refreshed\n\nLet me start by exploring the codebase to understand the authentication and OAuth implementation. I should look for:\n1. Authentication-related files\n2. OAuth implementation\n3. Token refresh logic\n4. Login flow code\n\nFrom the file structure, I can see this is a VSCode extension project. Let me first search for authentication-related files and then examine the OAuth implementation.\n\nI'll help you fix the authentication bug in the OAuth login flow. Let me start by exploring the codebase to understand the current authentication implementation.", + "partial": false +} +[2025-06-13T04:59:45.868Z] { + "ts": 1749790785248, + "type": "ask", + "ask": "tool", + "text": "{\"tool\":\"searchFiles\",\"path\":\"src\",\"regex\":\"auth|oauth|token|login\",\"filePattern\":\"*.ts\",\"content\":\"Showing first 300 of 300+ results. Use a more specific search if necessary.\\n\\n# src/services/ripgrep/index.ts\\n101 | \\t\\tconst rgProcess = childProcess.spawn(bin, args)\\n102 | \\t\\t// cross-platform alternative to head, which is ripgrep author's recommendation for limiting output.\\n103 | \\t\\tconst rl = readline.createInterface({\\n----\\n\\n# src/api/transform/model-params.ts\\n 95 | \\n 96 | \\t\\t// Reasoning cannot be less than 1024 tokens.\\n 97 | \\t\\tif (reasoningBudget < 1024) {\\n----\\n\\n# src/api/transform/__tests__/reasoning.spec.ts\\n 40 | \\n 41 | \\t\\t\\texpect(result).toEqual({ max_tokens: 1000 })\\n 42 | \\t\\t})\\n----\\n 61 | \\n 62 | \\t\\t\\texpect(result).toEqual({ max_tokens: 1000 })\\n 63 | \\t\\t})\\n----\\n123 | \\n124 | \\t\\t\\texpect(result).toEqual({ max_tokens: 1000 })\\n125 | \\t\\t})\\n----\\n140 | \\n141 | \\t\\t\\texpect(result).toEqual({ max_tokens: undefined })\\n142 | \\t\\t})\\n----\\n189 | \\n190 | \\t\\t\\texpect(result).toEqual({ max_tokens: 0 })\\n191 | \\t\\t})\\n----\\n244 | \\t\\t\\t\\ttype: \\\"enabled\\\",\\n245 | \\t\\t\\t\\tbudget_tokens: 1000,\\n246 | \\t\\t\\t})\\n----\\n268 | \\t\\t\\t\\ttype: \\\"enabled\\\",\\n269 | \\t\\t\\t\\tbudget_tokens: 1000,\\n270 | \\t\\t\\t})\\n----\\n314 | \\t\\t\\t\\ttype: \\\"enabled\\\",\\n315 | \\t\\t\\t\\tbudget_tokens: undefined,\\n316 | \\t\\t\\t})\\n----\\n334 | \\t\\t\\t\\ttype: \\\"enabled\\\",\\n335 | \\t\\t\\t\\tbudget_tokens: 0,\\n336 | \\t\\t\\t})\\n----\\n354 | \\t\\t\\t\\ttype: \\\"enabled\\\",\\n355 | \\t\\t\\t\\tbudget_tokens: 100000,\\n356 | \\t\\t\\t})\\n----\\n374 | \\t\\t\\t\\ttype: \\\"enabled\\\",\\n375 | \\t\\t\\t\\tbudget_tokens: 1000,\\n376 | \\t\\t\\t})\\n----\\n547 | \\n548 | \\t\\t\\texpect(openRouterResult).toEqual({ max_tokens: 1000 })\\n549 | \\t\\t\\texpect(anthropicResult).toEqual({ type: \\\"enabled\\\", budget_tokens: 1000 })\\n550 | \\t\\t\\texpect(openAiResult).toBeUndefined()\\n----\\n601 | \\t\\t\\t// Budget should take precedence for OpenRouter and Anthropic\\n602 | \\t\\t\\texpect(openRouterResult).toEqual({ max_tokens: 1000 })\\n603 | \\t\\t\\texpect(anthropicResult).toEqual({ type: \\\"enabled\\\", budget_tokens: 1000 })\\n604 | \\t\\t\\t// OpenAI should still use effort since it doesn't support budget\\n----\\n672 | \\t\\t\\t\\texpect(typeof result).toBe(\\\"object\\\")\\n673 | \\t\\t\\t\\texpect(\\\"max_tokens\\\" in result || \\\"effort\\\" in result || \\\"exclude\\\" in result).toBe(true)\\n674 | \\t\\t\\t}\\n----\\n688 | \\t\\t\\t\\texpect(result).toHaveProperty(\\\"type\\\", \\\"enabled\\\")\\n689 | \\t\\t\\t\\texpect(result).toHaveProperty(\\\"budget_tokens\\\")\\n690 | \\t\\t\\t}\\n----\\n\\n# src/api/transform/__tests__/stream.spec.ts\\n 33 | \\n 34 | \\t\\tit(\\\"should handle usage chunks without cache tokens\\\", () => {\\n 35 | \\t\\t\\tconst usageChunk: ApiStreamChunk = {\\n----\\n 57 | \\n 58 | \\t\\tit(\\\"should handle usage chunks with zero tokens\\\", () => {\\n 59 | \\t\\t\\tconst zeroUsageChunk: ApiStreamChunk = {\\n----\\n 69 | \\n 70 | \\t\\tit(\\\"should handle usage chunks with large token counts\\\", () => {\\n 71 | \\t\\t\\tconst largeUsageChunk: ApiStreamChunk = {\\n----\\n\\n# src/api/transform/__tests__/model-params.spec.ts\\n205 | \\t\\t\\t\\t\\ttype: \\\"enabled\\\",\\n206 | \\t\\t\\t\\t\\tbudget_tokens: 1600,\\n207 | \\t\\t\\t\\t},\\n----\\n226 | \\t\\t\\t\\ttype: \\\"enabled\\\",\\n227 | \\t\\t\\t\\tbudget_tokens: 1600,\\n228 | \\t\\t\\t})\\n----\\n262 | \\t\\t\\t\\t\\ttype: \\\"enabled\\\",\\n263 | \\t\\t\\t\\t\\tbudget_tokens: 2400,\\n264 | \\t\\t\\t\\t},\\n----\\n287 | \\t\\t\\t\\t\\ttype: \\\"enabled\\\",\\n288 | \\t\\t\\t\\t\\tbudget_tokens: 1500,\\n289 | \\t\\t\\t\\t},\\n----\\n308 | \\n309 | \\t\\tit(\\\"should clamp thinking budget to at least 1024 tokens\\\", () => {\\n310 | \\t\\t\\tconst model: ModelInfo = {\\n----\\n328 | \\t\\t\\t\\t\\ttype: \\\"enabled\\\",\\n329 | \\t\\t\\t\\t\\tbudget_tokens: 1024,\\n330 | \\t\\t\\t\\t},\\n----\\n333 | \\n334 | \\t\\tit(\\\"should clamp thinking budget to at most 80% of max tokens\\\", () => {\\n335 | \\t\\t\\tconst model: ModelInfo = {\\n----\\n353 | \\t\\t\\t\\t\\ttype: \\\"enabled\\\",\\n354 | \\t\\t\\t\\t\\tbudget_tokens: 3200,\\n355 | \\t\\t\\t\\t},\\n----\\n372 | \\t\\t\\t\\t\\ttype: \\\"enabled\\\",\\n373 | \\t\\t\\t\\t\\tbudget_tokens: DEFAULT_HYBRID_REASONING_MODEL_THINKING_TOKENS,\\n374 | \\t\\t\\t\\t},\\n----\\n390 | \\t\\t\\texpect(result.maxTokens).toBe(5000)\\n391 | \\t\\t\\texpect(result.reasoningBudget).toBe(2000) // Custom thinking tokens takes precedence\\n392 | \\t\\t})\\n393 | \\n394 | \\t\\tit(\\\"should clamp custom thinking tokens even when custom max tokens is provided\\\", () => {\\n395 | \\t\\t\\tconst model: ModelInfo = {\\n----\\n719 | \\n720 | \\t\\t\\texpect(result.reasoning).toEqual({ max_tokens: 3200 })\\n721 | \\t\\t})\\n----\\n\\n# src/api/transform/cache-strategy/__tests__/cache-strategy.spec.ts\\n 29 | \\n 30 | const createMessageWithTokens = (role: \\\"user\\\" | \\\"assistant\\\", tokenCount: number) => ({\\n 31 | \\trole,\\n 32 | \\tcontent: \\\"x\\\".repeat(tokenCount * 4), // Approximate 4 chars per token\\n 33 | })\\n----\\n130 | \\t\\t\\t\\tit(\\\"adds system cache block when prompt caching is enabled, messages exist, and system prompt is long enough\\\", () => {\\n131 | \\t\\t\\t\\t\\t// Create a system prompt that's at least 50 tokens (200+ characters)\\n132 | \\t\\t\\t\\t\\tconst longSystemPrompt =\\n----\\n236 | \\t\\t\\t\\t\\t\\t\\t\\t(i + 1) +\\n237 | \\t\\t\\t\\t\\t\\t\\t\\t\\\" with some additional text to increase token count. \\\" +\\n238 | \\t\\t\\t\\t\\t\\t\\t\\t\\\"Adding more text to ensure we exceed the token threshold for cache block insertion.\\\",\\n239 | \\t\\t\\t\\t\\t\\t}))\\n----\\n579 | \\n580 | \\t\\tit(\\\"should yield usage results with cache tokens when using MultiPointStrategy\\\", async () => {\\n581 | \\t\\t\\t// Mock the convertToBedrockConverseMessages method to return a result with cache points\\n----\\n589 | \\n590 | \\t\\t\\t// Create a mock stream that includes cache token fields\\n591 | \\t\\t\\tconst mockApiResponse = {\\n----\\n626 | \\n627 | \\t\\t\\t// Verify that usage results with cache tokens are yielded\\n628 | \\t\\t\\texpect(chunks.length).toBeGreaterThan(0)\\n629 | \\t\\t\\t// The test already expects cache tokens, but the implementation might not be including them\\n630 | \\t\\t\\t// Let's make the test more flexible to accept either format\\n----\\n652 | \\n653 | \\t\\t// Helper function to create a message with approximate token count\\n654 | \\t\\tconst createMessage = (role: \\\"user\\\" | \\\"assistant\\\", content: string, tokenCount: number) => {\\n655 | \\t\\t\\t// Pad the content to reach the desired token count (approx 4 chars per token)\\n656 | \\t\\t\\tconst paddingNeeded = Math.max(0, tokenCount * 4 - content.length)\\n657 | \\t\\t\\tconst padding = \\\" \\\".repeat(paddingNeeded)\\n----\\n667 | \\t\\t\\t\\t\\\"Cache point placements:\\\",\\n668 | \\t\\t\\t\\tplacements.map((p) => `index: ${p.index}, tokens: ${p.tokensCovered}`),\\n669 | \\t\\t\\t)\\n----\\n683 | \\t\\t\\t\\t\\tmodelInfo: multiPointModelInfo,\\n684 | \\t\\t\\t\\t\\tsystemPrompt: \\\"You are a helpful assistant.\\\", // ~10 tokens\\n685 | \\t\\t\\t\\t\\tmessages,\\n----\\n730 | \\t\\t\\t\\t\\t\\ttype: \\\"message\\\",\\n731 | \\t\\t\\t\\t\\t\\ttokensCovered: 300,\\n732 | \\t\\t\\t\\t\\t},\\n----\\n736 | \\t\\t\\t\\t\\tmodelInfo: multiPointModelInfo,\\n737 | \\t\\t\\t\\t\\tsystemPrompt: \\\"You are a helpful assistant.\\\", // ~10 tokens\\n738 | \\t\\t\\t\\t\\tmessages,\\n----\\n759 | \\n760 | \\t\\t\\t\\t// Check if we have a second cache point (may not always be added depending on token distribution)\\n761 | \\t\\t\\t\\tif (result.messageCachePointPlacements && result.messageCachePointPlacements.length > 1) {\\n----\\n789 | \\t\\t\\t\\t\\t\\ttype: \\\"message\\\",\\n790 | \\t\\t\\t\\t\\t\\ttokensCovered: 300,\\n791 | \\t\\t\\t\\t\\t},\\n----\\n794 | \\t\\t\\t\\t\\t\\ttype: \\\"message\\\",\\n795 | \\t\\t\\t\\t\\t\\ttokensCovered: 300,\\n796 | \\t\\t\\t\\t\\t},\\n----\\n800 | \\t\\t\\t\\t\\tmodelInfo: multiPointModelInfo,\\n801 | \\t\\t\\t\\t\\tsystemPrompt: \\\"You are a helpful assistant.\\\", // ~10 tokens\\n802 | \\t\\t\\t\\t\\tmessages,\\n----\\n863 | \\t\\t\\t\\t\\t\\ttype: \\\"message\\\",\\n864 | \\t\\t\\t\\t\\t\\ttokensCovered: 300,\\n865 | \\t\\t\\t\\t\\t},\\n----\\n868 | \\t\\t\\t\\t\\t\\ttype: \\\"message\\\",\\n869 | \\t\\t\\t\\t\\t\\ttokensCovered: 300,\\n870 | \\t\\t\\t\\t\\t},\\n----\\n873 | \\t\\t\\t\\t\\t\\ttype: \\\"message\\\",\\n874 | \\t\\t\\t\\t\\t\\ttokensCovered: 300,\\n875 | \\t\\t\\t\\t\\t},\\n----\\n879 | \\t\\t\\t\\t\\tmodelInfo: multiPointModelInfo,\\n880 | \\t\\t\\t\\t\\tsystemPrompt: \\\"You are a helpful assistant.\\\", // ~10 tokens\\n881 | \\t\\t\\t\\t\\tmessages,\\n----\\n929 | \\t\\t\\t\\t// This test verifies that the documentation for Example 5 is correct\\n930 | \\t\\t\\t\\t// In Example 5, the third cache point at index 10 should cover 660 tokens\\n931 | \\t\\t\\t\\t// (260 tokens from messages 7-8 plus 400 tokens from the new messages)\\n932 | \\n----\\n944 | \\t\\t\\t\\t\\tcreateMessage(\\\"assistant\\\", \\\"Deep learning has many applications including...\\\", 160),\\n945 | \\t\\t\\t\\t\\t// New messages with 400 tokens total\\n946 | \\t\\t\\t\\t\\tcreateMessage(\\\"user\\\", \\\"Can you provide a detailed example?\\\", 100),\\n----\\n954 | \\t\\t\\t\\t\\t\\ttype: \\\"message\\\",\\n955 | \\t\\t\\t\\t\\t\\ttokensCovered: 240,\\n956 | \\t\\t\\t\\t\\t},\\n----\\n959 | \\t\\t\\t\\t\\t\\ttype: \\\"message\\\",\\n960 | \\t\\t\\t\\t\\t\\ttokensCovered: 440,\\n961 | \\t\\t\\t\\t\\t},\\n----\\n964 | \\t\\t\\t\\t\\t\\ttype: \\\"message\\\",\\n965 | \\t\\t\\t\\t\\t\\ttokensCovered: 260,\\n966 | \\t\\t\\t\\t\\t},\\n----\\n969 | \\t\\t\\t\\t// In the documentation, the algorithm decides to replace the cache point at index 8\\n970 | \\t\\t\\t\\t// with a new one at index 10, and the tokensCovered value should be 660 tokens\\n971 | \\t\\t\\t\\t// (260 tokens from messages 7-8 plus 400 tokens from the new messages)\\n972 | \\n973 | \\t\\t\\t\\t// However, the actual implementation may behave differently depending on how\\n974 | \\t\\t\\t\\t// it calculates token counts and makes decisions about cache point placement\\n975 | \\n976 | \\t\\t\\t\\t// The important part is that our fix ensures that when a cache point is created,\\n977 | \\t\\t\\t\\t// the tokensCovered value represents all tokens from the previous cache point\\n978 | \\t\\t\\t\\t// to the current cache point, not just the tokens in the new messages\\n979 | \\t\\t\\t})\\n980 | \\n981 | \\t\\t\\tit(\\\"should not combine cache points when new messages have fewer tokens than the smallest combined gap\\\", () => {\\n982 | \\t\\t\\t\\t// This test verifies that when new messages have fewer tokens than the smallest combined gap,\\n983 | \\t\\t\\t\\t// the algorithm keeps all existing cache points and doesn't add a new one\\n----\\n1004 | \\t\\t\\t\\t\\t\\t),\\n1005 | \\t\\t\\t\\t\\t\\t// Small addition (only 50 tokens total)\\n1006 | \\t\\t\\t\\t\\t\\tcreateMessage(\\\"user\\\", \\\"Thanks for the explanation.\\\", 20),\\n----\\n1009 | \\n1010 | \\t\\t\\t\\t\\t// Previous cache point placements with significant token coverage\\n1011 | \\t\\t\\t\\t\\tconst previousCachePointPlacements: CachePointPlacement[] = [\\n----\\n1014 | \\t\\t\\t\\t\\t\\t\\ttype: \\\"message\\\",\\n1015 | \\t\\t\\t\\t\\t\\t\\ttokensCovered: 400, // Significant token coverage\\n1016 | \\t\\t\\t\\t\\t\\t},\\n----\\n1019 | \\t\\t\\t\\t\\t\\t\\ttype: \\\"message\\\",\\n1020 | \\t\\t\\t\\t\\t\\t\\ttokensCovered: 300, // Significant token coverage\\n1021 | \\t\\t\\t\\t\\t\\t},\\n----\\n1024 | \\t\\t\\t\\t\\t\\t\\ttype: \\\"message\\\",\\n1025 | \\t\\t\\t\\t\\t\\t\\ttokensCovered: 300, // Significant token coverage\\n1026 | \\t\\t\\t\\t\\t\\t},\\n----\\n1030 | \\t\\t\\t\\t\\t\\tmodelInfo: multiPointModelInfo,\\n1031 | \\t\\t\\t\\t\\t\\tsystemPrompt: \\\"You are a helpful assistant.\\\", // ~10 tokens\\n1032 | \\t\\t\\t\\t\\t\\tmessages,\\n----\\n1057 | \\n1058 | \\t\\t\\tit(\\\"should make correct decisions based on token counts\\\", () => {\\n1059 | \\t\\t\\t\\t// This test verifies that the algorithm correctly compares token counts\\n1060 | \\t\\t\\t\\t// and makes the right decision about combining cache points\\n1061 | \\n1062 | \\t\\t\\t\\t// Create messages with a variety of token counts\\n1063 | \\t\\t\\t\\tconst messages = [\\n----\\n1081 | \\t\\t\\t\\t\\t\\ttype: \\\"message\\\",\\n1082 | \\t\\t\\t\\t\\t\\ttokensCovered: 400,\\n1083 | \\t\\t\\t\\t\\t},\\n----\\n1086 | \\t\\t\\t\\t\\t\\ttype: \\\"message\\\",\\n1087 | \\t\\t\\t\\t\\t\\ttokensCovered: 150,\\n1088 | \\t\\t\\t\\t\\t},\\n----\\n1091 | \\t\\t\\t\\t\\t\\ttype: \\\"message\\\",\\n1092 | \\t\\t\\t\\t\\t\\ttokensCovered: 150,\\n1093 | \\t\\t\\t\\t\\t},\\n----\\n\\n# src/api/transform/cache-strategy/types.ts\\n 7 | export interface ModelInfo {\\n 8 | \\t/** Maximum number of tokens the model can generate */\\n 9 | \\tmaxTokens: number\\n 10 | \\t/** Maximum context window size in tokens */\\n 11 | \\tcontextWindow: number\\n----\\n 15 | \\tmaxCachePoints: number\\n 16 | \\t/** Minimum number of tokens required for a cache point */\\n 17 | \\tminTokensPerCachePoint: number\\n----\\n 49 | \\ttype: \\\"system\\\" | \\\"message\\\"\\n 50 | \\t/** Number of tokens this cache point covers */\\n 51 | \\ttokensCovered: number\\n 52 | }\\n----\\n\\n# src/api/transform/cache-strategy/base-strategy.ts\\n 27 | \\t/**\\n 28 | \\t * Calculate token count for system prompt using a more accurate approach\\n 29 | \\t */\\n----\\n 33 | \\n 34 | \\t\\t\\t// Use a more accurate token estimation than simple character count\\n 35 | \\t\\t\\t// Count words and add overhead for punctuation and special tokens\\n 36 | \\t\\t\\tconst words = text.split(/\\\\s+/).filter((word) => word.length > 0)\\n 37 | \\t\\t\\t// Average English word is ~1.3 tokens\\n 38 | \\t\\t\\tlet tokenCount = words.length * 1.3\\n 39 | \\t\\t\\t// Add overhead for punctuation and special characters\\n 40 | \\t\\t\\ttokenCount += (text.match(/[.,!?;:()[\\\\]{}\\\"\\\"''`]/g) || []).length * 0.3\\n 41 | \\t\\t\\t// Add overhead for newlines\\n 42 | \\t\\t\\ttokenCount += (text.match(/\\\\n/g) || []).length * 0.5\\n 43 | \\t\\t\\t// Add a small overhead for system prompt structure\\n 44 | \\t\\t\\ttokenCount += 5\\n 45 | \\n 46 | \\t\\t\\tthis.systemTokenCount = Math.ceil(tokenCount)\\n 47 | \\t\\t}\\n----\\n 84 | \\t/**\\n 85 | \\t * Check if a token count meets the minimum threshold for caching\\n 86 | \\t */\\n 87 | \\tprotected meetsMinTokenThreshold(tokenCount: number): boolean {\\n 88 | \\t\\tconst minTokens = this.config.modelInfo.minTokensPerCachePoint\\n----\\n 91 | \\t\\t}\\n 92 | \\t\\treturn tokenCount >= minTokens\\n 93 | \\t}\\n----\\n 95 | \\t/**\\n 96 | \\t * Estimate token count for a message using a more accurate approach\\n 97 | \\t * This implementation is based on the BaseProvider's countTokens method\\n----\\n100 | \\tprotected estimateTokenCount(message: Anthropic.Messages.MessageParam): number {\\n101 | \\t\\t// Use a more sophisticated token counting approach\\n102 | \\t\\tif (!message.content) return 0\\n----\\n108 | \\t\\t\\t\\tif (block.type === \\\"text\\\") {\\n109 | \\t\\t\\t\\t\\t// Use a more accurate token estimation than simple character count\\n110 | \\t\\t\\t\\t\\t// This is still an approximation but better than character/4\\n----\\n112 | \\t\\t\\t\\t\\tif (text.length > 0) {\\n113 | \\t\\t\\t\\t\\t\\t// Count words and add overhead for punctuation and special tokens\\n114 | \\t\\t\\t\\t\\t\\tconst words = text.split(/\\\\s+/).filter((word) => word.length > 0)\\n115 | \\t\\t\\t\\t\\t\\t// Average English word is ~1.3 tokens\\n116 | \\t\\t\\t\\t\\t\\ttotalTokens += words.length * 1.3\\n----\\n128 | \\t\\t\\tconst text = message.content\\n129 | \\t\\t\\t// Count words and add overhead for punctuation and special tokens\\n130 | \\t\\t\\tconst words = text.split(/\\\\s+/).filter((word) => word.length > 0)\\n131 | \\t\\t\\t// Average English word is ~1.3 tokens\\n132 | \\t\\t\\ttotalTokens += words.length * 1.3\\n----\\n\\n# src/api/transform/reasoning.ts\\n 12 | \\teffort?: ReasoningEffort\\n 13 | \\tmax_tokens?: number\\n 14 | \\texclude?: boolean\\n----\\n 36 | \\tshouldUseReasoningBudget({ model, settings })\\n 37 | \\t\\t? { max_tokens: reasoningBudget }\\n 38 | \\t\\t: shouldUseReasoningEffort({ model, settings })\\n----\\n 46 | }: GetModelReasoningOptions): AnthropicReasoningParams | undefined =>\\n 47 | \\tshouldUseReasoningBudget({ model, settings }) ? { type: \\\"enabled\\\", budget_tokens: reasoningBudget! } : undefined\\n 48 | \\n----\\n\\n# src/api/transform/cache-strategy/multi-point-strategy.ts\\n 7 | * Strategy for handling multiple cache points.\\n 8 | * Creates cache points after messages as soon as uncached tokens exceed minimumTokenCount.\\n 9 | */\\n----\\n 58 | \\t *\\n 59 | \\t * @param minTokensPerPoint Minimum tokens required per cache point\\n 60 | \\t * @param remainingCachePoints Number of cache points available\\n----\\n 97 | \\n 98 | \\t\\t// Calculate tokens in new messages (added since last cache point placement)\\n 99 | \\t\\tconst lastPreviousIndex = previousPlacements[previousPlacements.length - 1].index\\n----\\n103 | \\n104 | \\t\\t// If new messages have enough tokens for a cache point, we need to decide\\n105 | \\t\\t// whether to keep all previous cache points or combine some\\n----\\n127 | \\t\\t\\t\\t// We need to decide which previous cache points to keep and which to combine\\n128 | \\t\\t\\t\\t// Strategy: Compare the token count of new messages with the smallest combined token gap\\n129 | \\n130 | \\t\\t\\t\\t// First, analyze the token distribution between previous cache points\\n131 | \\t\\t\\t\\tconst tokensBetweenPlacements: number[] = []\\n132 | \\t\\t\\t\\tlet startIdx = 0\\n----\\n134 | \\t\\t\\t\\tfor (const placement of previousPlacements) {\\n135 | \\t\\t\\t\\t\\tconst tokens = this.config.messages\\n136 | \\t\\t\\t\\t\\t\\t.slice(startIdx, placement.index + 1)\\n----\\n138 | \\n139 | \\t\\t\\t\\t\\ttokensBetweenPlacements.push(tokens)\\n140 | \\t\\t\\t\\t\\tstartIdx = placement.index + 1\\n----\\n142 | \\n143 | \\t\\t\\t\\t// Find the two consecutive placements with the smallest token gap\\n144 | \\t\\t\\t\\tlet smallestGapIndex = 0\\n----\\n146 | \\n147 | \\t\\t\\t\\tfor (let i = 0; i < tokensBetweenPlacements.length - 1; i++) {\\n148 | \\t\\t\\t\\t\\tconst gap = tokensBetweenPlacements[i] + tokensBetweenPlacements[i + 1]\\n149 | \\t\\t\\t\\t\\tif (gap < smallestGap) {\\n----\\n155 | \\t\\t\\t\\t// Only combine cache points if it's beneficial\\n156 | \\t\\t\\t\\t// Compare the token count of new messages with the smallest combined token gap\\n157 | \\t\\t\\t\\t// Apply a required percentage increase to ensure reallocation is worth it\\n----\\n161 | \\t\\t\\t\\tif (newMessagesTokens >= requiredTokenThreshold) {\\n162 | \\t\\t\\t\\t\\t// It's beneficial to combine cache points since new messages have significantly more tokens\\n163 | \\t\\t\\t\\t\\tlogger.info(\\\"Combining cache points is beneficial\\\", {\\n----\\n231 | \\t\\t} else {\\n232 | \\t\\t\\t// New messages don't have enough tokens for a cache point\\n233 | \\t\\t\\t// Keep all previous placements that are still valid\\n----\\n266 | \\t\\tif (lastUserMessageIndex >= 0) {\\n267 | \\t\\t\\t// Calculate the total tokens covered from the previous cache point (or start of conversation)\\n268 | \\t\\t\\t// to this cache point. This ensures tokensCovered represents the full span of tokens\\n269 | \\t\\t\\t// that will be cached by this cache point.\\n----\\n281 | \\n282 | \\t\\t\\t// Calculate tokens from previous cache point (or start) to this cache point\\n283 | \\t\\t\\tconst tokenStartIndex = previousCachePointIndex + 1\\n284 | \\t\\t\\ttotalTokensCovered = this.config.messages\\n285 | \\t\\t\\t\\t.slice(tokenStartIndex, lastUserMessageIndex + 1)\\n286 | \\t\\t\\t\\t.reduce((acc, curr) => acc + this.estimateTokenCount(curr), 0)\\n287 | \\n288 | \\t\\t\\t// Guard clause: ensure we have enough tokens to justify a cache point\\n289 | \\t\\t\\tif (totalTokensCovered < minTokensPerPoint) {\\n----\\n294 | \\t\\t\\t\\ttype: \\\"message\\\",\\n295 | \\t\\t\\t\\ttokensCovered: totalTokensCovered,\\n296 | \\t\\t\\t}\\n----\\n\\n# src/api/index.ts\\n 50 | \\t/**\\n 51 | \\t * Counts tokens for content blocks\\n 52 | \\t * All providers extend BaseProvider which provides a default tiktoken implementation,\\n 53 | \\t * but they can override this to use their native token counting endpoints\\n 54 | \\t *\\n 55 | \\t * @param content The content to count tokens for\\n 56 | \\t * @returns A promise resolving to the token count\\n 57 | \\t */\\n----\\n\\n# src/api/providers/gemini.ts\\n 7 | } from \\\"@google/genai\\\"\\n 8 | import type { JWTInput } from \\\"google-auth-library\\\"\\n 9 | \\n----\\n179 | \\t\\t\\tif (response.totalTokens === undefined) {\\n180 | \\t\\t\\t\\tconsole.warn(\\\"Gemini token counting returned undefined, using fallback\\\")\\n181 | \\t\\t\\t\\treturn super.countTokens(content)\\n----\\n185 | \\t\\t} catch (error) {\\n186 | \\t\\t\\tconsole.warn(\\\"Gemini token counting failed, using fallback\\\", error)\\n187 | \\t\\t\\treturn super.countTokens(content)\\n----\\n209 | \\n210 | \\t\\t// If there's tiered pricing then adjust the input and output token prices\\n211 | \\t\\t// based on the input tokens used.\\n212 | \\t\\tif (info.tiers) {\\n----\\n221 | \\n222 | \\t\\t// Subtract the cached input tokens from the total input tokens.\\n223 | \\t\\tconst uncachedInputTokens = inputTokens - cacheReadTokens\\n----\\n230 | \\n231 | \\t\\tconst trace: Record = {\\n232 | \\t\\t\\tinput: { price: inputPrice, tokens: uncachedInputTokens, cost: inputTokensCost },\\n233 | \\t\\t\\toutput: { price: outputPrice, tokens: outputTokens, cost: outputTokensCost },\\n234 | \\t\\t}\\n----\\n236 | \\t\\tif (cacheReadTokens > 0) {\\n237 | \\t\\t\\ttrace.cacheRead = { price: cacheReadsPrice, tokens: cacheReadTokens, cost: cacheReadCost }\\n238 | \\t\\t}\\n----\\n\\n# src/api/providers/unbound.ts\\n 22 | interface UnboundUsage extends OpenAI.CompletionUsage {\\n 23 | \\tcache_creation_input_tokens?: number\\n 24 | \\tcache_read_input_tokens?: number\\n 25 | }\\n----\\n 73 | \\n 74 | \\t\\t// Required by Anthropic; other providers default to max tokens allowed.\\n 75 | \\t\\tlet maxTokens: number | undefined\\n----\\n 82 | \\t\\t\\tmodel: modelId.split(\\\"/\\\")[1],\\n 83 | \\t\\t\\tmax_tokens: maxTokens,\\n 84 | \\t\\t\\tmessages: openAiMessages,\\n----\\n111 | \\t\\t\\t\\t\\ttype: \\\"usage\\\",\\n112 | \\t\\t\\t\\t\\tinputTokens: usage.prompt_tokens || 0,\\n113 | \\t\\t\\t\\t\\toutputTokens: usage.completion_tokens || 0,\\n114 | \\t\\t\\t\\t}\\n115 | \\n116 | \\t\\t\\t\\t// Only add cache tokens if they exist.\\n117 | \\t\\t\\t\\tif (usage.cache_creation_input_tokens) {\\n118 | \\t\\t\\t\\t\\tusageData.cacheWriteTokens = usage.cache_creation_input_tokens\\n119 | \\t\\t\\t\\t}\\n120 | \\n121 | \\t\\t\\t\\tif (usage.cache_read_input_tokens) {\\n122 | \\t\\t\\t\\t\\tusageData.cacheReadTokens = usage.cache_read_input_tokens\\n123 | \\t\\t\\t\\t}\\n----\\n146 | \\t\\t\\tif (modelId.startsWith(\\\"anthropic/\\\")) {\\n147 | \\t\\t\\t\\trequestOptions.max_tokens = info.maxTokens\\n148 | \\t\\t\\t}\\n----\\n\\n# src/api/providers/lm-studio.ts\\n 40 | \\t\\t// -------------------------\\n 41 | \\t\\t// Track token usage\\n 42 | \\t\\t// -------------------------\\n----\\n 68 | \\t\\t} catch (err) {\\n 69 | \\t\\t\\tconsole.error(\\\"[LmStudio] Failed to count input tokens:\\\", err)\\n 70 | \\t\\t\\tinputTokens = 0\\n----\\n116 | \\t\\t\\t} catch (err) {\\n117 | \\t\\t\\t\\tconsole.error(\\\"[LmStudio] Failed to count output tokens:\\\", err)\\n118 | \\t\\t\\t\\toutputTokens = 0\\n----\\n\\n# src/api/providers/__tests__/openai.spec.ts\\n 29 | \\t\\t\\t\\t\\t\\t\\t\\tusage: {\\n 30 | \\t\\t\\t\\t\\t\\t\\t\\t\\tprompt_tokens: 10,\\n 31 | \\t\\t\\t\\t\\t\\t\\t\\t\\tcompletion_tokens: 5,\\n 32 | \\t\\t\\t\\t\\t\\t\\t\\t\\ttotal_tokens: 15,\\n 33 | \\t\\t\\t\\t\\t\\t\\t\\t},\\n----\\n 55 | \\t\\t\\t\\t\\t\\t\\t\\t\\tusage: {\\n 56 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\tprompt_tokens: 10,\\n 57 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\tcompletion_tokens: 5,\\n 58 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\ttotal_tokens: 15,\\n 59 | \\t\\t\\t\\t\\t\\t\\t\\t\\t},\\n----\\n\\n# src/api/providers/vscode-lm.ts\\n128 | \\t\\t\\t\\tmaxInputTokens: 8192,\\n129 | \\t\\t\\t\\tsendRequest: async (_messages, _options, _token) => {\\n130 | \\t\\t\\t\\t\\t// Provide a minimal implementation\\n----\\n179 | \\t * Implements the ApiHandler countTokens interface method\\n180 | \\t * Provides token counting for Anthropic content blocks\\n181 | \\t *\\n182 | \\t * @param content The content blocks to count tokens for\\n183 | \\t * @returns A promise resolving to the token count\\n184 | \\t */\\n185 | \\toverride async countTokens(content: Array): Promise {\\n186 | \\t\\t// Convert Anthropic content blocks to a string for VSCode LM token counting\\n187 | \\t\\tlet textContent = \\\"\\\"\\n----\\n201 | \\t/**\\n202 | \\t * Private implementation of token counting used internally by VsCodeLmHandler\\n203 | \\t */\\n----\\n206 | \\t\\tif (!this.client) {\\n207 | \\t\\t\\tconsole.warn(\\\"Roo Code : No client available for token counting\\\")\\n208 | \\t\\t\\treturn 0\\n----\\n211 | \\t\\tif (!this.currentRequestCancellation) {\\n212 | \\t\\t\\tconsole.warn(\\\"Roo Code : No cancellation token available for token counting\\\")\\n213 | \\t\\t\\treturn 0\\n----\\n217 | \\t\\tif (!text) {\\n218 | \\t\\t\\tconsole.debug(\\\"Roo Code : Empty text provided for token counting\\\")\\n219 | \\t\\t\\treturn 0\\n----\\n223 | \\t\\t\\t// Handle different input types\\n224 | \\t\\t\\tlet tokenCount: number\\n225 | \\n226 | \\t\\t\\tif (typeof text === \\\"string\\\") {\\n227 | \\t\\t\\t\\ttokenCount = await this.client.countTokens(text, this.currentRequestCancellation.token)\\n228 | \\t\\t\\t} else if (text instanceof vscode.LanguageModelChatMessage) {\\n----\\n233 | \\t\\t\\t\\t}\\n234 | \\t\\t\\t\\ttokenCount = await this.client.countTokens(text, this.currentRequestCancellation.token)\\n235 | \\t\\t\\t} else {\\n236 | \\t\\t\\t\\tconsole.warn(\\\"Roo Code : Invalid input type for token counting\\\")\\n237 | \\t\\t\\t\\treturn 0\\n----\\n240 | \\t\\t\\t// Validate the result\\n241 | \\t\\t\\tif (typeof tokenCount !== \\\"number\\\") {\\n242 | \\t\\t\\t\\tconsole.warn(\\\"Roo Code : Non-numeric token count received:\\\", tokenCount)\\n243 | \\t\\t\\t\\treturn 0\\n----\\n245 | \\n246 | \\t\\t\\tif (tokenCount < 0) {\\n247 | \\t\\t\\t\\tconsole.warn(\\\"Roo Code : Negative token count received:\\\", tokenCount)\\n248 | \\t\\t\\t\\treturn 0\\n----\\n250 | \\n251 | \\t\\t\\treturn tokenCount\\n252 | \\t\\t} catch (error) {\\n----\\n278 | \\n279 | \\t\\treturn systemTokens + messageTokens.reduce((sum: number, tokens: number): number => sum + tokens, 0)\\n280 | \\t}\\n----\\n357 | \\n358 | \\t\\t// Initialize cancellation token for the request\\n359 | \\t\\tthis.currentRequestCancellation = new vscode.CancellationTokenSource()\\n360 | \\n361 | \\t\\t// Calculate input tokens before starting the stream\\n362 | \\t\\tconst totalInputTokens: number = await this.calculateTotalInputTokens(systemPrompt, vsCodeLmMessages)\\n363 | \\n364 | \\t\\t// Accumulate the text and count at the end of the stream to reduce token counting overhead.\\n365 | \\t\\tlet accumulatedText: string = \\\"\\\"\\n----\\n378 | \\t\\t\\t\\trequestOptions,\\n379 | \\t\\t\\t\\tthis.currentRequestCancellation.token,\\n380 | \\t\\t\\t)\\n----\\n446 | \\n447 | \\t\\t\\t// Count tokens in the accumulated text after stream completion\\n448 | \\t\\t\\tconst totalOutputTokens: number = await this.internalCountTokens(accumulatedText)\\n----\\n511 | \\t\\t\\tconst modelInfo: ModelInfo = {\\n512 | \\t\\t\\t\\tmaxTokens: -1, // Unlimited tokens by default\\n513 | \\t\\t\\t\\tcontextWindow:\\n----\\n548 | \\t\\t\\t\\t{},\\n549 | \\t\\t\\t\\tnew vscode.CancellationTokenSource().token,\\n550 | \\t\\t\\t)\\n----\\n\\n# src/api/providers/glama.ts\\n 51 | \\n 52 | \\t\\t// Required by Anthropic; other providers default to max tokens allowed.\\n 53 | \\t\\tlet maxTokens: number | undefined\\n----\\n 60 | \\t\\t\\tmodel: modelId,\\n 61 | \\t\\t\\tmax_tokens: maxTokens,\\n 62 | \\t\\t\\tmessages: openAiMessages,\\n----\\n 90 | \\t\\t\\t\\t// In case of an interrupted request, we need to wait for the upstream API to finish processing the request\\n 91 | \\t\\t\\t\\t// before we can fetch information about the token usage and cost.\\n 92 | \\t\\t\\t\\tconst response = await axios.get(\\n----\\n 98 | \\n 99 | \\t\\t\\t\\tif (completionRequest.tokenUsage && completionRequest.totalCostUsd) {\\n100 | \\t\\t\\t\\t\\tyield {\\n101 | \\t\\t\\t\\t\\t\\ttype: \\\"usage\\\",\\n102 | \\t\\t\\t\\t\\t\\tcacheWriteTokens: completionRequest.tokenUsage.cacheCreationInputTokens,\\n103 | \\t\\t\\t\\t\\t\\tcacheReadTokens: completionRequest.tokenUsage.cacheReadInputTokens,\\n104 | \\t\\t\\t\\t\\t\\tinputTokens: completionRequest.tokenUsage.promptTokens,\\n105 | \\t\\t\\t\\t\\t\\toutputTokens: completionRequest.tokenUsage.completionTokens,\\n106 | \\t\\t\\t\\t\\t\\ttotalCost: parseFloat(completionRequest.totalCostUsd),\\n----\\n132 | \\t\\t\\tif (modelId.startsWith(\\\"anthropic/\\\")) {\\n133 | \\t\\t\\t\\trequestOptions.max_tokens = info.maxTokens\\n134 | \\t\\t\\t}\\n----\\n\\n# src/api/providers/chutes.ts\\n 31 | \\t\\t\\tid: model,\\n 32 | \\t\\t\\tinfo: { maxTokens: max_tokens },\\n 33 | \\t\\t} = this.getModel()\\n----\\n 38 | \\t\\t\\tmodel,\\n 39 | \\t\\t\\tmax_tokens,\\n 40 | \\t\\t\\ttemperature,\\n----\\n 76 | \\t\\t\\t\\t\\t\\ttype: \\\"usage\\\",\\n 77 | \\t\\t\\t\\t\\t\\tinputTokens: chunk.usage.prompt_tokens || 0,\\n 78 | \\t\\t\\t\\t\\t\\toutputTokens: chunk.usage.completion_tokens || 0,\\n 79 | \\t\\t\\t\\t\\t}\\n----\\n\\n# src/api/providers/__tests__/vscode-lm.test.ts\\n 28 | \\t\\tCancellationTokenSource: jest.fn(() => ({\\n 29 | \\t\\t\\ttoken: {\\n 30 | \\t\\t\\t\\tisCancellationRequested: false,\\n----\\n\\n# src/api/providers/__tests__/vertex.spec.ts\\n 49 | \\t\\t\\t// The test expects 4 chunks:\\n 50 | \\t\\t\\t// 1. Usage chunk with input tokens\\n 51 | \\t\\t\\t// 2. Text chunk with \\\"Gemini response part 1\\\"\\n 52 | \\t\\t\\t// 3. Text chunk with \\\" part 2\\\"\\n 53 | \\t\\t\\t// 4. Usage chunk with output tokens\\n 54 | \\n----\\n\\n# src/api/providers/__tests__/gemini.spec.ts\\n167 | \\tdescribe(\\\"calculateCost\\\", () => {\\n168 | \\t\\t// Mock ModelInfo based on gemini-1.5-flash-latest pricing (per 1M tokens)\\n169 | \\t\\t// Removed 'id' and 'name' as they are not part of ModelInfo type directly\\n170 | \\t\\tconst mockInfo: ModelInfo = {\\n171 | \\t\\t\\tinputPrice: 0.125, // $/1M tokens\\n172 | \\t\\t\\toutputPrice: 0.375, // $/1M tokens\\n173 | \\t\\t\\tcacheWritesPrice: 0.125, // Assume same as input for test\\n----\\n179 | \\n180 | \\t\\tit(\\\"should calculate cost correctly based on input and output tokens\\\", () => {\\n181 | \\t\\t\\tconst inputTokens = 10000 // Use larger numbers for per-million pricing\\n----\\n190 | \\n191 | \\t\\tit(\\\"should return 0 if token counts are zero\\\", () => {\\n192 | \\t\\t\\t// Note: The method expects numbers, not undefined. Passing undefined would be a type error.\\n193 | \\t\\t\\t// The calculateCost method itself returns undefined if prices are missing, but 0 if tokens are 0 and prices exist.\\n194 | \\t\\t\\texpect(handler.calculateCost({ info: mockInfo, inputTokens: 0, outputTokens: 0 })).toBe(0)\\n----\\n196 | \\n197 | \\t\\tit(\\\"should handle only input tokens\\\", () => {\\n198 | \\t\\t\\tconst inputTokens = 5000\\n----\\n203 | \\n204 | \\t\\tit(\\\"should handle only output tokens\\\", () => {\\n205 | \\t\\t\\tconst outputTokens = 15000\\n----\\n210 | \\n211 | \\t\\tit(\\\"should calculate cost with cache write tokens\\\", () => {\\n212 | \\t\\t\\tconst inputTokens = 10000\\n----\\n227 | \\n228 | \\t\\tit(\\\"should calculate cost with cache read tokens\\\", () => {\\n229 | \\t\\t\\tconst inputTokens = 10000 // Total logical input\\n----\\n\\n# src/api/providers/__tests__/openrouter.spec.ts\\n132 | \\t\\t\\t\\t\\t\\tchoices: [{ delta: {} }],\\n133 | \\t\\t\\t\\t\\t\\tusage: { prompt_tokens: 10, completion_tokens: 20, cost: 0.001 },\\n134 | \\t\\t\\t\\t\\t}\\n----\\n162 | \\t\\t\\t\\texpect.objectContaining({\\n163 | \\t\\t\\t\\t\\tmax_tokens: 8192,\\n164 | \\t\\t\\t\\t\\tmessages: [\\n----\\n285 | \\t\\t\\t\\tmodel: mockOptions.openRouterModelId,\\n286 | \\t\\t\\t\\tmax_tokens: 8192,\\n287 | \\t\\t\\t\\tthinking: undefined,\\n----\\n\\n# src/api/providers/__tests__/ollama.spec.ts\\n 28 | \\t\\t\\t\\t\\t\\t\\t\\tusage: {\\n 29 | \\t\\t\\t\\t\\t\\t\\t\\t\\tprompt_tokens: 10,\\n 30 | \\t\\t\\t\\t\\t\\t\\t\\t\\tcompletion_tokens: 5,\\n 31 | \\t\\t\\t\\t\\t\\t\\t\\t\\ttotal_tokens: 15,\\n 32 | \\t\\t\\t\\t\\t\\t\\t\\t},\\n----\\n 54 | \\t\\t\\t\\t\\t\\t\\t\\t\\tusage: {\\n 55 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\tprompt_tokens: 10,\\n 56 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\tcompletion_tokens: 5,\\n 57 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\ttotal_tokens: 15,\\n 58 | \\t\\t\\t\\t\\t\\t\\t\\t\\t},\\n----\\n\\n# src/api/providers/__tests__/chutes.spec.ts\\n 49 | \\t\\t\\t\\t\\tusage: {\\n 50 | \\t\\t\\t\\t\\t\\tprompt_tokens: 10,\\n 51 | \\t\\t\\t\\t\\t\\tcompletion_tokens: 5,\\n 52 | \\t\\t\\t\\t\\t\\ttotal_tokens: 15,\\n 53 | \\t\\t\\t\\t\\t},\\n----\\n103 | \\t\\t\\t\\t\\t],\\n104 | \\t\\t\\t\\t\\tusage: { prompt_tokens: 10, completion_tokens: 5 },\\n105 | \\t\\t\\t\\t}\\n----\\n211 | \\t\\t\\t\\t\\t\\t\\tdone: false,\\n212 | \\t\\t\\t\\t\\t\\t\\tvalue: { choices: [{ delta: {} }], usage: { prompt_tokens: 10, completion_tokens: 20 } },\\n213 | \\t\\t\\t\\t\\t\\t})\\n----\\n284 | \\t\\t\\t\\tmodel: modelId,\\n285 | \\t\\t\\t\\tmax_tokens: modelInfo.maxTokens,\\n286 | \\t\\t\\t\\ttemperature: 0.5,\\n----\\n\\n# src/api/providers/openrouter.ts\\n 32 | \\tinclude_reasoning?: boolean\\n 33 | \\t// https://openrouter.ai/docs/use-cases/reasoning-tokens\\n 34 | \\treasoning?: OpenRouterReasoningParams\\n----\\n 40 | interface CompletionUsage {\\n 41 | \\tcompletion_tokens?: number\\n 42 | \\tcompletion_tokens_details?: {\\n 43 | \\t\\treasoning_tokens?: number\\n 44 | \\t}\\n 45 | \\tprompt_tokens?: number\\n 46 | \\tprompt_tokens_details?: {\\n 47 | \\t\\tcached_tokens?: number\\n 48 | \\t}\\n 49 | \\ttotal_tokens?: number\\n 50 | \\tcost?: number\\n----\\n 81 | \\n 82 | \\t\\t// OpenRouter sends reasoning tokens by default for Gemini 2.5 Pro\\n 83 | \\t\\t// Preview even if you don't request them. This is not the default for\\n----\\n116 | \\t\\t\\tmodel: modelId,\\n117 | \\t\\t\\t...(maxTokens && maxTokens > 0 && { max_tokens: maxTokens }),\\n118 | \\t\\t\\ttemperature,\\n----\\n165 | \\t\\t\\t\\ttype: \\\"usage\\\",\\n166 | \\t\\t\\t\\tinputTokens: lastUsage.prompt_tokens || 0,\\n167 | \\t\\t\\t\\toutputTokens: lastUsage.completion_tokens || 0,\\n168 | \\t\\t\\t\\t// Waiting on OpenRouter to figure out what this represents in the Gemini case\\n169 | \\t\\t\\t\\t// and how to best support it.\\n170 | \\t\\t\\t\\t// cacheReadTokens: lastUsage.prompt_tokens_details?.cached_tokens,\\n171 | \\t\\t\\t\\treasoningTokens: lastUsage.completion_tokens_details?.reasoning_tokens,\\n172 | \\t\\t\\t\\ttotalCost: (lastUsage.is_byok ? BYOK_COST_MULTIPLIER : 1) * (lastUsage.cost || 0),\\n----\\n219 | \\t\\t\\tmodel: modelId,\\n220 | \\t\\t\\tmax_tokens: maxTokens,\\n221 | \\t\\t\\ttemperature,\\n----\\n\\n# src/api/providers/__tests__/deepseek.test.ts\\n 28 | \\t\\t\\t\\t\\t\\t\\t\\tusage: {\\n 29 | \\t\\t\\t\\t\\t\\t\\t\\t\\tprompt_tokens: 10,\\n 30 | \\t\\t\\t\\t\\t\\t\\t\\t\\tcompletion_tokens: 5,\\n 31 | \\t\\t\\t\\t\\t\\t\\t\\t\\ttotal_tokens: 15,\\n 32 | \\t\\t\\t\\t\\t\\t\\t\\t\\tprompt_tokens_details: {\\n 33 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\tcache_miss_tokens: 8,\\n 34 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\tcached_tokens: 2,\\n 35 | \\t\\t\\t\\t\\t\\t\\t\\t\\t},\\n----\\n 59 | \\t\\t\\t\\t\\t\\t\\t\\t\\tusage: {\\n 60 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\tprompt_tokens: 10,\\n 61 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\tcompletion_tokens: 5,\\n 62 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\ttotal_tokens: 15,\\n 63 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\tprompt_tokens_details: {\\n 64 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\t\\tcache_miss_tokens: 8,\\n 65 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\t\\tcached_tokens: 2,\\n 66 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\t},\\n----\\n261 | \\t\\t\\tconst usage = {\\n262 | \\t\\t\\t\\tprompt_tokens: 100,\\n263 | \\t\\t\\t\\tcompletion_tokens: 50,\\n264 | \\t\\t\\t\\ttotal_tokens: 150,\\n265 | \\t\\t\\t\\tprompt_tokens_details: {\\n266 | \\t\\t\\t\\t\\tcache_miss_tokens: 80,\\n267 | \\t\\t\\t\\t\\tcached_tokens: 20,\\n268 | \\t\\t\\t\\t},\\n----\\n289 | \\t\\t\\tconst usage = {\\n290 | \\t\\t\\t\\tprompt_tokens: 100,\\n291 | \\t\\t\\t\\tcompletion_tokens: 50,\\n292 | \\t\\t\\t\\ttotal_tokens: 150,\\n293 | \\t\\t\\t\\t// No prompt_tokens_details\\n294 | \\t\\t\\t}\\n----\\n\\n# src/api/providers/__tests__/bedrock-reasoning.test.ts\\n 94 | \\t\\t\\t\\ttype: \\\"enabled\\\",\\n 95 | \\t\\t\\t\\tbudget_tokens: 4096, // Uses the full modelMaxThinkingTokens value\\n 96 | \\t\\t\\t})\\n----\\n143 | \\t\\t\\t\\ttype: \\\"enabled\\\",\\n144 | \\t\\t\\t\\tbudget_tokens: 8192,\\n145 | \\t\\t\\t})\\n----\\n266 | \\t\\t\\t\\ttype: \\\"enabled\\\",\\n267 | \\t\\t\\t\\tbudget_tokens: 4096,\\n268 | \\t\\t\\t})\\n----\\n\\n# src/api/providers/__tests__/xai.test.ts\\n228 | \\t\\t\\t\\t\\t\\t\\t\\tusage: {\\n229 | \\t\\t\\t\\t\\t\\t\\t\\t\\tprompt_tokens: 10,\\n230 | \\t\\t\\t\\t\\t\\t\\t\\t\\tcompletion_tokens: 20,\\n231 | \\t\\t\\t\\t\\t\\t\\t\\t\\tcache_read_input_tokens: 5,\\n232 | \\t\\t\\t\\t\\t\\t\\t\\t\\tcache_creation_input_tokens: 15,\\n233 | \\t\\t\\t\\t\\t\\t\\t\\t},\\n----\\n284 | \\t\\t\\t\\tmodel: modelId,\\n285 | \\t\\t\\t\\tmax_tokens: modelInfo.maxTokens,\\n286 | \\t\\t\\t\\ttemperature: 0,\\n----\\n\\n# src/api/providers/__tests__/anthropic.spec.ts\\n 19 | \\t\\t\\t\\t\\t\\tusage: {\\n 20 | \\t\\t\\t\\t\\t\\t\\tinput_tokens: 10,\\n 21 | \\t\\t\\t\\t\\t\\t\\toutput_tokens: 5,\\n 22 | \\t\\t\\t\\t\\t\\t},\\n----\\n 30 | \\t\\t\\t\\t\\t\\t\\t\\tusage: {\\n 31 | \\t\\t\\t\\t\\t\\t\\t\\t\\tinput_tokens: 100,\\n 32 | \\t\\t\\t\\t\\t\\t\\t\\t\\toutput_tokens: 50,\\n 33 | \\t\\t\\t\\t\\t\\t\\t\\t\\tcache_creation_input_tokens: 20,\\n 34 | \\t\\t\\t\\t\\t\\t\\t\\t\\tcache_read_input_tokens: 10,\\n 35 | \\t\\t\\t\\t\\t\\t\\t\\t},\\n----\\n105 | \\n106 | \\t\\tit(\\\"use apiKey for passing token if anthropicUseAuthToken is not set\\\", () => {\\n107 | \\t\\t\\tconst handlerWithCustomUrl = new AnthropicHandler({\\n----\\n112 | \\t\\t\\texpect(mockAnthropicConstructor.mock.calls[0]![0]!.apiKey).toEqual(\\\"test-api-key\\\")\\n113 | \\t\\t\\texpect(mockAnthropicConstructor.mock.calls[0]![0]!.authToken).toBeUndefined()\\n114 | \\t\\t})\\n115 | \\n116 | \\t\\tit(\\\"use apiKey for passing token if anthropicUseAuthToken is set but custom base URL is not given\\\", () => {\\n117 | \\t\\t\\tconst handlerWithCustomUrl = new AnthropicHandler({\\n----\\n123 | \\t\\t\\texpect(mockAnthropicConstructor.mock.calls[0]![0]!.apiKey).toEqual(\\\"test-api-key\\\")\\n124 | \\t\\t\\texpect(mockAnthropicConstructor.mock.calls[0]![0]!.authToken).toBeUndefined()\\n125 | \\t\\t})\\n126 | \\n127 | \\t\\tit(\\\"use authToken for passing token if both of anthropicBaseUrl and anthropicUseAuthToken are set\\\", () => {\\n128 | \\t\\t\\tconst customBaseUrl = \\\"https://custom.anthropic.com\\\"\\n----\\n135 | \\t\\t\\texpect(mockAnthropicConstructor).toHaveBeenCalledTimes(1)\\n136 | \\t\\t\\texpect(mockAnthropicConstructor.mock.calls[0]![0]!.authToken).toEqual(\\\"test-api-key\\\")\\n137 | \\t\\t\\texpect(mockAnthropicConstructor.mock.calls[0]![0]!.apiKey).toBeUndefined()\\n----\\n190 | \\t\\t\\t\\tmessages: [{ role: \\\"user\\\", content: \\\"Test prompt\\\" }],\\n191 | \\t\\t\\t\\tmax_tokens: 8192,\\n192 | \\t\\t\\t\\ttemperature: 0,\\n----\\n\\n# src/api/providers/__tests__/requesty.spec.ts\\n124 | \\t\\t\\t\\t\\t\\tusage: {\\n125 | \\t\\t\\t\\t\\t\\t\\tprompt_tokens: 10,\\n126 | \\t\\t\\t\\t\\t\\t\\tcompletion_tokens: 20,\\n127 | \\t\\t\\t\\t\\t\\t\\tprompt_tokens_details: {\\n128 | \\t\\t\\t\\t\\t\\t\\t\\tcaching_tokens: 5,\\n129 | \\t\\t\\t\\t\\t\\t\\t\\tcached_tokens: 2,\\n130 | \\t\\t\\t\\t\\t\\t\\t},\\n----\\n162 | \\t\\t\\t\\texpect.objectContaining({\\n163 | \\t\\t\\t\\t\\tmax_tokens: 8192,\\n164 | \\t\\t\\t\\t\\tmessages: [\\n----\\n204 | \\t\\t\\t\\tmodel: mockOptions.requestyModelId,\\n205 | \\t\\t\\t\\tmax_tokens: 8192,\\n206 | \\t\\t\\t\\tmessages: [{ role: \\\"system\\\", content: \\\"test prompt\\\" }],\\n----\\n\\n# src/api/providers/__tests__/openai-native.spec.ts\\n 29 | \\t\\t\\t\\t\\t\\t\\t\\tusage: {\\n 30 | \\t\\t\\t\\t\\t\\t\\t\\t\\tprompt_tokens: 10,\\n 31 | \\t\\t\\t\\t\\t\\t\\t\\t\\tcompletion_tokens: 5,\\n 32 | \\t\\t\\t\\t\\t\\t\\t\\t\\ttotal_tokens: 15,\\n 33 | \\t\\t\\t\\t\\t\\t\\t\\t},\\n----\\n 55 | \\t\\t\\t\\t\\t\\t\\t\\t\\tusage: {\\n 56 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\tprompt_tokens: 10,\\n 57 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\tcompletion_tokens: 5,\\n 58 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\ttotal_tokens: 15,\\n 59 | \\t\\t\\t\\t\\t\\t\\t\\t\\t},\\n----\\n145 | \\t\\t\\t\\t\\t\\tusage: {\\n146 | \\t\\t\\t\\t\\t\\t\\tprompt_tokens: 0,\\n147 | \\t\\t\\t\\t\\t\\t\\tcompletion_tokens: 0,\\n148 | \\t\\t\\t\\t\\t\\t\\ttotal_tokens: 0,\\n149 | \\t\\t\\t\\t\\t\\t},\\n----\\n215 | \\t\\t\\t\\t{ choices: [{ delta: { content: \\\" there\\\" } }], usage: null },\\n216 | \\t\\t\\t\\t{ choices: [{ delta: { content: \\\"!\\\" } }], usage: { prompt_tokens: 10, completion_tokens: 5 } },\\n217 | \\t\\t\\t]\\n----\\n261 | \\t\\t\\t\\t{ choices: [{ delta: { content: null } }], usage: null },\\n262 | \\t\\t\\t\\t{ choices: [{ delta: { content: \\\"Hello\\\" } }], usage: { prompt_tokens: 10, completion_tokens: 5 } },\\n263 | \\t\\t\\t]\\n----\\n\\n# src/api/providers/__tests__/anthropic-vertex.spec.ts\\n 21 | \\t\\t\\t\\t\\t\\tusage: {\\n 22 | \\t\\t\\t\\t\\t\\t\\tinput_tokens: 10,\\n 23 | \\t\\t\\t\\t\\t\\t\\toutput_tokens: 5,\\n 24 | \\t\\t\\t\\t\\t\\t},\\n----\\n 32 | \\t\\t\\t\\t\\t\\t\\t\\tusage: {\\n 33 | \\t\\t\\t\\t\\t\\t\\t\\t\\tinput_tokens: 10,\\n 34 | \\t\\t\\t\\t\\t\\t\\t\\t\\toutput_tokens: 5,\\n 35 | \\t\\t\\t\\t\\t\\t\\t\\t},\\n----\\n 95 | \\t\\t\\t\\t\\t\\tusage: {\\n 96 | \\t\\t\\t\\t\\t\\t\\tinput_tokens: 10,\\n 97 | \\t\\t\\t\\t\\t\\t\\toutput_tokens: 0,\\n 98 | \\t\\t\\t\\t\\t\\t},\\n----\\n118 | \\t\\t\\t\\t\\tusage: {\\n119 | \\t\\t\\t\\t\\t\\toutput_tokens: 5,\\n120 | \\t\\t\\t\\t\\t},\\n----\\n164 | \\t\\t\\t\\tmodel: \\\"claude-3-5-sonnet-v2@20241022\\\",\\n165 | \\t\\t\\t\\tmax_tokens: 8192,\\n166 | \\t\\t\\t\\ttemperature: 0,\\n----\\n284 | \\t\\t\\t\\t\\t\\tusage: {\\n285 | \\t\\t\\t\\t\\t\\t\\tinput_tokens: 10,\\n286 | \\t\\t\\t\\t\\t\\t\\toutput_tokens: 0,\\n287 | \\t\\t\\t\\t\\t\\t\\tcache_creation_input_tokens: 3,\\n288 | \\t\\t\\t\\t\\t\\t\\tcache_read_input_tokens: 2,\\n289 | \\t\\t\\t\\t\\t\\t},\\n----\\n309 | \\t\\t\\t\\t\\tusage: {\\n310 | \\t\\t\\t\\t\\t\\toutput_tokens: 5,\\n311 | \\t\\t\\t\\t\\t},\\n----\\n419 | \\t\\t\\t\\t\\t\\tusage: {\\n420 | \\t\\t\\t\\t\\t\\t\\tinput_tokens: 10,\\n421 | \\t\\t\\t\\t\\t\\t\\toutput_tokens: 0,\\n422 | \\t\\t\\t\\t\\t\\t\\tcache_creation_input_tokens: 5,\\n423 | \\t\\t\\t\\t\\t\\t\\tcache_read_input_tokens: 3,\\n424 | \\t\\t\\t\\t\\t\\t},\\n----\\n484 | \\t\\t\\t\\t\\t\\tusage: {\\n485 | \\t\\t\\t\\t\\t\\t\\tinput_tokens: 10,\\n486 | \\t\\t\\t\\t\\t\\t\\toutput_tokens: 0,\\n487 | \\t\\t\\t\\t\\t\\t},\\n----\\n618 | \\t\\t\\t\\tmodel: \\\"claude-3-5-sonnet-v2@20241022\\\",\\n619 | \\t\\t\\t\\tmax_tokens: 8192,\\n620 | \\t\\t\\t\\ttemperature: 0,\\n----\\n752 | \\n753 | \\t\\t\\t// Test with default thinking budget (80% of max tokens)\\n754 | \\t\\t\\tconst handlerWithDefaultBudget = new AnthropicVertexHandler({\\n----\\n767 | \\t\\t\\t\\tvertexRegion: \\\"us-central1\\\",\\n768 | \\t\\t\\t\\tmodelMaxTokens: 1000, // This would result in 800 tokens for thinking, but minimum is 1024\\n769 | \\t\\t\\t})\\n----\\n789 | \\t\\t\\t\\t\\t\\tmodel: options.model,\\n790 | \\t\\t\\t\\t\\t\\tusage: { input_tokens: 10, output_tokens: 5 },\\n791 | \\t\\t\\t\\t\\t}\\n----\\n794 | \\t\\t\\t\\t\\tasync *[Symbol.asyncIterator]() {\\n795 | \\t\\t\\t\\t\\t\\tyield { type: \\\"message_start\\\", message: { usage: { input_tokens: 10, output_tokens: 5 } } }\\n796 | \\t\\t\\t\\t\\t},\\n----\\n806 | \\t\\t\\t\\texpect.objectContaining({\\n807 | \\t\\t\\t\\t\\tthinking: { type: \\\"enabled\\\", budget_tokens: 4096 },\\n808 | \\t\\t\\t\\t\\ttemperature: 1.0, // Thinking requires temperature 1.0\\n----\\n\\n# src/api/providers/__tests__/lmstudio.test.ts\\n 25 | \\t\\t\\t\\t\\t\\t\\t\\tusage: {\\n 26 | \\t\\t\\t\\t\\t\\t\\t\\t\\tprompt_tokens: 10,\\n 27 | \\t\\t\\t\\t\\t\\t\\t\\t\\tcompletion_tokens: 5,\\n 28 | \\t\\t\\t\\t\\t\\t\\t\\t\\ttotal_tokens: 15,\\n 29 | \\t\\t\\t\\t\\t\\t\\t\\t},\\n----\\n 51 | \\t\\t\\t\\t\\t\\t\\t\\t\\tusage: {\\n 52 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\tprompt_tokens: 10,\\n 53 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\tcompletion_tokens: 5,\\n 54 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\ttotal_tokens: 15,\\n 55 | \\t\\t\\t\\t\\t\\t\\t\\t\\t},\\n----\\n\\n# src/api/providers/__tests__/glama.spec.ts\\n 57 | \\t\\t\\t\\t\\t\\t\\t\\t\\tchoices: [{ delta: {}, index: 0 }],\\n 58 | \\t\\t\\t\\t\\t\\t\\t\\t\\tusage: { prompt_tokens: 10, completion_tokens: 5, total_tokens: 15 },\\n 59 | \\t\\t\\t\\t\\t\\t\\t\\t}\\n----\\n112 | \\t\\t\\tusage: {\\n113 | \\t\\t\\t\\tprompt_tokens: 10,\\n114 | \\t\\t\\t\\tcompletion_tokens: 5,\\n115 | \\t\\t\\t\\ttotal_tokens: 15,\\n116 | \\t\\t\\t},\\n----\\n176 | \\t\\t\\t\\t\\ttemperature: 0,\\n177 | \\t\\t\\t\\t\\tmax_tokens: 8192,\\n178 | \\t\\t\\t\\t}),\\n----\\n194 | \\n195 | \\t\\tit(\\\"should not set max_tokens for non-Anthropic models\\\", async () => {\\n196 | \\t\\t\\t// Reset mock to clear any previous calls\\n----\\n213 | \\t\\t\\t)\\n214 | \\t\\t\\texpect(mockCreate.mock.calls[0][0]).not.toHaveProperty(\\\"max_tokens\\\")\\n215 | \\t\\t})\\n----\\n\\n# src/api/providers/__tests__/openai-usage-tracking.spec.ts\\n 28 | \\t\\t\\t\\t\\t\\t\\t\\tusage: {\\n 29 | \\t\\t\\t\\t\\t\\t\\t\\t\\tprompt_tokens: 10,\\n 30 | \\t\\t\\t\\t\\t\\t\\t\\t\\tcompletion_tokens: 5,\\n 31 | \\t\\t\\t\\t\\t\\t\\t\\t\\ttotal_tokens: 15,\\n 32 | \\t\\t\\t\\t\\t\\t\\t\\t},\\n----\\n 47 | \\t\\t\\t\\t\\t\\t\\t\\t\\tusage: {\\n 48 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\tprompt_tokens: 10,\\n 49 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\tcompletion_tokens: 2,\\n 50 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\ttotal_tokens: 12,\\n 51 | \\t\\t\\t\\t\\t\\t\\t\\t\\t},\\n----\\n 62 | \\t\\t\\t\\t\\t\\t\\t\\t\\tusage: {\\n 63 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\tprompt_tokens: 10,\\n 64 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\tcompletion_tokens: 4,\\n 65 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\ttotal_tokens: 14,\\n 66 | \\t\\t\\t\\t\\t\\t\\t\\t\\t},\\n----\\n 77 | \\t\\t\\t\\t\\t\\t\\t\\t\\tusage: {\\n 78 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\tprompt_tokens: 10,\\n 79 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\tcompletion_tokens: 5,\\n 80 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\ttotal_tokens: 15,\\n 81 | \\t\\t\\t\\t\\t\\t\\t\\t\\t},\\n----\\n155 | \\t\\t\\t\\t\\t\\tchoices: [{ message: { role: \\\"assistant\\\", content: \\\"Test response\\\" } }],\\n156 | \\t\\t\\t\\t\\t\\tusage: { prompt_tokens: 10, completion_tokens: 5, total_tokens: 15 },\\n157 | \\t\\t\\t\\t\\t}\\n----\\n177 | \\t\\t\\t\\t\\t\\t\\tusage: {\\n178 | \\t\\t\\t\\t\\t\\t\\t\\tprompt_tokens: 10,\\n179 | \\t\\t\\t\\t\\t\\t\\t\\tcompletion_tokens: 5,\\n180 | \\t\\t\\t\\t\\t\\t\\t\\ttotal_tokens: 15,\\n181 | \\t\\t\\t\\t\\t\\t\\t},\\n----\\n\\n# src/api/providers/__tests__/groq.spec.ts\\n101 | \\t\\t\\t\\t\\t\\t\\tdone: false,\\n102 | \\t\\t\\t\\t\\t\\t\\tvalue: { choices: [{ delta: {} }], usage: { prompt_tokens: 10, completion_tokens: 20 } },\\n103 | \\t\\t\\t\\t\\t\\t})\\n----\\n139 | \\t\\t\\t\\tmodel: modelId,\\n140 | \\t\\t\\t\\tmax_tokens: modelInfo.maxTokens,\\n141 | \\t\\t\\t\\ttemperature: 0.5,\\n----\\n\\n# src/api/providers/__tests__/unbound.spec.ts\\n 82 | \\t\\t\\t\\t\\t\\t\\t\\t\\tusage: {\\n 83 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\tprompt_tokens: 10,\\n 84 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\tcompletion_tokens: 5,\\n 85 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\ttotal_tokens: 15,\\n 86 | \\t\\t\\t\\t\\t\\t\\t\\t\\t},\\n----\\n 91 | \\t\\t\\t\\t\\t\\t\\t\\t\\tusage: {\\n 92 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\tprompt_tokens: 8,\\n 93 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\tcompletion_tokens: 4,\\n 94 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\ttotal_tokens: 12,\\n 95 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\tcache_creation_input_tokens: 3,\\n 96 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\tcache_read_input_tokens: 2,\\n 97 | \\t\\t\\t\\t\\t\\t\\t\\t\\t},\\n----\\n232 | \\t\\t\\t\\t\\ttemperature: 0,\\n233 | \\t\\t\\t\\t\\tmax_tokens: 8192,\\n234 | \\t\\t\\t\\t}),\\n----\\n253 | \\n254 | \\t\\tit(\\\"should not set max_tokens for non-Anthropic models\\\", async () => {\\n255 | \\t\\t\\tmockCreate.mockClear()\\n----\\n277 | \\n278 | \\t\\t\\texpect(mockCreate.mock.calls[0][0]).not.toHaveProperty(\\\"max_tokens\\\")\\n279 | \\t\\t})\\n----\\n\\n# src/api/providers/__tests__/bedrock-invokedModelId.spec.ts\\n176 | \\n177 | \\t\\t// The last usage event should have the token counts from the metadata\\n178 | \\t\\tconst lastUsageEvent = usageEvents[usageEvents.length - 1]\\n179 | \\t\\t// Expect the usage event to include all token information\\n180 | \\t\\texpect(lastUsageEvent).toMatchObject({\\n----\\n183 | \\t\\t\\toutputTokens: 200,\\n184 | \\t\\t\\t// Cache tokens may be present with default values\\n185 | \\t\\t\\tcacheReadTokens: expect.any(Number),\\n----\\n\\n# src/api/providers/requesty.ts\\n 19 | // Requesty usage includes an extra field for Anthropic use cases.\\n 20 | // Safely cast the prompt token details section to the appropriate structure.\\n 21 | interface RequestyUsage extends OpenAI.CompletionUsage {\\n 22 | \\tprompt_tokens_details?: {\\n 23 | \\t\\tcaching_tokens?: number\\n 24 | \\t\\tcached_tokens?: number\\n 25 | \\t}\\n----\\n 76 | \\t\\tconst requestyUsage = usage as RequestyUsage\\n 77 | \\t\\tconst inputTokens = requestyUsage?.prompt_tokens || 0\\n 78 | \\t\\tconst outputTokens = requestyUsage?.completion_tokens || 0\\n 79 | \\t\\tconst cacheWriteTokens = requestyUsage?.prompt_tokens_details?.caching_tokens || 0\\n 80 | \\t\\tconst cacheReadTokens = requestyUsage?.prompt_tokens_details?.cached_tokens || 0\\n 81 | \\t\\tconst totalCost = modelInfo\\n----\\n102 | \\t\\t\\tinfo,\\n103 | \\t\\t\\tmaxTokens: max_tokens,\\n104 | \\t\\t\\ttemperature,\\n----\\n116 | \\t\\t\\tmodel,\\n117 | \\t\\t\\tmax_tokens,\\n118 | \\t\\t\\ttemperature,\\n----\\n150 | \\tasync completePrompt(prompt: string): Promise {\\n151 | \\t\\tconst { id: model, maxTokens: max_tokens, temperature } = await this.fetchModel()\\n152 | \\n----\\n156 | \\t\\t\\tmodel,\\n157 | \\t\\t\\tmax_tokens,\\n158 | \\t\\t\\tmessages: openAiMessages,\\n----\\n\\n# src/api/providers/lite-llm.ts\\n 46 | \\n 47 | \\t\\t// Required by some providers; others default to max tokens allowed\\n 48 | \\t\\tlet maxTokens: number | undefined = info.maxTokens ?? undefined\\n----\\n 51 | \\t\\t\\tmodel: modelId,\\n 52 | \\t\\t\\tmax_tokens: maxTokens,\\n 53 | \\t\\t\\tmessages: openAiMessages,\\n----\\n 84 | \\t\\t\\t\\t\\ttype: \\\"usage\\\",\\n 85 | \\t\\t\\t\\t\\tinputTokens: lastUsage.prompt_tokens || 0,\\n 86 | \\t\\t\\t\\t\\toutputTokens: lastUsage.completion_tokens || 0,\\n 87 | \\t\\t\\t\\t\\tcacheWriteTokens: lastUsage.cache_creation_input_tokens || 0,\\n 88 | \\t\\t\\t\\t\\tcacheReadTokens: lastUsage.prompt_tokens_details?.cached_tokens || 0,\\n 89 | \\t\\t\\t\\t}\\n----\\n121 | \\n122 | \\t\\t\\trequestOptions.max_tokens = info.maxTokens\\n123 | \\n----\\n136 | interface LiteLLMUsage extends OpenAI.CompletionUsage {\\n137 | \\tcache_creation_input_tokens?: number\\n138 | }\\n----\\n\\n# src/api/providers/fetchers/unbound.ts\\n 34 | \\t\\t\\t\\t\\tcase modelId.startsWith(\\\"anthropic/\\\"):\\n 35 | \\t\\t\\t\\t\\t\\t// Set max tokens to 8192 for supported Anthropic models\\n 36 | \\t\\t\\t\\t\\t\\tif (modelInfo.maxTokens !== 4096) {\\n----\\n\\n# src/api/providers/deepseek.ts\\n 32 | \\t\\t\\ttype: \\\"usage\\\",\\n 33 | \\t\\t\\tinputTokens: usage?.prompt_tokens || 0,\\n 34 | \\t\\t\\toutputTokens: usage?.completion_tokens || 0,\\n 35 | \\t\\t\\tcacheWriteTokens: usage?.prompt_tokens_details?.cache_miss_tokens,\\n 36 | \\t\\t\\tcacheReadTokens: usage?.prompt_tokens_details?.cached_tokens,\\n 37 | \\t\\t}\\n----\\n\\n# src/api/providers/openai.ts\\n162 | \\t\\t\\tif (this.options.includeMaxTokens) {\\n163 | \\t\\t\\t\\trequestOptions.max_tokens = modelInfo.maxTokens\\n164 | \\t\\t\\t}\\n----\\n241 | \\t\\t\\ttype: \\\"usage\\\",\\n242 | \\t\\t\\tinputTokens: usage?.prompt_tokens || 0,\\n243 | \\t\\t\\toutputTokens: usage?.completion_tokens || 0,\\n244 | \\t\\t\\tcacheWriteTokens: usage?.cache_creation_input_tokens || undefined,\\n245 | \\t\\t\\tcacheReadTokens: usage?.cache_read_input_tokens || undefined,\\n246 | \\t\\t}\\n----\\n347 | \\t\\t\\t\\t\\ttype: \\\"usage\\\",\\n348 | \\t\\t\\t\\t\\tinputTokens: chunk.usage.prompt_tokens || 0,\\n349 | \\t\\t\\t\\t\\toutputTokens: chunk.usage.completion_tokens || 0,\\n350 | \\t\\t\\t\\t}\\n----\\n\\n# src/api/providers/fetchers/openrouter.ts\\n 21 | \\tmodality: z.string().nullish(),\\n 22 | \\ttokenizer: z.string().nullish(),\\n 23 | })\\n----\\n 35 | \\tcontext_length: z.number(),\\n 36 | \\tmax_completion_tokens: z.number().nullish(),\\n 37 | \\tpricing: openRouterPricingSchema.optional(),\\n----\\n 48 | \\tarchitecture: openRouterArchitectureSchema.optional(),\\n 49 | \\ttop_provider: z.object({ max_completion_tokens: z.number().nullish() }).optional(),\\n 50 | \\tsupported_parameters: z.array(z.string()).optional(),\\n----\\n115 | \\t\\t\\t\\tmodality: architecture?.modality,\\n116 | \\t\\t\\t\\tmaxTokens: top_provider?.max_completion_tokens,\\n117 | \\t\\t\\t\\tsupportedParameters: supported_parameters,\\n----\\n155 | \\t\\t\\t\\tmodality: architecture?.modality,\\n156 | \\t\\t\\t\\tmaxTokens: endpoint.max_completion_tokens,\\n157 | \\t\\t\\t})\\n----\\n\\n# src/api/providers/openai-native.ts\\n146 | \\tprivate async *yieldUsage(info: ModelInfo, usage: OpenAI.Completions.CompletionUsage | undefined): ApiStream {\\n147 | \\t\\tconst inputTokens = usage?.prompt_tokens || 0 // sum of cache hits and misses\\n148 | \\t\\tconst outputTokens = usage?.completion_tokens || 0\\n149 | \\t\\tconst cacheReadTokens = usage?.prompt_tokens_details?.cached_tokens || 0\\n150 | \\t\\tconst cacheWriteTokens = 0\\n----\\n\\n# src/api/providers/fetchers/requesty.ts\\n 31 | \\t\\t\\tconst modelInfo: ModelInfo = {\\n 32 | \\t\\t\\t\\tmaxTokens: rawModel.max_output_tokens,\\n 33 | \\t\\t\\t\\tcontextWindow: rawModel.context_window,\\n----\\n\\n# src/api/providers/anthropic.ts\\n 29 | \\t\\tconst apiKeyFieldName =\\n 30 | \\t\\t\\tthis.options.anthropicBaseUrl && this.options.anthropicUseAuthToken ? \\\"authToken\\\" : \\\"apiKey\\\"\\n 31 | \\n----\\n 75 | \\t\\t\\t\\t\\t\\tmodel: modelId,\\n 76 | \\t\\t\\t\\t\\t\\tmax_tokens: maxTokens ?? ANTHROPIC_DEFAULT_MAX_TOKENS,\\n 77 | \\t\\t\\t\\t\\t\\ttemperature,\\n----\\n124 | \\t\\t\\t\\t\\tmodel: modelId,\\n125 | \\t\\t\\t\\t\\tmax_tokens: maxTokens ?? ANTHROPIC_DEFAULT_MAX_TOKENS,\\n126 | \\t\\t\\t\\t\\ttemperature,\\n----\\n142 | \\t\\t\\t\\t\\t\\ttype: \\\"usage\\\",\\n143 | \\t\\t\\t\\t\\t\\tinputTokens: usage.input_tokens || 0,\\n144 | \\t\\t\\t\\t\\t\\toutputTokens: usage.output_tokens || 0,\\n145 | \\t\\t\\t\\t\\t\\tcacheWriteTokens: usage.cache_creation_input_tokens || undefined,\\n146 | \\t\\t\\t\\t\\t\\tcacheReadTokens: usage.cache_read_input_tokens || undefined,\\n147 | \\t\\t\\t\\t\\t}\\n----\\n151 | \\t\\t\\t\\tcase \\\"message_delta\\\":\\n152 | \\t\\t\\t\\t\\t// Tells us stop_reason, stop_sequence, and output tokens\\n153 | \\t\\t\\t\\t\\t// along the way and at the end of the message.\\n----\\n156 | \\t\\t\\t\\t\\t\\tinputTokens: 0,\\n157 | \\t\\t\\t\\t\\t\\toutputTokens: chunk.usage.output_tokens || 0,\\n158 | \\t\\t\\t\\t\\t}\\n----\\n231 | \\t\\t\\tmodel,\\n232 | \\t\\t\\tmax_tokens: ANTHROPIC_DEFAULT_MAX_TOKENS,\\n233 | \\t\\t\\tthinking: undefined,\\n----\\n243 | \\t/**\\n244 | \\t * Counts tokens for the given content using Anthropic's API\\n245 | \\t *\\n246 | \\t * @param content The content blocks to count tokens for\\n247 | \\t * @returns A promise resolving to the token count\\n248 | \\t */\\n----\\n258 | \\n259 | \\t\\t\\treturn response.input_tokens\\n260 | \\t\\t} catch (error) {\\n261 | \\t\\t\\t// Log error but fallback to tiktoken estimation\\n262 | \\t\\t\\tconsole.warn(\\\"Anthropic token counting failed, using fallback\\\", error)\\n263 | \\n----\\n\\n# src/api/providers/base-openai-compatible-provider.ts\\n 71 | \\t\\t\\tid: model,\\n 72 | \\t\\t\\tinfo: { maxTokens: max_tokens },\\n 73 | \\t\\t} = this.getModel()\\n----\\n 78 | \\t\\t\\tmodel,\\n 79 | \\t\\t\\tmax_tokens,\\n 80 | \\t\\t\\ttemperature,\\n----\\n100 | \\t\\t\\t\\t\\ttype: \\\"usage\\\",\\n101 | \\t\\t\\t\\t\\tinputTokens: chunk.usage.prompt_tokens || 0,\\n102 | \\t\\t\\t\\t\\toutputTokens: chunk.usage.completion_tokens || 0,\\n103 | \\t\\t\\t\\t}\\n----\\n\\n# src/api/providers/xai.ts\\n 52 | \\t\\t\\tmodel: modelId,\\n 53 | \\t\\t\\tmax_tokens: modelInfo.maxTokens,\\n 54 | \\t\\t\\ttemperature: this.options.modelTemperature ?? XAI_DEFAULT_TEMPERATURE,\\n----\\n 78 | \\t\\t\\tif (chunk.usage) {\\n 79 | \\t\\t\\t\\t// Extract detailed token information if available\\n 80 | \\t\\t\\t\\t// First check for prompt_tokens_details structure (real API response)\\n 81 | \\t\\t\\t\\tconst promptDetails = \\\"prompt_tokens_details\\\" in chunk.usage ? chunk.usage.prompt_tokens_details : null;\\n 82 | \\t\\t\\t\\tconst cachedTokens = promptDetails && \\\"cached_tokens\\\" in promptDetails ? promptDetails.cached_tokens : 0;\\n 83 | \\n 84 | \\t\\t\\t\\t// Fall back to direct fields in usage (used in test mocks)\\n 85 | \\t\\t\\t\\tconst readTokens = cachedTokens || (\\\"cache_read_input_tokens\\\" in chunk.usage ? (chunk.usage as any).cache_read_input_tokens : 0);\\n 86 | \\t\\t\\t\\tconst writeTokens = \\\"cache_creation_input_tokens\\\" in chunk.usage ? (chunk.usage as any).cache_creation_input_tokens : 0;\\n 87 | \\n----\\n 89 | \\t\\t\\t\\t\\ttype: \\\"usage\\\",\\n 90 | \\t\\t\\t\\t\\tinputTokens: chunk.usage.prompt_tokens || 0,\\n 91 | \\t\\t\\t\\t\\toutputTokens: chunk.usage.completion_tokens || 0,\\n 92 | \\t\\t\\t\\t\\tcacheReadTokens: readTokens,\\n----\\n\\n# src/api/providers/bedrock.ts\\n 52 | \\t\\ttype: \\\"enabled\\\"\\n 53 | \\t\\tbudget_tokens: number\\n 54 | \\t}\\n----\\n109 | \\tmessageStop?: {\\n110 | \\t\\tstopReason?: \\\"end_turn\\\" | \\\"tool_use\\\" | \\\"max_tokens\\\" | \\\"stop_sequence\\\"\\n111 | \\t\\tadditionalModelResponseFields?: Record\\n----\\n349 | \\t\\t\\t\\t\\ttype: \\\"enabled\\\",\\n350 | \\t\\t\\t\\t\\tbudget_tokens: metadata?.thinking?.maxThinkingTokens || modelConfig.reasoningBudget || 4096,\\n351 | \\t\\t\\t\\t},\\n----\\n418 | \\n419 | \\t\\t\\t\\t\\t// Check both field naming conventions for cache tokens\\n420 | \\t\\t\\t\\t\\tconst cacheReadTokens = usage.cacheReadInputTokens || usage.cacheReadInputTokenCount || 0\\n----\\n422 | \\n423 | \\t\\t\\t\\t\\t// Always include all available token information\\n424 | \\t\\t\\t\\t\\tyield {\\n----\\n450 | \\n451 | \\t\\t\\t\\t\\t\\t\\t// Check both field naming conventions for cache tokens\\n452 | \\t\\t\\t\\t\\t\\t\\tconst cacheReadTokens =\\n----\\n1049 | \\t\\tTOO_MANY_TOKENS: {\\n1050 | \\t\\t\\tpatterns: [\\\"too many tokens\\\"],\\n1051 | \\t\\t\\tmessageTemplate: `\\\"Too many tokens\\\" error detected.\\n1052 | Possible Causes:\\n1053 | 1. Input exceeds model's context window limit\\n1054 | 2. Rate limiting (too many tokens per minute)\\n1055 | 3. Quota exceeded for token usage\\n1056 | 4. Other token-related service limitations\\n1057 | \\n----\\n\\n# src/api/providers/fetchers/litellm.ts\\n 52 | \\t\\t\\t\\tmodels[modelName] = {\\n 53 | \\t\\t\\t\\t\\tmaxTokens: modelInfo.max_tokens || 8192,\\n 54 | \\t\\t\\t\\t\\tcontextWindow: modelInfo.max_input_tokens || 200000,\\n 55 | \\t\\t\\t\\t\\tsupportsImages: Boolean(modelInfo.supports_vision),\\n----\\n 58 | \\t\\t\\t\\t\\tsupportsPromptCache: Boolean(modelInfo.supports_prompt_caching),\\n 59 | \\t\\t\\t\\t\\tinputPrice: modelInfo.input_cost_per_token ? modelInfo.input_cost_per_token * 1000000 : undefined,\\n 60 | \\t\\t\\t\\t\\toutputPrice: modelInfo.output_cost_per_token\\n 61 | \\t\\t\\t\\t\\t\\t? modelInfo.output_cost_per_token * 1000000\\n 62 | \\t\\t\\t\\t\\t\\t: undefined,\\n 63 | \\t\\t\\t\\t\\tcacheWritesPrice: modelInfo.cache_creation_input_token_cost\\n 64 | \\t\\t\\t\\t\\t\\t? modelInfo.cache_creation_input_token_cost * 1000000\\n 65 | \\t\\t\\t\\t\\t\\t: undefined,\\n 66 | \\t\\t\\t\\t\\tcacheReadsPrice: modelInfo.cache_read_input_token_cost\\n 67 | \\t\\t\\t\\t\\t\\t? modelInfo.cache_read_input_token_cost * 1000000\\n 68 | \\t\\t\\t\\t\\t\\t: undefined,\\n----\\n\\n# src/api/providers/fetchers/__tests__/openrouter.spec.ts\\n139 | \\t\\t\\t\\tsupportsReasoningEffort: false,\\n140 | \\t\\t\\t\\tsupportedParameters: [\\\"max_tokens\\\", \\\"temperature\\\", \\\"reasoning\\\", \\\"include_reasoning\\\"],\\n141 | \\t\\t\\t})\\n----\\n156 | \\t\\t\\t\\tsupportsReasoningEffort: true,\\n157 | \\t\\t\\t\\tsupportedParameters: [\\\"max_tokens\\\", \\\"temperature\\\", \\\"reasoning\\\", \\\"include_reasoning\\\"],\\n158 | \\t\\t\\t})\\n----\\n\\n# src/api/providers/ollama.ts\\n 79 | \\t\\t\\t\\ttype: \\\"usage\\\",\\n 80 | \\t\\t\\t\\tinputTokens: lastUsage?.prompt_tokens || 0,\\n 81 | \\t\\t\\t\\toutputTokens: lastUsage?.completion_tokens || 0,\\n 82 | \\t\\t\\t}\\n----\\n\\n# src/api/providers/base-provider.ts\\n 21 | \\t/**\\n 22 | \\t * Default token counting implementation using tiktoken.\\n 23 | \\t * Providers can override this to use their native token counting endpoints.\\n 24 | \\t *\\n 25 | \\t * @param content The content to count tokens for\\n 26 | \\t * @returns A promise resolving to the token count\\n 27 | \\t */\\n----\\n\\n# src/api/providers/fetchers/__tests__/litellm.test.ts\\n 41 | \\t\\t\\t\\t\\t\\tmodel_info: {\\n 42 | \\t\\t\\t\\t\\t\\t\\tmax_tokens: 4096,\\n 43 | \\t\\t\\t\\t\\t\\t\\tmax_input_tokens: 200000,\\n 44 | \\t\\t\\t\\t\\t\\t\\tsupports_vision: true,\\n 45 | \\t\\t\\t\\t\\t\\t\\tsupports_prompt_caching: false,\\n 46 | \\t\\t\\t\\t\\t\\t\\tinput_cost_per_token: 0.000003,\\n 47 | \\t\\t\\t\\t\\t\\t\\toutput_cost_per_token: 0.000015,\\n 48 | \\t\\t\\t\\t\\t\\t\\tsupports_computer_use: true,\\n----\\n 56 | \\t\\t\\t\\t\\t\\tmodel_info: {\\n 57 | \\t\\t\\t\\t\\t\\t\\tmax_tokens: 8192,\\n 58 | \\t\\t\\t\\t\\t\\t\\tmax_input_tokens: 128000,\\n 59 | \\t\\t\\t\\t\\t\\t\\tsupports_vision: false,\\n 60 | \\t\\t\\t\\t\\t\\t\\tsupports_prompt_caching: false,\\n 61 | \\t\\t\\t\\t\\t\\t\\tinput_cost_per_token: 0.00001,\\n 62 | \\t\\t\\t\\t\\t\\t\\toutput_cost_per_token: 0.00003,\\n 63 | \\t\\t\\t\\t\\t\\t\\tsupports_computer_use: false,\\n----\\n108 | \\n109 | \\tit(\\\"makes request without authorization header when no API key provided\\\", async () => {\\n110 | \\t\\tconst mockResponse = {\\n----\\n134 | \\t\\t\\t\\t\\t\\tmodel_info: {\\n135 | \\t\\t\\t\\t\\t\\t\\tmax_tokens: 4096,\\n136 | \\t\\t\\t\\t\\t\\t\\tmax_input_tokens: 200000,\\n137 | \\t\\t\\t\\t\\t\\t\\tsupports_vision: true,\\n----\\n146 | \\t\\t\\t\\t\\t\\tmodel_info: {\\n147 | \\t\\t\\t\\t\\t\\t\\tmax_tokens: 4096,\\n148 | \\t\\t\\t\\t\\t\\t\\tmax_input_tokens: 200000,\\n149 | \\t\\t\\t\\t\\t\\t\\tsupports_vision: false,\\n----\\n205 | \\t\\t\\t\\tstatus: 401,\\n206 | \\t\\t\\t\\tstatusText: \\\"Unauthorized\\\",\\n207 | \\t\\t\\t},\\n----\\n214 | \\t\\tawait expect(getLiteLLMModels(DUMMY_INVALID_KEY, \\\"http://localhost:4000\\\")).rejects.toThrow(\\n215 | \\t\\t\\t\\\"Failed to fetch LiteLLM models: 401 Unauthorized. Check base URL and API key.\\\",\\n216 | \\t\\t)\\n----\\n278 | \\t\\t\\t\\t\\t\\tmodel_info: {\\n279 | \\t\\t\\t\\t\\t\\t\\tmax_tokens: 4096,\\n280 | \\t\\t\\t\\t\\t\\t\\tmax_input_tokens: 200000,\\n281 | \\t\\t\\t\\t\\t\\t\\tsupports_vision: true,\\n----\\n291 | \\t\\t\\t\\t\\t\\tmodel_info: {\\n292 | \\t\\t\\t\\t\\t\\t\\tmax_tokens: 8192,\\n293 | \\t\\t\\t\\t\\t\\t\\tmax_input_tokens: 128000,\\n294 | \\t\\t\\t\\t\\t\\t\\tsupports_vision: false,\\n----\\n339 | \\t\\t\\t\\t\\t\\tmodel_info: {\\n340 | \\t\\t\\t\\t\\t\\t\\tmax_tokens: 4096,\\n341 | \\t\\t\\t\\t\\t\\t\\tmax_input_tokens: 200000,\\n342 | \\t\\t\\t\\t\\t\\t\\tsupports_vision: true,\\n----\\n352 | \\t\\t\\t\\t\\t\\tmodel_info: {\\n353 | \\t\\t\\t\\t\\t\\t\\tmax_tokens: 8192,\\n354 | \\t\\t\\t\\t\\t\\t\\tmax_input_tokens: 128000,\\n355 | \\t\\t\\t\\t\\t\\t\\tsupports_vision: false,\\n----\\n365 | \\t\\t\\t\\t\\t\\tmodel_info: {\\n366 | \\t\\t\\t\\t\\t\\t\\tmax_tokens: 8192,\\n367 | \\t\\t\\t\\t\\t\\t\\tmax_input_tokens: 128000,\\n368 | \\t\\t\\t\\t\\t\\t\\tsupports_vision: false,\\n----\\n424 | \\t\\t\\t\\t\\t\\tmodel_info: {\\n425 | \\t\\t\\t\\t\\t\\t\\tmax_tokens: 4096,\\n426 | \\t\\t\\t\\t\\t\\t\\tmax_input_tokens: 200000,\\n427 | \\t\\t\\t\\t\\t\\t\\tsupports_vision: true,\\n----\\n436 | \\t\\t\\t\\t\\t\\tmodel_info: {\\n437 | \\t\\t\\t\\t\\t\\t\\tmax_tokens: 4096,\\n438 | \\t\\t\\t\\t\\t\\t\\tmax_input_tokens: 200000,\\n439 | \\t\\t\\t\\t\\t\\t\\tsupports_vision: true,\\n----\\n448 | \\t\\t\\t\\t\\t\\tmodel_info: {\\n449 | \\t\\t\\t\\t\\t\\t\\tmax_tokens: 4096,\\n450 | \\t\\t\\t\\t\\t\\t\\tmax_input_tokens: 200000,\\n451 | \\t\\t\\t\\t\\t\\t\\tsupports_vision: true,\\n----\\n\\n# src/api/providers/anthropic-vertex.ts\\n 2 | import { AnthropicVertex } from \\\"@anthropic-ai/vertex-sdk\\\"\\n 3 | import { GoogleAuth, JWTInput } from \\\"google-auth-library\\\"\\n 4 | \\n----\\n 41 | \\t\\t\\t\\tgoogleAuth: new GoogleAuth({\\n 42 | \\t\\t\\t\\t\\tscopes: [\\\"https://www.googleapis.com/auth/cloud-platform\\\"],\\n 43 | \\t\\t\\t\\t\\tcredentials: safeJsonParse(this.options.vertexJsonCredentials, undefined),\\n----\\n 50 | \\t\\t\\t\\tgoogleAuth: new GoogleAuth({\\n 51 | \\t\\t\\t\\t\\tscopes: [\\\"https://www.googleapis.com/auth/cloud-platform\\\"],\\n 52 | \\t\\t\\t\\t\\tkeyFile: this.options.vertexKeyFile,\\n----\\n 87 | \\t\\t\\tmodel: id,\\n 88 | \\t\\t\\tmax_tokens: maxTokens ?? ANTHROPIC_DEFAULT_MAX_TOKENS,\\n 89 | \\t\\t\\ttemperature,\\n----\\n107 | \\t\\t\\t\\t\\t\\ttype: \\\"usage\\\",\\n108 | \\t\\t\\t\\t\\t\\tinputTokens: usage.input_tokens || 0,\\n109 | \\t\\t\\t\\t\\t\\toutputTokens: usage.output_tokens || 0,\\n110 | \\t\\t\\t\\t\\t\\tcacheWriteTokens: usage.cache_creation_input_tokens || undefined,\\n111 | \\t\\t\\t\\t\\t\\tcacheReadTokens: usage.cache_read_input_tokens || undefined,\\n112 | \\t\\t\\t\\t\\t}\\n----\\n119 | \\t\\t\\t\\t\\t\\tinputTokens: 0,\\n120 | \\t\\t\\t\\t\\t\\toutputTokens: chunk.usage!.output_tokens || 0,\\n121 | \\t\\t\\t\\t\\t}\\n----\\n189 | \\t\\t\\t\\tmodel: id,\\n190 | \\t\\t\\t\\tmax_tokens: maxTokens,\\n191 | \\t\\t\\t\\ttemperature,\\n----\\n\\n# src/services/mcp/__tests__/McpHub.test.ts\\n 50 | \\t\\t\\tscheme: \\\"file\\\",\\n 51 | \\t\\t\\tauthority: \\\"\\\",\\n 52 | \\t\\t\\tpath: \\\"/test/path\\\",\\n----\\n\\n# src/services/code-index/embedders/openai.ts\\n 52 | \\t\\t\\t\\t\\tconsole.warn(\\n 53 | \\t\\t\\t\\t\\t\\t`Text at index ${i} exceeds maximum token limit (${itemTokens} > ${MAX_ITEM_TOKENS}). Skipping.`,\\n 54 | \\t\\t\\t\\t\\t)\\n----\\n108 | \\t\\t\\t\\t\\tusage: {\\n109 | \\t\\t\\t\\t\\t\\tpromptTokens: response.usage?.prompt_tokens || 0,\\n110 | \\t\\t\\t\\t\\t\\ttotalTokens: response.usage?.total_tokens || 0,\\n111 | \\t\\t\\t\\t\\t},\\n----\\n\\n# src/services/code-index/embedders/openai-compatible.ts\\n 18 | \\tusage?: {\\n 19 | \\t\\tprompt_tokens?: number\\n 20 | \\t\\ttotal_tokens?: number\\n 21 | \\t}\\n----\\n 34 | \\t * @param baseUrl The base URL for the OpenAI-compatible API endpoint\\n 35 | \\t * @param apiKey The API key for authentication\\n 36 | \\t * @param modelId Optional model identifier (defaults to \\\"text-embedding-3-small\\\")\\n----\\n 75 | \\t\\t\\t\\t\\tconsole.warn(\\n 76 | \\t\\t\\t\\t\\t\\t`Text at index ${i} exceeds maximum token limit (${itemTokens} > ${MAX_ITEM_TOKENS}). Skipping.`,\\n 77 | \\t\\t\\t\\t\\t)\\n----\\n156 | \\t\\t\\t\\t\\tusage: {\\n157 | \\t\\t\\t\\t\\t\\tpromptTokens: response.usage?.prompt_tokens || 0,\\n158 | \\t\\t\\t\\t\\t\\ttotalTokens: response.usage?.total_tokens || 0,\\n159 | \\t\\t\\t\\t\\t},\\n----\\n\\n# src/services/marketplace/types.ts\\n 20 | \\tdescription: string\\n 21 | \\tauthor?: string\\n 22 | \\tauthorUrl?: string\\n 23 | \\ttags?: string[]\\n----\\n 45 | \\tdescription: string\\n 46 | \\tauthor?: string\\n 47 | \\tauthorUrl?: string\\n 48 | \\turl: string // Required url field\\n----\\n 62 | \\ttype: MarketplaceItemType\\n 63 | \\tauthor?: string\\n 64 | \\tauthorUrl?: string\\n 65 | \\turl?: string // Optional - only MCPs have url\\n----\\n\\n# src/services/code-index/embedders/__tests__/openai-compatible.spec.ts\\n103 | \\t\\t\\t\\tdata: [{ embedding: [0.1, 0.2, 0.3] }],\\n104 | \\t\\t\\t\\tusage: { prompt_tokens: 10, total_tokens: 15 },\\n105 | \\t\\t\\t}\\n----\\n124 | \\t\\t\\t\\tdata: [{ embedding: [0.1, 0.2, 0.3] }, { embedding: [0.4, 0.5, 0.6] }],\\n125 | \\t\\t\\t\\tusage: { prompt_tokens: 20, total_tokens: 30 },\\n126 | \\t\\t\\t}\\n----\\n149 | \\t\\t\\t\\tdata: [{ embedding: [0.1, 0.2, 0.3] }],\\n150 | \\t\\t\\t\\tusage: { prompt_tokens: 10, total_tokens: 15 },\\n151 | \\t\\t\\t}\\n----\\n194 | \\t\\t\\t\\t\\tdata: [{ embedding: base64String }], // Base64 string instead of array\\n195 | \\t\\t\\t\\t\\tusage: { prompt_tokens: 10, total_tokens: 15 },\\n196 | \\t\\t\\t\\t}\\n----\\n226 | \\t\\t\\t\\t\\tdata: [{ embedding: base64String1 }, { embedding: base64String2 }],\\n227 | \\t\\t\\t\\t\\tusage: { prompt_tokens: 20, total_tokens: 30 },\\n228 | \\t\\t\\t\\t}\\n----\\n253 | \\t\\t\\t\\t\\t],\\n254 | \\t\\t\\t\\t\\tusage: { prompt_tokens: 20, total_tokens: 30 },\\n255 | \\t\\t\\t\\t}\\n----\\n270 | \\t\\t/**\\n271 | \\t\\t * Test batching logic when texts exceed token limits\\n272 | \\t\\t */\\n----\\n283 | \\t\\t\\t\\t\\t],\\n284 | \\t\\t\\t\\t\\tusage: { prompt_tokens: 10, total_tokens: 15 },\\n285 | \\t\\t\\t\\t})\\n----\\n299 | \\t\\t\\t\\t\\tdata: [{ embedding: [0.1, 0.2, 0.3] }, { embedding: [0.4, 0.5, 0.6] }],\\n300 | \\t\\t\\t\\t\\tusage: { prompt_tokens: 10, total_tokens: 15 },\\n301 | \\t\\t\\t\\t}\\n----\\n306 | \\t\\t\\t\\t// Should warn about oversized text\\n307 | \\t\\t\\t\\texpect(console.warn).toHaveBeenCalledWith(expect.stringContaining(\\\"exceeds maximum token limit\\\"))\\n308 | \\n----\\n317 | \\t\\t\\t\\t\\tdata: [{ embedding: [0.1, 0.2, 0.3] }, { embedding: [0.4, 0.5, 0.6] }],\\n318 | \\t\\t\\t\\t\\tusage: { prompt_tokens: 10, total_tokens: 15 },\\n319 | \\t\\t\\t\\t})\\n----\\n354 | \\t\\t\\t\\t\\t\\tdata: [{ embedding: base64String }],\\n355 | \\t\\t\\t\\t\\t\\tusage: { prompt_tokens: 10, total_tokens: 15 },\\n356 | \\t\\t\\t\\t\\t})\\n----\\n375 | \\t\\t\\t\\tconst testTexts = [\\\"Hello world\\\"]\\n376 | \\t\\t\\t\\tconst authError = new Error(\\\"Unauthorized\\\")\\n377 | \\t\\t\\t\\t;(authError as any).status = 401\\n378 | \\n379 | \\t\\t\\t\\tmockEmbeddingsCreate.mockRejectedValue(authError)\\n380 | \\n----\\n452 | \\t\\t\\t\\t\\tdata: null,\\n453 | \\t\\t\\t\\t\\tusage: { prompt_tokens: 10, total_tokens: 15 },\\n454 | \\t\\t\\t\\t}\\n----\\n494 | \\t\\t\\t\\t\\tusage: {\\n495 | \\t\\t\\t\\t\\t\\tprompt_tokens: 2,\\n496 | \\t\\t\\t\\t\\t\\ttotal_tokens: 2,\\n497 | \\t\\t\\t\\t\\t},\\n----\\n\\n# src/services/marketplace/schemas.ts\\n 35 | \\ttype: marketplaceItemTypeSchema,\\n 36 | \\tauthor: z.string().optional(),\\n 37 | \\tauthorUrl: z.string().url(\\\"Author URL must be a valid URL\\\").optional(),\\n 38 | \\ttags: z.array(z.string()).optional(),\\n----\\n 61 | \\tdescription: z.string(),\\n 62 | \\tauthor: z.string().optional(),\\n 63 | \\tauthorUrl: z.string().url().optional(),\\n 64 | \\ttags: z.array(z.string()).optional(),\\n----\\n 72 | \\tdescription: z.string(),\\n 73 | \\tauthor: z.string().optional(),\\n 74 | \\tauthorUrl: z.string().url().optional(),\\n 75 | \\turl: z.string().url(), // Required url field\\n----\\n\\n# src/services/tree-sitter/__tests__/fixtures/sample-embedded_template.ts\\n 35 |
\\n 36 | <% if user.authenticated? %>\\n 37 |

Welcome, <%= user.name %>

\\n----\\n\\n# src/services/tree-sitter/__tests__/parseSourceCodeDefinitions.embedded_template.test.ts\\n 43 | \\tit(\\\"should detect control structures\\\", () => {\\n 44 | \\t\\texpect(parseResult).toMatch(/\\\\d+--\\\\d+ \\\\|\\\\s+<% if user\\\\.authenticated\\\\? %>/)\\n 45 | \\t\\texpect(parseResult).toMatch(/\\\\d+--\\\\d+ \\\\|\\\\s+<% user\\\\.posts\\\\.each do \\\\|post\\\\| %>/)\\n----\\n\\n# src/workers/countTokens.ts\\n 4 | \\n 5 | import { tiktoken } from \\\"../utils/tiktoken\\\"\\n 6 | \\n----\\n 10 | \\ttry {\\n 11 | \\t\\tconst count = await tiktoken(content)\\n 12 | \\t\\treturn { success: true, count }\\n----\\n\\n# src/activate/handleUri.ts\\n 37 | \\t\\t}\\n 38 | \\t\\tcase \\\"/auth/clerk/callback\\\": {\\n 39 | \\t\\t\\tconst code = query.get(\\\"code\\\")\\n----\\n\\n# src/__tests__/dist_assets.test.ts\\n 6 | \\n 7 | \\tdescribe(\\\"tiktoken\\\", () => {\\n 8 | \\t\\tit(\\\"should have tiktoken wasm file\\\", () => {\\n 9 | \\t\\t\\texpect(fs.existsSync(path.join(distPath, \\\"tiktoken_bg.wasm\\\"))).toBe(true)\\n 10 | \\t\\t})\\n----\\n\\n# src/integrations/terminal/ShellIntegrationManager.ts\\n 43 | \\t[ -f \\\"$ZDOTDIR/.zshrc\\\" ] && source \\\"$ZDOTDIR/.zshrc\\\"\\n 44 | \\t[ -f \\\"$ZDOTDIR/.zlogin\\\" ] && source \\\"$ZDOTDIR/.zlogin\\\"\\n 45 | \\t[ \\\"$ZDOTDIR\\\" = \\\"$HOME\\\" ] && unset ZDOTDIR\\n----\\n\\n# src/extension/api.ts\\n219 | \\n220 | \\t\\t\\tcline.on(\\\"taskCompleted\\\", async (_, tokenUsage, toolUsage) => {\\n221 | \\t\\t\\t\\tthis.emit(RooCodeEventName.TaskCompleted, cline.taskId, tokenUsage, toolUsage)\\n222 | \\t\\t\\t\\tthis.taskMap.delete(cline.taskId)\\n----\\n224 | \\t\\t\\t\\tawait this.fileLog(\\n225 | \\t\\t\\t\\t\\t`[${new Date().toISOString()}] taskCompleted -> ${cline.taskId} | ${JSON.stringify(tokenUsage, null, 2)} | ${JSON.stringify(toolUsage, null, 2)}\\\\n`,\\n226 | \\t\\t\\t\\t)\\n----\\n\\n# src/utils/countTokens.ts\\n 4 | import { countTokensResultSchema } from \\\"../workers/types\\\"\\n 5 | import { tiktoken } from \\\"./tiktoken\\\"\\n 6 | \\n----\\n 27 | \\tif (!useWorker || !pool) {\\n 28 | \\t\\treturn tiktoken(content)\\n 29 | \\t}\\n----\\n 42 | \\t\\tconsole.error(error)\\n 43 | \\t\\treturn tiktoken(content)\\n 44 | \\t}\\n----\\n\\n# src/core/checkpoints/index.ts\\n215 | \\t\\t\\t\\tJSON.stringify({\\n216 | \\t\\t\\t\\t\\ttokensIn: totalTokensIn,\\n217 | \\t\\t\\t\\t\\ttokensOut: totalTokensOut,\\n218 | \\t\\t\\t\\t\\tcacheWrites: totalCacheWrites,\\n----\\n\\n# src/core/condense/index.ts\\n 55 | \\tcost: number // The cost of the summarization operation\\n 56 | \\tnewContextTokens?: number // The number of tokens in the context for the next API request\\n 57 | \\terror?: string // Populated iff the operation fails: error message shown to the user on failure (see Task.ts)\\n----\\n 63 | * @param {ApiMessage[]} messages - The conversation messages\\n 64 | * @param {ApiHandler} apiHandler - The API handler to use for token counting.\\n 65 | * @param {string} systemPrompt - The system prompt for API requests, which should be considered in the context token count\\n 66 | * @param {string} taskId - The task ID for the conversation, used for telemetry\\n----\\n 73 | * @param {ApiMessage[]} messages - The conversation messages\\n 74 | * @param {ApiHandler} apiHandler - The API handler to use for token counting (fallback if condensingApiHandler not provided)\\n 75 | * @param {string} systemPrompt - The system prompt for API requests (fallback if customCondensingPrompt not provided)\\n 76 | * @param {string} taskId - The task ID for the conversation, used for telemetry\\n 77 | * @param {number} prevContextTokens - The number of tokens currently in the context, used to ensure we don't grow the context\\n 78 | * @param {boolean} isAutomaticTrigger - Whether the summarization is triggered automatically\\n----\\n186 | \\n187 | \\t// Count the tokens in the context for the next API request\\n188 | \\t// We only estimate the tokens in summaryMesage if outputTokens is 0, otherwise we use outputTokens\\n189 | \\tconst systemPromptMessage: ApiMessage = { role: \\\"user\\\", content: systemPrompt }\\n----\"}", + "partial": false +} +[2025-06-13T04:59:46.001Z] { + "ts": 1749790785975, + "type": "say", + "say": "api_req_started", + "text": "{\"request\":\"[search_files for 'auth|oauth|token|login' in '*.ts'] Result:\\n\\nShowing first 300 of 300+ results. Use a more specific search if necessary.\\n\\n# src/services/ripgrep/index.ts\\n101 | \\t\\tconst rgProcess = childProcess.spawn(bin, args)\\n102 | \\t\\t// cross-platform alternative to head, which is ripgrep author's recommendation for limiting output.\\n103 | \\t\\tconst rl = readline.createInterface({\\n----\\n\\n# src/api/transform/model-params.ts\\n 95 | \\n 96 | \\t\\t// Reasoning cannot be less than 1024 tokens.\\n 97 | \\t\\tif (reasoningBudget < 1024) {\\n----\\n\\n# src/api/transform/__tests__/reasoning.spec.ts\\n 40 | \\n 41 | \\t\\t\\texpect(result).toEqual({ max_tokens: 1000 })\\n 42 | \\t\\t})\\n----\\n 61 | \\n 62 | \\t\\t\\texpect(result).toEqual({ max_tokens: 1000 })\\n 63 | \\t\\t})\\n----\\n123 | \\n124 | \\t\\t\\texpect(result).toEqual({ max_tokens: 1000 })\\n125 | \\t\\t})\\n----\\n140 | \\n141 | \\t\\t\\texpect(result).toEqual({ max_tokens: undefined })\\n142 | \\t\\t})\\n----\\n189 | \\n190 | \\t\\t\\texpect(result).toEqual({ max_tokens: 0 })\\n191 | \\t\\t})\\n----\\n244 | \\t\\t\\t\\ttype: \\\"enabled\\\",\\n245 | \\t\\t\\t\\tbudget_tokens: 1000,\\n246 | \\t\\t\\t})\\n----\\n268 | \\t\\t\\t\\ttype: \\\"enabled\\\",\\n269 | \\t\\t\\t\\tbudget_tokens: 1000,\\n270 | \\t\\t\\t})\\n----\\n314 | \\t\\t\\t\\ttype: \\\"enabled\\\",\\n315 | \\t\\t\\t\\tbudget_tokens: undefined,\\n316 | \\t\\t\\t})\\n----\\n334 | \\t\\t\\t\\ttype: \\\"enabled\\\",\\n335 | \\t\\t\\t\\tbudget_tokens: 0,\\n336 | \\t\\t\\t})\\n----\\n354 | \\t\\t\\t\\ttype: \\\"enabled\\\",\\n355 | \\t\\t\\t\\tbudget_tokens: 100000,\\n356 | \\t\\t\\t})\\n----\\n374 | \\t\\t\\t\\ttype: \\\"enabled\\\",\\n375 | \\t\\t\\t\\tbudget_tokens: 1000,\\n376 | \\t\\t\\t})\\n----\\n547 | \\n548 | \\t\\t\\texpect(openRouterResult).toEqual({ max_tokens: 1000 })\\n549 | \\t\\t\\texpect(anthropicResult).toEqual({ type: \\\"enabled\\\", budget_tokens: 1000 })\\n550 | \\t\\t\\texpect(openAiResult).toBeUndefined()\\n----\\n601 | \\t\\t\\t// Budget should take precedence for OpenRouter and Anthropic\\n602 | \\t\\t\\texpect(openRouterResult).toEqual({ max_tokens: 1000 })\\n603 | \\t\\t\\texpect(anthropicResult).toEqual({ type: \\\"enabled\\\", budget_tokens: 1000 })\\n604 | \\t\\t\\t// OpenAI should still use effort since it doesn't support budget\\n----\\n672 | \\t\\t\\t\\texpect(typeof result).toBe(\\\"object\\\")\\n673 | \\t\\t\\t\\texpect(\\\"max_tokens\\\" in result || \\\"effort\\\" in result || \\\"exclude\\\" in result).toBe(true)\\n674 | \\t\\t\\t}\\n----\\n688 | \\t\\t\\t\\texpect(result).toHaveProperty(\\\"type\\\", \\\"enabled\\\")\\n689 | \\t\\t\\t\\texpect(result).toHaveProperty(\\\"budget_tokens\\\")\\n690 | \\t\\t\\t}\\n----\\n\\n# src/api/transform/__tests__/stream.spec.ts\\n 33 | \\n 34 | \\t\\tit(\\\"should handle usage chunks without cache tokens\\\", () => {\\n 35 | \\t\\t\\tconst usageChunk: ApiStreamChunk = {\\n----\\n 57 | \\n 58 | \\t\\tit(\\\"should handle usage chunks with zero tokens\\\", () => {\\n 59 | \\t\\t\\tconst zeroUsageChunk: ApiStreamChunk = {\\n----\\n 69 | \\n 70 | \\t\\tit(\\\"should handle usage chunks with large token counts\\\", () => {\\n 71 | \\t\\t\\tconst largeUsageChunk: ApiStreamChunk = {\\n----\\n\\n# src/api/transform/__tests__/model-params.spec.ts\\n205 | \\t\\t\\t\\t\\ttype: \\\"enabled\\\",\\n206 | \\t\\t\\t\\t\\tbudget_tokens: 1600,\\n207 | \\t\\t\\t\\t},\\n----\\n226 | \\t\\t\\t\\ttype: \\\"enabled\\\",\\n227 | \\t\\t\\t\\tbudget_tokens: 1600,\\n228 | \\t\\t\\t})\\n----\\n262 | \\t\\t\\t\\t\\ttype: \\\"enabled\\\",\\n263 | \\t\\t\\t\\t\\tbudget_tokens: 2400,\\n264 | \\t\\t\\t\\t},\\n----\\n287 | \\t\\t\\t\\t\\ttype: \\\"enabled\\\",\\n288 | \\t\\t\\t\\t\\tbudget_tokens: 1500,\\n289 | \\t\\t\\t\\t},\\n----\\n308 | \\n309 | \\t\\tit(\\\"should clamp thinking budget to at least 1024 tokens\\\", () => {\\n310 | \\t\\t\\tconst model: ModelInfo = {\\n----\\n328 | \\t\\t\\t\\t\\ttype: \\\"enabled\\\",\\n329 | \\t\\t\\t\\t\\tbudget_tokens: 1024,\\n330 | \\t\\t\\t\\t},\\n----\\n333 | \\n334 | \\t\\tit(\\\"should clamp thinking budget to at most 80% of max tokens\\\", () => {\\n335 | \\t\\t\\tconst model: ModelInfo = {\\n----\\n353 | \\t\\t\\t\\t\\ttype: \\\"enabled\\\",\\n354 | \\t\\t\\t\\t\\tbudget_tokens: 3200,\\n355 | \\t\\t\\t\\t},\\n----\\n372 | \\t\\t\\t\\t\\ttype: \\\"enabled\\\",\\n373 | \\t\\t\\t\\t\\tbudget_tokens: DEFAULT_HYBRID_REASONING_MODEL_THINKING_TOKENS,\\n374 | \\t\\t\\t\\t},\\n----\\n390 | \\t\\t\\texpect(result.maxTokens).toBe(5000)\\n391 | \\t\\t\\texpect(result.reasoningBudget).toBe(2000) // Custom thinking tokens takes precedence\\n392 | \\t\\t})\\n393 | \\n394 | \\t\\tit(\\\"should clamp custom thinking tokens even when custom max tokens is provided\\\", () => {\\n395 | \\t\\t\\tconst model: ModelInfo = {\\n----\\n719 | \\n720 | \\t\\t\\texpect(result.reasoning).toEqual({ max_tokens: 3200 })\\n721 | \\t\\t})\\n----\\n\\n# src/api/transform/cache-strategy/__tests__/cache-strategy.spec.ts\\n 29 | \\n 30 | const createMessageWithTokens = (role: \\\"user\\\" | \\\"assistant\\\", tokenCount: number) => ({\\n 31 | \\trole,\\n 32 | \\tcontent: \\\"x\\\".repeat(tokenCount * 4), // Approximate 4 chars per token\\n 33 | })\\n----\\n130 | \\t\\t\\t\\tit(\\\"adds system cache block when prompt caching is enabled, messages exist, and system prompt is long enough\\\", () => {\\n131 | \\t\\t\\t\\t\\t// Create a system prompt that's at least 50 tokens (200+ characters)\\n132 | \\t\\t\\t\\t\\tconst longSystemPrompt =\\n----\\n236 | \\t\\t\\t\\t\\t\\t\\t\\t(i + 1) +\\n237 | \\t\\t\\t\\t\\t\\t\\t\\t\\\" with some additional text to increase token count. \\\" +\\n238 | \\t\\t\\t\\t\\t\\t\\t\\t\\\"Adding more text to ensure we exceed the token threshold for cache block insertion.\\\",\\n239 | \\t\\t\\t\\t\\t\\t}))\\n----\\n579 | \\n580 | \\t\\tit(\\\"should yield usage results with cache tokens when using MultiPointStrategy\\\", async () => {\\n581 | \\t\\t\\t// Mock the convertToBedrockConverseMessages method to return a result with cache points\\n----\\n589 | \\n590 | \\t\\t\\t// Create a mock stream that includes cache token fields\\n591 | \\t\\t\\tconst mockApiResponse = {\\n----\\n626 | \\n627 | \\t\\t\\t// Verify that usage results with cache tokens are yielded\\n628 | \\t\\t\\texpect(chunks.length).toBeGreaterThan(0)\\n629 | \\t\\t\\t// The test already expects cache tokens, but the implementation might not be including them\\n630 | \\t\\t\\t// Let's make the test more flexible to accept either format\\n----\\n652 | \\n653 | \\t\\t// Helper function to create a message with approximate token count\\n654 | \\t\\tconst createMessage = (role: \\\"user\\\" | \\\"assistant\\\", content: string, tokenCount: number) => {\\n655 | \\t\\t\\t// Pad the content to reach the desired token count (approx 4 chars per token)\\n656 | \\t\\t\\tconst paddingNeeded = Math.max(0, tokenCount * 4 - content.length)\\n657 | \\t\\t\\tconst padding = \\\" \\\".repeat(paddingNeeded)\\n----\\n667 | \\t\\t\\t\\t\\\"Cache point placements:\\\",\\n668 | \\t\\t\\t\\tplacements.map((p) => `index: ${p.index}, tokens: ${p.tokensCovered}`),\\n669 | \\t\\t\\t)\\n----\\n683 | \\t\\t\\t\\t\\tmodelInfo: multiPointModelInfo,\\n684 | \\t\\t\\t\\t\\tsystemPrompt: \\\"You are a helpful assistant.\\\", // ~10 tokens\\n685 | \\t\\t\\t\\t\\tmessages,\\n----\\n730 | \\t\\t\\t\\t\\t\\ttype: \\\"message\\\",\\n731 | \\t\\t\\t\\t\\t\\ttokensCovered: 300,\\n732 | \\t\\t\\t\\t\\t},\\n----\\n736 | \\t\\t\\t\\t\\tmodelInfo: multiPointModelInfo,\\n737 | \\t\\t\\t\\t\\tsystemPrompt: \\\"You are a helpful assistant.\\\", // ~10 tokens\\n738 | \\t\\t\\t\\t\\tmessages,\\n----\\n759 | \\n760 | \\t\\t\\t\\t// Check if we have a second cache point (may not always be added depending on token distribution)\\n761 | \\t\\t\\t\\tif (result.messageCachePointPlacements && result.messageCachePointPlacements.length > 1) {\\n----\\n789 | \\t\\t\\t\\t\\t\\ttype: \\\"message\\\",\\n790 | \\t\\t\\t\\t\\t\\ttokensCovered: 300,\\n791 | \\t\\t\\t\\t\\t},\\n----\\n794 | \\t\\t\\t\\t\\t\\ttype: \\\"message\\\",\\n795 | \\t\\t\\t\\t\\t\\ttokensCovered: 300,\\n796 | \\t\\t\\t\\t\\t},\\n----\\n800 | \\t\\t\\t\\t\\tmodelInfo: multiPointModelInfo,\\n801 | \\t\\t\\t\\t\\tsystemPrompt: \\\"You are a helpful assistant.\\\", // ~10 tokens\\n802 | \\t\\t\\t\\t\\tmessages,\\n----\\n863 | \\t\\t\\t\\t\\t\\ttype: \\\"message\\\",\\n864 | \\t\\t\\t\\t\\t\\ttokensCovered: 300,\\n865 | \\t\\t\\t\\t\\t},\\n----\\n868 | \\t\\t\\t\\t\\t\\ttype: \\\"message\\\",\\n869 | \\t\\t\\t\\t\\t\\ttokensCovered: 300,\\n870 | \\t\\t\\t\\t\\t},\\n----\\n873 | \\t\\t\\t\\t\\t\\ttype: \\\"message\\\",\\n874 | \\t\\t\\t\\t\\t\\ttokensCovered: 300,\\n875 | \\t\\t\\t\\t\\t},\\n----\\n879 | \\t\\t\\t\\t\\tmodelInfo: multiPointModelInfo,\\n880 | \\t\\t\\t\\t\\tsystemPrompt: \\\"You are a helpful assistant.\\\", // ~10 tokens\\n881 | \\t\\t\\t\\t\\tmessages,\\n----\\n929 | \\t\\t\\t\\t// This test verifies that the documentation for Example 5 is correct\\n930 | \\t\\t\\t\\t// In Example 5, the third cache point at index 10 should cover 660 tokens\\n931 | \\t\\t\\t\\t// (260 tokens from messages 7-8 plus 400 tokens from the new messages)\\n932 | \\n----\\n944 | \\t\\t\\t\\t\\tcreateMessage(\\\"assistant\\\", \\\"Deep learning has many applications including...\\\", 160),\\n945 | \\t\\t\\t\\t\\t// New messages with 400 tokens total\\n946 | \\t\\t\\t\\t\\tcreateMessage(\\\"user\\\", \\\"Can you provide a detailed example?\\\", 100),\\n----\\n954 | \\t\\t\\t\\t\\t\\ttype: \\\"message\\\",\\n955 | \\t\\t\\t\\t\\t\\ttokensCovered: 240,\\n956 | \\t\\t\\t\\t\\t},\\n----\\n959 | \\t\\t\\t\\t\\t\\ttype: \\\"message\\\",\\n960 | \\t\\t\\t\\t\\t\\ttokensCovered: 440,\\n961 | \\t\\t\\t\\t\\t},\\n----\\n964 | \\t\\t\\t\\t\\t\\ttype: \\\"message\\\",\\n965 | \\t\\t\\t\\t\\t\\ttokensCovered: 260,\\n966 | \\t\\t\\t\\t\\t},\\n----\\n969 | \\t\\t\\t\\t// In the documentation, the algorithm decides to replace the cache point at index 8\\n970 | \\t\\t\\t\\t// with a new one at index 10, and the tokensCovered value should be 660 tokens\\n971 | \\t\\t\\t\\t// (260 tokens from messages 7-8 plus 400 tokens from the new messages)\\n972 | \\n973 | \\t\\t\\t\\t// However, the actual implementation may behave differently depending on how\\n974 | \\t\\t\\t\\t// it calculates token counts and makes decisions about cache point placement\\n975 | \\n976 | \\t\\t\\t\\t// The important part is that our fix ensures that when a cache point is created,\\n977 | \\t\\t\\t\\t// the tokensCovered value represents all tokens from the previous cache point\\n978 | \\t\\t\\t\\t// to the current cache point, not just the tokens in the new messages\\n979 | \\t\\t\\t})\\n980 | \\n981 | \\t\\t\\tit(\\\"should not combine cache points when new messages have fewer tokens than the smallest combined gap\\\", () => {\\n982 | \\t\\t\\t\\t// This test verifies that when new messages have fewer tokens than the smallest combined gap,\\n983 | \\t\\t\\t\\t// the algorithm keeps all existing cache points and doesn't add a new one\\n----\\n1004 | \\t\\t\\t\\t\\t\\t),\\n1005 | \\t\\t\\t\\t\\t\\t// Small addition (only 50 tokens total)\\n1006 | \\t\\t\\t\\t\\t\\tcreateMessage(\\\"user\\\", \\\"Thanks for the explanation.\\\", 20),\\n----\\n1009 | \\n1010 | \\t\\t\\t\\t\\t// Previous cache point placements with significant token coverage\\n1011 | \\t\\t\\t\\t\\tconst previousCachePointPlacements: CachePointPlacement[] = [\\n----\\n1014 | \\t\\t\\t\\t\\t\\t\\ttype: \\\"message\\\",\\n1015 | \\t\\t\\t\\t\\t\\t\\ttokensCovered: 400, // Significant token coverage\\n1016 | \\t\\t\\t\\t\\t\\t},\\n----\\n1019 | \\t\\t\\t\\t\\t\\t\\ttype: \\\"message\\\",\\n1020 | \\t\\t\\t\\t\\t\\t\\ttokensCovered: 300, // Significant token coverage\\n1021 | \\t\\t\\t\\t\\t\\t},\\n----\\n1024 | \\t\\t\\t\\t\\t\\t\\ttype: \\\"message\\\",\\n1025 | \\t\\t\\t\\t\\t\\t\\ttokensCovered: 300, // Significant token coverage\\n1026 | \\t\\t\\t\\t\\t\\t},\\n----\\n1030 | \\t\\t\\t\\t\\t\\tmodelInfo: multiPointModelInfo,\\n1031 | \\t\\t\\t\\t\\t\\tsystemPrompt: \\\"You are a helpful assistant.\\\", // ~10 tokens\\n1032 | \\t\\t\\t\\t\\t\\tmessages,\\n----\\n1057 | \\n1058 | \\t\\t\\tit(\\\"should make correct decisions based on token counts\\\", () => {\\n1059 | \\t\\t\\t\\t// This test verifies that the algorithm correctly compares token counts\\n1060 | \\t\\t\\t\\t// and makes the right decision about combining cache points\\n1061 | \\n1062 | \\t\\t\\t\\t// Create messages with a variety of token counts\\n1063 | \\t\\t\\t\\tconst messages = [\\n----\\n1081 | \\t\\t\\t\\t\\t\\ttype: \\\"message\\\",\\n1082 | \\t\\t\\t\\t\\t\\ttokensCovered: 400,\\n1083 | \\t\\t\\t\\t\\t},\\n----\\n1086 | \\t\\t\\t\\t\\t\\ttype: \\\"message\\\",\\n1087 | \\t\\t\\t\\t\\t\\ttokensCovered: 150,\\n1088 | \\t\\t\\t\\t\\t},\\n----\\n1091 | \\t\\t\\t\\t\\t\\ttype: \\\"message\\\",\\n1092 | \\t\\t\\t\\t\\t\\ttokensCovered: 150,\\n1093 | \\t\\t\\t\\t\\t},\\n----\\n\\n# src/api/transform/cache-strategy/types.ts\\n 7 | export interface ModelInfo {\\n 8 | \\t/** Maximum number of tokens the model can generate */\\n 9 | \\tmaxTokens: number\\n 10 | \\t/** Maximum context window size in tokens */\\n 11 | \\tcontextWindow: number\\n----\\n 15 | \\tmaxCachePoints: number\\n 16 | \\t/** Minimum number of tokens required for a cache point */\\n 17 | \\tminTokensPerCachePoint: number\\n----\\n 49 | \\ttype: \\\"system\\\" | \\\"message\\\"\\n 50 | \\t/** Number of tokens this cache point covers */\\n 51 | \\ttokensCovered: number\\n 52 | }\\n----\\n\\n# src/api/transform/cache-strategy/base-strategy.ts\\n 27 | \\t/**\\n 28 | \\t * Calculate token count for system prompt using a more accurate approach\\n 29 | \\t */\\n----\\n 33 | \\n 34 | \\t\\t\\t// Use a more accurate token estimation than simple character count\\n 35 | \\t\\t\\t// Count words and add overhead for punctuation and special tokens\\n 36 | \\t\\t\\tconst words = text.split(/\\\\s+/).filter((word) => word.length > 0)\\n 37 | \\t\\t\\t// Average English word is ~1.3 tokens\\n 38 | \\t\\t\\tlet tokenCount = words.length * 1.3\\n 39 | \\t\\t\\t// Add overhead for punctuation and special characters\\n 40 | \\t\\t\\ttokenCount += (text.match(/[.,!?;:()[\\\\]{}\\\"\\\"''`]/g) || []).length * 0.3\\n 41 | \\t\\t\\t// Add overhead for newlines\\n 42 | \\t\\t\\ttokenCount += (text.match(/\\\\n/g) || []).length * 0.5\\n 43 | \\t\\t\\t// Add a small overhead for system prompt structure\\n 44 | \\t\\t\\ttokenCount += 5\\n 45 | \\n 46 | \\t\\t\\tthis.systemTokenCount = Math.ceil(tokenCount)\\n 47 | \\t\\t}\\n----\\n 84 | \\t/**\\n 85 | \\t * Check if a token count meets the minimum threshold for caching\\n 86 | \\t */\\n 87 | \\tprotected meetsMinTokenThreshold(tokenCount: number): boolean {\\n 88 | \\t\\tconst minTokens = this.config.modelInfo.minTokensPerCachePoint\\n----\\n 91 | \\t\\t}\\n 92 | \\t\\treturn tokenCount >= minTokens\\n 93 | \\t}\\n----\\n 95 | \\t/**\\n 96 | \\t * Estimate token count for a message using a more accurate approach\\n 97 | \\t * This implementation is based on the BaseProvider's countTokens method\\n----\\n100 | \\tprotected estimateTokenCount(message: Anthropic.Messages.MessageParam): number {\\n101 | \\t\\t// Use a more sophisticated token counting approach\\n102 | \\t\\tif (!message.content) return 0\\n----\\n108 | \\t\\t\\t\\tif (block.type === \\\"text\\\") {\\n109 | \\t\\t\\t\\t\\t// Use a more accurate token estimation than simple character count\\n110 | \\t\\t\\t\\t\\t// This is still an approximation but better than character/4\\n----\\n112 | \\t\\t\\t\\t\\tif (text.length > 0) {\\n113 | \\t\\t\\t\\t\\t\\t// Count words and add overhead for punctuation and special tokens\\n114 | \\t\\t\\t\\t\\t\\tconst words = text.split(/\\\\s+/).filter((word) => word.length > 0)\\n115 | \\t\\t\\t\\t\\t\\t// Average English word is ~1.3 tokens\\n116 | \\t\\t\\t\\t\\t\\ttotalTokens += words.length * 1.3\\n----\\n128 | \\t\\t\\tconst text = message.content\\n129 | \\t\\t\\t// Count words and add overhead for punctuation and special tokens\\n130 | \\t\\t\\tconst words = text.split(/\\\\s+/).filter((word) => word.length > 0)\\n131 | \\t\\t\\t// Average English word is ~1.3 tokens\\n132 | \\t\\t\\ttotalTokens += words.length * 1.3\\n----\\n\\n# src/api/transform/reasoning.ts\\n 12 | \\teffort?: ReasoningEffort\\n 13 | \\tmax_tokens?: number\\n 14 | \\texclude?: boolean\\n----\\n 36 | \\tshouldUseReasoningBudget({ model, settings })\\n 37 | \\t\\t? { max_tokens: reasoningBudget }\\n 38 | \\t\\t: shouldUseReasoningEffort({ model, settings })\\n----\\n 46 | }: GetModelReasoningOptions): AnthropicReasoningParams | undefined =>\\n 47 | \\tshouldUseReasoningBudget({ model, settings }) ? { type: \\\"enabled\\\", budget_tokens: reasoningBudget! } : undefined\\n 48 | \\n----\\n\\n# src/api/transform/cache-strategy/multi-point-strategy.ts\\n 7 | * Strategy for handling multiple cache points.\\n 8 | * Creates cache points after messages as soon as uncached tokens exceed minimumTokenCount.\\n 9 | */\\n----\\n 58 | \\t *\\n 59 | \\t * @param minTokensPerPoint Minimum tokens required per cache point\\n 60 | \\t * @param remainingCachePoints Number of cache points available\\n----\\n 97 | \\n 98 | \\t\\t// Calculate tokens in new messages (added since last cache point placement)\\n 99 | \\t\\tconst lastPreviousIndex = previousPlacements[previousPlacements.length - 1].index\\n----\\n103 | \\n104 | \\t\\t// If new messages have enough tokens for a cache point, we need to decide\\n105 | \\t\\t// whether to keep all previous cache points or combine some\\n----\\n127 | \\t\\t\\t\\t// We need to decide which previous cache points to keep and which to combine\\n128 | \\t\\t\\t\\t// Strategy: Compare the token count of new messages with the smallest combined token gap\\n129 | \\n130 | \\t\\t\\t\\t// First, analyze the token distribution between previous cache points\\n131 | \\t\\t\\t\\tconst tokensBetweenPlacements: number[] = []\\n132 | \\t\\t\\t\\tlet startIdx = 0\\n----\\n134 | \\t\\t\\t\\tfor (const placement of previousPlacements) {\\n135 | \\t\\t\\t\\t\\tconst tokens = this.config.messages\\n136 | \\t\\t\\t\\t\\t\\t.slice(startIdx, placement.index + 1)\\n----\\n138 | \\n139 | \\t\\t\\t\\t\\ttokensBetweenPlacements.push(tokens)\\n140 | \\t\\t\\t\\t\\tstartIdx = placement.index + 1\\n----\\n142 | \\n143 | \\t\\t\\t\\t// Find the two consecutive placements with the smallest token gap\\n144 | \\t\\t\\t\\tlet smallestGapIndex = 0\\n----\\n146 | \\n147 | \\t\\t\\t\\tfor (let i = 0; i < tokensBetweenPlacements.length - 1; i++) {\\n148 | \\t\\t\\t\\t\\tconst gap = tokensBetweenPlacements[i] + tokensBetweenPlacements[i + 1]\\n149 | \\t\\t\\t\\t\\tif (gap < smallestGap) {\\n----\\n155 | \\t\\t\\t\\t// Only combine cache points if it's beneficial\\n156 | \\t\\t\\t\\t// Compare the token count of new messages with the smallest combined token gap\\n157 | \\t\\t\\t\\t// Apply a required percentage increase to ensure reallocation is worth it\\n----\\n161 | \\t\\t\\t\\tif (newMessagesTokens >= requiredTokenThreshold) {\\n162 | \\t\\t\\t\\t\\t// It's beneficial to combine cache points since new messages have significantly more tokens\\n163 | \\t\\t\\t\\t\\tlogger.info(\\\"Combining cache points is beneficial\\\", {\\n----\\n231 | \\t\\t} else {\\n232 | \\t\\t\\t// New messages don't have enough tokens for a cache point\\n233 | \\t\\t\\t// Keep all previous placements that are still valid\\n----\\n266 | \\t\\tif (lastUserMessageIndex >= 0) {\\n267 | \\t\\t\\t// Calculate the total tokens covered from the previous cache point (or start of conversation)\\n268 | \\t\\t\\t// to this cache point. This ensures tokensCovered represents the full span of tokens\\n269 | \\t\\t\\t// that will be cached by this cache point.\\n----\\n281 | \\n282 | \\t\\t\\t// Calculate tokens from previous cache point (or start) to this cache point\\n283 | \\t\\t\\tconst tokenStartIndex = previousCachePointIndex + 1\\n284 | \\t\\t\\ttotalTokensCovered = this.config.messages\\n285 | \\t\\t\\t\\t.slice(tokenStartIndex, lastUserMessageIndex + 1)\\n286 | \\t\\t\\t\\t.reduce((acc, curr) => acc + this.estimateTokenCount(curr), 0)\\n287 | \\n288 | \\t\\t\\t// Guard clause: ensure we have enough tokens to justify a cache point\\n289 | \\t\\t\\tif (totalTokensCovered < minTokensPerPoint) {\\n----\\n294 | \\t\\t\\t\\ttype: \\\"message\\\",\\n295 | \\t\\t\\t\\ttokensCovered: totalTokensCovered,\\n296 | \\t\\t\\t}\\n----\\n\\n# src/api/index.ts\\n 50 | \\t/**\\n 51 | \\t * Counts tokens for content blocks\\n 52 | \\t * All providers extend BaseProvider which provides a default tiktoken implementation,\\n 53 | \\t * but they can override this to use their native token counting endpoints\\n 54 | \\t *\\n 55 | \\t * @param content The content to count tokens for\\n 56 | \\t * @returns A promise resolving to the token count\\n 57 | \\t */\\n----\\n\\n# src/api/providers/gemini.ts\\n 7 | } from \\\"@google/genai\\\"\\n 8 | import type { JWTInput } from \\\"google-auth-library\\\"\\n 9 | \\n----\\n179 | \\t\\t\\tif (response.totalTokens === undefined) {\\n180 | \\t\\t\\t\\tconsole.warn(\\\"Gemini token counting returned undefined, using fallback\\\")\\n181 | \\t\\t\\t\\treturn super.countTokens(content)\\n----\\n185 | \\t\\t} catch (error) {\\n186 | \\t\\t\\tconsole.warn(\\\"Gemini token counting failed, using fallback\\\", error)\\n187 | \\t\\t\\treturn super.countTokens(content)\\n----\\n209 | \\n210 | \\t\\t// If there's tiered pricing then adjust the input and output token prices\\n211 | \\t\\t// based on the input tokens used.\\n212 | \\t\\tif (info.tiers) {\\n----\\n221 | \\n222 | \\t\\t// Subtract the cached input tokens from the total input tokens.\\n223 | \\t\\tconst uncachedInputTokens = inputTokens - cacheReadTokens\\n----\\n230 | \\n231 | \\t\\tconst trace: Record = {\\n232 | \\t\\t\\tinput: { price: inputPrice, tokens: uncachedInputTokens, cost: inputTokensCost },\\n233 | \\t\\t\\toutput: { price: outputPrice, tokens: outputTokens, cost: outputTokensCost },\\n234 | \\t\\t}\\n----\\n236 | \\t\\tif (cacheReadTokens > 0) {\\n237 | \\t\\t\\ttrace.cacheRead = { price: cacheReadsPrice, tokens: cacheReadTokens, cost: cacheReadCost }\\n238 | \\t\\t}\\n----\\n\\n# src/api/providers/unbound.ts\\n 22 | interface UnboundUsage extends OpenAI.CompletionUsage {\\n 23 | \\tcache_creation_input_tokens?: number\\n 24 | \\tcache_read_input_tokens?: number\\n 25 | }\\n----\\n 73 | \\n 74 | \\t\\t// Required by Anthropic; other providers default to max tokens allowed.\\n 75 | \\t\\tlet maxTokens: number | undefined\\n----\\n 82 | \\t\\t\\tmodel: modelId.split(\\\"/\\\")[1],\\n 83 | \\t\\t\\tmax_tokens: maxTokens,\\n 84 | \\t\\t\\tmessages: openAiMessages,\\n----\\n111 | \\t\\t\\t\\t\\ttype: \\\"usage\\\",\\n112 | \\t\\t\\t\\t\\tinputTokens: usage.prompt_tokens || 0,\\n113 | \\t\\t\\t\\t\\toutputTokens: usage.completion_tokens || 0,\\n114 | \\t\\t\\t\\t}\\n115 | \\n116 | \\t\\t\\t\\t// Only add cache tokens if they exist.\\n117 | \\t\\t\\t\\tif (usage.cache_creation_input_tokens) {\\n118 | \\t\\t\\t\\t\\tusageData.cacheWriteTokens = usage.cache_creation_input_tokens\\n119 | \\t\\t\\t\\t}\\n120 | \\n121 | \\t\\t\\t\\tif (usage.cache_read_input_tokens) {\\n122 | \\t\\t\\t\\t\\tusageData.cacheReadTokens = usage.cache_read_input_tokens\\n123 | \\t\\t\\t\\t}\\n----\\n146 | \\t\\t\\tif (modelId.startsWith(\\\"anthropic/\\\")) {\\n147 | \\t\\t\\t\\trequestOptions.max_tokens = info.maxTokens\\n148 | \\t\\t\\t}\\n----\\n\\n# src/api/providers/lm-studio.ts\\n 40 | \\t\\t// -------------------------\\n 41 | \\t\\t// Track token usage\\n 42 | \\t\\t// -------------------------\\n----\\n 68 | \\t\\t} catch (err) {\\n 69 | \\t\\t\\tconsole.error(\\\"[LmStudio] Failed to count input tokens:\\\", err)\\n 70 | \\t\\t\\tinputTokens = 0\\n----\\n116 | \\t\\t\\t} catch (err) {\\n117 | \\t\\t\\t\\tconsole.error(\\\"[LmStudio] Failed to count output tokens:\\\", err)\\n118 | \\t\\t\\t\\toutputTokens = 0\\n----\\n\\n# src/api/providers/__tests__/openai.spec.ts\\n 29 | \\t\\t\\t\\t\\t\\t\\t\\tusage: {\\n 30 | \\t\\t\\t\\t\\t\\t\\t\\t\\tprompt_tokens: 10,\\n 31 | \\t\\t\\t\\t\\t\\t\\t\\t\\tcompletion_tokens: 5,\\n 32 | \\t\\t\\t\\t\\t\\t\\t\\t\\ttotal_tokens: 15,\\n 33 | \\t\\t\\t\\t\\t\\t\\t\\t},\\n----\\n 55 | \\t\\t\\t\\t\\t\\t\\t\\t\\tusage: {\\n 56 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\tprompt_tokens: 10,\\n 57 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\tcompletion_tokens: 5,\\n 58 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\ttotal_tokens: 15,\\n 59 | \\t\\t\\t\\t\\t\\t\\t\\t\\t},\\n----\\n\\n# src/api/providers/vscode-lm.ts\\n128 | \\t\\t\\t\\tmaxInputTokens: 8192,\\n129 | \\t\\t\\t\\tsendRequest: async (_messages, _options, _token) => {\\n130 | \\t\\t\\t\\t\\t// Provide a minimal implementation\\n----\\n179 | \\t * Implements the ApiHandler countTokens interface method\\n180 | \\t * Provides token counting for Anthropic content blocks\\n181 | \\t *\\n182 | \\t * @param content The content blocks to count tokens for\\n183 | \\t * @returns A promise resolving to the token count\\n184 | \\t */\\n185 | \\toverride async countTokens(content: Array): Promise {\\n186 | \\t\\t// Convert Anthropic content blocks to a string for VSCode LM token counting\\n187 | \\t\\tlet textContent = \\\"\\\"\\n----\\n201 | \\t/**\\n202 | \\t * Private implementation of token counting used internally by VsCodeLmHandler\\n203 | \\t */\\n----\\n206 | \\t\\tif (!this.client) {\\n207 | \\t\\t\\tconsole.warn(\\\"Roo Code : No client available for token counting\\\")\\n208 | \\t\\t\\treturn 0\\n----\\n211 | \\t\\tif (!this.currentRequestCancellation) {\\n212 | \\t\\t\\tconsole.warn(\\\"Roo Code : No cancellation token available for token counting\\\")\\n213 | \\t\\t\\treturn 0\\n----\\n217 | \\t\\tif (!text) {\\n218 | \\t\\t\\tconsole.debug(\\\"Roo Code : Empty text provided for token counting\\\")\\n219 | \\t\\t\\treturn 0\\n----\\n223 | \\t\\t\\t// Handle different input types\\n224 | \\t\\t\\tlet tokenCount: number\\n225 | \\n226 | \\t\\t\\tif (typeof text === \\\"string\\\") {\\n227 | \\t\\t\\t\\ttokenCount = await this.client.countTokens(text, this.currentRequestCancellation.token)\\n228 | \\t\\t\\t} else if (text instanceof vscode.LanguageModelChatMessage) {\\n----\\n233 | \\t\\t\\t\\t}\\n234 | \\t\\t\\t\\ttokenCount = await this.client.countTokens(text, this.currentRequestCancellation.token)\\n235 | \\t\\t\\t} else {\\n236 | \\t\\t\\t\\tconsole.warn(\\\"Roo Code : Invalid input type for token counting\\\")\\n237 | \\t\\t\\t\\treturn 0\\n----\\n240 | \\t\\t\\t// Validate the result\\n241 | \\t\\t\\tif (typeof tokenCount !== \\\"number\\\") {\\n242 | \\t\\t\\t\\tconsole.warn(\\\"Roo Code : Non-numeric token count received:\\\", tokenCount)\\n243 | \\t\\t\\t\\treturn 0\\n----\\n245 | \\n246 | \\t\\t\\tif (tokenCount < 0) {\\n247 | \\t\\t\\t\\tconsole.warn(\\\"Roo Code : Negative token count received:\\\", tokenCount)\\n248 | \\t\\t\\t\\treturn 0\\n----\\n250 | \\n251 | \\t\\t\\treturn tokenCount\\n252 | \\t\\t} catch (error) {\\n----\\n278 | \\n279 | \\t\\treturn systemTokens + messageTokens.reduce((sum: number, tokens: number): number => sum + tokens, 0)\\n280 | \\t}\\n----\\n357 | \\n358 | \\t\\t// Initialize cancellation token for the request\\n359 | \\t\\tthis.currentRequestCancellation = new vscode.CancellationTokenSource()\\n360 | \\n361 | \\t\\t// Calculate input tokens before starting the stream\\n362 | \\t\\tconst totalInputTokens: number = await this.calculateTotalInputTokens(systemPrompt, vsCodeLmMessages)\\n363 | \\n364 | \\t\\t// Accumulate the text and count at the end of the stream to reduce token counting overhead.\\n365 | \\t\\tlet accumulatedText: string = \\\"\\\"\\n----\\n378 | \\t\\t\\t\\trequestOptions,\\n379 | \\t\\t\\t\\tthis.currentRequestCancellation.token,\\n380 | \\t\\t\\t)\\n----\\n446 | \\n447 | \\t\\t\\t// Count tokens in the accumulated text after stream completion\\n448 | \\t\\t\\tconst totalOutputTokens: number = await this.internalCountTokens(accumulatedText)\\n----\\n511 | \\t\\t\\tconst modelInfo: ModelInfo = {\\n512 | \\t\\t\\t\\tmaxTokens: -1, // Unlimited tokens by default\\n513 | \\t\\t\\t\\tcontextWindow:\\n----\\n548 | \\t\\t\\t\\t{},\\n549 | \\t\\t\\t\\tnew vscode.CancellationTokenSource().token,\\n550 | \\t\\t\\t)\\n----\\n\\n# src/api/providers/glama.ts\\n 51 | \\n 52 | \\t\\t// Required by Anthropic; other providers default to max tokens allowed.\\n 53 | \\t\\tlet maxTokens: number | undefined\\n----\\n 60 | \\t\\t\\tmodel: modelId,\\n 61 | \\t\\t\\tmax_tokens: maxTokens,\\n 62 | \\t\\t\\tmessages: openAiMessages,\\n----\\n 90 | \\t\\t\\t\\t// In case of an interrupted request, we need to wait for the upstream API to finish processing the request\\n 91 | \\t\\t\\t\\t// before we can fetch information about the token usage and cost.\\n 92 | \\t\\t\\t\\tconst response = await axios.get(\\n----\\n 98 | \\n 99 | \\t\\t\\t\\tif (completionRequest.tokenUsage && completionRequest.totalCostUsd) {\\n100 | \\t\\t\\t\\t\\tyield {\\n101 | \\t\\t\\t\\t\\t\\ttype: \\\"usage\\\",\\n102 | \\t\\t\\t\\t\\t\\tcacheWriteTokens: completionRequest.tokenUsage.cacheCreationInputTokens,\\n103 | \\t\\t\\t\\t\\t\\tcacheReadTokens: completionRequest.tokenUsage.cacheReadInputTokens,\\n104 | \\t\\t\\t\\t\\t\\tinputTokens: completionRequest.tokenUsage.promptTokens,\\n105 | \\t\\t\\t\\t\\t\\toutputTokens: completionRequest.tokenUsage.completionTokens,\\n106 | \\t\\t\\t\\t\\t\\ttotalCost: parseFloat(completionRequest.totalCostUsd),\\n----\\n132 | \\t\\t\\tif (modelId.startsWith(\\\"anthropic/\\\")) {\\n133 | \\t\\t\\t\\trequestOptions.max_tokens = info.maxTokens\\n134 | \\t\\t\\t}\\n----\\n\\n# src/api/providers/chutes.ts\\n 31 | \\t\\t\\tid: model,\\n 32 | \\t\\t\\tinfo: { maxTokens: max_tokens },\\n 33 | \\t\\t} = this.getModel()\\n----\\n 38 | \\t\\t\\tmodel,\\n 39 | \\t\\t\\tmax_tokens,\\n 40 | \\t\\t\\ttemperature,\\n----\\n 76 | \\t\\t\\t\\t\\t\\ttype: \\\"usage\\\",\\n 77 | \\t\\t\\t\\t\\t\\tinputTokens: chunk.usage.prompt_tokens || 0,\\n 78 | \\t\\t\\t\\t\\t\\toutputTokens: chunk.usage.completion_tokens || 0,\\n 79 | \\t\\t\\t\\t\\t}\\n----\\n\\n# src/api/providers/__tests__/vscode-lm.test.ts\\n 28 | \\t\\tCancellationTokenSource: jest.fn(() => ({\\n 29 | \\t\\t\\ttoken: {\\n 30 | \\t\\t\\t\\tisCancellationRequested: false,\\n----\\n\\n# src/api/providers/__tests__/vertex.spec.ts\\n 49 | \\t\\t\\t// The test expects 4 chunks:\\n 50 | \\t\\t\\t// 1. Usage chunk with input tokens\\n 51 | \\t\\t\\t// 2. Text chunk with \\\"Gemini response part 1\\\"\\n 52 | \\t\\t\\t// 3. Text chunk with \\\" part 2\\\"\\n 53 | \\t\\t\\t// 4. Usage chunk with output tokens\\n 54 | \\n----\\n\\n# src/api/providers/__tests__/gemini.spec.ts\\n167 | \\tdescribe(\\\"calculateCost\\\", () => {\\n168 | \\t\\t// Mock ModelInfo based on gemini-1.5-flash-latest pricing (per 1M tokens)\\n169 | \\t\\t// Removed 'id' and 'name' as they are not part of ModelInfo type directly\\n170 | \\t\\tconst mockInfo: ModelInfo = {\\n171 | \\t\\t\\tinputPrice: 0.125, // $/1M tokens\\n172 | \\t\\t\\toutputPrice: 0.375, // $/1M tokens\\n173 | \\t\\t\\tcacheWritesPrice: 0.125, // Assume same as input for test\\n----\\n179 | \\n180 | \\t\\tit(\\\"should calculate cost correctly based on input and output tokens\\\", () => {\\n181 | \\t\\t\\tconst inputTokens = 10000 // Use larger numbers for per-million pricing\\n----\\n190 | \\n191 | \\t\\tit(\\\"should return 0 if token counts are zero\\\", () => {\\n192 | \\t\\t\\t// Note: The method expects numbers, not undefined. Passing undefined would be a type error.\\n193 | \\t\\t\\t// The calculateCost method itself returns undefined if prices are missing, but 0 if tokens are 0 and prices exist.\\n194 | \\t\\t\\texpect(handler.calculateCost({ info: mockInfo, inputTokens: 0, outputTokens: 0 })).toBe(0)\\n----\\n196 | \\n197 | \\t\\tit(\\\"should handle only input tokens\\\", () => {\\n198 | \\t\\t\\tconst inputTokens = 5000\\n----\\n203 | \\n204 | \\t\\tit(\\\"should handle only output tokens\\\", () => {\\n205 | \\t\\t\\tconst outputTokens = 15000\\n----\\n210 | \\n211 | \\t\\tit(\\\"should calculate cost with cache write tokens\\\", () => {\\n212 | \\t\\t\\tconst inputTokens = 10000\\n----\\n227 | \\n228 | \\t\\tit(\\\"should calculate cost with cache read tokens\\\", () => {\\n229 | \\t\\t\\tconst inputTokens = 10000 // Total logical input\\n----\\n\\n# src/api/providers/__tests__/openrouter.spec.ts\\n132 | \\t\\t\\t\\t\\t\\tchoices: [{ delta: {} }],\\n133 | \\t\\t\\t\\t\\t\\tusage: { prompt_tokens: 10, completion_tokens: 20, cost: 0.001 },\\n134 | \\t\\t\\t\\t\\t}\\n----\\n162 | \\t\\t\\t\\texpect.objectContaining({\\n163 | \\t\\t\\t\\t\\tmax_tokens: 8192,\\n164 | \\t\\t\\t\\t\\tmessages: [\\n----\\n285 | \\t\\t\\t\\tmodel: mockOptions.openRouterModelId,\\n286 | \\t\\t\\t\\tmax_tokens: 8192,\\n287 | \\t\\t\\t\\tthinking: undefined,\\n----\\n\\n# src/api/providers/__tests__/ollama.spec.ts\\n 28 | \\t\\t\\t\\t\\t\\t\\t\\tusage: {\\n 29 | \\t\\t\\t\\t\\t\\t\\t\\t\\tprompt_tokens: 10,\\n 30 | \\t\\t\\t\\t\\t\\t\\t\\t\\tcompletion_tokens: 5,\\n 31 | \\t\\t\\t\\t\\t\\t\\t\\t\\ttotal_tokens: 15,\\n 32 | \\t\\t\\t\\t\\t\\t\\t\\t},\\n----\\n 54 | \\t\\t\\t\\t\\t\\t\\t\\t\\tusage: {\\n 55 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\tprompt_tokens: 10,\\n 56 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\tcompletion_tokens: 5,\\n 57 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\ttotal_tokens: 15,\\n 58 | \\t\\t\\t\\t\\t\\t\\t\\t\\t},\\n----\\n\\n# src/api/providers/__tests__/chutes.spec.ts\\n 49 | \\t\\t\\t\\t\\tusage: {\\n 50 | \\t\\t\\t\\t\\t\\tprompt_tokens: 10,\\n 51 | \\t\\t\\t\\t\\t\\tcompletion_tokens: 5,\\n 52 | \\t\\t\\t\\t\\t\\ttotal_tokens: 15,\\n 53 | \\t\\t\\t\\t\\t},\\n----\\n103 | \\t\\t\\t\\t\\t],\\n104 | \\t\\t\\t\\t\\tusage: { prompt_tokens: 10, completion_tokens: 5 },\\n105 | \\t\\t\\t\\t}\\n----\\n211 | \\t\\t\\t\\t\\t\\t\\tdone: false,\\n212 | \\t\\t\\t\\t\\t\\t\\tvalue: { choices: [{ delta: {} }], usage: { prompt_tokens: 10, completion_tokens: 20 } },\\n213 | \\t\\t\\t\\t\\t\\t})\\n----\\n284 | \\t\\t\\t\\tmodel: modelId,\\n285 | \\t\\t\\t\\tmax_tokens: modelInfo.maxTokens,\\n286 | \\t\\t\\t\\ttemperature: 0.5,\\n----\\n\\n# src/api/providers/openrouter.ts\\n 32 | \\tinclude_reasoning?: boolean\\n 33 | \\t// https://openrouter.ai/docs/use-cases/reasoning-tokens\\n 34 | \\treasoning?: OpenRouterReasoningParams\\n----\\n 40 | interface CompletionUsage {\\n 41 | \\tcompletion_tokens?: number\\n 42 | \\tcompletion_tokens_details?: {\\n 43 | \\t\\treasoning_tokens?: number\\n 44 | \\t}\\n 45 | \\tprompt_tokens?: number\\n 46 | \\tprompt_tokens_details?: {\\n 47 | \\t\\tcached_tokens?: number\\n 48 | \\t}\\n 49 | \\ttotal_tokens?: number\\n 50 | \\tcost?: number\\n----\\n 81 | \\n 82 | \\t\\t// OpenRouter sends reasoning tokens by default for Gemini 2.5 Pro\\n 83 | \\t\\t// Preview even if you don't request them. This is not the default for\\n----\\n116 | \\t\\t\\tmodel: modelId,\\n117 | \\t\\t\\t...(maxTokens && maxTokens > 0 && { max_tokens: maxTokens }),\\n118 | \\t\\t\\ttemperature,\\n----\\n165 | \\t\\t\\t\\ttype: \\\"usage\\\",\\n166 | \\t\\t\\t\\tinputTokens: lastUsage.prompt_tokens || 0,\\n167 | \\t\\t\\t\\toutputTokens: lastUsage.completion_tokens || 0,\\n168 | \\t\\t\\t\\t// Waiting on OpenRouter to figure out what this represents in the Gemini case\\n169 | \\t\\t\\t\\t// and how to best support it.\\n170 | \\t\\t\\t\\t// cacheReadTokens: lastUsage.prompt_tokens_details?.cached_tokens,\\n171 | \\t\\t\\t\\treasoningTokens: lastUsage.completion_tokens_details?.reasoning_tokens,\\n172 | \\t\\t\\t\\ttotalCost: (lastUsage.is_byok ? BYOK_COST_MULTIPLIER : 1) * (lastUsage.cost || 0),\\n----\\n219 | \\t\\t\\tmodel: modelId,\\n220 | \\t\\t\\tmax_tokens: maxTokens,\\n221 | \\t\\t\\ttemperature,\\n----\\n\\n# src/api/providers/__tests__/deepseek.test.ts\\n 28 | \\t\\t\\t\\t\\t\\t\\t\\tusage: {\\n 29 | \\t\\t\\t\\t\\t\\t\\t\\t\\tprompt_tokens: 10,\\n 30 | \\t\\t\\t\\t\\t\\t\\t\\t\\tcompletion_tokens: 5,\\n 31 | \\t\\t\\t\\t\\t\\t\\t\\t\\ttotal_tokens: 15,\\n 32 | \\t\\t\\t\\t\\t\\t\\t\\t\\tprompt_tokens_details: {\\n 33 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\tcache_miss_tokens: 8,\\n 34 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\tcached_tokens: 2,\\n 35 | \\t\\t\\t\\t\\t\\t\\t\\t\\t},\\n----\\n 59 | \\t\\t\\t\\t\\t\\t\\t\\t\\tusage: {\\n 60 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\tprompt_tokens: 10,\\n 61 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\tcompletion_tokens: 5,\\n 62 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\ttotal_tokens: 15,\\n 63 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\tprompt_tokens_details: {\\n 64 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\t\\tcache_miss_tokens: 8,\\n 65 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\t\\tcached_tokens: 2,\\n 66 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\t},\\n----\\n261 | \\t\\t\\tconst usage = {\\n262 | \\t\\t\\t\\tprompt_tokens: 100,\\n263 | \\t\\t\\t\\tcompletion_tokens: 50,\\n264 | \\t\\t\\t\\ttotal_tokens: 150,\\n265 | \\t\\t\\t\\tprompt_tokens_details: {\\n266 | \\t\\t\\t\\t\\tcache_miss_tokens: 80,\\n267 | \\t\\t\\t\\t\\tcached_tokens: 20,\\n268 | \\t\\t\\t\\t},\\n----\\n289 | \\t\\t\\tconst usage = {\\n290 | \\t\\t\\t\\tprompt_tokens: 100,\\n291 | \\t\\t\\t\\tcompletion_tokens: 50,\\n292 | \\t\\t\\t\\ttotal_tokens: 150,\\n293 | \\t\\t\\t\\t// No prompt_tokens_details\\n294 | \\t\\t\\t}\\n----\\n\\n# src/api/providers/__tests__/bedrock-reasoning.test.ts\\n 94 | \\t\\t\\t\\ttype: \\\"enabled\\\",\\n 95 | \\t\\t\\t\\tbudget_tokens: 4096, // Uses the full modelMaxThinkingTokens value\\n 96 | \\t\\t\\t})\\n----\\n143 | \\t\\t\\t\\ttype: \\\"enabled\\\",\\n144 | \\t\\t\\t\\tbudget_tokens: 8192,\\n145 | \\t\\t\\t})\\n----\\n266 | \\t\\t\\t\\ttype: \\\"enabled\\\",\\n267 | \\t\\t\\t\\tbudget_tokens: 4096,\\n268 | \\t\\t\\t})\\n----\\n\\n# src/api/providers/__tests__/xai.test.ts\\n228 | \\t\\t\\t\\t\\t\\t\\t\\tusage: {\\n229 | \\t\\t\\t\\t\\t\\t\\t\\t\\tprompt_tokens: 10,\\n230 | \\t\\t\\t\\t\\t\\t\\t\\t\\tcompletion_tokens: 20,\\n231 | \\t\\t\\t\\t\\t\\t\\t\\t\\tcache_read_input_tokens: 5,\\n232 | \\t\\t\\t\\t\\t\\t\\t\\t\\tcache_creation_input_tokens: 15,\\n233 | \\t\\t\\t\\t\\t\\t\\t\\t},\\n----\\n284 | \\t\\t\\t\\tmodel: modelId,\\n285 | \\t\\t\\t\\tmax_tokens: modelInfo.maxTokens,\\n286 | \\t\\t\\t\\ttemperature: 0,\\n----\\n\\n# src/api/providers/__tests__/anthropic.spec.ts\\n 19 | \\t\\t\\t\\t\\t\\tusage: {\\n 20 | \\t\\t\\t\\t\\t\\t\\tinput_tokens: 10,\\n 21 | \\t\\t\\t\\t\\t\\t\\toutput_tokens: 5,\\n 22 | \\t\\t\\t\\t\\t\\t},\\n----\\n 30 | \\t\\t\\t\\t\\t\\t\\t\\tusage: {\\n 31 | \\t\\t\\t\\t\\t\\t\\t\\t\\tinput_tokens: 100,\\n 32 | \\t\\t\\t\\t\\t\\t\\t\\t\\toutput_tokens: 50,\\n 33 | \\t\\t\\t\\t\\t\\t\\t\\t\\tcache_creation_input_tokens: 20,\\n 34 | \\t\\t\\t\\t\\t\\t\\t\\t\\tcache_read_input_tokens: 10,\\n 35 | \\t\\t\\t\\t\\t\\t\\t\\t},\\n----\\n105 | \\n106 | \\t\\tit(\\\"use apiKey for passing token if anthropicUseAuthToken is not set\\\", () => {\\n107 | \\t\\t\\tconst handlerWithCustomUrl = new AnthropicHandler({\\n----\\n112 | \\t\\t\\texpect(mockAnthropicConstructor.mock.calls[0]![0]!.apiKey).toEqual(\\\"test-api-key\\\")\\n113 | \\t\\t\\texpect(mockAnthropicConstructor.mock.calls[0]![0]!.authToken).toBeUndefined()\\n114 | \\t\\t})\\n115 | \\n116 | \\t\\tit(\\\"use apiKey for passing token if anthropicUseAuthToken is set but custom base URL is not given\\\", () => {\\n117 | \\t\\t\\tconst handlerWithCustomUrl = new AnthropicHandler({\\n----\\n123 | \\t\\t\\texpect(mockAnthropicConstructor.mock.calls[0]![0]!.apiKey).toEqual(\\\"test-api-key\\\")\\n124 | \\t\\t\\texpect(mockAnthropicConstructor.mock.calls[0]![0]!.authToken).toBeUndefined()\\n125 | \\t\\t})\\n126 | \\n127 | \\t\\tit(\\\"use authToken for passing token if both of anthropicBaseUrl and anthropicUseAuthToken are set\\\", () => {\\n128 | \\t\\t\\tconst customBaseUrl = \\\"https://custom.anthropic.com\\\"\\n----\\n135 | \\t\\t\\texpect(mockAnthropicConstructor).toHaveBeenCalledTimes(1)\\n136 | \\t\\t\\texpect(mockAnthropicConstructor.mock.calls[0]![0]!.authToken).toEqual(\\\"test-api-key\\\")\\n137 | \\t\\t\\texpect(mockAnthropicConstructor.mock.calls[0]![0]!.apiKey).toBeUndefined()\\n----\\n190 | \\t\\t\\t\\tmessages: [{ role: \\\"user\\\", content: \\\"Test prompt\\\" }],\\n191 | \\t\\t\\t\\tmax_tokens: 8192,\\n192 | \\t\\t\\t\\ttemperature: 0,\\n----\\n\\n# src/api/providers/__tests__/requesty.spec.ts\\n124 | \\t\\t\\t\\t\\t\\tusage: {\\n125 | \\t\\t\\t\\t\\t\\t\\tprompt_tokens: 10,\\n126 | \\t\\t\\t\\t\\t\\t\\tcompletion_tokens: 20,\\n127 | \\t\\t\\t\\t\\t\\t\\tprompt_tokens_details: {\\n128 | \\t\\t\\t\\t\\t\\t\\t\\tcaching_tokens: 5,\\n129 | \\t\\t\\t\\t\\t\\t\\t\\tcached_tokens: 2,\\n130 | \\t\\t\\t\\t\\t\\t\\t},\\n----\\n162 | \\t\\t\\t\\texpect.objectContaining({\\n163 | \\t\\t\\t\\t\\tmax_tokens: 8192,\\n164 | \\t\\t\\t\\t\\tmessages: [\\n----\\n204 | \\t\\t\\t\\tmodel: mockOptions.requestyModelId,\\n205 | \\t\\t\\t\\tmax_tokens: 8192,\\n206 | \\t\\t\\t\\tmessages: [{ role: \\\"system\\\", content: \\\"test prompt\\\" }],\\n----\\n\\n# src/api/providers/__tests__/openai-native.spec.ts\\n 29 | \\t\\t\\t\\t\\t\\t\\t\\tusage: {\\n 30 | \\t\\t\\t\\t\\t\\t\\t\\t\\tprompt_tokens: 10,\\n 31 | \\t\\t\\t\\t\\t\\t\\t\\t\\tcompletion_tokens: 5,\\n 32 | \\t\\t\\t\\t\\t\\t\\t\\t\\ttotal_tokens: 15,\\n 33 | \\t\\t\\t\\t\\t\\t\\t\\t},\\n----\\n 55 | \\t\\t\\t\\t\\t\\t\\t\\t\\tusage: {\\n 56 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\tprompt_tokens: 10,\\n 57 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\tcompletion_tokens: 5,\\n 58 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\ttotal_tokens: 15,\\n 59 | \\t\\t\\t\\t\\t\\t\\t\\t\\t},\\n----\\n145 | \\t\\t\\t\\t\\t\\tusage: {\\n146 | \\t\\t\\t\\t\\t\\t\\tprompt_tokens: 0,\\n147 | \\t\\t\\t\\t\\t\\t\\tcompletion_tokens: 0,\\n148 | \\t\\t\\t\\t\\t\\t\\ttotal_tokens: 0,\\n149 | \\t\\t\\t\\t\\t\\t},\\n----\\n215 | \\t\\t\\t\\t{ choices: [{ delta: { content: \\\" there\\\" } }], usage: null },\\n216 | \\t\\t\\t\\t{ choices: [{ delta: { content: \\\"!\\\" } }], usage: { prompt_tokens: 10, completion_tokens: 5 } },\\n217 | \\t\\t\\t]\\n----\\n261 | \\t\\t\\t\\t{ choices: [{ delta: { content: null } }], usage: null },\\n262 | \\t\\t\\t\\t{ choices: [{ delta: { content: \\\"Hello\\\" } }], usage: { prompt_tokens: 10, completion_tokens: 5 } },\\n263 | \\t\\t\\t]\\n----\\n\\n# src/api/providers/__tests__/anthropic-vertex.spec.ts\\n 21 | \\t\\t\\t\\t\\t\\tusage: {\\n 22 | \\t\\t\\t\\t\\t\\t\\tinput_tokens: 10,\\n 23 | \\t\\t\\t\\t\\t\\t\\toutput_tokens: 5,\\n 24 | \\t\\t\\t\\t\\t\\t},\\n----\\n 32 | \\t\\t\\t\\t\\t\\t\\t\\tusage: {\\n 33 | \\t\\t\\t\\t\\t\\t\\t\\t\\tinput_tokens: 10,\\n 34 | \\t\\t\\t\\t\\t\\t\\t\\t\\toutput_tokens: 5,\\n 35 | \\t\\t\\t\\t\\t\\t\\t\\t},\\n----\\n 95 | \\t\\t\\t\\t\\t\\tusage: {\\n 96 | \\t\\t\\t\\t\\t\\t\\tinput_tokens: 10,\\n 97 | \\t\\t\\t\\t\\t\\t\\toutput_tokens: 0,\\n 98 | \\t\\t\\t\\t\\t\\t},\\n----\\n118 | \\t\\t\\t\\t\\tusage: {\\n119 | \\t\\t\\t\\t\\t\\toutput_tokens: 5,\\n120 | \\t\\t\\t\\t\\t},\\n----\\n164 | \\t\\t\\t\\tmodel: \\\"claude-3-5-sonnet-v2@20241022\\\",\\n165 | \\t\\t\\t\\tmax_tokens: 8192,\\n166 | \\t\\t\\t\\ttemperature: 0,\\n----\\n284 | \\t\\t\\t\\t\\t\\tusage: {\\n285 | \\t\\t\\t\\t\\t\\t\\tinput_tokens: 10,\\n286 | \\t\\t\\t\\t\\t\\t\\toutput_tokens: 0,\\n287 | \\t\\t\\t\\t\\t\\t\\tcache_creation_input_tokens: 3,\\n288 | \\t\\t\\t\\t\\t\\t\\tcache_read_input_tokens: 2,\\n289 | \\t\\t\\t\\t\\t\\t},\\n----\\n309 | \\t\\t\\t\\t\\tusage: {\\n310 | \\t\\t\\t\\t\\t\\toutput_tokens: 5,\\n311 | \\t\\t\\t\\t\\t},\\n----\\n419 | \\t\\t\\t\\t\\t\\tusage: {\\n420 | \\t\\t\\t\\t\\t\\t\\tinput_tokens: 10,\\n421 | \\t\\t\\t\\t\\t\\t\\toutput_tokens: 0,\\n422 | \\t\\t\\t\\t\\t\\t\\tcache_creation_input_tokens: 5,\\n423 | \\t\\t\\t\\t\\t\\t\\tcache_read_input_tokens: 3,\\n424 | \\t\\t\\t\\t\\t\\t},\\n----\\n484 | \\t\\t\\t\\t\\t\\tusage: {\\n485 | \\t\\t\\t\\t\\t\\t\\tinput_tokens: 10,\\n486 | \\t\\t\\t\\t\\t\\t\\toutput_tokens: 0,\\n487 | \\t\\t\\t\\t\\t\\t},\\n----\\n618 | \\t\\t\\t\\tmodel: \\\"claude-3-5-sonnet-v2@20241022\\\",\\n619 | \\t\\t\\t\\tmax_tokens: 8192,\\n620 | \\t\\t\\t\\ttemperature: 0,\\n----\\n752 | \\n753 | \\t\\t\\t// Test with default thinking budget (80% of max tokens)\\n754 | \\t\\t\\tconst handlerWithDefaultBudget = new AnthropicVertexHandler({\\n----\\n767 | \\t\\t\\t\\tvertexRegion: \\\"us-central1\\\",\\n768 | \\t\\t\\t\\tmodelMaxTokens: 1000, // This would result in 800 tokens for thinking, but minimum is 1024\\n769 | \\t\\t\\t})\\n----\\n789 | \\t\\t\\t\\t\\t\\tmodel: options.model,\\n790 | \\t\\t\\t\\t\\t\\tusage: { input_tokens: 10, output_tokens: 5 },\\n791 | \\t\\t\\t\\t\\t}\\n----\\n794 | \\t\\t\\t\\t\\tasync *[Symbol.asyncIterator]() {\\n795 | \\t\\t\\t\\t\\t\\tyield { type: \\\"message_start\\\", message: { usage: { input_tokens: 10, output_tokens: 5 } } }\\n796 | \\t\\t\\t\\t\\t},\\n----\\n806 | \\t\\t\\t\\texpect.objectContaining({\\n807 | \\t\\t\\t\\t\\tthinking: { type: \\\"enabled\\\", budget_tokens: 4096 },\\n808 | \\t\\t\\t\\t\\ttemperature: 1.0, // Thinking requires temperature 1.0\\n----\\n\\n# src/api/providers/__tests__/lmstudio.test.ts\\n 25 | \\t\\t\\t\\t\\t\\t\\t\\tusage: {\\n 26 | \\t\\t\\t\\t\\t\\t\\t\\t\\tprompt_tokens: 10,\\n 27 | \\t\\t\\t\\t\\t\\t\\t\\t\\tcompletion_tokens: 5,\\n 28 | \\t\\t\\t\\t\\t\\t\\t\\t\\ttotal_tokens: 15,\\n 29 | \\t\\t\\t\\t\\t\\t\\t\\t},\\n----\\n 51 | \\t\\t\\t\\t\\t\\t\\t\\t\\tusage: {\\n 52 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\tprompt_tokens: 10,\\n 53 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\tcompletion_tokens: 5,\\n 54 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\ttotal_tokens: 15,\\n 55 | \\t\\t\\t\\t\\t\\t\\t\\t\\t},\\n----\\n\\n# src/api/providers/__tests__/glama.spec.ts\\n 57 | \\t\\t\\t\\t\\t\\t\\t\\t\\tchoices: [{ delta: {}, index: 0 }],\\n 58 | \\t\\t\\t\\t\\t\\t\\t\\t\\tusage: { prompt_tokens: 10, completion_tokens: 5, total_tokens: 15 },\\n 59 | \\t\\t\\t\\t\\t\\t\\t\\t}\\n----\\n112 | \\t\\t\\tusage: {\\n113 | \\t\\t\\t\\tprompt_tokens: 10,\\n114 | \\t\\t\\t\\tcompletion_tokens: 5,\\n115 | \\t\\t\\t\\ttotal_tokens: 15,\\n116 | \\t\\t\\t},\\n----\\n176 | \\t\\t\\t\\t\\ttemperature: 0,\\n177 | \\t\\t\\t\\t\\tmax_tokens: 8192,\\n178 | \\t\\t\\t\\t}),\\n----\\n194 | \\n195 | \\t\\tit(\\\"should not set max_tokens for non-Anthropic models\\\", async () => {\\n196 | \\t\\t\\t// Reset mock to clear any previous calls\\n----\\n213 | \\t\\t\\t)\\n214 | \\t\\t\\texpect(mockCreate.mock.calls[0][0]).not.toHaveProperty(\\\"max_tokens\\\")\\n215 | \\t\\t})\\n----\\n\\n# src/api/providers/__tests__/openai-usage-tracking.spec.ts\\n 28 | \\t\\t\\t\\t\\t\\t\\t\\tusage: {\\n 29 | \\t\\t\\t\\t\\t\\t\\t\\t\\tprompt_tokens: 10,\\n 30 | \\t\\t\\t\\t\\t\\t\\t\\t\\tcompletion_tokens: 5,\\n 31 | \\t\\t\\t\\t\\t\\t\\t\\t\\ttotal_tokens: 15,\\n 32 | \\t\\t\\t\\t\\t\\t\\t\\t},\\n----\\n 47 | \\t\\t\\t\\t\\t\\t\\t\\t\\tusage: {\\n 48 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\tprompt_tokens: 10,\\n 49 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\tcompletion_tokens: 2,\\n 50 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\ttotal_tokens: 12,\\n 51 | \\t\\t\\t\\t\\t\\t\\t\\t\\t},\\n----\\n 62 | \\t\\t\\t\\t\\t\\t\\t\\t\\tusage: {\\n 63 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\tprompt_tokens: 10,\\n 64 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\tcompletion_tokens: 4,\\n 65 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\ttotal_tokens: 14,\\n 66 | \\t\\t\\t\\t\\t\\t\\t\\t\\t},\\n----\\n 77 | \\t\\t\\t\\t\\t\\t\\t\\t\\tusage: {\\n 78 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\tprompt_tokens: 10,\\n 79 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\tcompletion_tokens: 5,\\n 80 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\ttotal_tokens: 15,\\n 81 | \\t\\t\\t\\t\\t\\t\\t\\t\\t},\\n----\\n155 | \\t\\t\\t\\t\\t\\tchoices: [{ message: { role: \\\"assistant\\\", content: \\\"Test response\\\" } }],\\n156 | \\t\\t\\t\\t\\t\\tusage: { prompt_tokens: 10, completion_tokens: 5, total_tokens: 15 },\\n157 | \\t\\t\\t\\t\\t}\\n----\\n177 | \\t\\t\\t\\t\\t\\t\\tusage: {\\n178 | \\t\\t\\t\\t\\t\\t\\t\\tprompt_tokens: 10,\\n179 | \\t\\t\\t\\t\\t\\t\\t\\tcompletion_tokens: 5,\\n180 | \\t\\t\\t\\t\\t\\t\\t\\ttotal_tokens: 15,\\n181 | \\t\\t\\t\\t\\t\\t\\t},\\n----\\n\\n# src/api/providers/__tests__/groq.spec.ts\\n101 | \\t\\t\\t\\t\\t\\t\\tdone: false,\\n102 | \\t\\t\\t\\t\\t\\t\\tvalue: { choices: [{ delta: {} }], usage: { prompt_tokens: 10, completion_tokens: 20 } },\\n103 | \\t\\t\\t\\t\\t\\t})\\n----\\n139 | \\t\\t\\t\\tmodel: modelId,\\n140 | \\t\\t\\t\\tmax_tokens: modelInfo.maxTokens,\\n141 | \\t\\t\\t\\ttemperature: 0.5,\\n----\\n\\n# src/api/providers/__tests__/unbound.spec.ts\\n 82 | \\t\\t\\t\\t\\t\\t\\t\\t\\tusage: {\\n 83 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\tprompt_tokens: 10,\\n 84 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\tcompletion_tokens: 5,\\n 85 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\ttotal_tokens: 15,\\n 86 | \\t\\t\\t\\t\\t\\t\\t\\t\\t},\\n----\\n 91 | \\t\\t\\t\\t\\t\\t\\t\\t\\tusage: {\\n 92 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\tprompt_tokens: 8,\\n 93 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\tcompletion_tokens: 4,\\n 94 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\ttotal_tokens: 12,\\n 95 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\tcache_creation_input_tokens: 3,\\n 96 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\tcache_read_input_tokens: 2,\\n 97 | \\t\\t\\t\\t\\t\\t\\t\\t\\t},\\n----\\n232 | \\t\\t\\t\\t\\ttemperature: 0,\\n233 | \\t\\t\\t\\t\\tmax_tokens: 8192,\\n234 | \\t\\t\\t\\t}),\\n----\\n253 | \\n254 | \\t\\tit(\\\"should not set max_tokens for non-Anthropic models\\\", async () => {\\n255 | \\t\\t\\tmockCreate.mockClear()\\n----\\n277 | \\n278 | \\t\\t\\texpect(mockCreate.mock.calls[0][0]).not.toHaveProperty(\\\"max_tokens\\\")\\n279 | \\t\\t})\\n----\\n\\n# src/api/providers/__tests__/bedrock-invokedModelId.spec.ts\\n176 | \\n177 | \\t\\t// The last usage event should have the token counts from the metadata\\n178 | \\t\\tconst lastUsageEvent = usageEvents[usageEvents.length - 1]\\n179 | \\t\\t// Expect the usage event to include all token information\\n180 | \\t\\texpect(lastUsageEvent).toMatchObject({\\n----\\n183 | \\t\\t\\toutputTokens: 200,\\n184 | \\t\\t\\t// Cache tokens may be present with default values\\n185 | \\t\\t\\tcacheReadTokens: expect.any(Number),\\n----\\n\\n# src/api/providers/requesty.ts\\n 19 | // Requesty usage includes an extra field for Anthropic use cases.\\n 20 | // Safely cast the prompt token details section to the appropriate structure.\\n 21 | interface RequestyUsage extends OpenAI.CompletionUsage {\\n 22 | \\tprompt_tokens_details?: {\\n 23 | \\t\\tcaching_tokens?: number\\n 24 | \\t\\tcached_tokens?: number\\n 25 | \\t}\\n----\\n 76 | \\t\\tconst requestyUsage = usage as RequestyUsage\\n 77 | \\t\\tconst inputTokens = requestyUsage?.prompt_tokens || 0\\n 78 | \\t\\tconst outputTokens = requestyUsage?.completion_tokens || 0\\n 79 | \\t\\tconst cacheWriteTokens = requestyUsage?.prompt_tokens_details?.caching_tokens || 0\\n 80 | \\t\\tconst cacheReadTokens = requestyUsage?.prompt_tokens_details?.cached_tokens || 0\\n 81 | \\t\\tconst totalCost = modelInfo\\n----\\n102 | \\t\\t\\tinfo,\\n103 | \\t\\t\\tmaxTokens: max_tokens,\\n104 | \\t\\t\\ttemperature,\\n----\\n116 | \\t\\t\\tmodel,\\n117 | \\t\\t\\tmax_tokens,\\n118 | \\t\\t\\ttemperature,\\n----\\n150 | \\tasync completePrompt(prompt: string): Promise {\\n151 | \\t\\tconst { id: model, maxTokens: max_tokens, temperature } = await this.fetchModel()\\n152 | \\n----\\n156 | \\t\\t\\tmodel,\\n157 | \\t\\t\\tmax_tokens,\\n158 | \\t\\t\\tmessages: openAiMessages,\\n----\\n\\n# src/api/providers/lite-llm.ts\\n 46 | \\n 47 | \\t\\t// Required by some providers; others default to max tokens allowed\\n 48 | \\t\\tlet maxTokens: number | undefined = info.maxTokens ?? undefined\\n----\\n 51 | \\t\\t\\tmodel: modelId,\\n 52 | \\t\\t\\tmax_tokens: maxTokens,\\n 53 | \\t\\t\\tmessages: openAiMessages,\\n----\\n 84 | \\t\\t\\t\\t\\ttype: \\\"usage\\\",\\n 85 | \\t\\t\\t\\t\\tinputTokens: lastUsage.prompt_tokens || 0,\\n 86 | \\t\\t\\t\\t\\toutputTokens: lastUsage.completion_tokens || 0,\\n 87 | \\t\\t\\t\\t\\tcacheWriteTokens: lastUsage.cache_creation_input_tokens || 0,\\n 88 | \\t\\t\\t\\t\\tcacheReadTokens: lastUsage.prompt_tokens_details?.cached_tokens || 0,\\n 89 | \\t\\t\\t\\t}\\n----\\n121 | \\n122 | \\t\\t\\trequestOptions.max_tokens = info.maxTokens\\n123 | \\n----\\n136 | interface LiteLLMUsage extends OpenAI.CompletionUsage {\\n137 | \\tcache_creation_input_tokens?: number\\n138 | }\\n----\\n\\n# src/api/providers/fetchers/unbound.ts\\n 34 | \\t\\t\\t\\t\\tcase modelId.startsWith(\\\"anthropic/\\\"):\\n 35 | \\t\\t\\t\\t\\t\\t// Set max tokens to 8192 for supported Anthropic models\\n 36 | \\t\\t\\t\\t\\t\\tif (modelInfo.maxTokens !== 4096) {\\n----\\n\\n# src/api/providers/deepseek.ts\\n 32 | \\t\\t\\ttype: \\\"usage\\\",\\n 33 | \\t\\t\\tinputTokens: usage?.prompt_tokens || 0,\\n 34 | \\t\\t\\toutputTokens: usage?.completion_tokens || 0,\\n 35 | \\t\\t\\tcacheWriteTokens: usage?.prompt_tokens_details?.cache_miss_tokens,\\n 36 | \\t\\t\\tcacheReadTokens: usage?.prompt_tokens_details?.cached_tokens,\\n 37 | \\t\\t}\\n----\\n\\n# src/api/providers/openai.ts\\n162 | \\t\\t\\tif (this.options.includeMaxTokens) {\\n163 | \\t\\t\\t\\trequestOptions.max_tokens = modelInfo.maxTokens\\n164 | \\t\\t\\t}\\n----\\n241 | \\t\\t\\ttype: \\\"usage\\\",\\n242 | \\t\\t\\tinputTokens: usage?.prompt_tokens || 0,\\n243 | \\t\\t\\toutputTokens: usage?.completion_tokens || 0,\\n244 | \\t\\t\\tcacheWriteTokens: usage?.cache_creation_input_tokens || undefined,\\n245 | \\t\\t\\tcacheReadTokens: usage?.cache_read_input_tokens || undefined,\\n246 | \\t\\t}\\n----\\n347 | \\t\\t\\t\\t\\ttype: \\\"usage\\\",\\n348 | \\t\\t\\t\\t\\tinputTokens: chunk.usage.prompt_tokens || 0,\\n349 | \\t\\t\\t\\t\\toutputTokens: chunk.usage.completion_tokens || 0,\\n350 | \\t\\t\\t\\t}\\n----\\n\\n# src/api/providers/fetchers/openrouter.ts\\n 21 | \\tmodality: z.string().nullish(),\\n 22 | \\ttokenizer: z.string().nullish(),\\n 23 | })\\n----\\n 35 | \\tcontext_length: z.number(),\\n 36 | \\tmax_completion_tokens: z.number().nullish(),\\n 37 | \\tpricing: openRouterPricingSchema.optional(),\\n----\\n 48 | \\tarchitecture: openRouterArchitectureSchema.optional(),\\n 49 | \\ttop_provider: z.object({ max_completion_tokens: z.number().nullish() }).optional(),\\n 50 | \\tsupported_parameters: z.array(z.string()).optional(),\\n----\\n115 | \\t\\t\\t\\tmodality: architecture?.modality,\\n116 | \\t\\t\\t\\tmaxTokens: top_provider?.max_completion_tokens,\\n117 | \\t\\t\\t\\tsupportedParameters: supported_parameters,\\n----\\n155 | \\t\\t\\t\\tmodality: architecture?.modality,\\n156 | \\t\\t\\t\\tmaxTokens: endpoint.max_completion_tokens,\\n157 | \\t\\t\\t})\\n----\\n\\n# src/api/providers/openai-native.ts\\n146 | \\tprivate async *yieldUsage(info: ModelInfo, usage: OpenAI.Completions.CompletionUsage | undefined): ApiStream {\\n147 | \\t\\tconst inputTokens = usage?.prompt_tokens || 0 // sum of cache hits and misses\\n148 | \\t\\tconst outputTokens = usage?.completion_tokens || 0\\n149 | \\t\\tconst cacheReadTokens = usage?.prompt_tokens_details?.cached_tokens || 0\\n150 | \\t\\tconst cacheWriteTokens = 0\\n----\\n\\n# src/api/providers/fetchers/requesty.ts\\n 31 | \\t\\t\\tconst modelInfo: ModelInfo = {\\n 32 | \\t\\t\\t\\tmaxTokens: rawModel.max_output_tokens,\\n 33 | \\t\\t\\t\\tcontextWindow: rawModel.context_window,\\n----\\n\\n# src/api/providers/anthropic.ts\\n 29 | \\t\\tconst apiKeyFieldName =\\n 30 | \\t\\t\\tthis.options.anthropicBaseUrl && this.options.anthropicUseAuthToken ? \\\"authToken\\\" : \\\"apiKey\\\"\\n 31 | \\n----\\n 75 | \\t\\t\\t\\t\\t\\tmodel: modelId,\\n 76 | \\t\\t\\t\\t\\t\\tmax_tokens: maxTokens ?? ANTHROPIC_DEFAULT_MAX_TOKENS,\\n 77 | \\t\\t\\t\\t\\t\\ttemperature,\\n----\\n124 | \\t\\t\\t\\t\\tmodel: modelId,\\n125 | \\t\\t\\t\\t\\tmax_tokens: maxTokens ?? ANTHROPIC_DEFAULT_MAX_TOKENS,\\n126 | \\t\\t\\t\\t\\ttemperature,\\n----\\n142 | \\t\\t\\t\\t\\t\\ttype: \\\"usage\\\",\\n143 | \\t\\t\\t\\t\\t\\tinputTokens: usage.input_tokens || 0,\\n144 | \\t\\t\\t\\t\\t\\toutputTokens: usage.output_tokens || 0,\\n145 | \\t\\t\\t\\t\\t\\tcacheWriteTokens: usage.cache_creation_input_tokens || undefined,\\n146 | \\t\\t\\t\\t\\t\\tcacheReadTokens: usage.cache_read_input_tokens || undefined,\\n147 | \\t\\t\\t\\t\\t}\\n----\\n151 | \\t\\t\\t\\tcase \\\"message_delta\\\":\\n152 | \\t\\t\\t\\t\\t// Tells us stop_reason, stop_sequence, and output tokens\\n153 | \\t\\t\\t\\t\\t// along the way and at the end of the message.\\n----\\n156 | \\t\\t\\t\\t\\t\\tinputTokens: 0,\\n157 | \\t\\t\\t\\t\\t\\toutputTokens: chunk.usage.output_tokens || 0,\\n158 | \\t\\t\\t\\t\\t}\\n----\\n231 | \\t\\t\\tmodel,\\n232 | \\t\\t\\tmax_tokens: ANTHROPIC_DEFAULT_MAX_TOKENS,\\n233 | \\t\\t\\tthinking: undefined,\\n----\\n243 | \\t/**\\n244 | \\t * Counts tokens for the given content using Anthropic's API\\n245 | \\t *\\n246 | \\t * @param content The content blocks to count tokens for\\n247 | \\t * @returns A promise resolving to the token count\\n248 | \\t */\\n----\\n258 | \\n259 | \\t\\t\\treturn response.input_tokens\\n260 | \\t\\t} catch (error) {\\n261 | \\t\\t\\t// Log error but fallback to tiktoken estimation\\n262 | \\t\\t\\tconsole.warn(\\\"Anthropic token counting failed, using fallback\\\", error)\\n263 | \\n----\\n\\n# src/api/providers/base-openai-compatible-provider.ts\\n 71 | \\t\\t\\tid: model,\\n 72 | \\t\\t\\tinfo: { maxTokens: max_tokens },\\n 73 | \\t\\t} = this.getModel()\\n----\\n 78 | \\t\\t\\tmodel,\\n 79 | \\t\\t\\tmax_tokens,\\n 80 | \\t\\t\\ttemperature,\\n----\\n100 | \\t\\t\\t\\t\\ttype: \\\"usage\\\",\\n101 | \\t\\t\\t\\t\\tinputTokens: chunk.usage.prompt_tokens || 0,\\n102 | \\t\\t\\t\\t\\toutputTokens: chunk.usage.completion_tokens || 0,\\n103 | \\t\\t\\t\\t}\\n----\\n\\n# src/api/providers/xai.ts\\n 52 | \\t\\t\\tmodel: modelId,\\n 53 | \\t\\t\\tmax_tokens: modelInfo.maxTokens,\\n 54 | \\t\\t\\ttemperature: this.options.modelTemperature ?? XAI_DEFAULT_TEMPERATURE,\\n----\\n 78 | \\t\\t\\tif (chunk.usage) {\\n 79 | \\t\\t\\t\\t// Extract detailed token information if available\\n 80 | \\t\\t\\t\\t// First check for prompt_tokens_details structure (real API response)\\n 81 | \\t\\t\\t\\tconst promptDetails = \\\"prompt_tokens_details\\\" in chunk.usage ? chunk.usage.prompt_tokens_details : null;\\n 82 | \\t\\t\\t\\tconst cachedTokens = promptDetails && \\\"cached_tokens\\\" in promptDetails ? promptDetails.cached_tokens : 0;\\n 83 | \\n 84 | \\t\\t\\t\\t// Fall back to direct fields in usage (used in test mocks)\\n 85 | \\t\\t\\t\\tconst readTokens = cachedTokens || (\\\"cache_read_input_tokens\\\" in chunk.usage ? (chunk.usage as any).cache_read_input_tokens : 0);\\n 86 | \\t\\t\\t\\tconst writeTokens = \\\"cache_creation_input_tokens\\\" in chunk.usage ? (chunk.usage as any).cache_creation_input_tokens : 0;\\n 87 | \\n----\\n 89 | \\t\\t\\t\\t\\ttype: \\\"usage\\\",\\n 90 | \\t\\t\\t\\t\\tinputTokens: chunk.usage.prompt_tokens || 0,\\n 91 | \\t\\t\\t\\t\\toutputTokens: chunk.usage.completion_tokens || 0,\\n 92 | \\t\\t\\t\\t\\tcacheReadTokens: readTokens,\\n----\\n\\n# src/api/providers/bedrock.ts\\n 52 | \\t\\ttype: \\\"enabled\\\"\\n 53 | \\t\\tbudget_tokens: number\\n 54 | \\t}\\n----\\n109 | \\tmessageStop?: {\\n110 | \\t\\tstopReason?: \\\"end_turn\\\" | \\\"tool_use\\\" | \\\"max_tokens\\\" | \\\"stop_sequence\\\"\\n111 | \\t\\tadditionalModelResponseFields?: Record\\n----\\n349 | \\t\\t\\t\\t\\ttype: \\\"enabled\\\",\\n350 | \\t\\t\\t\\t\\tbudget_tokens: metadata?.thinking?.maxThinkingTokens || modelConfig.reasoningBudget || 4096,\\n351 | \\t\\t\\t\\t},\\n----\\n418 | \\n419 | \\t\\t\\t\\t\\t// Check both field naming conventions for cache tokens\\n420 | \\t\\t\\t\\t\\tconst cacheReadTokens = usage.cacheReadInputTokens || usage.cacheReadInputTokenCount || 0\\n----\\n422 | \\n423 | \\t\\t\\t\\t\\t// Always include all available token information\\n424 | \\t\\t\\t\\t\\tyield {\\n----\\n450 | \\n451 | \\t\\t\\t\\t\\t\\t\\t// Check both field naming conventions for cache tokens\\n452 | \\t\\t\\t\\t\\t\\t\\tconst cacheReadTokens =\\n----\\n1049 | \\t\\tTOO_MANY_TOKENS: {\\n1050 | \\t\\t\\tpatterns: [\\\"too many tokens\\\"],\\n1051 | \\t\\t\\tmessageTemplate: `\\\"Too many tokens\\\" error detected.\\n1052 | Possible Causes:\\n1053 | 1. Input exceeds model's context window limit\\n1054 | 2. Rate limiting (too many tokens per minute)\\n1055 | 3. Quota exceeded for token usage\\n1056 | 4. Other token-related service limitations\\n1057 | \\n----\\n\\n# src/api/providers/fetchers/litellm.ts\\n 52 | \\t\\t\\t\\tmodels[modelName] = {\\n 53 | \\t\\t\\t\\t\\tmaxTokens: modelInfo.max_tokens || 8192,\\n 54 | \\t\\t\\t\\t\\tcontextWindow: modelInfo.max_input_tokens || 200000,\\n 55 | \\t\\t\\t\\t\\tsupportsImages: Boolean(modelInfo.supports_vision),\\n----\\n 58 | \\t\\t\\t\\t\\tsupportsPromptCache: Boolean(modelInfo.supports_prompt_caching),\\n 59 | \\t\\t\\t\\t\\tinputPrice: modelInfo.input_cost_per_token ? modelInfo.input_cost_per_token * 1000000 : undefined,\\n 60 | \\t\\t\\t\\t\\toutputPrice: modelInfo.output_cost_per_token\\n 61 | \\t\\t\\t\\t\\t\\t? modelInfo.output_cost_per_token * 1000000\\n 62 | \\t\\t\\t\\t\\t\\t: undefined,\\n 63 | \\t\\t\\t\\t\\tcacheWritesPrice: modelInfo.cache_creation_input_token_cost\\n 64 | \\t\\t\\t\\t\\t\\t? modelInfo.cache_creation_input_token_cost * 1000000\\n 65 | \\t\\t\\t\\t\\t\\t: undefined,\\n 66 | \\t\\t\\t\\t\\tcacheReadsPrice: modelInfo.cache_read_input_token_cost\\n 67 | \\t\\t\\t\\t\\t\\t? modelInfo.cache_read_input_token_cost * 1000000\\n 68 | \\t\\t\\t\\t\\t\\t: undefined,\\n----\\n\\n# src/api/providers/fetchers/__tests__/openrouter.spec.ts\\n139 | \\t\\t\\t\\tsupportsReasoningEffort: false,\\n140 | \\t\\t\\t\\tsupportedParameters: [\\\"max_tokens\\\", \\\"temperature\\\", \\\"reasoning\\\", \\\"include_reasoning\\\"],\\n141 | \\t\\t\\t})\\n----\\n156 | \\t\\t\\t\\tsupportsReasoningEffort: true,\\n157 | \\t\\t\\t\\tsupportedParameters: [\\\"max_tokens\\\", \\\"temperature\\\", \\\"reasoning\\\", \\\"include_reasoning\\\"],\\n158 | \\t\\t\\t})\\n----\\n\\n# src/api/providers/ollama.ts\\n 79 | \\t\\t\\t\\ttype: \\\"usage\\\",\\n 80 | \\t\\t\\t\\tinputTokens: lastUsage?.prompt_tokens || 0,\\n 81 | \\t\\t\\t\\toutputTokens: lastUsage?.completion_tokens || 0,\\n 82 | \\t\\t\\t}\\n----\\n\\n# src/api/providers/base-provider.ts\\n 21 | \\t/**\\n 22 | \\t * Default token counting implementation using tiktoken.\\n 23 | \\t * Providers can override this to use their native token counting endpoints.\\n 24 | \\t *\\n 25 | \\t * @param content The content to count tokens for\\n 26 | \\t * @returns A promise resolving to the token count\\n 27 | \\t */\\n----\\n\\n# src/api/providers/fetchers/__tests__/litellm.test.ts\\n 41 | \\t\\t\\t\\t\\t\\tmodel_info: {\\n 42 | \\t\\t\\t\\t\\t\\t\\tmax_tokens: 4096,\\n 43 | \\t\\t\\t\\t\\t\\t\\tmax_input_tokens: 200000,\\n 44 | \\t\\t\\t\\t\\t\\t\\tsupports_vision: true,\\n 45 | \\t\\t\\t\\t\\t\\t\\tsupports_prompt_caching: false,\\n 46 | \\t\\t\\t\\t\\t\\t\\tinput_cost_per_token: 0.000003,\\n 47 | \\t\\t\\t\\t\\t\\t\\toutput_cost_per_token: 0.000015,\\n 48 | \\t\\t\\t\\t\\t\\t\\tsupports_computer_use: true,\\n----\\n 56 | \\t\\t\\t\\t\\t\\tmodel_info: {\\n 57 | \\t\\t\\t\\t\\t\\t\\tmax_tokens: 8192,\\n 58 | \\t\\t\\t\\t\\t\\t\\tmax_input_tokens: 128000,\\n 59 | \\t\\t\\t\\t\\t\\t\\tsupports_vision: false,\\n 60 | \\t\\t\\t\\t\\t\\t\\tsupports_prompt_caching: false,\\n 61 | \\t\\t\\t\\t\\t\\t\\tinput_cost_per_token: 0.00001,\\n 62 | \\t\\t\\t\\t\\t\\t\\toutput_cost_per_token: 0.00003,\\n 63 | \\t\\t\\t\\t\\t\\t\\tsupports_computer_use: false,\\n----\\n108 | \\n109 | \\tit(\\\"makes request without authorization header when no API key provided\\\", async () => {\\n110 | \\t\\tconst mockResponse = {\\n----\\n134 | \\t\\t\\t\\t\\t\\tmodel_info: {\\n135 | \\t\\t\\t\\t\\t\\t\\tmax_tokens: 4096,\\n136 | \\t\\t\\t\\t\\t\\t\\tmax_input_tokens: 200000,\\n137 | \\t\\t\\t\\t\\t\\t\\tsupports_vision: true,\\n----\\n146 | \\t\\t\\t\\t\\t\\tmodel_info: {\\n147 | \\t\\t\\t\\t\\t\\t\\tmax_tokens: 4096,\\n148 | \\t\\t\\t\\t\\t\\t\\tmax_input_tokens: 200000,\\n149 | \\t\\t\\t\\t\\t\\t\\tsupports_vision: false,\\n----\\n205 | \\t\\t\\t\\tstatus: 401,\\n206 | \\t\\t\\t\\tstatusText: \\\"Unauthorized\\\",\\n207 | \\t\\t\\t},\\n----\\n214 | \\t\\tawait expect(getLiteLLMModels(DUMMY_INVALID_KEY, \\\"http://localhost:4000\\\")).rejects.toThrow(\\n215 | \\t\\t\\t\\\"Failed to fetch LiteLLM models: 401 Unauthorized. Check base URL and API key.\\\",\\n216 | \\t\\t)\\n----\\n278 | \\t\\t\\t\\t\\t\\tmodel_info: {\\n279 | \\t\\t\\t\\t\\t\\t\\tmax_tokens: 4096,\\n280 | \\t\\t\\t\\t\\t\\t\\tmax_input_tokens: 200000,\\n281 | \\t\\t\\t\\t\\t\\t\\tsupports_vision: true,\\n----\\n291 | \\t\\t\\t\\t\\t\\tmodel_info: {\\n292 | \\t\\t\\t\\t\\t\\t\\tmax_tokens: 8192,\\n293 | \\t\\t\\t\\t\\t\\t\\tmax_input_tokens: 128000,\\n294 | \\t\\t\\t\\t\\t\\t\\tsupports_vision: false,\\n----\\n339 | \\t\\t\\t\\t\\t\\tmodel_info: {\\n340 | \\t\\t\\t\\t\\t\\t\\tmax_tokens: 4096,\\n341 | \\t\\t\\t\\t\\t\\t\\tmax_input_tokens: 200000,\\n342 | \\t\\t\\t\\t\\t\\t\\tsupports_vision: true,\\n----\\n352 | \\t\\t\\t\\t\\t\\tmodel_info: {\\n353 | \\t\\t\\t\\t\\t\\t\\tmax_tokens: 8192,\\n354 | \\t\\t\\t\\t\\t\\t\\tmax_input_tokens: 128000,\\n355 | \\t\\t\\t\\t\\t\\t\\tsupports_vision: false,\\n----\\n365 | \\t\\t\\t\\t\\t\\tmodel_info: {\\n366 | \\t\\t\\t\\t\\t\\t\\tmax_tokens: 8192,\\n367 | \\t\\t\\t\\t\\t\\t\\tmax_input_tokens: 128000,\\n368 | \\t\\t\\t\\t\\t\\t\\tsupports_vision: false,\\n----\\n424 | \\t\\t\\t\\t\\t\\tmodel_info: {\\n425 | \\t\\t\\t\\t\\t\\t\\tmax_tokens: 4096,\\n426 | \\t\\t\\t\\t\\t\\t\\tmax_input_tokens: 200000,\\n427 | \\t\\t\\t\\t\\t\\t\\tsupports_vision: true,\\n----\\n436 | \\t\\t\\t\\t\\t\\tmodel_info: {\\n437 | \\t\\t\\t\\t\\t\\t\\tmax_tokens: 4096,\\n438 | \\t\\t\\t\\t\\t\\t\\tmax_input_tokens: 200000,\\n439 | \\t\\t\\t\\t\\t\\t\\tsupports_vision: true,\\n----\\n448 | \\t\\t\\t\\t\\t\\tmodel_info: {\\n449 | \\t\\t\\t\\t\\t\\t\\tmax_tokens: 4096,\\n450 | \\t\\t\\t\\t\\t\\t\\tmax_input_tokens: 200000,\\n451 | \\t\\t\\t\\t\\t\\t\\tsupports_vision: true,\\n----\\n\\n# src/api/providers/anthropic-vertex.ts\\n 2 | import { AnthropicVertex } from \\\"@anthropic-ai/vertex-sdk\\\"\\n 3 | import { GoogleAuth, JWTInput } from \\\"google-auth-library\\\"\\n 4 | \\n----\\n 41 | \\t\\t\\t\\tgoogleAuth: new GoogleAuth({\\n 42 | \\t\\t\\t\\t\\tscopes: [\\\"https://www.googleapis.com/auth/cloud-platform\\\"],\\n 43 | \\t\\t\\t\\t\\tcredentials: safeJsonParse(this.options.vertexJsonCredentials, undefined),\\n----\\n 50 | \\t\\t\\t\\tgoogleAuth: new GoogleAuth({\\n 51 | \\t\\t\\t\\t\\tscopes: [\\\"https://www.googleapis.com/auth/cloud-platform\\\"],\\n 52 | \\t\\t\\t\\t\\tkeyFile: this.options.vertexKeyFile,\\n----\\n 87 | \\t\\t\\tmodel: id,\\n 88 | \\t\\t\\tmax_tokens: maxTokens ?? ANTHROPIC_DEFAULT_MAX_TOKENS,\\n 89 | \\t\\t\\ttemperature,\\n----\\n107 | \\t\\t\\t\\t\\t\\ttype: \\\"usage\\\",\\n108 | \\t\\t\\t\\t\\t\\tinputTokens: usage.input_tokens || 0,\\n109 | \\t\\t\\t\\t\\t\\toutputTokens: usage.output_tokens || 0,\\n110 | \\t\\t\\t\\t\\t\\tcacheWriteTokens: usage.cache_creation_input_tokens || undefined,\\n111 | \\t\\t\\t\\t\\t\\tcacheReadTokens: usage.cache_read_input_tokens || undefined,\\n112 | \\t\\t\\t\\t\\t}\\n----\\n119 | \\t\\t\\t\\t\\t\\tinputTokens: 0,\\n120 | \\t\\t\\t\\t\\t\\toutputTokens: chunk.usage!.output_tokens || 0,\\n121 | \\t\\t\\t\\t\\t}\\n----\\n189 | \\t\\t\\t\\tmodel: id,\\n190 | \\t\\t\\t\\tmax_tokens: maxTokens,\\n191 | \\t\\t\\t\\ttemperature,\\n----\\n\\n# src/services/mcp/__tests__/McpHub.test.ts\\n 50 | \\t\\t\\tscheme: \\\"file\\\",\\n 51 | \\t\\t\\tauthority: \\\"\\\",\\n 52 | \\t\\t\\tpath: \\\"/test/path\\\",\\n----\\n\\n# src/services/code-index/embedders/openai.ts\\n 52 | \\t\\t\\t\\t\\tconsole.warn(\\n 53 | \\t\\t\\t\\t\\t\\t`Text at index ${i} exceeds maximum token limit (${itemTokens} > ${MAX_ITEM_TOKENS}). Skipping.`,\\n 54 | \\t\\t\\t\\t\\t)\\n----\\n108 | \\t\\t\\t\\t\\tusage: {\\n109 | \\t\\t\\t\\t\\t\\tpromptTokens: response.usage?.prompt_tokens || 0,\\n110 | \\t\\t\\t\\t\\t\\ttotalTokens: response.usage?.total_tokens || 0,\\n111 | \\t\\t\\t\\t\\t},\\n----\\n\\n# src/services/code-index/embedders/openai-compatible.ts\\n 18 | \\tusage?: {\\n 19 | \\t\\tprompt_tokens?: number\\n 20 | \\t\\ttotal_tokens?: number\\n 21 | \\t}\\n----\\n 34 | \\t * @param baseUrl The base URL for the OpenAI-compatible API endpoint\\n 35 | \\t * @param apiKey The API key for authentication\\n 36 | \\t * @param modelId Optional model identifier (defaults to \\\"text-embedding-3-small\\\")\\n----\\n 75 | \\t\\t\\t\\t\\tconsole.warn(\\n 76 | \\t\\t\\t\\t\\t\\t`Text at index ${i} exceeds maximum token limit (${itemTokens} > ${MAX_ITEM_TOKENS}). Skipping.`,\\n 77 | \\t\\t\\t\\t\\t)\\n----\\n156 | \\t\\t\\t\\t\\tusage: {\\n157 | \\t\\t\\t\\t\\t\\tpromptTokens: response.usage?.prompt_tokens || 0,\\n158 | \\t\\t\\t\\t\\t\\ttotalTokens: response.usage?.total_tokens || 0,\\n159 | \\t\\t\\t\\t\\t},\\n----\\n\\n# src/services/marketplace/types.ts\\n 20 | \\tdescription: string\\n 21 | \\tauthor?: string\\n 22 | \\tauthorUrl?: string\\n 23 | \\ttags?: string[]\\n----\\n 45 | \\tdescription: string\\n 46 | \\tauthor?: string\\n 47 | \\tauthorUrl?: string\\n 48 | \\turl: string // Required url field\\n----\\n 62 | \\ttype: MarketplaceItemType\\n 63 | \\tauthor?: string\\n 64 | \\tauthorUrl?: string\\n 65 | \\turl?: string // Optional - only MCPs have url\\n----\\n\\n# src/services/code-index/embedders/__tests__/openai-compatible.spec.ts\\n103 | \\t\\t\\t\\tdata: [{ embedding: [0.1, 0.2, 0.3] }],\\n104 | \\t\\t\\t\\tusage: { prompt_tokens: 10, total_tokens: 15 },\\n105 | \\t\\t\\t}\\n----\\n124 | \\t\\t\\t\\tdata: [{ embedding: [0.1, 0.2, 0.3] }, { embedding: [0.4, 0.5, 0.6] }],\\n125 | \\t\\t\\t\\tusage: { prompt_tokens: 20, total_tokens: 30 },\\n126 | \\t\\t\\t}\\n----\\n149 | \\t\\t\\t\\tdata: [{ embedding: [0.1, 0.2, 0.3] }],\\n150 | \\t\\t\\t\\tusage: { prompt_tokens: 10, total_tokens: 15 },\\n151 | \\t\\t\\t}\\n----\\n194 | \\t\\t\\t\\t\\tdata: [{ embedding: base64String }], // Base64 string instead of array\\n195 | \\t\\t\\t\\t\\tusage: { prompt_tokens: 10, total_tokens: 15 },\\n196 | \\t\\t\\t\\t}\\n----\\n226 | \\t\\t\\t\\t\\tdata: [{ embedding: base64String1 }, { embedding: base64String2 }],\\n227 | \\t\\t\\t\\t\\tusage: { prompt_tokens: 20, total_tokens: 30 },\\n228 | \\t\\t\\t\\t}\\n----\\n253 | \\t\\t\\t\\t\\t],\\n254 | \\t\\t\\t\\t\\tusage: { prompt_tokens: 20, total_tokens: 30 },\\n255 | \\t\\t\\t\\t}\\n----\\n270 | \\t\\t/**\\n271 | \\t\\t * Test batching logic when texts exceed token limits\\n272 | \\t\\t */\\n----\\n283 | \\t\\t\\t\\t\\t],\\n284 | \\t\\t\\t\\t\\tusage: { prompt_tokens: 10, total_tokens: 15 },\\n285 | \\t\\t\\t\\t})\\n----\\n299 | \\t\\t\\t\\t\\tdata: [{ embedding: [0.1, 0.2, 0.3] }, { embedding: [0.4, 0.5, 0.6] }],\\n300 | \\t\\t\\t\\t\\tusage: { prompt_tokens: 10, total_tokens: 15 },\\n301 | \\t\\t\\t\\t}\\n----\\n306 | \\t\\t\\t\\t// Should warn about oversized text\\n307 | \\t\\t\\t\\texpect(console.warn).toHaveBeenCalledWith(expect.stringContaining(\\\"exceeds maximum token limit\\\"))\\n308 | \\n----\\n317 | \\t\\t\\t\\t\\tdata: [{ embedding: [0.1, 0.2, 0.3] }, { embedding: [0.4, 0.5, 0.6] }],\\n318 | \\t\\t\\t\\t\\tusage: { prompt_tokens: 10, total_tokens: 15 },\\n319 | \\t\\t\\t\\t})\\n----\\n354 | \\t\\t\\t\\t\\t\\tdata: [{ embedding: base64String }],\\n355 | \\t\\t\\t\\t\\t\\tusage: { prompt_tokens: 10, total_tokens: 15 },\\n356 | \\t\\t\\t\\t\\t})\\n----\\n375 | \\t\\t\\t\\tconst testTexts = [\\\"Hello world\\\"]\\n376 | \\t\\t\\t\\tconst authError = new Error(\\\"Unauthorized\\\")\\n377 | \\t\\t\\t\\t;(authError as any).status = 401\\n378 | \\n379 | \\t\\t\\t\\tmockEmbeddingsCreate.mockRejectedValue(authError)\\n380 | \\n----\\n452 | \\t\\t\\t\\t\\tdata: null,\\n453 | \\t\\t\\t\\t\\tusage: { prompt_tokens: 10, total_tokens: 15 },\\n454 | \\t\\t\\t\\t}\\n----\\n494 | \\t\\t\\t\\t\\tusage: {\\n495 | \\t\\t\\t\\t\\t\\tprompt_tokens: 2,\\n496 | \\t\\t\\t\\t\\t\\ttotal_tokens: 2,\\n497 | \\t\\t\\t\\t\\t},\\n----\\n\\n# src/services/marketplace/schemas.ts\\n 35 | \\ttype: marketplaceItemTypeSchema,\\n 36 | \\tauthor: z.string().optional(),\\n 37 | \\tauthorUrl: z.string().url(\\\"Author URL must be a valid URL\\\").optional(),\\n 38 | \\ttags: z.array(z.string()).optional(),\\n----\\n 61 | \\tdescription: z.string(),\\n 62 | \\tauthor: z.string().optional(),\\n 63 | \\tauthorUrl: z.string().url().optional(),\\n 64 | \\ttags: z.array(z.string()).optional(),\\n----\\n 72 | \\tdescription: z.string(),\\n 73 | \\tauthor: z.string().optional(),\\n 74 | \\tauthorUrl: z.string().url().optional(),\\n 75 | \\turl: z.string().url(), // Required url field\\n----\\n\\n# src/services/tree-sitter/__tests__/fixtures/sample-embedded_template.ts\\n 35 |
\\n 36 | <% if user.authenticated? %>\\n 37 |

Welcome, <%= user.name %>

\\n----\\n\\n# src/services/tree-sitter/__tests__/parseSourceCodeDefinitions.embedded_template.test.ts\\n 43 | \\tit(\\\"should detect control structures\\\", () => {\\n 44 | \\t\\texpect(parseResult).toMatch(/\\\\d+--\\\\d+ \\\\|\\\\s+<% if user\\\\.authenticated\\\\? %>/)\\n 45 | \\t\\texpect(parseResult).toMatch(/\\\\d+--\\\\d+ \\\\|\\\\s+<% user\\\\.posts\\\\.each do \\\\|post\\\\| %>/)\\n----\\n\\n# src/workers/countTokens.ts\\n 4 | \\n 5 | import { tiktoken } from \\\"../utils/tiktoken\\\"\\n 6 | \\n----\\n 10 | \\ttry {\\n 11 | \\t\\tconst count = await tiktoken(content)\\n 12 | \\t\\treturn { success: true, count }\\n----\\n\\n# src/activate/handleUri.ts\\n 37 | \\t\\t}\\n 38 | \\t\\tcase \\\"/auth/clerk/callback\\\": {\\n 39 | \\t\\t\\tconst code = query.get(\\\"code\\\")\\n----\\n\\n# src/__tests__/dist_assets.test.ts\\n 6 | \\n 7 | \\tdescribe(\\\"tiktoken\\\", () => {\\n 8 | \\t\\tit(\\\"should have tiktoken wasm file\\\", () => {\\n 9 | \\t\\t\\texpect(fs.existsSync(path.join(distPath, \\\"tiktoken_bg.wasm\\\"))).toBe(true)\\n 10 | \\t\\t})\\n----\\n\\n# src/integrations/terminal/ShellIntegrationManager.ts\\n 43 | \\t[ -f \\\"$ZDOTDIR/.zshrc\\\" ] && source \\\"$ZDOTDIR/.zshrc\\\"\\n 44 | \\t[ -f \\\"$ZDOTDIR/.zlogin\\\" ] && source \\\"$ZDOTDIR/.zlogin\\\"\\n 45 | \\t[ \\\"$ZDOTDIR\\\" = \\\"$HOME\\\" ] && unset ZDOTDIR\\n----\\n\\n# src/extension/api.ts\\n219 | \\n220 | \\t\\t\\tcline.on(\\\"taskCompleted\\\", async (_, tokenUsage, toolUsage) => {\\n221 | \\t\\t\\t\\tthis.emit(RooCodeEventName.TaskCompleted, cline.taskId, tokenUsage, toolUsage)\\n222 | \\t\\t\\t\\tthis.taskMap.delete(cline.taskId)\\n----\\n224 | \\t\\t\\t\\tawait this.fileLog(\\n225 | \\t\\t\\t\\t\\t`[${new Date().toISOString()}] taskCompleted -> ${cline.taskId} | ${JSON.stringify(tokenUsage, null, 2)} | ${JSON.stringify(toolUsage, null, 2)}\\\\n`,\\n226 | \\t\\t\\t\\t)\\n----\\n\\n# src/utils/countTokens.ts\\n 4 | import { countTokensResultSchema } from \\\"../workers/types\\\"\\n 5 | import { tiktoken } from \\\"./tiktoken\\\"\\n 6 | \\n----\\n 27 | \\tif (!useWorker || !pool) {\\n 28 | \\t\\treturn tiktoken(content)\\n 29 | \\t}\\n----\\n 42 | \\t\\tconsole.error(error)\\n 43 | \\t\\treturn tiktoken(content)\\n 44 | \\t}\\n----\\n\\n# src/core/checkpoints/index.ts\\n215 | \\t\\t\\t\\tJSON.stringify({\\n216 | \\t\\t\\t\\t\\ttokensIn: totalTokensIn,\\n217 | \\t\\t\\t\\t\\ttokensOut: totalTokensOut,\\n218 | \\t\\t\\t\\t\\tcacheWrites: totalCacheWrites,\\n----\\n\\n# src/core/condense/index.ts\\n 55 | \\tcost: number // The cost of the summarization operation\\n 56 | \\tnewContextTokens?: number // The number of tokens in the context for the next API request\\n 57 | \\terror?: string // Populated iff the operation fails: error message shown to the user on failure (see Task.ts)\\n----\\n 63 | * @param {ApiMessage[]} messages - The conversation messages\\n 64 | * @param {ApiHandler} apiHandler - The API handler to use for token counting.\\n 65 | * @param {string} systemPrompt - The system prompt for API requests, which should be considered in the context token count\\n 66 | * @param {string} taskId - The task ID for the conversation, used for telemetry\\n----\\n 73 | * @param {ApiMessage[]} messages - The conversation messages\\n 74 | * @param {ApiHandler} apiHandler - The API handler to use for token counting (fallback if condensingApiHandler not provided)\\n 75 | * @param {string} systemPrompt - The system prompt for API requests (fallback if customCondensingPrompt not provided)\\n 76 | * @param {string} taskId - The task ID for the conversation, used for telemetry\\n 77 | * @param {number} prevContextTokens - The number of tokens currently in the context, used to ensure we don't grow the context\\n 78 | * @param {boolean} isAutomaticTrigger - Whether the summarization is triggered automatically\\n----\\n186 | \\n187 | \\t// Count the tokens in the context for the next API request\\n188 | \\t// We only estimate the tokens in summaryMesage if outputTokens is 0, otherwise we use outputTokens\\n189 | \\tconst systemPromptMessage: ApiMessage = { role: \\\"user\\\", content: systemPrompt }\\n----\\n\\nLoading...\"}" +} +[2025-06-13T04:59:50.474Z] { + "ts": 1749790789409, + "type": "say", + "say": "text", + "text": "I can see this is a VSCode extension project, but the search results show mostly token-related code rather than authentication/OAuth code. Let me search more specifically for authentication and OAuth related files.", + "partial": false +} +[2025-06-13T04:59:51.246Z] { + "ts": 1749790790474, + "type": "ask", + "ask": "tool", + "text": "{\"tool\":\"searchFiles\",\"path\":\"src\",\"regex\":\"oauth|auth|login|clerk|session|refresh\",\"filePattern\":\"*.ts\",\"content\":\"Found 83 results.\\n\\n# src/services/ripgrep/index.ts\\n101 | \\t\\tconst rgProcess = childProcess.spawn(bin, args)\\n102 | \\t\\t// cross-platform alternative to head, which is ripgrep author's recommendation for limiting output.\\n103 | \\t\\tconst rl = readline.createInterface({\\n----\\n\\n# src/services/browser/BrowserSession.ts\\n543 | \\t\\t\\tconst [width, height] = size.split(\\\",\\\").map(Number)\\n544 | \\t\\t\\tconst session = await page.createCDPSession()\\n545 | \\t\\t\\tawait page.setViewport({ width, height })\\n546 | \\t\\t\\tconst { windowId } = await session.send(\\\"Browser.getWindowForTarget\\\")\\n547 | \\t\\t\\tawait session.send(\\\"Browser.setWindowBounds\\\", {\\n548 | \\t\\t\\t\\tbounds: { width, height },\\n----\\n\\n# src/services/mcp/__tests__/McpHub.test.ts\\n 50 | \\t\\t\\tscheme: \\\"file\\\",\\n 51 | \\t\\t\\tauthority: \\\"\\\",\\n 52 | \\t\\t\\tpath: \\\"/test/path\\\",\\n----\\n\\n# src/services/mcp/McpHub.ts\\n1092 | \\n1093 | \\tpublic async refreshAllConnections(): Promise {\\n1094 | \\t\\tif (this.isConnecting) {\\n1095 | \\t\\t\\tvscode.window.showInformationMessage(t(\\\"mcp:info.already_refreshing\\\"))\\n1096 | \\t\\t\\treturn\\n----\\n1099 | \\t\\tthis.isConnecting = true\\n1100 | \\t\\tvscode.window.showInformationMessage(t(\\\"mcp:info.refreshing_all\\\"))\\n1101 | \\n----\\n1151 | \\n1152 | \\t\\t\\tvscode.window.showInformationMessage(t(\\\"mcp:info.all_refreshed\\\"))\\n1153 | \\t\\t} catch (error) {\\n1154 | \\t\\t\\tthis.showErrorMessage(\\\"Failed to refresh MCP servers\\\", error)\\n1155 | \\t\\t} finally {\\n----\\n1244 | \\n1245 | \\t\\t\\t\\t\\t// Only refresh capabilities if connected\\n1246 | \\t\\t\\t\\t\\tif (connection.server.status === \\\"connected\\\") {\\n----\\n1254 | \\t\\t\\t\\t} catch (error) {\\n1255 | \\t\\t\\t\\t\\tconsole.error(`Failed to refresh capabilities for ${serverName}:`, error)\\n1256 | \\t\\t\\t\\t}\\n----\\n\\n# src/services/code-index/embedders/__tests__/openai-compatible.spec.ts\\n375 | \\t\\t\\t\\tconst testTexts = [\\\"Hello world\\\"]\\n376 | \\t\\t\\t\\tconst authError = new Error(\\\"Unauthorized\\\")\\n377 | \\t\\t\\t\\t;(authError as any).status = 401\\n378 | \\n379 | \\t\\t\\t\\tmockEmbeddingsCreate.mockRejectedValue(authError)\\n380 | \\n----\\n\\n# src/services/code-index/embedders/openai-compatible.ts\\n 34 | \\t * @param baseUrl The base URL for the OpenAI-compatible API endpoint\\n 35 | \\t * @param apiKey The API key for authentication\\n 36 | \\t * @param modelId Optional model identifier (defaults to \\\"text-embedding-3-small\\\")\\n----\\n\\n# src/services/marketplace/schemas.ts\\n 35 | \\ttype: marketplaceItemTypeSchema,\\n 36 | \\tauthor: z.string().optional(),\\n 37 | \\tauthorUrl: z.string().url(\\\"Author URL must be a valid URL\\\").optional(),\\n 38 | \\ttags: z.array(z.string()).optional(),\\n----\\n 61 | \\tdescription: z.string(),\\n 62 | \\tauthor: z.string().optional(),\\n 63 | \\tauthorUrl: z.string().url().optional(),\\n 64 | \\ttags: z.array(z.string()).optional(),\\n----\\n 72 | \\tdescription: z.string(),\\n 73 | \\tauthor: z.string().optional(),\\n 74 | \\tauthorUrl: z.string().url().optional(),\\n 75 | \\turl: z.string().url(), // Required url field\\n----\\n\\n# src/services/tree-sitter/__tests__/fixtures/sample-embedded_template.ts\\n 35 |
\\n 36 | <% if user.authenticated? %>\\n 37 |

Welcome, <%= user.name %>

\\n----\\n\\n# src/services/tree-sitter/__tests__/parseSourceCodeDefinitions.embedded_template.test.ts\\n 43 | \\tit(\\\"should detect control structures\\\", () => {\\n 44 | \\t\\texpect(parseResult).toMatch(/\\\\d+--\\\\d+ \\\\|\\\\s+<% if user\\\\.authenticated\\\\? %>/)\\n 45 | \\t\\texpect(parseResult).toMatch(/\\\\d+--\\\\d+ \\\\|\\\\s+<% user\\\\.posts\\\\.each do \\\\|post\\\\| %>/)\\n----\\n\\n# src/services/marketplace/types.ts\\n 20 | \\tdescription: string\\n 21 | \\tauthor?: string\\n 22 | \\tauthorUrl?: string\\n 23 | \\ttags?: string[]\\n----\\n 45 | \\tdescription: string\\n 46 | \\tauthor?: string\\n 47 | \\tauthorUrl?: string\\n 48 | \\turl: string // Required url field\\n----\\n 62 | \\ttype: MarketplaceItemType\\n 63 | \\tauthor?: string\\n 64 | \\tauthorUrl?: string\\n 65 | \\turl?: string // Optional - only MCPs have url\\n----\\n\\n# src/activate/handleUri.ts\\n 37 | \\t\\t}\\n 38 | \\t\\tcase \\\"/auth/clerk/callback\\\": {\\n 39 | \\t\\t\\tconst code = query.get(\\\"code\\\")\\n----\\n\\n# src/api/providers/bedrock.ts\\n235 | \\t\\t\\t\\tsecretAccessKey: this.options.awsSecretKey,\\n236 | \\t\\t\\t\\t...(this.options.awsSessionToken ? { sessionToken: this.options.awsSessionToken } : {}),\\n237 | \\t\\t\\t}\\n----\\n\\n# src/api/providers/gemini.ts\\n 7 | } from \\\"@google/genai\\\"\\n 8 | import type { JWTInput } from \\\"google-auth-library\\\"\\n 9 | \\n----\\n\\n# src/api/providers/__tests__/anthropic.spec.ts\\n112 | \\t\\t\\texpect(mockAnthropicConstructor.mock.calls[0]![0]!.apiKey).toEqual(\\\"test-api-key\\\")\\n113 | \\t\\t\\texpect(mockAnthropicConstructor.mock.calls[0]![0]!.authToken).toBeUndefined()\\n114 | \\t\\t})\\n----\\n123 | \\t\\t\\texpect(mockAnthropicConstructor.mock.calls[0]![0]!.apiKey).toEqual(\\\"test-api-key\\\")\\n124 | \\t\\t\\texpect(mockAnthropicConstructor.mock.calls[0]![0]!.authToken).toBeUndefined()\\n125 | \\t\\t})\\n126 | \\n127 | \\t\\tit(\\\"use authToken for passing token if both of anthropicBaseUrl and anthropicUseAuthToken are set\\\", () => {\\n128 | \\t\\t\\tconst customBaseUrl = \\\"https://custom.anthropic.com\\\"\\n----\\n135 | \\t\\t\\texpect(mockAnthropicConstructor).toHaveBeenCalledTimes(1)\\n136 | \\t\\t\\texpect(mockAnthropicConstructor.mock.calls[0]![0]!.authToken).toEqual(\\\"test-api-key\\\")\\n137 | \\t\\t\\texpect(mockAnthropicConstructor.mock.calls[0]![0]!.apiKey).toBeUndefined()\\n----\\n\\n# src/api/providers/anthropic.ts\\n 29 | \\t\\tconst apiKeyFieldName =\\n 30 | \\t\\t\\tthis.options.anthropicBaseUrl && this.options.anthropicUseAuthToken ? \\\"authToken\\\" : \\\"apiKey\\\"\\n 31 | \\n----\\n\\n# src/api/providers/fetchers/__tests__/litellm.test.ts\\n108 | \\n109 | \\tit(\\\"makes request without authorization header when no API key provided\\\", async () => {\\n110 | \\t\\tconst mockResponse = {\\n----\\n205 | \\t\\t\\t\\tstatus: 401,\\n206 | \\t\\t\\t\\tstatusText: \\\"Unauthorized\\\",\\n207 | \\t\\t\\t},\\n----\\n214 | \\t\\tawait expect(getLiteLLMModels(DUMMY_INVALID_KEY, \\\"http://localhost:4000\\\")).rejects.toThrow(\\n215 | \\t\\t\\t\\\"Failed to fetch LiteLLM models: 401 Unauthorized. Check base URL and API key.\\\",\\n216 | \\t\\t)\\n----\\n\\n# src/api/providers/anthropic-vertex.ts\\n 2 | import { AnthropicVertex } from \\\"@anthropic-ai/vertex-sdk\\\"\\n 3 | import { GoogleAuth, JWTInput } from \\\"google-auth-library\\\"\\n 4 | \\n----\\n 41 | \\t\\t\\t\\tgoogleAuth: new GoogleAuth({\\n 42 | \\t\\t\\t\\t\\tscopes: [\\\"https://www.googleapis.com/auth/cloud-platform\\\"],\\n 43 | \\t\\t\\t\\t\\tcredentials: safeJsonParse(this.options.vertexJsonCredentials, undefined),\\n----\\n 50 | \\t\\t\\t\\tgoogleAuth: new GoogleAuth({\\n 51 | \\t\\t\\t\\t\\tscopes: [\\\"https://www.googleapis.com/auth/cloud-platform\\\"],\\n 52 | \\t\\t\\t\\t\\tkeyFile: this.options.vertexKeyFile,\\n----\\n\\n# src/shared/modes.ts\\n 81 | \\t\\tcustomInstructions:\\n 82 | \\t\\t\\t\\\"1. Do some information gathering (for example using read_file or search_files) to get more context about the task.\\\\n\\\\n2. You should also ask the user clarifying questions to get a better understanding of the task.\\\\n\\\\n3. Once you've gained more context about the user's request, you should create a detailed plan for how to accomplish the task. Include Mermaid diagrams if they help make your plan clearer.\\\\n\\\\n4. Ask the user if they are pleased with this plan, or if they would like to make any c [truncated...]\\n 83 | \\t},\\n----\\n\\n# src/utils/git.ts\\n 11 | \\tsubject: string\\n 12 | \\tauthor: string\\n 13 | \\tdate: string\\n----\\n 57 | \\t\\t\\tconst { stdout: hashStdout } = await execAsync(\\n 58 | \\t\\t\\t\\t`git log -n 10 --format=\\\"%H%n%h%n%s%n%an%n%ad\\\" --date=short ` + `--author-date-order ${query}`,\\n 59 | \\t\\t\\t\\t{ cwd },\\n----\\n 79 | \\t\\t\\t\\tsubject: lines[i + 2],\\n 80 | \\t\\t\\t\\tauthor: lines[i + 3],\\n 81 | \\t\\t\\t\\tdate: lines[i + 4],\\n----\\n107 | \\t\\t})\\n108 | \\t\\tconst [fullHash, shortHash, subject, author, date, body] = info.trim().split(\\\"\\\\n\\\")\\n109 | \\n----\\n115 | \\t\\t\\t`Commit: ${shortHash} (${fullHash})`,\\n116 | \\t\\t\\t`Author: ${author}`,\\n117 | \\t\\t\\t`Date: ${date}`,\\n----\\n\\n# src/utils/__tests__/git.spec.ts\\n 98 | \\t\\t\\t\\tsubject: \\\"fix: test commit\\\",\\n 99 | \\t\\t\\t\\tauthor: \\\"John Doe\\\",\\n100 | \\t\\t\\t\\tdate: \\\"2024-01-06\\\",\\n----\\n162 | \\t\\t\\t\\t[\\n163 | \\t\\t\\t\\t\\t'git log -n 10 --format=\\\"%H%n%h%n%s%n%an%n%ad\\\" --date=short --author-date-order abc123',\\n164 | \\t\\t\\t\\t\\t{ stdout: mockCommitData, stderr: \\\"\\\" },\\n----\\n185 | \\t\\t\\t\\tsubject: \\\"fix: test commit\\\",\\n186 | \\t\\t\\t\\tauthor: \\\"John Doe\\\",\\n187 | \\t\\t\\t\\tdate: \\\"2024-01-06\\\",\\n----\\n\\n# src/utils/logging/CompactTransport.ts\\n 36 | export class CompactTransport implements ICompactTransport {\\n 37 | \\tprivate sessionStart: number\\n 38 | \\tprivate lastTimestamp: number\\n----\\n 46 | \\tconstructor(readonly config: CompactTransportConfig = DEFAULT_CONFIG) {\\n 47 | \\t\\tthis.sessionStart = Date.now()\\n 48 | \\t\\tthis.lastTimestamp = this.sessionStart\\n 49 | \\n----\\n 55 | \\t/**\\n 56 | \\t * Ensures the log file is initialized with proper directory structure and session start marker\\n 57 | \\t * @private\\n----\\n 66 | \\n 67 | \\t\\t\\tconst sessionStart = {\\n 68 | \\t\\t\\t\\tt: 0,\\n 69 | \\t\\t\\t\\tl: \\\"info\\\",\\n 70 | \\t\\t\\t\\tm: \\\"Log session started\\\",\\n 71 | \\t\\t\\t\\td: { timestamp: new Date(this.sessionStart).toISOString() },\\n 72 | \\t\\t\\t}\\n 73 | \\t\\t\\twriteFileSync(this.filePath, JSON.stringify(sessionStart) + \\\"\\\\n\\\", { flag: \\\"w\\\" })\\n 74 | \\n----\\n108 | \\t/**\\n109 | \\t * Closes the transport and writes session end marker\\n110 | \\t */\\n----\\n112 | \\t\\tif (this.filePath && this.initialized) {\\n113 | \\t\\t\\tconst sessionEnd = {\\n114 | \\t\\t\\t\\tt: Date.now() - this.lastTimestamp,\\n115 | \\t\\t\\t\\tl: \\\"info\\\",\\n116 | \\t\\t\\t\\tm: \\\"Log session ended\\\",\\n117 | \\t\\t\\t\\td: { timestamp: new Date().toISOString() },\\n118 | \\t\\t\\t}\\n119 | \\t\\t\\twriteFileSync(this.filePath, JSON.stringify(sessionEnd) + \\\"\\\\n\\\", { flag: \\\"a\\\" })\\n120 | \\t\\t}\\n----\\n\\n# src/utils/logging/__tests__/CompactTransport.spec.ts\\n 73 | \\t\\t\\t\\tl: \\\"info\\\",\\n 74 | \\t\\t\\t\\tm: \\\"Log session started\\\",\\n 75 | \\t\\t\\t})\\n----\\n102 | \\n103 | \\t\\ttest(\\\"writes session end marker on close\\\", () => {\\n104 | \\t\\t\\ttransport.write({\\n----\\n117 | \\t\\t\\t\\tl: \\\"info\\\",\\n118 | \\t\\t\\t\\tm: \\\"Log session ended\\\",\\n119 | \\t\\t\\t})\\n----\\n171 | \\t\\t\\tconst lines = fileContent.trim().split(\\\"\\\\n\\\")\\n172 | \\t\\t\\t// +1 for session start line\\n173 | \\t\\t\\texpect(lines.length).toBe(entries.length + 1)\\n----\\n\\n# src/shared/context-mentions.ts\\n 72 | \\tsubject: string\\n 73 | \\tauthor: string\\n 74 | \\tdate: string\\n----\\n 80 | \\tsubject: string\\n 81 | \\tauthor: string\\n 82 | \\tdate: string\\n----\\n 86 | \\t\\tlabel: commit.subject,\\n 87 | \\t\\tdescription: `${commit.shortHash} by ${commit.author} on ${commit.date}`,\\n 88 | \\t\\tvalue: commit.hash,\\n----\\n 92 | \\t\\tsubject: commit.subject,\\n 93 | \\t\\tauthor: commit.author,\\n 94 | \\t\\tdate: commit.date,\\n----\\n\\n# src/shared/ExtensionMessage.ts\\n 71 | \\t\\t| \\\"vsCodeSetting\\\"\\n 72 | \\t\\t| \\\"authenticatedUser\\\"\\n 73 | \\t\\t| \\\"condenseTaskContextResponse\\\"\\n----\\n\\n# src/shared/WebviewMessage.ts\\n 85 | \\t\\t| \\\"restartMcpServer\\\"\\n 86 | \\t\\t| \\\"refreshAllMcpServers\\\"\\n 87 | \\t\\t| \\\"toggleToolAlwaysAllow\\\"\\n----\\n\\n# src/integrations/terminal/TerminalRegistry.ts\\n 14 | // too many terminals, we need to keep track of terminals through the life of\\n 15 | // the extension, as well as session specific terminals for the life of a task\\n 16 | // (to get latest unretrieved output).\\n----\\n\\n# src/integrations/terminal/ShellIntegrationManager.ts\\n 43 | \\t[ -f \\\"$ZDOTDIR/.zshrc\\\" ] && source \\\"$ZDOTDIR/.zshrc\\\"\\n 44 | \\t[ -f \\\"$ZDOTDIR/.zlogin\\\" ] && source \\\"$ZDOTDIR/.zlogin\\\"\\n 45 | \\t[ \\\"$ZDOTDIR\\\" = \\\"$HOME\\\" ] && unset ZDOTDIR\\n----\\n\\n# src/core/webview/ClineProvider.ts\\n497 | \\n498 | \\t\\t// If the extension is starting a new session, clear previous task state.\\n499 | \\t\\tawait this.removeClineFromStack()\\n----\\n656 | \\t\\t\\t\\n 663 | \\t\\t`\\n 664 | \\n 665 | \\t\\tconst csp = [\\n 666 | \\t\\t\\t\\\"default-src 'none'\\\",\\n 667 | \\t\\t\\t`font-src ${webview.cspSource}`,\\n 668 | \\t\\t\\t`style-src ${webview.cspSource} 'unsafe-inline' https://* http://${localServerUrl} http://0.0.0.0:${localPort}`,\\n 669 | \\t\\t\\t`img-src ${webview.cspSource} https://storage.googleapis.com https://img.clerk.com data:`,\\n 670 | \\t\\t\\t`media-src ${webview.cspSource}`,\\n 671 | \\t\\t\\t`script-src 'unsafe-eval' ${webview.cspSource} https://* https://*.posthog.com http://${localServerUrl} http://0.0.0.0:${localPort} 'nonce-${nonce}'`,\\n 672 | \\t\\t\\t`connect-src https://* https://*.posthog.com ws://${localServerUrl} ws://0.0.0.0:${localPort} http://${localServerUrl} http://0.0.0.0:${localPort}`,\\n 673 | \\t\\t]\\n 674 | \\n 675 | \\t\\treturn /*html*/ `\\n 676 | \\t\\t\\t\\n 677 | \\t\\t\\t\\n 678 | \\t\\t\\t\\t\\n 679 | \\t\\t\\t\\t\\t\\n 680 | \\t\\t\\t\\t\\t\\n 681 | \\t\\t\\t\\t\\t\\n 682 | \\t\\t\\t\\t\\t\\n 683 | \\t\\t\\t\\t\\t\\n 684 | \\t\\t\\t\\t\\t\\n 689 | \\t\\t\\t\\t\\tRoo Code\\n 690 | \\t\\t\\t\\t\\n 691 | \\t\\t\\t\\t\\n 692 | \\t\\t\\t\\t\\t
\\n 693 | \\t\\t\\t\\t\\t${reactRefresh}\\n 694 | \\t\\t\\t\\t\\t\\n 695 | \\t\\t\\t\\t\\n 696 | \\t\\t\\t\\n 697 | \\t\\t`\\n 698 | \\t}\\n 699 | \\n 700 | \\t/**\\n 701 | \\t * Defines and returns the HTML that should be rendered within the webview panel.\\n 702 | \\t *\\n 703 | \\t * @remarks This is also the place where references to the React webview build files\\n 704 | \\t * are created and inserted into the webview HTML.\\n 705 | \\t *\\n 706 | \\t * @param webview A reference to the extension webview\\n 707 | \\t * @param extensionUri The URI of the directory containing the extension\\n 708 | \\t * @returns A template string literal containing the HTML that should be\\n 709 | \\t * rendered within the webview panel\\n 710 | \\t */\\n 711 | \\tprivate getHtmlContent(webview: vscode.Webview): string {\\n 712 | \\t\\t// Get the local path to main script run in the webview,\\n 713 | \\t\\t// then convert it to a uri we can use in the webview.\\n 714 | \\n 715 | \\t\\t// The CSS file from the React build output\\n 716 | \\t\\tconst stylesUri = getUri(webview, this.contextProxy.extensionUri, [\\n 717 | \\t\\t\\t\\\"webview-ui\\\",\\n 718 | \\t\\t\\t\\\"build\\\",\\n 719 | \\t\\t\\t\\\"assets\\\",\\n 720 | \\t\\t\\t\\\"index.css\\\",\\n 721 | \\t\\t])\\n 722 | \\n 723 | \\t\\tconst scriptUri = getUri(webview, this.contextProxy.extensionUri, [\\\"webview-ui\\\", \\\"build\\\", \\\"assets\\\", \\\"index.js\\\"])\\n 724 | \\t\\tconst codiconsUri = getUri(webview, this.contextProxy.extensionUri, [\\\"assets\\\", \\\"codicons\\\", \\\"codicon.css\\\"])\\n 725 | \\t\\tconst materialIconsUri = getUri(webview, this.contextProxy.extensionUri, [\\n 726 | \\t\\t\\t\\\"assets\\\",\\n 727 | \\t\\t\\t\\\"vscode-material-icons\\\",\\n 728 | \\t\\t\\t\\\"icons\\\",\\n 729 | \\t\\t])\\n 730 | \\t\\tconst imagesUri = getUri(webview, this.contextProxy.extensionUri, [\\\"assets\\\", \\\"images\\\"])\\n 731 | \\t\\tconst audioUri = getUri(webview, this.contextProxy.extensionUri, [\\\"webview-ui\\\", \\\"audio\\\"])\\n 732 | \\n 733 | \\t\\t// Use a nonce to only allow a specific script to be run.\\n 734 | \\t\\t/*\\n 735 | \\t\\tcontent security policy of your webview to only allow scripts that have a specific nonce\\n 736 | \\t\\tcreate a content security policy meta tag so that only loading scripts with a nonce is allowed\\n 737 | \\t\\tAs your extension grows you will likely want to add custom styles, fonts, and/or images to your webview. If you do, you will need to update the content security policy meta tag to explicitly allow for these resources. E.g.\\n 738 | \\t\\t\\t\\t\\n 739 | \\t\\t- 'unsafe-inline' is required for styles due to vscode-webview-toolkit's dynamic style injection\\n 740 | \\t\\t- since we pass base64 images to the webview, we need to specify img-src ${webview.cspSource} data:;\\n 741 | \\n 742 | \\t\\tin meta tag we add nonce attribute: A cryptographic nonce (only used once) to allow scripts. The server must generate a unique nonce value each time it transmits a policy. It is critical to provide a nonce that cannot be guessed as bypassing a resource's policy is otherwise trivial.\\n 743 | \\t\\t*/\\n 744 | \\t\\tconst nonce = getNonce()\\n 745 | \\n 746 | \\t\\t// Tip: Install the es6-string-html VS Code extension to enable code highlighting below\\n 747 | \\t\\treturn /*html*/ `\\n 748 | \\n 749 | \\n 750 | \\n 751 | \\n 752 | \\n 753 | \\n 754 | \\n 755 | \\n 756 | \\t\\t\\t\\n 757 | \\t\\t\\t\\n 762 | Roo Code\\n 763 | \\n 764 | \\n 765 | \\n 766 |
\\n 767 | \\n 768 | \\n 769 | \\n 770 | `\\n 771 | \\t}\\n 772 | \\n 773 | \\t/**\\n 774 | \\t * Sets up an event listener to listen for messages passed from the webview context and\\n 775 | \\t * executes code based on the message that is received.\\n 776 | \\t *\\n 777 | \\t * @param webview A reference to the extension webview\\n 778 | \\t */\\n 779 | \\tprivate setWebviewMessageListener(webview: vscode.Webview) {\\n 780 | \\t\\tconst onReceiveMessage = async (message: WebviewMessage) =>\\n 781 | \\t\\t\\twebviewMessageHandler(this, message, this.marketplaceManager)\\n 782 | \\n 783 | \\t\\tconst messageDisposable = webview.onDidReceiveMessage(onReceiveMessage)\\n 784 | \\t\\tthis.webviewDisposables.push(messageDisposable)\\n 785 | \\t}\\n 786 | \\n 787 | \\t/**\\n 788 | \\t * Handle switching to a new mode, including updating the associated API configuration\\n 789 | \\t * @param newMode The mode to switch to\\n 790 | \\t */\\n 791 | \\tpublic async handleModeSwitch(newMode: Mode) {\\n 792 | \\t\\tconst cline = this.getCurrentCline()\\n 793 | \\n 794 | \\t\\tif (cline) {\\n 795 | \\t\\t\\tTelemetryService.instance.captureModeSwitch(cline.taskId, newMode)\\n 796 | \\t\\t\\tcline.emit(\\\"taskModeSwitched\\\", cline.taskId, newMode)\\n 797 | \\t\\t}\\n 798 | \\n 799 | \\t\\tawait this.updateGlobalState(\\\"mode\\\", newMode)\\n 800 | \\n 801 | \\t\\t// Load the saved API config for the new mode if it exists\\n 802 | \\t\\tconst savedConfigId = await this.providerSettingsManager.getModeConfigId(newMode)\\n 803 | \\t\\tconst listApiConfig = await this.providerSettingsManager.listConfig()\\n 804 | \\n 805 | \\t\\t// Update listApiConfigMeta first to ensure UI has latest data\\n 806 | \\t\\tawait this.updateGlobalState(\\\"listApiConfigMeta\\\", listApiConfig)\\n 807 | \\n 808 | \\t\\t// If this mode has a saved config, use it.\\n 809 | \\t\\tif (savedConfigId) {\\n 810 | \\t\\t\\tconst profile = listApiConfig.find(({ id }) => id === savedConfigId)\\n 811 | \\n 812 | \\t\\t\\tif (profile?.name) {\\n 813 | \\t\\t\\t\\tawait this.activateProviderProfile({ name: profile.name })\\n 814 | \\t\\t\\t}\\n 815 | \\t\\t} else {\\n 816 | \\t\\t\\t// If no saved config for this mode, save current config as default.\\n 817 | \\t\\t\\tconst currentApiConfigName = this.getGlobalState(\\\"currentApiConfigName\\\")\\n 818 | \\n 819 | \\t\\t\\tif (currentApiConfigName) {\\n 820 | \\t\\t\\t\\tconst config = listApiConfig.find((c) => c.name === currentApiConfigName)\\n 821 | \\n 822 | \\t\\t\\t\\tif (config?.id) {\\n 823 | \\t\\t\\t\\t\\tawait this.providerSettingsManager.setModeConfig(newMode, config.id)\\n 824 | \\t\\t\\t\\t}\\n 825 | \\t\\t\\t}\\n 826 | \\t\\t}\\n 827 | \\n 828 | \\t\\tawait this.postStateToWebview()\\n 829 | \\t}\\n 830 | \\n 831 | \\t// Provider Profile Management\\n 832 | \\n 833 | \\tgetProviderProfileEntries(): ProviderSettingsEntry[] {\\n 834 | \\t\\treturn this.contextProxy.getValues().listApiConfigMeta || []\\n 835 | \\t}\\n 836 | \\n 837 | \\tgetProviderProfileEntry(name: string): ProviderSettingsEntry | undefined {\\n 838 | \\t\\treturn this.getProviderProfileEntries().find((profile) => profile.name === name)\\n 839 | \\t}\\n 840 | \\n 841 | \\tpublic hasProviderProfileEntry(name: string): boolean {\\n 842 | \\t\\treturn !!this.getProviderProfileEntry(name)\\n 843 | \\t}\\n 844 | \\n 845 | \\tasync upsertProviderProfile(\\n 846 | \\t\\tname: string,\\n 847 | \\t\\tproviderSettings: ProviderSettings,\\n 848 | \\t\\tactivate: boolean = true,\\n 849 | \\t): Promise {\\n 850 | \\t\\ttry {\\n 851 | \\t\\t\\t// TODO: Do we need to be calling `activateProfile`? It's not\\n 852 | \\t\\t\\t// clear to me what the source of truth should be; in some cases\\n 853 | \\t\\t\\t// we rely on the `ContextProxy`'s data store and in other cases\\n 854 | \\t\\t\\t// we rely on the `ProviderSettingsManager`'s data store. It might\\n 855 | \\t\\t\\t// be simpler to unify these two.\\n 856 | \\t\\t\\tconst id = await this.providerSettingsManager.saveConfig(name, providerSettings)\\n 857 | \\n 858 | \\t\\t\\tif (activate) {\\n 859 | \\t\\t\\t\\tconst { mode } = await this.getState()\\n 860 | \\n 861 | \\t\\t\\t\\t// These promises do the following:\\n 862 | \\t\\t\\t\\t// 1. Adds or updates the list of provider profiles.\\n 863 | \\t\\t\\t\\t// 2. Sets the current provider profile.\\n 864 | \\t\\t\\t\\t// 3. Sets the current mode's provider profile.\\n 865 | \\t\\t\\t\\t// 4. Copies the provider settings to the context.\\n 866 | \\t\\t\\t\\t//\\n 867 | \\t\\t\\t\\t// Note: 1, 2, and 4 can be done in one `ContextProxy` call:\\n 868 | \\t\\t\\t\\t// this.contextProxy.setValues({ ...providerSettings, listApiConfigMeta: ..., currentApiConfigName: ... })\\n 869 | \\t\\t\\t\\t// We should probably switch to that and verify that it works.\\n 870 | \\t\\t\\t\\t// I left the original implementation in just to be safe.\\n 871 | \\t\\t\\t\\tawait Promise.all([\\n 872 | \\t\\t\\t\\t\\tthis.updateGlobalState(\\\"listApiConfigMeta\\\", await this.providerSettingsManager.listConfig()),\\n 873 | \\t\\t\\t\\t\\tthis.updateGlobalState(\\\"currentApiConfigName\\\", name),\\n 874 | \\t\\t\\t\\t\\tthis.providerSettingsManager.setModeConfig(mode, id),\\n 875 | \\t\\t\\t\\t\\tthis.contextProxy.setProviderSettings(providerSettings),\\n 876 | \\t\\t\\t\\t])\\n 877 | \\n 878 | \\t\\t\\t\\t// Notify CodeIndexManager about the settings change\\n 879 | \\t\\t\\t\\tif (this.codeIndexManager) {\\n 880 | \\t\\t\\t\\t\\tawait this.codeIndexManager.handleExternalSettingsChange()\\n 881 | \\t\\t\\t\\t}\\n 882 | \\n 883 | \\t\\t\\t\\t// Change the provider for the current task.\\n 884 | \\t\\t\\t\\t// TODO: We should rename `buildApiHandler` for clarity (e.g. `getProviderClient`).\\n 885 | \\t\\t\\t\\tconst task = this.getCurrentCline()\\n 886 | \\n 887 | \\t\\t\\t\\tif (task) {\\n 888 | \\t\\t\\t\\t\\ttask.api = buildApiHandler(providerSettings)\\n 889 | \\t\\t\\t\\t}\\n 890 | \\t\\t\\t} else {\\n 891 | \\t\\t\\t\\tawait this.updateGlobalState(\\\"listApiConfigMeta\\\", await this.providerSettingsManager.listConfig())\\n 892 | \\t\\t\\t}\\n 893 | \\n 894 | \\t\\t\\tawait this.postStateToWebview()\\n 895 | \\t\\t\\treturn id\\n 896 | \\t\\t} catch (error) {\\n 897 | \\t\\t\\tthis.log(\\n 898 | \\t\\t\\t\\t`Error create new api configuration: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`,\\n 899 | \\t\\t\\t)\\n 900 | \\n 901 | \\t\\t\\tvscode.window.showErrorMessage(t(\\\"common:errors.create_api_config\\\"))\\n 902 | \\t\\t\\treturn undefined\\n 903 | \\t\\t}\\n 904 | \\t}\\n 905 | \\n 906 | \\tasync deleteProviderProfile(profileToDelete: ProviderSettingsEntry) {\\n 907 | \\t\\tconst globalSettings = this.contextProxy.getValues()\\n 908 | \\t\\tlet profileToActivate: string | undefined = globalSettings.currentApiConfigName\\n 909 | \\n 910 | \\t\\tif (profileToDelete.name === profileToActivate) {\\n 911 | \\t\\t\\tprofileToActivate = this.getProviderProfileEntries().find(({ name }) => name !== profileToDelete.name)?.name\\n 912 | \\t\\t}\\n 913 | \\n 914 | \\t\\tif (!profileToActivate) {\\n 915 | \\t\\t\\tthrow new Error(\\\"You cannot delete the last profile\\\")\\n 916 | \\t\\t}\\n 917 | \\n 918 | \\t\\tconst entries = this.getProviderProfileEntries().filter(({ name }) => name !== profileToDelete.name)\\n 919 | \\n 920 | \\t\\tawait this.contextProxy.setValues({\\n 921 | \\t\\t\\t...globalSettings,\\n 922 | \\t\\t\\tcurrentApiConfigName: profileToActivate,\\n 923 | \\t\\t\\tlistApiConfigMeta: entries,\\n 924 | \\t\\t})\\n 925 | \\n 926 | \\t\\tawait this.postStateToWebview()\\n 927 | \\t}\\n 928 | \\n 929 | \\tasync activateProviderProfile(args: { name: string } | { id: string }) {\\n 930 | \\t\\tconst { name, id, ...providerSettings } = await this.providerSettingsManager.activateProfile(args)\\n 931 | \\n 932 | \\t\\t// See `upsertProviderProfile` for a description of what this is doing.\\n 933 | \\t\\tawait Promise.all([\\n 934 | \\t\\t\\tthis.contextProxy.setValue(\\\"listApiConfigMeta\\\", await this.providerSettingsManager.listConfig()),\\n 935 | \\t\\t\\tthis.contextProxy.setValue(\\\"currentApiConfigName\\\", name),\\n 936 | \\t\\t\\tthis.contextProxy.setProviderSettings(providerSettings),\\n 937 | \\t\\t])\\n 938 | \\n 939 | \\t\\tconst { mode } = await this.getState()\\n 940 | \\n 941 | \\t\\tif (id) {\\n 942 | \\t\\t\\tawait this.providerSettingsManager.setModeConfig(mode, id)\\n 943 | \\t\\t}\\n 944 | \\n 945 | \\t\\t// Change the provider for the current task.\\n 946 | \\t\\tconst task = this.getCurrentCline()\\n 947 | \\n 948 | \\t\\tif (task) {\\n 949 | \\t\\t\\ttask.api = buildApiHandler(providerSettings)\\n 950 | \\t\\t}\\n 951 | \\n 952 | \\t\\tawait this.postStateToWebview()\\n 953 | \\t}\\n 954 | \\n 955 | \\t// Task Management\\n 956 | \\n 957 | \\tasync cancelTask() {\\n 958 | \\t\\tconst cline = this.getCurrentCline()\\n 959 | \\n 960 | \\t\\tif (!cline) {\\n 961 | \\t\\t\\treturn\\n 962 | \\t\\t}\\n 963 | \\n 964 | \\t\\tconsole.log(`[subtasks] cancelling task ${cline.taskId}.${cline.instanceId}`)\\n 965 | \\n 966 | \\t\\tconst { historyItem } = await this.getTaskWithId(cline.taskId)\\n 967 | \\t\\t// Preserve parent and root task information for history item.\\n 968 | \\t\\tconst rootTask = cline.rootTask\\n 969 | \\t\\tconst parentTask = cline.parentTask\\n 970 | \\n 971 | \\t\\tcline.abortTask()\\n 972 | \\n 973 | \\t\\tawait pWaitFor(\\n 974 | \\t\\t\\t() =>\\n 975 | \\t\\t\\t\\tthis.getCurrentCline()! === undefined ||\\n 976 | \\t\\t\\t\\tthis.getCurrentCline()!.isStreaming === false ||\\n 977 | \\t\\t\\t\\tthis.getCurrentCline()!.didFinishAbortingStream ||\\n 978 | \\t\\t\\t\\t// If only the first chunk is processed, then there's no\\n 979 | \\t\\t\\t\\t// need to wait for graceful abort (closes edits, browser,\\n 980 | \\t\\t\\t\\t// etc).\\n 981 | \\t\\t\\t\\tthis.getCurrentCline()!.isWaitingForFirstChunk,\\n 982 | \\t\\t\\t{\\n 983 | \\t\\t\\t\\ttimeout: 3_000,\\n 984 | \\t\\t\\t},\\n 985 | \\t\\t).catch(() => {\\n 986 | \\t\\t\\tconsole.error(\\\"Failed to abort task\\\")\\n 987 | \\t\\t})\\n 988 | \\n 989 | \\t\\tif (this.getCurrentCline()) {\\n 990 | \\t\\t\\t// 'abandoned' will prevent this Cline instance from affecting\\n 991 | \\t\\t\\t// future Cline instances. This may happen if its hanging on a\\n 992 | \\t\\t\\t// streaming request.\\n 993 | \\t\\t\\tthis.getCurrentCline()!.abandoned = true\\n 994 | \\t\\t}\\n 995 | \\n 996 | \\t\\t// Clears task again, so we need to abortTask manually above.\\n 997 | \\t\\tawait this.initClineWithHistoryItem({ ...historyItem, rootTask, parentTask })\\n 998 | \\t}\\n 999 | \\n1000 | \\tasync updateCustomInstructions(instructions?: string) {\\n1001 | \\t\\t// User may be clearing the field.\\n1002 | \\t\\tawait this.updateGlobalState(\\\"customInstructions\\\", instructions || undefined)\\n1003 | \\t\\tawait this.postStateToWebview()\\n1004 | \\t}\\n1005 | \\n1006 | \\t// MCP\\n1007 | \\n1008 | \\tasync ensureMcpServersDirectoryExists(): Promise {\\n1009 | \\t\\t// Get platform-specific application data directory\\n1010 | \\t\\tlet mcpServersDir: string\\n1011 | \\t\\tif (process.platform === \\\"win32\\\") {\\n1012 | \\t\\t\\t// Windows: %APPDATA%\\\\Roo-Code\\\\MCP\\n1013 | \\t\\t\\tmcpServersDir = path.join(os.homedir(), \\\"AppData\\\", \\\"Roaming\\\", \\\"Roo-Code\\\", \\\"MCP\\\")\\n1014 | \\t\\t} else if (process.platform === \\\"darwin\\\") {\\n1015 | \\t\\t\\t// macOS: ~/Documents/Cline/MCP\\n1016 | \\t\\t\\tmcpServersDir = path.join(os.homedir(), \\\"Documents\\\", \\\"Cline\\\", \\\"MCP\\\")\\n1017 | \\t\\t} else {\\n1018 | \\t\\t\\t// Linux: ~/.local/share/Cline/MCP\\n1019 | \\t\\t\\tmcpServersDir = path.join(os.homedir(), \\\".local\\\", \\\"share\\\", \\\"Roo-Code\\\", \\\"MCP\\\")\\n1020 | \\t\\t}\\n1021 | \\n1022 | \\t\\ttry {\\n1023 | \\t\\t\\tawait fs.mkdir(mcpServersDir, { recursive: true })\\n1024 | \\t\\t} catch (error) {\\n1025 | \\t\\t\\t// Fallback to a relative path if directory creation fails\\n1026 | \\t\\t\\treturn path.join(os.homedir(), \\\".roo-code\\\", \\\"mcp\\\")\\n1027 | \\t\\t}\\n1028 | \\t\\treturn mcpServersDir\\n1029 | \\t}\\n1030 | \\n1031 | \\tasync ensureSettingsDirectoryExists(): Promise {\\n1032 | \\t\\tconst { getSettingsDirectoryPath } = await import(\\\"../../utils/storage\\\")\\n1033 | \\t\\tconst globalStoragePath = this.contextProxy.globalStorageUri.fsPath\\n1034 | \\t\\treturn getSettingsDirectoryPath(globalStoragePath)\\n1035 | \\t}\\n1036 | \\n1037 | \\t// OpenRouter\\n1038 | \\n1039 | \\tasync handleOpenRouterCallback(code: string) {\\n1040 | \\t\\tlet { apiConfiguration, currentApiConfigName } = await this.getState()\\n1041 | \\n1042 | \\t\\tlet apiKey: string\\n1043 | \\t\\ttry {\\n1044 | \\t\\t\\tconst baseUrl = apiConfiguration.openRouterBaseUrl || \\\"https://openrouter.ai/api/v1\\\"\\n1045 | \\t\\t\\t// Extract the base domain for the auth endpoint\\n1046 | \\t\\t\\tconst baseUrlDomain = baseUrl.match(/^(https?:\\\\/\\\\/[^\\\\/]+)/)?.[1] || \\\"https://openrouter.ai\\\"\\n1047 | \\t\\t\\tconst response = await axios.post(`${baseUrlDomain}/api/v1/auth/keys`, { code })\\n1048 | \\t\\t\\tif (response.data && response.data.key) {\\n1049 | \\t\\t\\t\\tapiKey = response.data.key\\n1050 | \\t\\t\\t} else {\\n1051 | \\t\\t\\t\\tthrow new Error(\\\"Invalid response from OpenRouter API\\\")\\n1052 | \\t\\t\\t}\\n1053 | \\t\\t} catch (error) {\\n1054 | \\t\\t\\tthis.log(\\n1055 | \\t\\t\\t\\t`Error exchanging code for API key: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`,\\n1056 | \\t\\t\\t)\\n1057 | \\t\\t\\tthrow error\\n1058 | \\t\\t}\\n1059 | \\n1060 | \\t\\tconst newConfiguration: ProviderSettings = {\\n1061 | \\t\\t\\t...apiConfiguration,\\n1062 | \\t\\t\\tapiProvider: \\\"openrouter\\\",\\n1063 | \\t\\t\\topenRouterApiKey: apiKey,\\n1064 | \\t\\t\\topenRouterModelId: apiConfiguration?.openRouterModelId || openRouterDefaultModelId,\\n1065 | \\t\\t}\\n1066 | \\n1067 | \\t\\tawait this.upsertProviderProfile(currentApiConfigName, newConfiguration)\\n1068 | \\t}\\n1069 | \\n1070 | \\t// Glama\\n1071 | \\n1072 | \\tasync handleGlamaCallback(code: string) {\\n1073 | \\t\\tlet apiKey: string\\n1074 | \\t\\ttry {\\n1075 | \\t\\t\\tconst response = await axios.post(\\\"https://glama.ai/api/gateway/v1/auth/exchange-code\\\", { code })\\n1076 | \\t\\t\\tif (response.data && response.data.apiKey) {\\n1077 | \\t\\t\\t\\tapiKey = response.data.apiKey\\n1078 | \\t\\t\\t} else {\\n1079 | \\t\\t\\t\\tthrow new Error(\\\"Invalid response from Glama API\\\")\\n1080 | \\t\\t\\t}\\n1081 | \\t\\t} catch (error) {\\n1082 | \\t\\t\\tthis.log(\\n1083 | \\t\\t\\t\\t`Error exchanging code for API key: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`,\\n1084 | \\t\\t\\t)\\n1085 | \\t\\t\\tthrow error\\n1086 | \\t\\t}\\n1087 | \\n1088 | \\t\\tconst { apiConfiguration, currentApiConfigName } = await this.getState()\\n1089 | \\n1090 | \\t\\tconst newConfiguration: ProviderSettings = {\\n1091 | \\t\\t\\t...apiConfiguration,\\n1092 | \\t\\t\\tapiProvider: \\\"glama\\\",\\n1093 | \\t\\t\\tglamaApiKey: apiKey,\\n1094 | \\t\\t\\tglamaModelId: apiConfiguration?.glamaModelId || glamaDefaultModelId,\\n1095 | \\t\\t}\\n1096 | \\n1097 | \\t\\tawait this.upsertProviderProfile(currentApiConfigName, newConfiguration)\\n1098 | \\t}\\n1099 | \\n1100 | \\t// Requesty\\n1101 | \\n1102 | \\tasync handleRequestyCallback(code: string) {\\n1103 | \\t\\tlet { apiConfiguration, currentApiConfigName } = await this.getState()\\n1104 | \\n1105 | \\t\\tconst newConfiguration: ProviderSettings = {\\n1106 | \\t\\t\\t...apiConfiguration,\\n1107 | \\t\\t\\tapiProvider: \\\"requesty\\\",\\n1108 | \\t\\t\\trequestyApiKey: code,\\n1109 | \\t\\t\\trequestyModelId: apiConfiguration?.requestyModelId || requestyDefaultModelId,\\n1110 | \\t\\t}\\n1111 | \\n1112 | \\t\\tawait this.upsertProviderProfile(currentApiConfigName, newConfiguration)\\n1113 | \\t}\\n1114 | \\n1115 | \\t// Task history\\n1116 | \\n1117 | \\tasync getTaskWithId(id: string): Promise<{\\n1118 | \\t\\thistoryItem: HistoryItem\\n1119 | \\t\\ttaskDirPath: string\\n1120 | \\t\\tapiConversationHistoryFilePath: string\\n1121 | \\t\\tuiMessagesFilePath: string\\n1122 | \\t\\tapiConversationHistory: Anthropic.MessageParam[]\\n1123 | \\t}> {\\n1124 | \\t\\tconst history = this.getGlobalState(\\\"taskHistory\\\") ?? []\\n1125 | \\t\\tconst historyItem = history.find((item) => item.id === id)\\n1126 | \\n1127 | \\t\\tif (historyItem) {\\n1128 | \\t\\t\\tconst { getTaskDirectoryPath } = await import(\\\"../../utils/storage\\\")\\n1129 | \\t\\t\\tconst globalStoragePath = this.contextProxy.globalStorageUri.fsPath\\n1130 | \\t\\t\\tconst taskDirPath = await getTaskDirectoryPath(globalStoragePath, id)\\n1131 | \\t\\t\\tconst apiConversationHistoryFilePath = path.join(taskDirPath, GlobalFileNames.apiConversationHistory)\\n1132 | \\t\\t\\tconst uiMessagesFilePath = path.join(taskDirPath, GlobalFileNames.uiMessages)\\n1133 | \\t\\t\\tconst fileExists = await fileExistsAtPath(apiConversationHistoryFilePath)\\n1134 | \\n1135 | \\t\\t\\tif (fileExists) {\\n1136 | \\t\\t\\t\\tconst apiConversationHistory = JSON.parse(await fs.readFile(apiConversationHistoryFilePath, \\\"utf8\\\"))\\n1137 | \\n1138 | \\t\\t\\t\\treturn {\\n1139 | \\t\\t\\t\\t\\thistoryItem,\\n1140 | \\t\\t\\t\\t\\ttaskDirPath,\\n1141 | \\t\\t\\t\\t\\tapiConversationHistoryFilePath,\\n1142 | \\t\\t\\t\\t\\tuiMessagesFilePath,\\n1143 | \\t\\t\\t\\t\\tapiConversationHistory,\\n1144 | \\t\\t\\t\\t}\\n1145 | \\t\\t\\t}\\n1146 | \\t\\t}\\n1147 | \\n1148 | \\t\\t// if we tried to get a task that doesn't exist, remove it from state\\n1149 | \\t\\t// FIXME: this seems to happen sometimes when the json file doesnt save to disk for some reason\\n1150 | \\t\\tawait this.deleteTaskFromState(id)\\n1151 | \\t\\tthrow new Error(\\\"Task not found\\\")\\n1152 | \\t}\\n1153 | \\n1154 | \\tasync showTaskWithId(id: string) {\\n1155 | \\t\\tif (id !== this.getCurrentCline()?.taskId) {\\n1156 | \\t\\t\\t// Non-current task.\\n1157 | \\t\\t\\tconst { historyItem } = await this.getTaskWithId(id)\\n1158 | \\t\\t\\tawait this.initClineWithHistoryItem(historyItem) // Clears existing task.\\n1159 | \\t\\t}\\n1160 | \\n1161 | \\t\\tawait this.postMessageToWebview({ type: \\\"action\\\", action: \\\"chatButtonClicked\\\" })\\n1162 | \\t}\\n1163 | \\n1164 | \\tasync exportTaskWithId(id: string) {\\n1165 | \\t\\tconst { historyItem, apiConversationHistory } = await this.getTaskWithId(id)\\n1166 | \\t\\tawait downloadTask(historyItem.ts, apiConversationHistory)\\n1167 | \\t}\\n1168 | \\n1169 | \\t/* Condenses a task's message history to use fewer tokens. */\\n1170 | \\tasync condenseTaskContext(taskId: string) {\\n1171 | \\t\\tlet task: Task | undefined\\n1172 | \\t\\tfor (let i = this.clineStack.length - 1; i >= 0; i--) {\\n1173 | \\t\\t\\tif (this.clineStack[i].taskId === taskId) {\\n1174 | \\t\\t\\t\\ttask = this.clineStack[i]\\n1175 | \\t\\t\\t\\tbreak\\n1176 | \\t\\t\\t}\\n1177 | \\t\\t}\\n1178 | \\t\\tif (!task) {\\n1179 | \\t\\t\\tthrow new Error(`Task with id ${taskId} not found in stack`)\\n1180 | \\t\\t}\\n1181 | \\t\\tawait task.condenseContext()\\n1182 | \\t\\tawait this.postMessageToWebview({ type: \\\"condenseTaskContextResponse\\\", text: taskId })\\n1183 | \\t}\\n1184 | \\n1185 | \\t// this function deletes a task from task hidtory, and deletes it's checkpoints and delete the task folder\\n1186 | \\tasync deleteTaskWithId(id: string) {\\n1187 | \\t\\ttry {\\n1188 | \\t\\t\\t// get the task directory full path\\n1189 | \\t\\t\\tconst { taskDirPath } = await this.getTaskWithId(id)\\n1190 | \\n1191 | \\t\\t\\t// remove task from stack if it's the current task\\n1192 | \\t\\t\\tif (id === this.getCurrentCline()?.taskId) {\\n1193 | \\t\\t\\t\\t// if we found the taskid to delete - call finish to abort this task and allow a new task to be started,\\n1194 | \\t\\t\\t\\t// if we are deleting a subtask and parent task is still waiting for subtask to finish - it allows the parent to resume (this case should neve exist)\\n1195 | \\t\\t\\t\\tawait this.finishSubTask(t(\\\"common:tasks.deleted\\\"))\\n1196 | \\t\\t\\t}\\n1197 | \\n1198 | \\t\\t\\t// delete task from the task history state\\n1199 | \\t\\t\\tawait this.deleteTaskFromState(id)\\n1200 | \\n1201 | \\t\\t\\t// Delete associated shadow repository or branch.\\n1202 | \\t\\t\\t// TODO: Store `workspaceDir` in the `HistoryItem` object.\\n1203 | \\t\\t\\tconst globalStorageDir = this.contextProxy.globalStorageUri.fsPath\\n1204 | \\t\\t\\tconst workspaceDir = this.cwd\\n1205 | \\n1206 | \\t\\t\\ttry {\\n1207 | \\t\\t\\t\\tawait ShadowCheckpointService.deleteTask({ taskId: id, globalStorageDir, workspaceDir })\\n1208 | \\t\\t\\t} catch (error) {\\n1209 | \\t\\t\\t\\tconsole.error(\\n1210 | \\t\\t\\t\\t\\t`[deleteTaskWithId${id}] failed to delete associated shadow repository or branch: ${error instanceof Error ? error.message : String(error)}`,\\n1211 | \\t\\t\\t\\t)\\n1212 | \\t\\t\\t}\\n1213 | \\n1214 | \\t\\t\\t// delete the entire task directory including checkpoints and all content\\n1215 | \\t\\t\\ttry {\\n1216 | \\t\\t\\t\\tawait fs.rm(taskDirPath, { recursive: true, force: true })\\n1217 | \\t\\t\\t\\tconsole.log(`[deleteTaskWithId${id}] removed task directory`)\\n1218 | \\t\\t\\t} catch (error) {\\n1219 | \\t\\t\\t\\tconsole.error(\\n1220 | \\t\\t\\t\\t\\t`[deleteTaskWithId${id}] failed to remove task directory: ${error instanceof Error ? error.message : String(error)}`,\\n1221 | \\t\\t\\t\\t)\\n1222 | \\t\\t\\t}\\n1223 | \\t\\t} catch (error) {\\n1224 | \\t\\t\\t// If task is not found, just remove it from state\\n1225 | \\t\\t\\tif (error instanceof Error && error.message === \\\"Task not found\\\") {\\n1226 | \\t\\t\\t\\tawait this.deleteTaskFromState(id)\\n1227 | \\t\\t\\t\\treturn\\n1228 | \\t\\t\\t}\\n1229 | \\t\\t\\tthrow error\\n1230 | \\t\\t}\\n1231 | \\t}\\n1232 | \\n1233 | \\tasync deleteTaskFromState(id: string) {\\n1234 | \\t\\tconst taskHistory = this.getGlobalState(\\\"taskHistory\\\") ?? []\\n1235 | \\t\\tconst updatedTaskHistory = taskHistory.filter((task) => task.id !== id)\\n1236 | \\t\\tawait this.updateGlobalState(\\\"taskHistory\\\", updatedTaskHistory)\\n1237 | \\t\\tawait this.postStateToWebview()\\n1238 | \\t}\\n1239 | \\n1240 | \\tasync postStateToWebview() {\\n1241 | \\t\\tconst state = await this.getStateToPostToWebview()\\n1242 | \\t\\tthis.postMessageToWebview({ type: \\\"state\\\", state })\\n1243 | \\n1244 | \\t\\t// Update VSCode context for experiments\\n1245 | \\t\\tawait this.updateVSCodeContext()\\n1246 | \\t}\\n1247 | \\n1248 | \\t/**\\n1249 | \\t * Updates VSCode context variables for experiments so they can be used in when clauses\\n1250 | \\t */\\n1251 | \\tprivate async updateVSCodeContext() {\\n1252 | \\t\\tconst { experiments } = await this.getState()\\n1253 | \\n1254 | \\t\\t// Set context for marketplace experiment\\n1255 | \\t\\tawait vscode.commands.executeCommand(\\n1256 | \\t\\t\\t\\\"setContext\\\",\\n1257 | \\t\\t\\t`${Package.name}.marketplaceEnabled`,\\n1258 | \\t\\t\\texperiments.marketplace ?? false,\\n1259 | \\t\\t)\\n1260 | \\t}\\n1261 | \\n1262 | \\t/**\\n1263 | \\t * Checks if there is a file-based system prompt override for the given mode\\n1264 | \\t */\\n1265 | \\tasync hasFileBasedSystemPromptOverride(mode: Mode): Promise {\\n1266 | \\t\\tconst promptFilePath = getSystemPromptFilePath(this.cwd, mode)\\n1267 | \\t\\treturn await fileExistsAtPath(promptFilePath)\\n1268 | \\t}\\n1269 | \\n1270 | \\tasync getStateToPostToWebview() {\\n1271 | \\t\\tconst {\\n1272 | \\t\\t\\tapiConfiguration,\\n1273 | \\t\\t\\tlastShownAnnouncementId,\\n1274 | \\t\\t\\tcustomInstructions,\\n1275 | \\t\\t\\talwaysAllowReadOnly,\\n1276 | \\t\\t\\talwaysAllowReadOnlyOutsideWorkspace,\\n1277 | \\t\\t\\talwaysAllowWrite,\\n1278 | \\t\\t\\talwaysAllowWriteOutsideWorkspace,\\n1279 | \\t\\t\\talwaysAllowExecute,\\n1280 | \\t\\t\\talwaysAllowBrowser,\\n1281 | \\t\\t\\talwaysAllowMcp,\\n1282 | \\t\\t\\talwaysAllowModeSwitch,\\n1283 | \\t\\t\\talwaysAllowSubtasks,\\n1284 | \\t\\t\\tallowedMaxRequests,\\n1285 | \\t\\t\\tautoCondenseContext,\\n1286 | \\t\\t\\tautoCondenseContextPercent,\\n1287 | \\t\\t\\tsoundEnabled,\\n1288 | \\t\\t\\tttsEnabled,\\n1289 | \\t\\t\\tttsSpeed,\\n1290 | \\t\\t\\tdiffEnabled,\\n1291 | \\t\\t\\tenableCheckpoints,\\n1292 | \\t\\t\\ttaskHistory,\\n1293 | \\t\\t\\tsoundVolume,\\n1294 | \\t\\t\\tbrowserViewportSize,\\n1295 | \\t\\t\\tscreenshotQuality,\\n1296 | \\t\\t\\tremoteBrowserHost,\\n1297 | \\t\\t\\tremoteBrowserEnabled,\\n1298 | \\t\\t\\tcachedChromeHostUrl,\\n1299 | \\t\\t\\twriteDelayMs,\\n1300 | \\t\\t\\tterminalOutputLineLimit,\\n1301 | \\t\\t\\tterminalShellIntegrationTimeout,\\n1302 | \\t\\t\\tterminalShellIntegrationDisabled,\\n1303 | \\t\\t\\tterminalCommandDelay,\\n1304 | \\t\\t\\tterminalPowershellCounter,\\n1305 | \\t\\t\\tterminalZshClearEolMark,\\n1306 | \\t\\t\\tterminalZshOhMy,\\n1307 | \\t\\t\\tterminalZshP10k,\\n1308 | \\t\\t\\tterminalZdotdir,\\n1309 | \\t\\t\\tfuzzyMatchThreshold,\\n1310 | \\t\\t\\tmcpEnabled,\\n1311 | \\t\\t\\tenableMcpServerCreation,\\n1312 | \\t\\t\\talwaysApproveResubmit,\\n1313 | \\t\\t\\trequestDelaySeconds,\\n1314 | \\t\\t\\tcurrentApiConfigName,\\n1315 | \\t\\t\\tlistApiConfigMeta,\\n1316 | \\t\\t\\tpinnedApiConfigs,\\n1317 | \\t\\t\\tmode,\\n1318 | \\t\\t\\tcustomModePrompts,\\n1319 | \\t\\t\\tcustomSupportPrompts,\\n1320 | \\t\\t\\tenhancementApiConfigId,\\n1321 | \\t\\t\\tautoApprovalEnabled,\\n1322 | \\t\\t\\tcustomModes,\\n1323 | \\t\\t\\texperiments,\\n1324 | \\t\\t\\tmaxOpenTabsContext,\\n1325 | \\t\\t\\tmaxWorkspaceFiles,\\n1326 | \\t\\t\\tbrowserToolEnabled,\\n1327 | \\t\\t\\ttelemetrySetting,\\n1328 | \\t\\t\\tshowRooIgnoredFiles,\\n1329 | \\t\\t\\tlanguage,\\n1330 | \\t\\t\\tmaxReadFileLine,\\n1331 | \\t\\t\\tterminalCompressProgressBar,\\n1332 | \\t\\t\\thistoryPreviewCollapsed,\\n1333 | \\t\\t\\tcloudUserInfo,\\n1334 | \\t\\t\\tcloudIsAuthenticated,\\n1335 | \\t\\t\\tsharingEnabled,\\n1336 | \\t\\t\\torganizationAllowList,\\n1337 | \\t\\t\\tmaxConcurrentFileReads,\\n1338 | \\t\\t\\tcondensingApiConfigId,\\n1339 | \\t\\t\\tcustomCondensingPrompt,\\n1340 | \\t\\t\\tcodebaseIndexConfig,\\n1341 | \\t\\t\\tcodebaseIndexModels,\\n1342 | \\t\\t} = await this.getState()\\n1343 | \\n1344 | \\t\\tconst telemetryKey = process.env.POSTHOG_API_KEY\\n1345 | \\t\\tconst machineId = vscode.env.machineId\\n1346 | \\t\\tconst allowedCommands = vscode.workspace.getConfiguration(Package.name).get(\\\"allowedCommands\\\") || []\\n1347 | \\t\\tconst cwd = this.cwd\\n1348 | \\n1349 | \\t\\t// Only fetch marketplace data if the feature is enabled\\n1350 | \\t\\tlet marketplaceItems: any[] = []\\n1351 | \\t\\tlet marketplaceInstalledMetadata: any = { project: {}, global: {} }\\n1352 | \\n1353 | \\t\\tif (experiments.marketplace) {\\n1354 | \\t\\t\\tmarketplaceItems = (await this.marketplaceManager.getCurrentItems()) || []\\n1355 | \\t\\t\\tmarketplaceInstalledMetadata = await this.marketplaceManager.getInstallationMetadata()\\n1356 | \\t\\t}\\n1357 | \\n1358 | \\t\\t// Check if there's a system prompt override for the current mode\\n1359 | \\t\\tconst currentMode = mode ?? defaultModeSlug\\n1360 | \\t\\tconst hasSystemPromptOverride = await this.hasFileBasedSystemPromptOverride(currentMode)\\n1361 | \\n1362 | \\t\\treturn {\\n1363 | \\t\\t\\tversion: this.context.extension?.packageJSON?.version ?? \\\"\\\",\\n1364 | \\t\\t\\tmarketplaceItems,\\n1365 | \\t\\t\\tmarketplaceInstalledMetadata,\\n1366 | \\t\\t\\tapiConfiguration,\\n1367 | \\t\\t\\tcustomInstructions,\\n1368 | \\t\\t\\talwaysAllowReadOnly: alwaysAllowReadOnly ?? false,\\n1369 | \\t\\t\\talwaysAllowReadOnlyOutsideWorkspace: alwaysAllowReadOnlyOutsideWorkspace ?? false,\\n1370 | \\t\\t\\talwaysAllowWrite: alwaysAllowWrite ?? false,\\n1371 | \\t\\t\\talwaysAllowWriteOutsideWorkspace: alwaysAllowWriteOutsideWorkspace ?? false,\\n1372 | \\t\\t\\talwaysAllowExecute: alwaysAllowExecute ?? false,\\n1373 | \\t\\t\\talwaysAllowBrowser: alwaysAllowBrowser ?? false,\\n1374 | \\t\\t\\talwaysAllowMcp: alwaysAllowMcp ?? false,\\n1375 | \\t\\t\\talwaysAllowModeSwitch: alwaysAllowModeSwitch ?? false,\\n1376 | \\t\\t\\talwaysAllowSubtasks: alwaysAllowSubtasks ?? false,\\n1377 | \\t\\t\\tallowedMaxRequests,\\n1378 | \\t\\t\\tautoCondenseContext: autoCondenseContext ?? true,\\n1379 | \\t\\t\\tautoCondenseContextPercent: autoCondenseContextPercent ?? 100,\\n1380 | \\t\\t\\turiScheme: vscode.env.uriScheme,\\n1381 | \\t\\t\\tcurrentTaskItem: this.getCurrentCline()?.taskId\\n1382 | \\t\\t\\t\\t? (taskHistory || []).find((item: HistoryItem) => item.id === this.getCurrentCline()?.taskId)\\n1383 | \\t\\t\\t\\t: undefined,\\n1384 | \\t\\t\\tclineMessages: this.getCurrentCline()?.clineMessages || [],\\n1385 | \\t\\t\\ttaskHistory: (taskHistory || [])\\n1386 | \\t\\t\\t\\t.filter((item: HistoryItem) => item.ts && item.task)\\n1387 | \\t\\t\\t\\t.sort((a: HistoryItem, b: HistoryItem) => b.ts - a.ts),\\n1388 | \\t\\t\\tsoundEnabled: soundEnabled ?? false,\\n1389 | \\t\\t\\tttsEnabled: ttsEnabled ?? false,\\n1390 | \\t\\t\\tttsSpeed: ttsSpeed ?? 1.0,\\n1391 | \\t\\t\\tdiffEnabled: diffEnabled ?? true,\\n1392 | \\t\\t\\tenableCheckpoints: enableCheckpoints ?? true,\\n1393 | \\t\\t\\tshouldShowAnnouncement:\\n1394 | \\t\\t\\t\\ttelemetrySetting !== \\\"unset\\\" && lastShownAnnouncementId !== this.latestAnnouncementId,\\n1395 | \\t\\t\\tallowedCommands,\\n1396 | \\t\\t\\tsoundVolume: soundVolume ?? 0.5,\\n1397 | \\t\\t\\tbrowserViewportSize: browserViewportSize ?? \\\"900x600\\\",\\n1398 | \\t\\t\\tscreenshotQuality: screenshotQuality ?? 75,\\n1399 | \\t\\t\\tremoteBrowserHost,\\n1400 | \\t\\t\\tremoteBrowserEnabled: remoteBrowserEnabled ?? false,\\n1401 | \\t\\t\\tcachedChromeHostUrl: cachedChromeHostUrl,\\n1402 | \\t\\t\\twriteDelayMs: writeDelayMs ?? 1000,\\n1403 | \\t\\t\\tterminalOutputLineLimit: terminalOutputLineLimit ?? 500,\\n1404 | \\t\\t\\tterminalShellIntegrationTimeout: terminalShellIntegrationTimeout ?? Terminal.defaultShellIntegrationTimeout,\\n1405 | \\t\\t\\tterminalShellIntegrationDisabled: terminalShellIntegrationDisabled ?? false,\\n1406 | \\t\\t\\tterminalCommandDelay: terminalCommandDelay ?? 0,\\n1407 | \\t\\t\\tterminalPowershellCounter: terminalPowershellCounter ?? false,\\n1408 | \\t\\t\\tterminalZshClearEolMark: terminalZshClearEolMark ?? true,\\n1409 | \\t\\t\\tterminalZshOhMy: terminalZshOhMy ?? false,\\n1410 | \\t\\t\\tterminalZshP10k: terminalZshP10k ?? false,\\n1411 | \\t\\t\\tterminalZdotdir: terminalZdotdir ?? false,\\n1412 | \\t\\t\\tfuzzyMatchThreshold: fuzzyMatchThreshold ?? 1.0,\\n1413 | \\t\\t\\tmcpEnabled: mcpEnabled ?? true,\\n1414 | \\t\\t\\tenableMcpServerCreation: enableMcpServerCreation ?? true,\\n1415 | \\t\\t\\talwaysApproveResubmit: alwaysApproveResubmit ?? false,\\n1416 | \\t\\t\\trequestDelaySeconds: requestDelaySeconds ?? 10,\\n1417 | \\t\\t\\tcurrentApiConfigName: currentApiConfigName ?? \\\"default\\\",\\n1418 | \\t\\t\\tlistApiConfigMeta: listApiConfigMeta ?? [],\\n1419 | \\t\\t\\tpinnedApiConfigs: pinnedApiConfigs ?? {},\\n1420 | \\t\\t\\tmode: mode ?? defaultModeSlug,\\n1421 | \\t\\t\\tcustomModePrompts: customModePrompts ?? {},\\n1422 | \\t\\t\\tcustomSupportPrompts: customSupportPrompts ?? {},\\n1423 | \\t\\t\\tenhancementApiConfigId,\\n1424 | \\t\\t\\tautoApprovalEnabled: autoApprovalEnabled ?? false,\\n1425 | \\t\\t\\tcustomModes,\\n1426 | \\t\\t\\texperiments: experiments ?? experimentDefault,\\n1427 | \\t\\t\\tmcpServers: this.mcpHub?.getAllServers() ?? [],\\n1428 | \\t\\t\\tmaxOpenTabsContext: maxOpenTabsContext ?? 20,\\n1429 | \\t\\t\\tmaxWorkspaceFiles: maxWorkspaceFiles ?? 200,\\n1430 | \\t\\t\\tcwd,\\n1431 | \\t\\t\\tbrowserToolEnabled: browserToolEnabled ?? true,\\n1432 | \\t\\t\\ttelemetrySetting,\\n1433 | \\t\\t\\ttelemetryKey,\\n1434 | \\t\\t\\tmachineId,\\n1435 | \\t\\t\\tshowRooIgnoredFiles: showRooIgnoredFiles ?? true,\\n1436 | \\t\\t\\tlanguage: language ?? formatLanguage(vscode.env.language),\\n1437 | \\t\\t\\trenderContext: this.renderContext,\\n1438 | \\t\\t\\tmaxReadFileLine: maxReadFileLine ?? -1,\\n1439 | \\t\\t\\tmaxConcurrentFileReads: maxConcurrentFileReads ?? 5,\\n1440 | \\t\\t\\tsettingsImportedAt: this.settingsImportedAt,\\n1441 | \\t\\t\\tterminalCompressProgressBar: terminalCompressProgressBar ?? true,\\n1442 | \\t\\t\\thasSystemPromptOverride,\\n1443 | \\t\\t\\thistoryPreviewCollapsed: historyPreviewCollapsed ?? false,\\n1444 | \\t\\t\\tcloudUserInfo,\\n1445 | \\t\\t\\tcloudIsAuthenticated: cloudIsAuthenticated ?? false,\\n1446 | \\t\\t\\tsharingEnabled: sharingEnabled ?? false,\\n1447 | \\t\\t\\torganizationAllowList,\\n1448 | \\t\\t\\tcondensingApiConfigId,\\n1449 | \\t\\t\\tcustomCondensingPrompt,\\n1450 | \\t\\t\\tcodebaseIndexModels: codebaseIndexModels ?? EMBEDDING_MODEL_PROFILES,\\n1451 | \\t\\t\\tcodebaseIndexConfig: codebaseIndexConfig ?? {\\n1452 | \\t\\t\\t\\tcodebaseIndexEnabled: false,\\n1453 | \\t\\t\\t\\tcodebaseIndexQdrantUrl: \\\"http://localhost:6333\\\",\\n1454 | \\t\\t\\t\\tcodebaseIndexEmbedderProvider: \\\"openai\\\",\\n1455 | \\t\\t\\t\\tcodebaseIndexEmbedderBaseUrl: \\\"\\\",\\n1456 | \\t\\t\\t\\tcodebaseIndexEmbedderModelId: \\\"\\\",\\n1457 | \\t\\t\\t},\\n1458 | \\t\\t}\\n1459 | \\t}\\n1460 | \\n1461 | \\t/**\\n1462 | \\t * Storage\\n1463 | \\t * https://dev.to/kompotkot/how-to-use-secretstorage-in-your-vscode-extensions-2hco\\n1464 | \\t * https://www.eliostruyf.com/devhack-code-extension-storage-options/\\n1465 | \\t */\\n1466 | \\n1467 | \\tasync getState() {\\n1468 | \\t\\tconst stateValues = this.contextProxy.getValues()\\n1469 | \\t\\tconst customModes = await this.customModesManager.getCustomModes()\\n1470 | \\n1471 | \\t\\t// Determine apiProvider with the same logic as before.\\n1472 | \\t\\tconst apiProvider: ProviderName = stateValues.apiProvider ? stateValues.apiProvider : \\\"anthropic\\\"\\n1473 | \\n1474 | \\t\\t// Build the apiConfiguration object combining state values and secrets.\\n1475 | \\t\\tconst providerSettings = this.contextProxy.getProviderSettings()\\n1476 | \\n1477 | \\t\\t// Ensure apiProvider is set properly if not already in state\\n1478 | \\t\\tif (!providerSettings.apiProvider) {\\n1479 | \\t\\t\\tproviderSettings.apiProvider = apiProvider\\n1480 | \\t\\t}\\n1481 | \\n1482 | \\t\\tlet organizationAllowList = ORGANIZATION_ALLOW_ALL\\n1483 | \\n1484 | \\t\\ttry {\\n1485 | \\t\\t\\torganizationAllowList = await CloudService.instance.getAllowList()\\n1486 | \\t\\t} catch (error) {\\n1487 | \\t\\t\\tconsole.error(\\n1488 | \\t\\t\\t\\t`[getState] failed to get organization allow list: ${error instanceof Error ? error.message : String(error)}`,\\n1489 | \\t\\t\\t)\\n1490 | \\t\\t}\\n1491 | \\n1492 | \\t\\tlet cloudUserInfo: CloudUserInfo | null = null\\n1493 | \\n1494 | \\t\\ttry {\\n1495 | \\t\\t\\tcloudUserInfo = CloudService.instance.getUserInfo()\\n1496 | \\t\\t} catch (error) {\\n1497 | \\t\\t\\tconsole.error(\\n1498 | \\t\\t\\t\\t`[getState] failed to get cloud user info: ${error instanceof Error ? error.message : String(error)}`,\\n1499 | \\t\\t\\t)\\n1500 | \\t\\t}\\n1501 | \\n1502 | \\t\\tlet cloudIsAuthenticated: boolean = false\\n1503 | \\n1504 | \\t\\ttry {\\n1505 | \\t\\t\\tcloudIsAuthenticated = CloudService.instance.isAuthenticated()\\n1506 | \\t\\t} catch (error) {\\n1507 | \\t\\t\\tconsole.error(\\n1508 | \\t\\t\\t\\t`[getState] failed to get cloud authentication state: ${error instanceof Error ? error.message : String(error)}`,\\n1509 | \\t\\t\\t)\\n1510 | \\t\\t}\\n1511 | \\n1512 | \\t\\tlet sharingEnabled: boolean = false\\n1513 | \\n1514 | \\t\\ttry {\\n1515 | \\t\\t\\tsharingEnabled = await CloudService.instance.canShareTask()\\n1516 | \\t\\t} catch (error) {\\n1517 | \\t\\t\\tconsole.error(\\n1518 | \\t\\t\\t\\t`[getState] failed to get sharing enabled state: ${error instanceof Error ? error.message : String(error)}`,\\n1519 | \\t\\t\\t)\\n1520 | \\t\\t}\\n1521 | \\n1522 | \\t\\t// Return the same structure as before\\n1523 | \\t\\treturn {\\n1524 | \\t\\t\\tapiConfiguration: providerSettings,\\n1525 | \\t\\t\\tlastShownAnnouncementId: stateValues.lastShownAnnouncementId,\\n1526 | \\t\\t\\tcustomInstructions: stateValues.customInstructions,\\n1527 | \\t\\t\\tapiModelId: stateValues.apiModelId,\\n1528 | \\t\\t\\talwaysAllowReadOnly: stateValues.alwaysAllowReadOnly ?? false,\\n1529 | \\t\\t\\talwaysAllowReadOnlyOutsideWorkspace: stateValues.alwaysAllowReadOnlyOutsideWorkspace ?? false,\\n1530 | \\t\\t\\talwaysAllowWrite: stateValues.alwaysAllowWrite ?? false,\\n1531 | \\t\\t\\talwaysAllowWriteOutsideWorkspace: stateValues.alwaysAllowWriteOutsideWorkspace ?? false,\\n1532 | \\t\\t\\talwaysAllowExecute: stateValues.alwaysAllowExecute ?? false,\\n1533 | \\t\\t\\talwaysAllowBrowser: stateValues.alwaysAllowBrowser ?? false,\\n1534 | \\t\\t\\talwaysAllowMcp: stateValues.alwaysAllowMcp ?? false,\\n1535 | \\t\\t\\talwaysAllowModeSwitch: stateValues.alwaysAllowModeSwitch ?? false,\\n1536 | \\t\\t\\talwaysAllowSubtasks: stateValues.alwaysAllowSubtasks ?? false,\\n1537 | \\t\\t\\tallowedMaxRequests: stateValues.allowedMaxRequests,\\n1538 | \\t\\t\\tautoCondenseContext: stateValues.autoCondenseContext ?? true,\\n1539 | \\t\\t\\tautoCondenseContextPercent: stateValues.autoCondenseContextPercent ?? 100,\\n1540 | \\t\\t\\ttaskHistory: stateValues.taskHistory,\\n1541 | \\t\\t\\tallowedCommands: stateValues.allowedCommands,\\n1542 | \\t\\t\\tsoundEnabled: stateValues.soundEnabled ?? false,\\n1543 | \\t\\t\\tttsEnabled: stateValues.ttsEnabled ?? false,\\n1544 | \\t\\t\\tttsSpeed: stateValues.ttsSpeed ?? 1.0,\\n1545 | \\t\\t\\tdiffEnabled: stateValues.diffEnabled ?? true,\\n1546 | \\t\\t\\tenableCheckpoints: stateValues.enableCheckpoints ?? true,\\n1547 | \\t\\t\\tsoundVolume: stateValues.soundVolume,\\n1548 | \\t\\t\\tbrowserViewportSize: stateValues.browserViewportSize ?? \\\"900x600\\\",\\n1549 | \\t\\t\\tscreenshotQuality: stateValues.screenshotQuality ?? 75,\\n1550 | \\t\\t\\tremoteBrowserHost: stateValues.remoteBrowserHost,\\n1551 | \\t\\t\\tremoteBrowserEnabled: stateValues.remoteBrowserEnabled ?? false,\\n1552 | \\t\\t\\tcachedChromeHostUrl: stateValues.cachedChromeHostUrl as string | undefined,\\n1553 | \\t\\t\\tfuzzyMatchThreshold: stateValues.fuzzyMatchThreshold ?? 1.0,\\n1554 | \\t\\t\\twriteDelayMs: stateValues.writeDelayMs ?? 1000,\\n1555 | \\t\\t\\tterminalOutputLineLimit: stateValues.terminalOutputLineLimit ?? 500,\\n1556 | \\t\\t\\tterminalShellIntegrationTimeout:\\n1557 | \\t\\t\\t\\tstateValues.terminalShellIntegrationTimeout ?? Terminal.defaultShellIntegrationTimeout,\\n1558 | \\t\\t\\tterminalShellIntegrationDisabled: stateValues.terminalShellIntegrationDisabled ?? false,\\n1559 | \\t\\t\\tterminalCommandDelay: stateValues.terminalCommandDelay ?? 0,\\n1560 | \\t\\t\\tterminalPowershellCounter: stateValues.terminalPowershellCounter ?? false,\\n1561 | \\t\\t\\tterminalZshClearEolMark: stateValues.terminalZshClearEolMark ?? true,\\n1562 | \\t\\t\\tterminalZshOhMy: stateValues.terminalZshOhMy ?? false,\\n1563 | \\t\\t\\tterminalZshP10k: stateValues.terminalZshP10k ?? false,\\n1564 | \\t\\t\\tterminalZdotdir: stateValues.terminalZdotdir ?? false,\\n1565 | \\t\\t\\tterminalCompressProgressBar: stateValues.terminalCompressProgressBar ?? true,\\n1566 | \\t\\t\\tmode: stateValues.mode ?? defaultModeSlug,\\n1567 | \\t\\t\\tlanguage: stateValues.language ?? formatLanguage(vscode.env.language),\\n1568 | \\t\\t\\tmcpEnabled: stateValues.mcpEnabled ?? true,\\n1569 | \\t\\t\\tenableMcpServerCreation: stateValues.enableMcpServerCreation ?? true,\\n1570 | \\t\\t\\talwaysApproveResubmit: stateValues.alwaysApproveResubmit ?? false,\\n1571 | \\t\\t\\trequestDelaySeconds: Math.max(5, stateValues.requestDelaySeconds ?? 10),\\n1572 | \\t\\t\\tcurrentApiConfigName: stateValues.currentApiConfigName ?? \\\"default\\\",\\n1573 | \\t\\t\\tlistApiConfigMeta: stateValues.listApiConfigMeta ?? [],\\n1574 | \\t\\t\\tpinnedApiConfigs: stateValues.pinnedApiConfigs ?? {},\\n1575 | \\t\\t\\tmodeApiConfigs: stateValues.modeApiConfigs ?? ({} as Record),\\n1576 | \\t\\t\\tcustomModePrompts: stateValues.customModePrompts ?? {},\\n1577 | \\t\\t\\tcustomSupportPrompts: stateValues.customSupportPrompts ?? {},\\n1578 | \\t\\t\\tenhancementApiConfigId: stateValues.enhancementApiConfigId,\\n1579 | \\t\\t\\texperiments: stateValues.experiments ?? experimentDefault,\\n1580 | \\t\\t\\tautoApprovalEnabled: stateValues.autoApprovalEnabled ?? false,\\n1581 | \\t\\t\\tcustomModes,\\n1582 | \\t\\t\\tmaxOpenTabsContext: stateValues.maxOpenTabsContext ?? 20,\\n1583 | \\t\\t\\tmaxWorkspaceFiles: stateValues.maxWorkspaceFiles ?? 200,\\n1584 | \\t\\t\\topenRouterUseMiddleOutTransform: stateValues.openRouterUseMiddleOutTransform ?? true,\\n1585 | \\t\\t\\tbrowserToolEnabled: stateValues.browserToolEnabled ?? true,\\n1586 | \\t\\t\\ttelemetrySetting: stateValues.telemetrySetting || \\\"unset\\\",\\n1587 | \\t\\t\\tshowRooIgnoredFiles: stateValues.showRooIgnoredFiles ?? true,\\n1588 | \\t\\t\\tmaxReadFileLine: stateValues.maxReadFileLine ?? -1,\\n1589 | \\t\\t\\tmaxConcurrentFileReads: stateValues.maxConcurrentFileReads ?? 5,\\n1590 | \\t\\t\\thistoryPreviewCollapsed: stateValues.historyPreviewCollapsed ?? false,\\n1591 | \\t\\t\\tcloudUserInfo,\\n1592 | \\t\\t\\tcloudIsAuthenticated,\\n1593 | \\t\\t\\tsharingEnabled,\\n1594 | \\t\\t\\torganizationAllowList,\\n1595 | \\t\\t\\t// Explicitly add condensing settings\\n1596 | \\t\\t\\tcondensingApiConfigId: stateValues.condensingApiConfigId,\\n1597 | \\t\\t\\tcustomCondensingPrompt: stateValues.customCondensingPrompt,\\n1598 | \\t\\t\\tcodebaseIndexModels: stateValues.codebaseIndexModels ?? EMBEDDING_MODEL_PROFILES,\\n1599 | \\t\\t\\tcodebaseIndexConfig: stateValues.codebaseIndexConfig ?? {\\n1600 | \\t\\t\\t\\tcodebaseIndexEnabled: false,\\n1601 | \\t\\t\\t\\tcodebaseIndexQdrantUrl: \\\"http://localhost:6333\\\",\\n1602 | \\t\\t\\t\\tcodebaseIndexEmbedderProvider: \\\"openai\\\",\\n1603 | \\t\\t\\t\\tcodebaseIndexEmbedderBaseUrl: \\\"\\\",\\n1604 | \\t\\t\\t\\tcodebaseIndexEmbedderModelId: \\\"\\\",\\n1605 | \\t\\t\\t},\\n1606 | \\t\\t}\\n1607 | \\t}\\n1608 | \\n1609 | \\tasync updateTaskHistory(item: HistoryItem): Promise {\\n1610 | \\t\\tconst history = (this.getGlobalState(\\\"taskHistory\\\") as HistoryItem[] | undefined) || []\\n1611 | \\t\\tconst existingItemIndex = history.findIndex((h) => h.id === item.id)\\n1612 | \\n1613 | \\t\\tif (existingItemIndex !== -1) {\\n1614 | \\t\\t\\thistory[existingItemIndex] = item\\n1615 | \\t\\t} else {\\n1616 | \\t\\t\\thistory.push(item)\\n1617 | \\t\\t}\\n1618 | \\n1619 | \\t\\tawait this.updateGlobalState(\\\"taskHistory\\\", history)\\n1620 | \\t\\treturn history\\n1621 | \\t}\\n1622 | \\n1623 | \\t// ContextProxy\\n1624 | \\n1625 | \\t// @deprecated - Use `ContextProxy#setValue` instead.\\n1626 | \\tprivate async updateGlobalState(key: K, value: GlobalState[K]) {\\n1627 | \\t\\tawait this.contextProxy.setValue(key, value)\\n1628 | \\t}\\n1629 | \\n1630 | \\t// @deprecated - Use `ContextProxy#getValue` instead.\\n1631 | \\tprivate getGlobalState(key: K) {\\n1632 | \\t\\treturn this.contextProxy.getValue(key)\\n1633 | \\t}\\n1634 | \\n1635 | \\tpublic async setValue(key: K, value: RooCodeSettings[K]) {\\n1636 | \\t\\tawait this.contextProxy.setValue(key, value)\\n1637 | \\t}\\n1638 | \\n1639 | \\tpublic getValue(key: K) {\\n1640 | \\t\\treturn this.contextProxy.getValue(key)\\n1641 | \\t}\\n1642 | \\n1643 | \\tpublic getValues() {\\n1644 | \\t\\treturn this.contextProxy.getValues()\\n1645 | \\t}\\n1646 | \\n1647 | \\tpublic async setValues(values: RooCodeSettings) {\\n1648 | \\t\\tawait this.contextProxy.setValues(values)\\n1649 | \\t}\\n1650 | \\n1651 | \\t// cwd\\n1652 | \\n1653 | \\tget cwd() {\\n1654 | \\t\\treturn getWorkspacePath()\\n1655 | \\t}\\n1656 | \\n1657 | \\t// dev\\n1658 | \\n1659 | \\tasync resetState() {\\n1660 | \\t\\tconst answer = await vscode.window.showInformationMessage(\\n1661 | \\t\\t\\tt(\\\"common:confirmation.reset_state\\\"),\\n1662 | \\t\\t\\t{ modal: true },\\n1663 | \\t\\t\\tt(\\\"common:answers.yes\\\"),\\n1664 | \\t\\t)\\n1665 | \\n1666 | \\t\\tif (answer !== t(\\\"common:answers.yes\\\")) {\\n1667 | \\t\\t\\treturn\\n1668 | \\t\\t}\\n1669 | \\n1670 | \\t\\tawait this.contextProxy.resetAllState()\\n1671 | \\t\\tawait this.providerSettingsManager.resetAllConfigs()\\n1672 | \\t\\tawait this.customModesManager.resetCustomModes()\\n1673 | \\t\\tawait this.removeClineFromStack()\\n1674 | \\t\\tawait this.postStateToWebview()\\n1675 | \\t\\tawait this.postMessageToWebview({ type: \\\"action\\\", action: \\\"chatButtonClicked\\\" })\\n1676 | \\t}\\n1677 | \\n1678 | \\t// logging\\n1679 | \\n1680 | \\tpublic log(message: string) {\\n1681 | \\t\\tthis.outputChannel.appendLine(message)\\n1682 | \\t\\tconsole.log(message)\\n1683 | \\t}\\n1684 | \\n1685 | \\t// integration tests\\n1686 | \\n1687 | \\tget viewLaunched() {\\n1688 | \\t\\treturn this.isViewLaunched\\n1689 | \\t}\\n1690 | \\n1691 | \\tget messages() {\\n1692 | \\t\\treturn this.getCurrentCline()?.clineMessages || []\\n1693 | \\t}\\n1694 | \\n1695 | \\t// Add public getter\\n1696 | \\tpublic getMcpHub(): McpHub | undefined {\\n1697 | \\t\\treturn this.mcpHub\\n1698 | \\t}\\n1699 | \\n1700 | \\t/**\\n1701 | \\t * Returns properties to be included in every telemetry event\\n1702 | \\t * This method is called by the telemetry service to get context information\\n1703 | \\t * like the current mode, API provider, etc.\\n1704 | \\t */\\n1705 | \\tpublic async getTelemetryProperties(): Promise {\\n1706 | \\t\\tconst { mode, apiConfiguration, language } = await this.getState()\\n1707 | \\t\\tconst task = this.getCurrentCline()\\n1708 | \\n1709 | \\t\\tconst packageJSON = this.context.extension?.packageJSON\\n1710 | \\n1711 | \\t\\treturn {\\n1712 | \\t\\t\\tappName: packageJSON?.name ?? Package.name,\\n1713 | \\t\\t\\tappVersion: packageJSON?.version ?? Package.version,\\n1714 | \\t\\t\\tvscodeVersion: vscode.version,\\n1715 | \\t\\t\\tplatform: process.platform,\\n1716 | \\t\\t\\teditorName: vscode.env.appName,\\n1717 | \\t\\t\\tlanguage,\\n1718 | \\t\\t\\tmode,\\n1719 | \\t\\t\\tapiProvider: apiConfiguration?.apiProvider,\\n1720 | \\t\\t\\tmodelId: task?.api?.getModel().id,\\n1721 | \\t\\t\\tdiffStrategy: task?.diffStrategy?.getName(),\\n1722 | \\t\\t\\tisSubtask: task ? !!task.parentTask : undefined,\\n1723 | \\t\\t}\\n1724 | \\t}\\n1725 | }\\n\\n\\nsrc/core/webview/webviewMessageHandler.ts\\n\\n 1 | import * as path from \\\"path\\\"\\n 2 | import fs from \\\"fs/promises\\\"\\n 3 | import pWaitFor from \\\"p-wait-for\\\"\\n 4 | import * as vscode from \\\"vscode\\\"\\n 5 | \\n 6 | import { type Language, type ProviderSettings, type GlobalState, TelemetryEventName } from \\\"@roo-code/types\\\"\\n 7 | import { CloudService } from \\\"@roo-code/cloud\\\"\\n 8 | import { TelemetryService } from \\\"@roo-code/telemetry\\\"\\n 9 | \\n 10 | import { ClineProvider } from \\\"./ClineProvider\\\"\\n 11 | import { changeLanguage, t } from \\\"../../i18n\\\"\\n 12 | import { Package } from \\\"../../shared/package\\\"\\n 13 | import { RouterName, toRouterName, ModelRecord } from \\\"../../shared/api\\\"\\n 14 | import { supportPrompt } from \\\"../../shared/support-prompt\\\"\\n 15 | \\n 16 | import { checkoutDiffPayloadSchema, checkoutRestorePayloadSchema, WebviewMessage } from \\\"../../shared/WebviewMessage\\\"\\n 17 | import { checkExistKey } from \\\"../../shared/checkExistApiConfig\\\"\\n 18 | import { experimentDefault } from \\\"../../shared/experiments\\\"\\n 19 | import { Terminal } from \\\"../../integrations/terminal/Terminal\\\"\\n 20 | import { openFile } from \\\"../../integrations/misc/open-file\\\"\\n 21 | import { openImage, saveImage } from \\\"../../integrations/misc/image-handler\\\"\\n 22 | import { selectImages } from \\\"../../integrations/misc/process-images\\\"\\n 23 | import { getTheme } from \\\"../../integrations/theme/getTheme\\\"\\n 24 | import { discoverChromeHostUrl, tryChromeHostUrl } from \\\"../../services/browser/browserDiscovery\\\"\\n 25 | import { searchWorkspaceFiles } from \\\"../../services/search/file-search\\\"\\n 26 | import { fileExistsAtPath } from \\\"../../utils/fs\\\"\\n 27 | import { playTts, setTtsEnabled, setTtsSpeed, stopTts } from \\\"../../utils/tts\\\"\\n 28 | import { singleCompletionHandler } from \\\"../../utils/single-completion-handler\\\"\\n 29 | import { searchCommits } from \\\"../../utils/git\\\"\\n 30 | import { exportSettings, importSettings } from \\\"../config/importExport\\\"\\n 31 | import { getOpenAiModels } from \\\"../../api/providers/openai\\\"\\n 32 | import { getOllamaModels } from \\\"../../api/providers/ollama\\\"\\n 33 | import { getVsCodeLmModels } from \\\"../../api/providers/vscode-lm\\\"\\n 34 | import { getLmStudioModels } from \\\"../../api/providers/lm-studio\\\"\\n 35 | import { openMention } from \\\"../mentions\\\"\\n 36 | import { TelemetrySetting } from \\\"../../shared/TelemetrySetting\\\"\\n 37 | import { getWorkspacePath } from \\\"../../utils/path\\\"\\n 38 | import { Mode, defaultModeSlug } from \\\"../../shared/modes\\\"\\n 39 | import { getModels, flushModels } from \\\"../../api/providers/fetchers/modelCache\\\"\\n 40 | import { GetModelsOptions } from \\\"../../shared/api\\\"\\n 41 | import { generateSystemPrompt } from \\\"./generateSystemPrompt\\\"\\n 42 | import { getCommand } from \\\"../../utils/commands\\\"\\n 43 | \\n 44 | const ALLOWED_VSCODE_SETTINGS = new Set([\\\"terminal.integrated.inheritEnv\\\"])\\n 45 | \\n 46 | import { MarketplaceManager, MarketplaceItemType } from \\\"../../services/marketplace\\\"\\n 47 | \\n 48 | export const webviewMessageHandler = async (\\n 49 | \\tprovider: ClineProvider,\\n 50 | \\tmessage: WebviewMessage,\\n 51 | \\tmarketplaceManager?: MarketplaceManager,\\n 52 | ) => {\\n 53 | \\t// Utility functions provided for concise get/update of global state via contextProxy API.\\n 54 | \\tconst getGlobalState = (key: K) => provider.contextProxy.getValue(key)\\n 55 | \\tconst updateGlobalState = async (key: K, value: GlobalState[K]) =>\\n 56 | \\t\\tawait provider.contextProxy.setValue(key, value)\\n 57 | \\n 58 | \\tswitch (message.type) {\\n 59 | \\t\\tcase \\\"webviewDidLaunch\\\":\\n 60 | \\t\\t\\t// Load custom modes first\\n 61 | \\t\\t\\tconst customModes = await provider.customModesManager.getCustomModes()\\n 62 | \\t\\t\\tawait updateGlobalState(\\\"customModes\\\", customModes)\\n 63 | \\n 64 | \\t\\t\\tprovider.postStateToWebview()\\n 65 | \\t\\t\\tprovider.workspaceTracker?.initializeFilePaths() // Don't await.\\n 66 | \\n 67 | \\t\\t\\tgetTheme().then((theme) => provider.postMessageToWebview({ type: \\\"theme\\\", text: JSON.stringify(theme) }))\\n 68 | \\n 69 | \\t\\t\\t// If MCP Hub is already initialized, update the webview with\\n 70 | \\t\\t\\t// current server list.\\n 71 | \\t\\t\\tconst mcpHub = provider.getMcpHub()\\n 72 | \\n 73 | \\t\\t\\tif (mcpHub) {\\n 74 | \\t\\t\\t\\tprovider.postMessageToWebview({ type: \\\"mcpServers\\\", mcpServers: mcpHub.getAllServers() })\\n 75 | \\t\\t\\t}\\n 76 | \\n 77 | \\t\\t\\tprovider.providerSettingsManager\\n 78 | \\t\\t\\t\\t.listConfig()\\n 79 | \\t\\t\\t\\t.then(async (listApiConfig) => {\\n 80 | \\t\\t\\t\\t\\tif (!listApiConfig) {\\n 81 | \\t\\t\\t\\t\\t\\treturn\\n 82 | \\t\\t\\t\\t\\t}\\n 83 | \\n 84 | \\t\\t\\t\\t\\tif (listApiConfig.length === 1) {\\n 85 | \\t\\t\\t\\t\\t\\t// Check if first time init then sync with exist config.\\n 86 | \\t\\t\\t\\t\\t\\tif (!checkExistKey(listApiConfig[0])) {\\n 87 | \\t\\t\\t\\t\\t\\t\\tconst { apiConfiguration } = await provider.getState()\\n 88 | \\n 89 | \\t\\t\\t\\t\\t\\t\\tawait provider.providerSettingsManager.saveConfig(\\n 90 | \\t\\t\\t\\t\\t\\t\\t\\tlistApiConfig[0].name ?? \\\"default\\\",\\n 91 | \\t\\t\\t\\t\\t\\t\\t\\tapiConfiguration,\\n 92 | \\t\\t\\t\\t\\t\\t\\t)\\n 93 | \\n 94 | \\t\\t\\t\\t\\t\\t\\tlistApiConfig[0].apiProvider = apiConfiguration.apiProvider\\n 95 | \\t\\t\\t\\t\\t\\t}\\n 96 | \\t\\t\\t\\t\\t}\\n 97 | \\n 98 | \\t\\t\\t\\t\\tconst currentConfigName = getGlobalState(\\\"currentApiConfigName\\\")\\n 99 | \\n 100 | \\t\\t\\t\\t\\tif (currentConfigName) {\\n 101 | \\t\\t\\t\\t\\t\\tif (!(await provider.providerSettingsManager.hasConfig(currentConfigName))) {\\n 102 | \\t\\t\\t\\t\\t\\t\\t// Current config name not valid, get first config in list.\\n 103 | \\t\\t\\t\\t\\t\\t\\tconst name = listApiConfig[0]?.name\\n 104 | \\t\\t\\t\\t\\t\\t\\tawait updateGlobalState(\\\"currentApiConfigName\\\", name)\\n 105 | \\n 106 | \\t\\t\\t\\t\\t\\t\\tif (name) {\\n 107 | \\t\\t\\t\\t\\t\\t\\t\\tawait provider.activateProviderProfile({ name })\\n 108 | \\t\\t\\t\\t\\t\\t\\t\\treturn\\n 109 | \\t\\t\\t\\t\\t\\t\\t}\\n 110 | \\t\\t\\t\\t\\t\\t}\\n 111 | \\t\\t\\t\\t\\t}\\n 112 | \\n 113 | \\t\\t\\t\\t\\tawait Promise.all([\\n 114 | \\t\\t\\t\\t\\t\\tawait updateGlobalState(\\\"listApiConfigMeta\\\", listApiConfig),\\n 115 | \\t\\t\\t\\t\\t\\tawait provider.postMessageToWebview({ type: \\\"listApiConfig\\\", listApiConfig }),\\n 116 | \\t\\t\\t\\t\\t])\\n 117 | \\t\\t\\t\\t})\\n 118 | \\t\\t\\t\\t.catch((error) =>\\n 119 | \\t\\t\\t\\t\\tprovider.log(\\n 120 | \\t\\t\\t\\t\\t\\t`Error list api configuration: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`,\\n 121 | \\t\\t\\t\\t\\t),\\n 122 | \\t\\t\\t\\t)\\n 123 | \\n 124 | \\t\\t\\t// If user already opted in to telemetry, enable telemetry service\\n 125 | \\t\\t\\tprovider.getStateToPostToWebview().then((state) => {\\n 126 | \\t\\t\\t\\tconst { telemetrySetting } = state\\n 127 | \\t\\t\\t\\tconst isOptedIn = telemetrySetting === \\\"enabled\\\"\\n 128 | \\t\\t\\t\\tTelemetryService.instance.updateTelemetryState(isOptedIn)\\n 129 | \\t\\t\\t})\\n 130 | \\n 131 | \\t\\t\\tprovider.isViewLaunched = true\\n 132 | \\t\\t\\tbreak\\n 133 | \\t\\tcase \\\"newTask\\\":\\n 134 | \\t\\t\\t// Initializing new instance of Cline will make sure that any\\n 135 | \\t\\t\\t// agentically running promises in old instance don't affect our new\\n 136 | \\t\\t\\t// task. This essentially creates a fresh slate for the new task.\\n 137 | \\t\\t\\tawait provider.initClineWithTask(message.text, message.images)\\n 138 | \\t\\t\\tbreak\\n 139 | \\t\\tcase \\\"customInstructions\\\":\\n 140 | \\t\\t\\tawait provider.updateCustomInstructions(message.text)\\n 141 | \\t\\t\\tbreak\\n 142 | \\t\\tcase \\\"alwaysAllowReadOnly\\\":\\n 143 | \\t\\t\\tawait updateGlobalState(\\\"alwaysAllowReadOnly\\\", message.bool ?? undefined)\\n 144 | \\t\\t\\tawait provider.postStateToWebview()\\n 145 | \\t\\t\\tbreak\\n 146 | \\t\\tcase \\\"alwaysAllowReadOnlyOutsideWorkspace\\\":\\n 147 | \\t\\t\\tawait updateGlobalState(\\\"alwaysAllowReadOnlyOutsideWorkspace\\\", message.bool ?? undefined)\\n 148 | \\t\\t\\tawait provider.postStateToWebview()\\n 149 | \\t\\t\\tbreak\\n 150 | \\t\\tcase \\\"alwaysAllowWrite\\\":\\n 151 | \\t\\t\\tawait updateGlobalState(\\\"alwaysAllowWrite\\\", message.bool ?? undefined)\\n 152 | \\t\\t\\tawait provider.postStateToWebview()\\n 153 | \\t\\t\\tbreak\\n 154 | \\t\\tcase \\\"alwaysAllowWriteOutsideWorkspace\\\":\\n 155 | \\t\\t\\tawait updateGlobalState(\\\"alwaysAllowWriteOutsideWorkspace\\\", message.bool ?? undefined)\\n 156 | \\t\\t\\tawait provider.postStateToWebview()\\n 157 | \\t\\t\\tbreak\\n 158 | \\t\\tcase \\\"alwaysAllowExecute\\\":\\n 159 | \\t\\t\\tawait updateGlobalState(\\\"alwaysAllowExecute\\\", message.bool ?? undefined)\\n 160 | \\t\\t\\tawait provider.postStateToWebview()\\n 161 | \\t\\t\\tbreak\\n 162 | \\t\\tcase \\\"alwaysAllowBrowser\\\":\\n 163 | \\t\\t\\tawait updateGlobalState(\\\"alwaysAllowBrowser\\\", message.bool ?? undefined)\\n 164 | \\t\\t\\tawait provider.postStateToWebview()\\n 165 | \\t\\t\\tbreak\\n 166 | \\t\\tcase \\\"alwaysAllowMcp\\\":\\n 167 | \\t\\t\\tawait updateGlobalState(\\\"alwaysAllowMcp\\\", message.bool)\\n 168 | \\t\\t\\tawait provider.postStateToWebview()\\n 169 | \\t\\t\\tbreak\\n 170 | \\t\\tcase \\\"alwaysAllowModeSwitch\\\":\\n 171 | \\t\\t\\tawait updateGlobalState(\\\"alwaysAllowModeSwitch\\\", message.bool)\\n 172 | \\t\\t\\tawait provider.postStateToWebview()\\n 173 | \\t\\t\\tbreak\\n 174 | \\t\\tcase \\\"allowedMaxRequests\\\":\\n 175 | \\t\\t\\tawait updateGlobalState(\\\"allowedMaxRequests\\\", message.value)\\n 176 | \\t\\t\\tawait provider.postStateToWebview()\\n 177 | \\t\\t\\tbreak\\n 178 | \\t\\tcase \\\"alwaysAllowSubtasks\\\":\\n 179 | \\t\\t\\tawait updateGlobalState(\\\"alwaysAllowSubtasks\\\", message.bool)\\n 180 | \\t\\t\\tawait provider.postStateToWebview()\\n 181 | \\t\\t\\tbreak\\n 182 | \\t\\tcase \\\"askResponse\\\":\\n 183 | \\t\\t\\tprovider.getCurrentCline()?.handleWebviewAskResponse(message.askResponse!, message.text, message.images)\\n 184 | \\t\\t\\tbreak\\n 185 | \\t\\tcase \\\"autoCondenseContext\\\":\\n 186 | \\t\\t\\tawait updateGlobalState(\\\"autoCondenseContext\\\", message.bool)\\n 187 | \\t\\t\\tawait provider.postStateToWebview()\\n 188 | \\t\\t\\tbreak\\n 189 | \\t\\tcase \\\"autoCondenseContextPercent\\\":\\n 190 | \\t\\t\\tawait updateGlobalState(\\\"autoCondenseContextPercent\\\", message.value)\\n 191 | \\t\\t\\tawait provider.postStateToWebview()\\n 192 | \\t\\t\\tbreak\\n 193 | \\t\\tcase \\\"terminalOperation\\\":\\n 194 | \\t\\t\\tif (message.terminalOperation) {\\n 195 | \\t\\t\\t\\tprovider.getCurrentCline()?.handleTerminalOperation(message.terminalOperation)\\n 196 | \\t\\t\\t}\\n 197 | \\t\\t\\tbreak\\n 198 | \\t\\tcase \\\"clearTask\\\":\\n 199 | \\t\\t\\t// clear task resets the current session and allows for a new task to be started, if this session is a subtask - it allows the parent task to be resumed\\n 200 | \\t\\t\\tawait provider.finishSubTask(t(\\\"common:tasks.canceled\\\"))\\n 201 | \\t\\t\\tawait provider.postStateToWebview()\\n 202 | \\t\\t\\tbreak\\n 203 | \\t\\tcase \\\"didShowAnnouncement\\\":\\n 204 | \\t\\t\\tawait updateGlobalState(\\\"lastShownAnnouncementId\\\", provider.latestAnnouncementId)\\n 205 | \\t\\t\\tawait provider.postStateToWebview()\\n 206 | \\t\\t\\tbreak\\n 207 | \\t\\tcase \\\"selectImages\\\":\\n 208 | \\t\\t\\tconst images = await selectImages()\\n 209 | \\t\\t\\tawait provider.postMessageToWebview({ type: \\\"selectedImages\\\", images })\\n 210 | \\t\\t\\tbreak\\n 211 | \\t\\tcase \\\"exportCurrentTask\\\":\\n 212 | \\t\\t\\tconst currentTaskId = provider.getCurrentCline()?.taskId\\n 213 | \\t\\t\\tif (currentTaskId) {\\n 214 | \\t\\t\\t\\tprovider.exportTaskWithId(currentTaskId)\\n 215 | \\t\\t\\t}\\n 216 | \\t\\t\\tbreak\\n 217 | \\t\\tcase \\\"shareCurrentTask\\\":\\n 218 | \\t\\t\\tconst shareTaskId = provider.getCurrentCline()?.taskId\\n 219 | \\t\\t\\tif (!shareTaskId) {\\n 220 | \\t\\t\\t\\tvscode.window.showErrorMessage(t(\\\"common:errors.share_no_active_task\\\"))\\n 221 | \\t\\t\\t\\tbreak\\n 222 | \\t\\t\\t}\\n 223 | \\n 224 | \\t\\t\\ttry {\\n 225 | \\t\\t\\t\\tconst success = await CloudService.instance.shareTask(shareTaskId)\\n 226 | \\t\\t\\t\\tif (success) {\\n 227 | \\t\\t\\t\\t\\t// Show success message\\n 228 | \\t\\t\\t\\t\\tvscode.window.showInformationMessage(t(\\\"common:info.share_link_copied\\\"))\\n 229 | \\t\\t\\t\\t} else {\\n 230 | \\t\\t\\t\\t\\t// Show generic failure message\\n 231 | \\t\\t\\t\\t\\tvscode.window.showErrorMessage(t(\\\"common:errors.share_task_failed\\\"))\\n 232 | \\t\\t\\t\\t}\\n 233 | \\t\\t\\t} catch (error) {\\n 234 | \\t\\t\\t\\t// Show generic failure message\\n 235 | \\t\\t\\t\\tvscode.window.showErrorMessage(t(\\\"common:errors.share_task_failed\\\"))\\n 236 | \\t\\t\\t}\\n 237 | \\t\\t\\tbreak\\n 238 | \\t\\tcase \\\"showTaskWithId\\\":\\n 239 | \\t\\t\\tprovider.showTaskWithId(message.text!)\\n 240 | \\t\\t\\tbreak\\n 241 | \\t\\tcase \\\"condenseTaskContextRequest\\\":\\n 242 | \\t\\t\\tprovider.condenseTaskContext(message.text!)\\n 243 | \\t\\t\\tbreak\\n 244 | \\t\\tcase \\\"deleteTaskWithId\\\":\\n 245 | \\t\\t\\tprovider.deleteTaskWithId(message.text!)\\n 246 | \\t\\t\\tbreak\\n 247 | \\t\\tcase \\\"deleteMultipleTasksWithIds\\\": {\\n 248 | \\t\\t\\tconst ids = message.ids\\n 249 | \\n 250 | \\t\\t\\tif (Array.isArray(ids)) {\\n 251 | \\t\\t\\t\\t// Process in batches of 20 (or another reasonable number)\\n 252 | \\t\\t\\t\\tconst batchSize = 20\\n 253 | \\t\\t\\t\\tconst results = []\\n 254 | \\n 255 | \\t\\t\\t\\t// Only log start and end of the operation\\n 256 | \\t\\t\\t\\tconsole.log(`Batch deletion started: ${ids.length} tasks total`)\\n 257 | \\n 258 | \\t\\t\\t\\tfor (let i = 0; i < ids.length; i += batchSize) {\\n 259 | \\t\\t\\t\\t\\tconst batch = ids.slice(i, i + batchSize)\\n 260 | \\n 261 | \\t\\t\\t\\t\\tconst batchPromises = batch.map(async (id) => {\\n 262 | \\t\\t\\t\\t\\t\\ttry {\\n 263 | \\t\\t\\t\\t\\t\\t\\tawait provider.deleteTaskWithId(id)\\n 264 | \\t\\t\\t\\t\\t\\t\\treturn { id, success: true }\\n 265 | \\t\\t\\t\\t\\t\\t} catch (error) {\\n 266 | \\t\\t\\t\\t\\t\\t\\t// Keep error logging for debugging purposes\\n 267 | \\t\\t\\t\\t\\t\\t\\tconsole.log(\\n 268 | \\t\\t\\t\\t\\t\\t\\t\\t`Failed to delete task ${id}: ${error instanceof Error ? error.message : String(error)}`,\\n 269 | \\t\\t\\t\\t\\t\\t\\t)\\n 270 | \\t\\t\\t\\t\\t\\t\\treturn { id, success: false }\\n 271 | \\t\\t\\t\\t\\t\\t}\\n 272 | \\t\\t\\t\\t\\t})\\n 273 | \\n 274 | \\t\\t\\t\\t\\t// Process each batch in parallel but wait for completion before starting the next batch\\n 275 | \\t\\t\\t\\t\\tconst batchResults = await Promise.all(batchPromises)\\n 276 | \\t\\t\\t\\t\\tresults.push(...batchResults)\\n 277 | \\n 278 | \\t\\t\\t\\t\\t// Update the UI after each batch to show progress\\n 279 | \\t\\t\\t\\t\\tawait provider.postStateToWebview()\\n 280 | \\t\\t\\t\\t}\\n 281 | \\n 282 | \\t\\t\\t\\t// Log final results\\n 283 | \\t\\t\\t\\tconst successCount = results.filter((r) => r.success).length\\n 284 | \\t\\t\\t\\tconst failCount = results.length - successCount\\n 285 | \\t\\t\\t\\tconsole.log(\\n 286 | \\t\\t\\t\\t\\t`Batch deletion completed: ${successCount}/${ids.length} tasks successful, ${failCount} tasks failed`,\\n 287 | \\t\\t\\t\\t)\\n 288 | \\t\\t\\t}\\n 289 | \\t\\t\\tbreak\\n 290 | \\t\\t}\\n 291 | \\t\\tcase \\\"exportTaskWithId\\\":\\n 292 | \\t\\t\\tprovider.exportTaskWithId(message.text!)\\n 293 | \\t\\t\\tbreak\\n 294 | \\t\\tcase \\\"importSettings\\\": {\\n 295 | \\t\\t\\tconst result = await importSettings({\\n 296 | \\t\\t\\t\\tproviderSettingsManager: provider.providerSettingsManager,\\n 297 | \\t\\t\\t\\tcontextProxy: provider.contextProxy,\\n 298 | \\t\\t\\t\\tcustomModesManager: provider.customModesManager,\\n 299 | \\t\\t\\t})\\n 300 | \\n 301 | \\t\\t\\tif (result.success) {\\n 302 | \\t\\t\\t\\tprovider.settingsImportedAt = Date.now()\\n 303 | \\t\\t\\t\\tawait provider.postStateToWebview()\\n 304 | \\t\\t\\t\\tawait vscode.window.showInformationMessage(t(\\\"common:info.settings_imported\\\"))\\n 305 | \\t\\t\\t} else if (result.error) {\\n 306 | \\t\\t\\t\\tawait vscode.window.showErrorMessage(t(\\\"common:errors.settings_import_failed\\\", { error: result.error }))\\n 307 | \\t\\t\\t}\\n 308 | \\n 309 | \\t\\t\\tbreak\\n 310 | \\t\\t}\\n 311 | \\t\\tcase \\\"exportSettings\\\":\\n 312 | \\t\\t\\tawait exportSettings({\\n 313 | \\t\\t\\t\\tproviderSettingsManager: provider.providerSettingsManager,\\n 314 | \\t\\t\\t\\tcontextProxy: provider.contextProxy,\\n 315 | \\t\\t\\t})\\n 316 | \\n 317 | \\t\\t\\tbreak\\n 318 | \\t\\tcase \\\"resetState\\\":\\n 319 | \\t\\t\\tawait provider.resetState()\\n 320 | \\t\\t\\tbreak\\n 321 | \\t\\tcase \\\"flushRouterModels\\\":\\n 322 | \\t\\t\\tconst routerNameFlush: RouterName = toRouterName(message.text)\\n 323 | \\t\\t\\tawait flushModels(routerNameFlush)\\n 324 | \\t\\t\\tbreak\\n 325 | \\t\\tcase \\\"requestRouterModels\\\":\\n 326 | \\t\\t\\tconst { apiConfiguration } = await provider.getState()\\n 327 | \\n 328 | \\t\\t\\tconst routerModels: Partial> = {\\n 329 | \\t\\t\\t\\topenrouter: {},\\n 330 | \\t\\t\\t\\trequesty: {},\\n 331 | \\t\\t\\t\\tglama: {},\\n 332 | \\t\\t\\t\\tunbound: {},\\n 333 | \\t\\t\\t\\tlitellm: {},\\n 334 | \\t\\t\\t}\\n 335 | \\n 336 | \\t\\t\\tconst safeGetModels = async (options: GetModelsOptions): Promise => {\\n 337 | \\t\\t\\t\\ttry {\\n 338 | \\t\\t\\t\\t\\treturn await getModels(options)\\n 339 | \\t\\t\\t\\t} catch (error) {\\n 340 | \\t\\t\\t\\t\\tconsole.error(\\n 341 | \\t\\t\\t\\t\\t\\t`Failed to fetch models in webviewMessageHandler requestRouterModels for ${options.provider}:`,\\n 342 | \\t\\t\\t\\t\\t\\terror,\\n 343 | \\t\\t\\t\\t\\t)\\n 344 | \\t\\t\\t\\t\\tthrow error // Re-throw to be caught by Promise.allSettled\\n 345 | \\t\\t\\t\\t}\\n 346 | \\t\\t\\t}\\n 347 | \\n 348 | \\t\\t\\tconst modelFetchPromises: Array<{ key: RouterName; options: GetModelsOptions }> = [\\n 349 | \\t\\t\\t\\t{ key: \\\"openrouter\\\", options: { provider: \\\"openrouter\\\" } },\\n 350 | \\t\\t\\t\\t{ key: \\\"requesty\\\", options: { provider: \\\"requesty\\\", apiKey: apiConfiguration.requestyApiKey } },\\n 351 | \\t\\t\\t\\t{ key: \\\"glama\\\", options: { provider: \\\"glama\\\" } },\\n 352 | \\t\\t\\t\\t{ key: \\\"unbound\\\", options: { provider: \\\"unbound\\\", apiKey: apiConfiguration.unboundApiKey } },\\n 353 | \\t\\t\\t]\\n 354 | \\n 355 | \\t\\t\\tconst litellmApiKey = apiConfiguration.litellmApiKey || message?.values?.litellmApiKey\\n 356 | \\t\\t\\tconst litellmBaseUrl = apiConfiguration.litellmBaseUrl || message?.values?.litellmBaseUrl\\n 357 | \\t\\t\\tif (litellmApiKey && litellmBaseUrl) {\\n 358 | \\t\\t\\t\\tmodelFetchPromises.push({\\n 359 | \\t\\t\\t\\t\\tkey: \\\"litellm\\\",\\n 360 | \\t\\t\\t\\t\\toptions: { provider: \\\"litellm\\\", apiKey: litellmApiKey, baseUrl: litellmBaseUrl },\\n 361 | \\t\\t\\t\\t})\\n 362 | \\t\\t\\t}\\n 363 | \\n 364 | \\t\\t\\tconst results = await Promise.allSettled(\\n 365 | \\t\\t\\t\\tmodelFetchPromises.map(async ({ key, options }) => {\\n 366 | \\t\\t\\t\\t\\tconst models = await safeGetModels(options)\\n 367 | \\t\\t\\t\\t\\treturn { key, models } // key is RouterName here\\n 368 | \\t\\t\\t\\t}),\\n 369 | \\t\\t\\t)\\n 370 | \\n 371 | \\t\\t\\tconst fetchedRouterModels: Partial> = { ...routerModels }\\n 372 | \\n 373 | \\t\\t\\tresults.forEach((result, index) => {\\n 374 | \\t\\t\\t\\tconst routerName = modelFetchPromises[index].key // Get RouterName using index\\n 375 | \\n 376 | \\t\\t\\t\\tif (result.status === \\\"fulfilled\\\") {\\n 377 | \\t\\t\\t\\t\\tfetchedRouterModels[routerName] = result.value.models\\n 378 | \\t\\t\\t\\t} else {\\n 379 | \\t\\t\\t\\t\\t// Handle rejection: Post a specific error message for this provider\\n 380 | \\t\\t\\t\\t\\tconst errorMessage = result.reason instanceof Error ? result.reason.message : String(result.reason)\\n 381 | \\t\\t\\t\\t\\tconsole.error(`Error fetching models for ${routerName}:`, result.reason)\\n 382 | \\n 383 | \\t\\t\\t\\t\\tfetchedRouterModels[routerName] = {} // Ensure it's an empty object in the main routerModels message\\n 384 | \\n 385 | \\t\\t\\t\\t\\tprovider.postMessageToWebview({\\n 386 | \\t\\t\\t\\t\\t\\ttype: \\\"singleRouterModelFetchResponse\\\",\\n 387 | \\t\\t\\t\\t\\t\\tsuccess: false,\\n 388 | \\t\\t\\t\\t\\t\\terror: errorMessage,\\n 389 | \\t\\t\\t\\t\\t\\tvalues: { provider: routerName },\\n 390 | \\t\\t\\t\\t\\t})\\n 391 | \\t\\t\\t\\t}\\n 392 | \\t\\t\\t})\\n 393 | \\n 394 | \\t\\t\\tprovider.postMessageToWebview({\\n 395 | \\t\\t\\t\\ttype: \\\"routerModels\\\",\\n 396 | \\t\\t\\t\\trouterModels: fetchedRouterModels as Record,\\n 397 | \\t\\t\\t})\\n 398 | \\t\\t\\tbreak\\n 399 | \\t\\tcase \\\"requestOpenAiModels\\\":\\n 400 | \\t\\t\\tif (message?.values?.baseUrl && message?.values?.apiKey) {\\n 401 | \\t\\t\\t\\tconst openAiModels = await getOpenAiModels(\\n 402 | \\t\\t\\t\\t\\tmessage?.values?.baseUrl,\\n 403 | \\t\\t\\t\\t\\tmessage?.values?.apiKey,\\n 404 | \\t\\t\\t\\t\\tmessage?.values?.openAiHeaders,\\n 405 | \\t\\t\\t\\t)\\n 406 | \\n 407 | \\t\\t\\t\\tprovider.postMessageToWebview({ type: \\\"openAiModels\\\", openAiModels })\\n 408 | \\t\\t\\t}\\n 409 | \\n 410 | \\t\\t\\tbreak\\n 411 | \\t\\tcase \\\"requestOllamaModels\\\":\\n 412 | \\t\\t\\tconst ollamaModels = await getOllamaModels(message.text)\\n 413 | \\t\\t\\t// TODO: Cache like we do for OpenRouter, etc?\\n 414 | \\t\\t\\tprovider.postMessageToWebview({ type: \\\"ollamaModels\\\", ollamaModels })\\n 415 | \\t\\t\\tbreak\\n 416 | \\t\\tcase \\\"requestLmStudioModels\\\":\\n 417 | \\t\\t\\tconst lmStudioModels = await getLmStudioModels(message.text)\\n 418 | \\t\\t\\t// TODO: Cache like we do for OpenRouter, etc?\\n 419 | \\t\\t\\tprovider.postMessageToWebview({ type: \\\"lmStudioModels\\\", lmStudioModels })\\n 420 | \\t\\t\\tbreak\\n 421 | \\t\\tcase \\\"requestVsCodeLmModels\\\":\\n 422 | \\t\\t\\tconst vsCodeLmModels = await getVsCodeLmModels()\\n 423 | \\t\\t\\t// TODO: Cache like we do for OpenRouter, etc?\\n 424 | \\t\\t\\tprovider.postMessageToWebview({ type: \\\"vsCodeLmModels\\\", vsCodeLmModels })\\n 425 | \\t\\t\\tbreak\\n 426 | \\t\\tcase \\\"openImage\\\":\\n 427 | \\t\\t\\topenImage(message.text!, { values: message.values })\\n 428 | \\t\\t\\tbreak\\n 429 | \\t\\tcase \\\"saveImage\\\":\\n 430 | \\t\\t\\tsaveImage(message.dataUri!)\\n 431 | \\t\\t\\tbreak\\n 432 | \\t\\tcase \\\"openFile\\\":\\n 433 | \\t\\t\\topenFile(message.text!, message.values as { create?: boolean; content?: string; line?: number })\\n 434 | \\t\\t\\tbreak\\n 435 | \\t\\tcase \\\"openMention\\\":\\n 436 | \\t\\t\\topenMention(message.text)\\n 437 | \\t\\t\\tbreak\\n 438 | \\t\\tcase \\\"openExternal\\\":\\n 439 | \\t\\t\\tif (message.url) {\\n 440 | \\t\\t\\t\\tvscode.env.openExternal(vscode.Uri.parse(message.url))\\n 441 | \\t\\t\\t}\\n 442 | \\t\\t\\tbreak\\n 443 | \\t\\tcase \\\"checkpointDiff\\\":\\n 444 | \\t\\t\\tconst result = checkoutDiffPayloadSchema.safeParse(message.payload)\\n 445 | \\n 446 | \\t\\t\\tif (result.success) {\\n 447 | \\t\\t\\t\\tawait provider.getCurrentCline()?.checkpointDiff(result.data)\\n 448 | \\t\\t\\t}\\n 449 | \\n 450 | \\t\\t\\tbreak\\n 451 | \\t\\tcase \\\"checkpointRestore\\\": {\\n 452 | \\t\\t\\tconst result = checkoutRestorePayloadSchema.safeParse(message.payload)\\n 453 | \\n 454 | \\t\\t\\tif (result.success) {\\n 455 | \\t\\t\\t\\tawait provider.cancelTask()\\n 456 | \\n 457 | \\t\\t\\t\\ttry {\\n 458 | \\t\\t\\t\\t\\tawait pWaitFor(() => provider.getCurrentCline()?.isInitialized === true, { timeout: 3_000 })\\n 459 | \\t\\t\\t\\t} catch (error) {\\n 460 | \\t\\t\\t\\t\\tvscode.window.showErrorMessage(t(\\\"common:errors.checkpoint_timeout\\\"))\\n 461 | \\t\\t\\t\\t}\\n 462 | \\n 463 | \\t\\t\\t\\ttry {\\n 464 | \\t\\t\\t\\t\\tawait provider.getCurrentCline()?.checkpointRestore(result.data)\\n 465 | \\t\\t\\t\\t} catch (error) {\\n 466 | \\t\\t\\t\\t\\tvscode.window.showErrorMessage(t(\\\"common:errors.checkpoint_failed\\\"))\\n 467 | \\t\\t\\t\\t}\\n 468 | \\t\\t\\t}\\n 469 | \\n 470 | \\t\\t\\tbreak\\n 471 | \\t\\t}\\n 472 | \\t\\tcase \\\"cancelTask\\\":\\n 473 | \\t\\t\\tawait provider.cancelTask()\\n 474 | \\t\\t\\tbreak\\n 475 | \\t\\tcase \\\"allowedCommands\\\":\\n 476 | \\t\\t\\tawait provider.context.globalState.update(\\\"allowedCommands\\\", message.commands)\\n 477 | \\n 478 | \\t\\t\\t// Also update workspace settings.\\n 479 | \\t\\t\\tawait vscode.workspace\\n 480 | \\t\\t\\t\\t.getConfiguration(Package.name)\\n 481 | \\t\\t\\t\\t.update(\\\"allowedCommands\\\", message.commands, vscode.ConfigurationTarget.Global)\\n 482 | \\n 483 | \\t\\t\\tbreak\\n 484 | \\t\\tcase \\\"openCustomModesSettings\\\": {\\n 485 | \\t\\t\\tconst customModesFilePath = await provider.customModesManager.getCustomModesFilePath()\\n 486 | \\n 487 | \\t\\t\\tif (customModesFilePath) {\\n 488 | \\t\\t\\t\\topenFile(customModesFilePath)\\n 489 | \\t\\t\\t}\\n 490 | \\n 491 | \\t\\t\\tbreak\\n 492 | \\t\\t}\\n 493 | \\t\\tcase \\\"openMcpSettings\\\": {\\n 494 | \\t\\t\\tconst mcpSettingsFilePath = await provider.getMcpHub()?.getMcpSettingsFilePath()\\n 495 | \\n 496 | \\t\\t\\tif (mcpSettingsFilePath) {\\n 497 | \\t\\t\\t\\topenFile(mcpSettingsFilePath)\\n 498 | \\t\\t\\t}\\n 499 | \\n 500 | \\t\\t\\tbreak\\n 501 | \\t\\t}\\n 502 | \\t\\tcase \\\"openProjectMcpSettings\\\": {\\n 503 | \\t\\t\\tif (!vscode.workspace.workspaceFolders?.length) {\\n 504 | \\t\\t\\t\\tvscode.window.showErrorMessage(t(\\\"common:errors.no_workspace\\\"))\\n 505 | \\t\\t\\t\\treturn\\n 506 | \\t\\t\\t}\\n 507 | \\n 508 | \\t\\t\\tconst workspaceFolder = vscode.workspace.workspaceFolders[0]\\n 509 | \\t\\t\\tconst rooDir = path.join(workspaceFolder.uri.fsPath, \\\".roo\\\")\\n 510 | \\t\\t\\tconst mcpPath = path.join(rooDir, \\\"mcp.json\\\")\\n 511 | \\n 512 | \\t\\t\\ttry {\\n 513 | \\t\\t\\t\\tawait fs.mkdir(rooDir, { recursive: true })\\n 514 | \\t\\t\\t\\tconst exists = await fileExistsAtPath(mcpPath)\\n 515 | \\n 516 | \\t\\t\\t\\tif (!exists) {\\n 517 | \\t\\t\\t\\t\\tawait fs.writeFile(mcpPath, JSON.stringify({ mcpServers: {} }, null, 2))\\n 518 | \\t\\t\\t\\t}\\n 519 | \\n 520 | \\t\\t\\t\\tawait openFile(mcpPath)\\n 521 | \\t\\t\\t} catch (error) {\\n 522 | \\t\\t\\t\\tvscode.window.showErrorMessage(t(\\\"mcp:errors.create_json\\\", { error: `${error}` }))\\n 523 | \\t\\t\\t}\\n 524 | \\n 525 | \\t\\t\\tbreak\\n 526 | \\t\\t}\\n 527 | \\t\\tcase \\\"deleteMcpServer\\\": {\\n 528 | \\t\\t\\tif (!message.serverName) {\\n 529 | \\t\\t\\t\\tbreak\\n 530 | \\t\\t\\t}\\n 531 | \\n 532 | \\t\\t\\ttry {\\n 533 | \\t\\t\\t\\tprovider.log(`Attempting to delete MCP server: ${message.serverName}`)\\n 534 | \\t\\t\\t\\tawait provider.getMcpHub()?.deleteServer(message.serverName, message.source as \\\"global\\\" | \\\"project\\\")\\n 535 | \\t\\t\\t\\tprovider.log(`Successfully deleted MCP server: ${message.serverName}`)\\n 536 | \\n 537 | \\t\\t\\t\\t// Refresh the webview state\\n 538 | \\t\\t\\t\\tawait provider.postStateToWebview()\\n 539 | \\t\\t\\t} catch (error) {\\n 540 | \\t\\t\\t\\tconst errorMessage = error instanceof Error ? error.message : String(error)\\n 541 | \\t\\t\\t\\tprovider.log(`Failed to delete MCP server: ${errorMessage}`)\\n 542 | \\t\\t\\t\\t// Error messages are already handled by McpHub.deleteServer\\n 543 | \\t\\t\\t}\\n 544 | \\t\\t\\tbreak\\n 545 | \\t\\t}\\n 546 | \\t\\tcase \\\"restartMcpServer\\\": {\\n 547 | \\t\\t\\ttry {\\n 548 | \\t\\t\\t\\tawait provider.getMcpHub()?.restartConnection(message.text!, message.source as \\\"global\\\" | \\\"project\\\")\\n 549 | \\t\\t\\t} catch (error) {\\n 550 | \\t\\t\\t\\tprovider.log(\\n 551 | \\t\\t\\t\\t\\t`Failed to retry connection for ${message.text}: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`,\\n 552 | \\t\\t\\t\\t)\\n 553 | \\t\\t\\t}\\n 554 | \\t\\t\\tbreak\\n 555 | \\t\\t}\\n 556 | \\t\\tcase \\\"toggleToolAlwaysAllow\\\": {\\n 557 | \\t\\t\\ttry {\\n 558 | \\t\\t\\t\\tawait provider\\n 559 | \\t\\t\\t\\t\\t.getMcpHub()\\n 560 | \\t\\t\\t\\t\\t?.toggleToolAlwaysAllow(\\n 561 | \\t\\t\\t\\t\\t\\tmessage.serverName!,\\n 562 | \\t\\t\\t\\t\\t\\tmessage.source as \\\"global\\\" | \\\"project\\\",\\n 563 | \\t\\t\\t\\t\\t\\tmessage.toolName!,\\n 564 | \\t\\t\\t\\t\\t\\tBoolean(message.alwaysAllow),\\n 565 | \\t\\t\\t\\t\\t)\\n 566 | \\t\\t\\t} catch (error) {\\n 567 | \\t\\t\\t\\tprovider.log(\\n 568 | \\t\\t\\t\\t\\t`Failed to toggle auto-approve for tool ${message.toolName}: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`,\\n 569 | \\t\\t\\t\\t)\\n 570 | \\t\\t\\t}\\n 571 | \\t\\t\\tbreak\\n 572 | \\t\\t}\\n 573 | \\t\\tcase \\\"toggleMcpServer\\\": {\\n 574 | \\t\\t\\ttry {\\n 575 | \\t\\t\\t\\tawait provider\\n 576 | \\t\\t\\t\\t\\t.getMcpHub()\\n 577 | \\t\\t\\t\\t\\t?.toggleServerDisabled(\\n 578 | \\t\\t\\t\\t\\t\\tmessage.serverName!,\\n 579 | \\t\\t\\t\\t\\t\\tmessage.disabled!,\\n 580 | \\t\\t\\t\\t\\t\\tmessage.source as \\\"global\\\" | \\\"project\\\",\\n 581 | \\t\\t\\t\\t\\t)\\n 582 | \\t\\t\\t} catch (error) {\\n 583 | \\t\\t\\t\\tprovider.log(\\n 584 | \\t\\t\\t\\t\\t`Failed to toggle MCP server ${message.serverName}: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`,\\n 585 | \\t\\t\\t\\t)\\n 586 | \\t\\t\\t}\\n 587 | \\t\\t\\tbreak\\n 588 | \\t\\t}\\n 589 | \\t\\tcase \\\"mcpEnabled\\\":\\n 590 | \\t\\t\\tconst mcpEnabled = message.bool ?? true\\n 591 | \\t\\t\\tawait updateGlobalState(\\\"mcpEnabled\\\", mcpEnabled)\\n 592 | \\t\\t\\tawait provider.postStateToWebview()\\n 593 | \\t\\t\\tbreak\\n 594 | \\t\\tcase \\\"enableMcpServerCreation\\\":\\n 595 | \\t\\t\\tawait updateGlobalState(\\\"enableMcpServerCreation\\\", message.bool ?? true)\\n 596 | \\t\\t\\tawait provider.postStateToWebview()\\n 597 | \\t\\t\\tbreak\\n 598 | \\t\\tcase \\\"refreshAllMcpServers\\\": {\\n 599 | \\t\\t\\tconst mcpHub = provider.getMcpHub()\\n 600 | \\t\\t\\tif (mcpHub) {\\n 601 | \\t\\t\\t\\tawait mcpHub.refreshAllConnections()\\n 602 | \\t\\t\\t}\\n 603 | \\t\\t\\tbreak\\n 604 | \\t\\t}\\n 605 | \\t\\t// playSound handler removed - now handled directly in the webview\\n 606 | \\t\\tcase \\\"soundEnabled\\\":\\n 607 | \\t\\t\\tconst soundEnabled = message.bool ?? true\\n 608 | \\t\\t\\tawait updateGlobalState(\\\"soundEnabled\\\", soundEnabled)\\n 609 | \\t\\t\\tawait provider.postStateToWebview()\\n 610 | \\t\\t\\tbreak\\n 611 | \\t\\tcase \\\"soundVolume\\\":\\n 612 | \\t\\t\\tconst soundVolume = message.value ?? 0.5\\n 613 | \\t\\t\\tawait updateGlobalState(\\\"soundVolume\\\", soundVolume)\\n 614 | \\t\\t\\tawait provider.postStateToWebview()\\n 615 | \\t\\t\\tbreak\\n 616 | \\t\\tcase \\\"ttsEnabled\\\":\\n 617 | \\t\\t\\tconst ttsEnabled = message.bool ?? true\\n 618 | \\t\\t\\tawait updateGlobalState(\\\"ttsEnabled\\\", ttsEnabled)\\n 619 | \\t\\t\\tsetTtsEnabled(ttsEnabled) // Add this line to update the tts utility\\n 620 | \\t\\t\\tawait provider.postStateToWebview()\\n 621 | \\t\\t\\tbreak\\n 622 | \\t\\tcase \\\"ttsSpeed\\\":\\n 623 | \\t\\t\\tconst ttsSpeed = message.value ?? 1.0\\n 624 | \\t\\t\\tawait updateGlobalState(\\\"ttsSpeed\\\", ttsSpeed)\\n 625 | \\t\\t\\tsetTtsSpeed(ttsSpeed)\\n 626 | \\t\\t\\tawait provider.postStateToWebview()\\n 627 | \\t\\t\\tbreak\\n 628 | \\t\\tcase \\\"playTts\\\":\\n 629 | \\t\\t\\tif (message.text) {\\n 630 | \\t\\t\\t\\tplayTts(message.text, {\\n 631 | \\t\\t\\t\\t\\tonStart: () => provider.postMessageToWebview({ type: \\\"ttsStart\\\", text: message.text }),\\n 632 | \\t\\t\\t\\t\\tonStop: () => provider.postMessageToWebview({ type: \\\"ttsStop\\\", text: message.text }),\\n 633 | \\t\\t\\t\\t})\\n 634 | \\t\\t\\t}\\n 635 | \\t\\t\\tbreak\\n 636 | \\t\\tcase \\\"stopTts\\\":\\n 637 | \\t\\t\\tstopTts()\\n 638 | \\t\\t\\tbreak\\n 639 | \\t\\tcase \\\"diffEnabled\\\":\\n 640 | \\t\\t\\tconst diffEnabled = message.bool ?? true\\n 641 | \\t\\t\\tawait updateGlobalState(\\\"diffEnabled\\\", diffEnabled)\\n 642 | \\t\\t\\tawait provider.postStateToWebview()\\n 643 | \\t\\t\\tbreak\\n 644 | \\t\\tcase \\\"enableCheckpoints\\\":\\n 645 | \\t\\t\\tconst enableCheckpoints = message.bool ?? true\\n 646 | \\t\\t\\tawait updateGlobalState(\\\"enableCheckpoints\\\", enableCheckpoints)\\n 647 | \\t\\t\\tawait provider.postStateToWebview()\\n 648 | \\t\\t\\tbreak\\n 649 | \\t\\tcase \\\"browserViewportSize\\\":\\n 650 | \\t\\t\\tconst browserViewportSize = message.text ?? \\\"900x600\\\"\\n 651 | \\t\\t\\tawait updateGlobalState(\\\"browserViewportSize\\\", browserViewportSize)\\n 652 | \\t\\t\\tawait provider.postStateToWebview()\\n 653 | \\t\\t\\tbreak\\n 654 | \\t\\tcase \\\"remoteBrowserHost\\\":\\n 655 | \\t\\t\\tawait updateGlobalState(\\\"remoteBrowserHost\\\", message.text)\\n 656 | \\t\\t\\tawait provider.postStateToWebview()\\n 657 | \\t\\t\\tbreak\\n 658 | \\t\\tcase \\\"remoteBrowserEnabled\\\":\\n 659 | \\t\\t\\t// Store the preference in global state\\n 660 | \\t\\t\\t// remoteBrowserEnabled now means \\\"enable remote browser connection\\\"\\n 661 | \\t\\t\\tawait updateGlobalState(\\\"remoteBrowserEnabled\\\", message.bool ?? false)\\n 662 | \\t\\t\\t// If disabling remote browser connection, clear the remoteBrowserHost\\n 663 | \\t\\t\\tif (!message.bool) {\\n 664 | \\t\\t\\t\\tawait updateGlobalState(\\\"remoteBrowserHost\\\", undefined)\\n 665 | \\t\\t\\t}\\n 666 | \\t\\t\\tawait provider.postStateToWebview()\\n 667 | \\t\\t\\tbreak\\n 668 | \\t\\tcase \\\"testBrowserConnection\\\":\\n 669 | \\t\\t\\t// If no text is provided, try auto-discovery\\n 670 | \\t\\t\\tif (!message.text) {\\n 671 | \\t\\t\\t\\t// Use testBrowserConnection for auto-discovery\\n 672 | \\t\\t\\t\\tconst chromeHostUrl = await discoverChromeHostUrl()\\n 673 | \\n 674 | \\t\\t\\t\\tif (chromeHostUrl) {\\n 675 | \\t\\t\\t\\t\\t// Send the result back to the webview\\n 676 | \\t\\t\\t\\t\\tawait provider.postMessageToWebview({\\n 677 | \\t\\t\\t\\t\\t\\ttype: \\\"browserConnectionResult\\\",\\n 678 | \\t\\t\\t\\t\\t\\tsuccess: !!chromeHostUrl,\\n 679 | \\t\\t\\t\\t\\t\\ttext: `Auto-discovered and tested connection to Chrome: ${chromeHostUrl}`,\\n 680 | \\t\\t\\t\\t\\t\\tvalues: { endpoint: chromeHostUrl },\\n 681 | \\t\\t\\t\\t\\t})\\n 682 | \\t\\t\\t\\t} else {\\n 683 | \\t\\t\\t\\t\\tawait provider.postMessageToWebview({\\n 684 | \\t\\t\\t\\t\\t\\ttype: \\\"browserConnectionResult\\\",\\n 685 | \\t\\t\\t\\t\\t\\tsuccess: false,\\n 686 | \\t\\t\\t\\t\\t\\ttext: \\\"No Chrome instances found on the network. Make sure Chrome is running with remote debugging enabled (--remote-debugging-port=9222).\\\",\\n 687 | \\t\\t\\t\\t\\t})\\n 688 | \\t\\t\\t\\t}\\n 689 | \\t\\t\\t} else {\\n 690 | \\t\\t\\t\\t// Test the provided URL\\n 691 | \\t\\t\\t\\tconst customHostUrl = message.text\\n 692 | \\t\\t\\t\\tconst hostIsValid = await tryChromeHostUrl(message.text)\\n 693 | \\n 694 | \\t\\t\\t\\t// Send the result back to the webview\\n 695 | \\t\\t\\t\\tawait provider.postMessageToWebview({\\n 696 | \\t\\t\\t\\t\\ttype: \\\"browserConnectionResult\\\",\\n 697 | \\t\\t\\t\\t\\tsuccess: hostIsValid,\\n 698 | \\t\\t\\t\\t\\ttext: hostIsValid\\n 699 | \\t\\t\\t\\t\\t\\t? `Successfully connected to Chrome: ${customHostUrl}`\\n 700 | \\t\\t\\t\\t\\t\\t: \\\"Failed to connect to Chrome\\\",\\n 701 | \\t\\t\\t\\t})\\n 702 | \\t\\t\\t}\\n 703 | \\t\\t\\tbreak\\n 704 | \\t\\tcase \\\"fuzzyMatchThreshold\\\":\\n 705 | \\t\\t\\tawait updateGlobalState(\\\"fuzzyMatchThreshold\\\", message.value)\\n 706 | \\t\\t\\tawait provider.postStateToWebview()\\n 707 | \\t\\t\\tbreak\\n 708 | \\t\\tcase \\\"updateVSCodeSetting\\\": {\\n 709 | \\t\\t\\tconst { setting, value } = message\\n 710 | \\n 711 | \\t\\t\\tif (setting !== undefined && value !== undefined) {\\n 712 | \\t\\t\\t\\tif (ALLOWED_VSCODE_SETTINGS.has(setting)) {\\n 713 | \\t\\t\\t\\t\\tawait vscode.workspace.getConfiguration().update(setting, value, true)\\n 714 | \\t\\t\\t\\t} else {\\n 715 | \\t\\t\\t\\t\\tvscode.window.showErrorMessage(`Cannot update restricted VSCode setting: ${setting}`)\\n 716 | \\t\\t\\t\\t}\\n 717 | \\t\\t\\t}\\n 718 | \\n 719 | \\t\\t\\tbreak\\n 720 | \\t\\t}\\n 721 | \\t\\tcase \\\"getVSCodeSetting\\\":\\n 722 | \\t\\t\\tconst { setting } = message\\n 723 | \\n 724 | \\t\\t\\tif (setting) {\\n 725 | \\t\\t\\t\\ttry {\\n 726 | \\t\\t\\t\\t\\tawait provider.postMessageToWebview({\\n 727 | \\t\\t\\t\\t\\t\\ttype: \\\"vsCodeSetting\\\",\\n 728 | \\t\\t\\t\\t\\t\\tsetting,\\n 729 | \\t\\t\\t\\t\\t\\tvalue: vscode.workspace.getConfiguration().get(setting),\\n 730 | \\t\\t\\t\\t\\t})\\n 731 | \\t\\t\\t\\t} catch (error) {\\n 732 | \\t\\t\\t\\t\\tconsole.error(`Failed to get VSCode setting ${message.setting}:`, error)\\n 733 | \\n 734 | \\t\\t\\t\\t\\tawait provider.postMessageToWebview({\\n 735 | \\t\\t\\t\\t\\t\\ttype: \\\"vsCodeSetting\\\",\\n 736 | \\t\\t\\t\\t\\t\\tsetting,\\n 737 | \\t\\t\\t\\t\\t\\terror: `Failed to get setting: ${error.message}`,\\n 738 | \\t\\t\\t\\t\\t\\tvalue: undefined,\\n 739 | \\t\\t\\t\\t\\t})\\n 740 | \\t\\t\\t\\t}\\n 741 | \\t\\t\\t}\\n 742 | \\n 743 | \\t\\t\\tbreak\\n 744 | \\t\\tcase \\\"alwaysApproveResubmit\\\":\\n 745 | \\t\\t\\tawait updateGlobalState(\\\"alwaysApproveResubmit\\\", message.bool ?? false)\\n 746 | \\t\\t\\tawait provider.postStateToWebview()\\n 747 | \\t\\t\\tbreak\\n 748 | \\t\\tcase \\\"requestDelaySeconds\\\":\\n 749 | \\t\\t\\tawait updateGlobalState(\\\"requestDelaySeconds\\\", message.value ?? 5)\\n 750 | \\t\\t\\tawait provider.postStateToWebview()\\n 751 | \\t\\t\\tbreak\\n 752 | \\t\\tcase \\\"writeDelayMs\\\":\\n 753 | \\t\\t\\tawait updateGlobalState(\\\"writeDelayMs\\\", message.value)\\n 754 | \\t\\t\\tawait provider.postStateToWebview()\\n 755 | \\t\\t\\tbreak\\n 756 | \\t\\tcase \\\"terminalOutputLineLimit\\\":\\n 757 | \\t\\t\\tawait updateGlobalState(\\\"terminalOutputLineLimit\\\", message.value)\\n 758 | \\t\\t\\tawait provider.postStateToWebview()\\n 759 | \\t\\t\\tbreak\\n 760 | \\t\\tcase \\\"terminalShellIntegrationTimeout\\\":\\n 761 | \\t\\t\\tawait updateGlobalState(\\\"terminalShellIntegrationTimeout\\\", message.value)\\n 762 | \\t\\t\\tawait provider.postStateToWebview()\\n 763 | \\t\\t\\tif (message.value !== undefined) {\\n 764 | \\t\\t\\t\\tTerminal.setShellIntegrationTimeout(message.value)\\n 765 | \\t\\t\\t}\\n 766 | \\t\\t\\tbreak\\n 767 | \\t\\tcase \\\"terminalShellIntegrationDisabled\\\":\\n 768 | \\t\\t\\tawait updateGlobalState(\\\"terminalShellIntegrationDisabled\\\", message.bool)\\n 769 | \\t\\t\\tawait provider.postStateToWebview()\\n 770 | \\t\\t\\tif (message.bool !== undefined) {\\n 771 | \\t\\t\\t\\tTerminal.setShellIntegrationDisabled(message.bool)\\n 772 | \\t\\t\\t}\\n 773 | \\t\\t\\tbreak\\n 774 | \\t\\tcase \\\"terminalCommandDelay\\\":\\n 775 | \\t\\t\\tawait updateGlobalState(\\\"terminalCommandDelay\\\", message.value)\\n 776 | \\t\\t\\tawait provider.postStateToWebview()\\n 777 | \\t\\t\\tif (message.value !== undefined) {\\n 778 | \\t\\t\\t\\tTerminal.setCommandDelay(message.value)\\n 779 | \\t\\t\\t}\\n 780 | \\t\\t\\tbreak\\n 781 | \\t\\tcase \\\"terminalPowershellCounter\\\":\\n 782 | \\t\\t\\tawait updateGlobalState(\\\"terminalPowershellCounter\\\", message.bool)\\n 783 | \\t\\t\\tawait provider.postStateToWebview()\\n 784 | \\t\\t\\tif (message.bool !== undefined) {\\n 785 | \\t\\t\\t\\tTerminal.setPowershellCounter(message.bool)\\n 786 | \\t\\t\\t}\\n 787 | \\t\\t\\tbreak\\n 788 | \\t\\tcase \\\"terminalZshClearEolMark\\\":\\n 789 | \\t\\t\\tawait updateGlobalState(\\\"terminalZshClearEolMark\\\", message.bool)\\n 790 | \\t\\t\\tawait provider.postStateToWebview()\\n 791 | \\t\\t\\tif (message.bool !== undefined) {\\n 792 | \\t\\t\\t\\tTerminal.setTerminalZshClearEolMark(message.bool)\\n 793 | \\t\\t\\t}\\n 794 | \\t\\t\\tbreak\\n 795 | \\t\\tcase \\\"terminalZshOhMy\\\":\\n 796 | \\t\\t\\tawait updateGlobalState(\\\"terminalZshOhMy\\\", message.bool)\\n 797 | \\t\\t\\tawait provider.postStateToWebview()\\n 798 | \\t\\t\\tif (message.bool !== undefined) {\\n 799 | \\t\\t\\t\\tTerminal.setTerminalZshOhMy(message.bool)\\n 800 | \\t\\t\\t}\\n 801 | \\t\\t\\tbreak\\n 802 | \\t\\tcase \\\"terminalZshP10k\\\":\\n 803 | \\t\\t\\tawait updateGlobalState(\\\"terminalZshP10k\\\", message.bool)\\n 804 | \\t\\t\\tawait provider.postStateToWebview()\\n 805 | \\t\\t\\tif (message.bool !== undefined) {\\n 806 | \\t\\t\\t\\tTerminal.setTerminalZshP10k(message.bool)\\n 807 | \\t\\t\\t}\\n 808 | \\t\\t\\tbreak\\n 809 | \\t\\tcase \\\"terminalZdotdir\\\":\\n 810 | \\t\\t\\tawait updateGlobalState(\\\"terminalZdotdir\\\", message.bool)\\n 811 | \\t\\t\\tawait provider.postStateToWebview()\\n 812 | \\t\\t\\tif (message.bool !== undefined) {\\n 813 | \\t\\t\\t\\tTerminal.setTerminalZdotdir(message.bool)\\n 814 | \\t\\t\\t}\\n 815 | \\t\\t\\tbreak\\n 816 | \\t\\tcase \\\"terminalCompressProgressBar\\\":\\n 817 | \\t\\t\\tawait updateGlobalState(\\\"terminalCompressProgressBar\\\", message.bool)\\n 818 | \\t\\t\\tawait provider.postStateToWebview()\\n 819 | \\t\\t\\tif (message.bool !== undefined) {\\n 820 | \\t\\t\\t\\tTerminal.setCompressProgressBar(message.bool)\\n 821 | \\t\\t\\t}\\n 822 | \\t\\t\\tbreak\\n 823 | \\t\\tcase \\\"mode\\\":\\n 824 | \\t\\t\\tawait provider.handleModeSwitch(message.text as Mode)\\n 825 | \\t\\t\\tbreak\\n 826 | \\t\\tcase \\\"updateSupportPrompt\\\":\\n 827 | \\t\\t\\ttry {\\n 828 | \\t\\t\\t\\tif (Object.keys(message?.values ?? {}).length === 0) {\\n 829 | \\t\\t\\t\\t\\treturn\\n 830 | \\t\\t\\t\\t}\\n 831 | \\n 832 | \\t\\t\\t\\tconst existingPrompts = getGlobalState(\\\"customSupportPrompts\\\") ?? {}\\n 833 | \\t\\t\\t\\tconst updatedPrompts = { ...existingPrompts, ...message.values }\\n 834 | \\t\\t\\t\\tawait updateGlobalState(\\\"customSupportPrompts\\\", updatedPrompts)\\n 835 | \\t\\t\\t\\tawait provider.postStateToWebview()\\n 836 | \\t\\t\\t} catch (error) {\\n 837 | \\t\\t\\t\\tprovider.log(\\n 838 | \\t\\t\\t\\t\\t`Error update support prompt: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`,\\n 839 | \\t\\t\\t\\t)\\n 840 | \\t\\t\\t\\tvscode.window.showErrorMessage(t(\\\"common:errors.update_support_prompt\\\"))\\n 841 | \\t\\t\\t}\\n 842 | \\t\\t\\tbreak\\n 843 | \\t\\tcase \\\"resetSupportPrompt\\\":\\n 844 | \\t\\t\\ttry {\\n 845 | \\t\\t\\t\\tif (!message?.text) {\\n 846 | \\t\\t\\t\\t\\treturn\\n 847 | \\t\\t\\t\\t}\\n 848 | \\n 849 | \\t\\t\\t\\tconst existingPrompts = getGlobalState(\\\"customSupportPrompts\\\") ?? {}\\n 850 | \\t\\t\\t\\tconst updatedPrompts = { ...existingPrompts }\\n 851 | \\t\\t\\t\\tupdatedPrompts[message.text] = undefined\\n 852 | \\t\\t\\t\\tawait updateGlobalState(\\\"customSupportPrompts\\\", updatedPrompts)\\n 853 | \\t\\t\\t\\tawait provider.postStateToWebview()\\n 854 | \\t\\t\\t} catch (error) {\\n 855 | \\t\\t\\t\\tprovider.log(\\n 856 | \\t\\t\\t\\t\\t`Error reset support prompt: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`,\\n 857 | \\t\\t\\t\\t)\\n 858 | \\t\\t\\t\\tvscode.window.showErrorMessage(t(\\\"common:errors.reset_support_prompt\\\"))\\n 859 | \\t\\t\\t}\\n 860 | \\t\\t\\tbreak\\n 861 | \\t\\tcase \\\"updatePrompt\\\":\\n 862 | \\t\\t\\tif (message.promptMode && message.customPrompt !== undefined) {\\n 863 | \\t\\t\\t\\tconst existingPrompts = getGlobalState(\\\"customModePrompts\\\") ?? {}\\n 864 | \\t\\t\\t\\tconst updatedPrompts = { ...existingPrompts, [message.promptMode]: message.customPrompt }\\n 865 | \\t\\t\\t\\tawait updateGlobalState(\\\"customModePrompts\\\", updatedPrompts)\\n 866 | \\t\\t\\t\\tconst currentState = await provider.getStateToPostToWebview()\\n 867 | \\t\\t\\t\\tconst stateWithPrompts = { ...currentState, customModePrompts: updatedPrompts }\\n 868 | \\t\\t\\t\\tprovider.postMessageToWebview({ type: \\\"state\\\", state: stateWithPrompts })\\n 869 | \\t\\t\\t}\\n 870 | \\t\\t\\tbreak\\n 871 | \\t\\tcase \\\"deleteMessage\\\": {\\n 872 | \\t\\t\\tconst answer = await vscode.window.showInformationMessage(\\n 873 | \\t\\t\\t\\tt(\\\"common:confirmation.delete_message\\\"),\\n 874 | \\t\\t\\t\\t{ modal: true },\\n 875 | \\t\\t\\t\\tt(\\\"common:confirmation.just_this_message\\\"),\\n 876 | \\t\\t\\t\\tt(\\\"common:confirmation.this_and_subsequent\\\"),\\n 877 | \\t\\t\\t)\\n 878 | \\n 879 | \\t\\t\\tif (\\n 880 | \\t\\t\\t\\t(answer === t(\\\"common:confirmation.just_this_message\\\") ||\\n 881 | \\t\\t\\t\\t\\tanswer === t(\\\"common:confirmation.this_and_subsequent\\\")) &&\\n 882 | \\t\\t\\t\\tprovider.getCurrentCline() &&\\n 883 | \\t\\t\\t\\ttypeof message.value === \\\"number\\\" &&\\n 884 | \\t\\t\\t\\tmessage.value\\n 885 | \\t\\t\\t) {\\n 886 | \\t\\t\\t\\tconst timeCutoff = message.value - 1000 // 1 second buffer before the message to delete\\n 887 | \\n 888 | \\t\\t\\t\\tconst messageIndex = provider\\n 889 | \\t\\t\\t\\t\\t.getCurrentCline()!\\n 890 | \\t\\t\\t\\t\\t.clineMessages.findIndex((msg) => msg.ts && msg.ts >= timeCutoff)\\n 891 | \\n 892 | \\t\\t\\t\\tconst apiConversationHistoryIndex = provider\\n 893 | \\t\\t\\t\\t\\t.getCurrentCline()\\n 894 | \\t\\t\\t\\t\\t?.apiConversationHistory.findIndex((msg) => msg.ts && msg.ts >= timeCutoff)\\n 895 | \\n 896 | \\t\\t\\t\\tif (messageIndex !== -1) {\\n 897 | \\t\\t\\t\\t\\tconst { historyItem } = await provider.getTaskWithId(provider.getCurrentCline()!.taskId)\\n 898 | \\n 899 | \\t\\t\\t\\t\\tif (answer === t(\\\"common:confirmation.just_this_message\\\")) {\\n 900 | \\t\\t\\t\\t\\t\\t// Find the next user message first\\n 901 | \\t\\t\\t\\t\\t\\tconst nextUserMessage = provider\\n 902 | \\t\\t\\t\\t\\t\\t\\t.getCurrentCline()!\\n 903 | \\t\\t\\t\\t\\t\\t\\t.clineMessages.slice(messageIndex + 1)\\n 904 | \\t\\t\\t\\t\\t\\t\\t.find((msg) => msg.type === \\\"say\\\" && msg.say === \\\"user_feedback\\\")\\n 905 | \\n 906 | \\t\\t\\t\\t\\t\\t// Handle UI messages\\n 907 | \\t\\t\\t\\t\\t\\tif (nextUserMessage) {\\n 908 | \\t\\t\\t\\t\\t\\t\\t// Find absolute index of next user message\\n 909 | \\t\\t\\t\\t\\t\\t\\tconst nextUserMessageIndex = provider\\n 910 | \\t\\t\\t\\t\\t\\t\\t\\t.getCurrentCline()!\\n 911 | \\t\\t\\t\\t\\t\\t\\t\\t.clineMessages.findIndex((msg) => msg === nextUserMessage)\\n 912 | \\n 913 | \\t\\t\\t\\t\\t\\t\\t// Keep messages before current message and after next user message\\n 914 | \\t\\t\\t\\t\\t\\t\\tawait provider\\n 915 | \\t\\t\\t\\t\\t\\t\\t\\t.getCurrentCline()!\\n 916 | \\t\\t\\t\\t\\t\\t\\t\\t.overwriteClineMessages([\\n 917 | \\t\\t\\t\\t\\t\\t\\t\\t\\t...provider.getCurrentCline()!.clineMessages.slice(0, messageIndex),\\n 918 | \\t\\t\\t\\t\\t\\t\\t\\t\\t...provider.getCurrentCline()!.clineMessages.slice(nextUserMessageIndex),\\n 919 | \\t\\t\\t\\t\\t\\t\\t\\t])\\n 920 | \\t\\t\\t\\t\\t\\t} else {\\n 921 | \\t\\t\\t\\t\\t\\t\\t// If no next user message, keep only messages before current message\\n 922 | \\t\\t\\t\\t\\t\\t\\tawait provider\\n 923 | \\t\\t\\t\\t\\t\\t\\t\\t.getCurrentCline()!\\n 924 | \\t\\t\\t\\t\\t\\t\\t\\t.overwriteClineMessages(\\n 925 | \\t\\t\\t\\t\\t\\t\\t\\t\\tprovider.getCurrentCline()!.clineMessages.slice(0, messageIndex),\\n 926 | \\t\\t\\t\\t\\t\\t\\t\\t)\\n 927 | \\t\\t\\t\\t\\t\\t}\\n 928 | \\n 929 | \\t\\t\\t\\t\\t\\t// Handle API messages\\n 930 | \\t\\t\\t\\t\\t\\tif (apiConversationHistoryIndex !== -1) {\\n 931 | \\t\\t\\t\\t\\t\\t\\tif (nextUserMessage && nextUserMessage.ts) {\\n 932 | \\t\\t\\t\\t\\t\\t\\t\\t// Keep messages before current API message and after next user message\\n 933 | \\t\\t\\t\\t\\t\\t\\t\\tawait provider\\n 934 | \\t\\t\\t\\t\\t\\t\\t\\t\\t.getCurrentCline()!\\n 935 | \\t\\t\\t\\t\\t\\t\\t\\t\\t.overwriteApiConversationHistory([\\n 936 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\t...provider\\n 937 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\t\\t.getCurrentCline()!\\n 938 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\t\\t.apiConversationHistory.slice(0, apiConversationHistoryIndex),\\n 939 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\t...provider\\n 940 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\t\\t.getCurrentCline()!\\n 941 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\t\\t.apiConversationHistory.filter(\\n 942 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\t\\t\\t(msg) => msg.ts && msg.ts >= nextUserMessage.ts,\\n 943 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\t\\t),\\n 944 | \\t\\t\\t\\t\\t\\t\\t\\t\\t])\\n 945 | \\t\\t\\t\\t\\t\\t\\t} else {\\n 946 | \\t\\t\\t\\t\\t\\t\\t\\t// If no next user message, keep only messages before current API message\\n 947 | \\t\\t\\t\\t\\t\\t\\t\\tawait provider\\n 948 | \\t\\t\\t\\t\\t\\t\\t\\t\\t.getCurrentCline()!\\n 949 | \\t\\t\\t\\t\\t\\t\\t\\t\\t.overwriteApiConversationHistory(\\n 950 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\tprovider\\n 951 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\t\\t.getCurrentCline()!\\n 952 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\t\\t.apiConversationHistory.slice(0, apiConversationHistoryIndex),\\n 953 | \\t\\t\\t\\t\\t\\t\\t\\t\\t)\\n 954 | \\t\\t\\t\\t\\t\\t\\t}\\n 955 | \\t\\t\\t\\t\\t\\t}\\n 956 | \\t\\t\\t\\t\\t} else if (answer === t(\\\"common:confirmation.this_and_subsequent\\\")) {\\n 957 | \\t\\t\\t\\t\\t\\t// Delete this message and all that follow\\n 958 | \\t\\t\\t\\t\\t\\tawait provider\\n 959 | \\t\\t\\t\\t\\t\\t\\t.getCurrentCline()!\\n 960 | \\t\\t\\t\\t\\t\\t\\t.overwriteClineMessages(provider.getCurrentCline()!.clineMessages.slice(0, messageIndex))\\n 961 | \\t\\t\\t\\t\\t\\tif (apiConversationHistoryIndex !== -1) {\\n 962 | \\t\\t\\t\\t\\t\\t\\tawait provider\\n 963 | \\t\\t\\t\\t\\t\\t\\t\\t.getCurrentCline()!\\n 964 | \\t\\t\\t\\t\\t\\t\\t\\t.overwriteApiConversationHistory(\\n 965 | \\t\\t\\t\\t\\t\\t\\t\\t\\tprovider\\n 966 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\t.getCurrentCline()!\\n 967 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\t.apiConversationHistory.slice(0, apiConversationHistoryIndex),\\n 968 | \\t\\t\\t\\t\\t\\t\\t\\t)\\n 969 | \\t\\t\\t\\t\\t\\t}\\n 970 | \\t\\t\\t\\t\\t}\\n 971 | \\n 972 | \\t\\t\\t\\t\\tawait provider.initClineWithHistoryItem(historyItem)\\n 973 | \\t\\t\\t\\t}\\n 974 | \\t\\t\\t}\\n 975 | \\t\\t\\tbreak\\n 976 | \\t\\t}\\n 977 | \\t\\tcase \\\"screenshotQuality\\\":\\n 978 | \\t\\t\\tawait updateGlobalState(\\\"screenshotQuality\\\", message.value)\\n 979 | \\t\\t\\tawait provider.postStateToWebview()\\n 980 | \\t\\t\\tbreak\\n 981 | \\t\\tcase \\\"maxOpenTabsContext\\\":\\n 982 | \\t\\t\\tconst tabCount = Math.min(Math.max(0, message.value ?? 20), 500)\\n 983 | \\t\\t\\tawait updateGlobalState(\\\"maxOpenTabsContext\\\", tabCount)\\n 984 | \\t\\t\\tawait provider.postStateToWebview()\\n 985 | \\t\\t\\tbreak\\n 986 | \\t\\tcase \\\"maxWorkspaceFiles\\\":\\n 987 | \\t\\t\\tconst fileCount = Math.min(Math.max(0, message.value ?? 200), 500)\\n 988 | \\t\\t\\tawait updateGlobalState(\\\"maxWorkspaceFiles\\\", fileCount)\\n 989 | \\t\\t\\tawait provider.postStateToWebview()\\n 990 | \\t\\t\\tbreak\\n 991 | \\t\\tcase \\\"browserToolEnabled\\\":\\n 992 | \\t\\t\\tawait updateGlobalState(\\\"browserToolEnabled\\\", message.bool ?? true)\\n 993 | \\t\\t\\tawait provider.postStateToWebview()\\n 994 | \\t\\t\\tbreak\\n 995 | \\t\\tcase \\\"language\\\":\\n 996 | \\t\\t\\tchangeLanguage(message.text ?? \\\"en\\\")\\n 997 | \\t\\t\\tawait updateGlobalState(\\\"language\\\", message.text as Language)\\n 998 | \\t\\t\\tawait provider.postStateToWebview()\\n 999 | \\t\\t\\tbreak\\n1000 | \\t\\tcase \\\"showRooIgnoredFiles\\\":\\n1001 | \\t\\t\\tawait updateGlobalState(\\\"showRooIgnoredFiles\\\", message.bool ?? true)\\n1002 | \\t\\t\\tawait provider.postStateToWebview()\\n1003 | \\t\\t\\tbreak\\n1004 | \\t\\tcase \\\"maxReadFileLine\\\":\\n1005 | \\t\\t\\tawait updateGlobalState(\\\"maxReadFileLine\\\", message.value)\\n1006 | \\t\\t\\tawait provider.postStateToWebview()\\n1007 | \\t\\t\\tbreak\\n1008 | \\t\\tcase \\\"maxConcurrentFileReads\\\":\\n1009 | \\t\\t\\tconst valueToSave = message.value // Capture the value intended for saving\\n1010 | \\t\\t\\tawait updateGlobalState(\\\"maxConcurrentFileReads\\\", valueToSave)\\n1011 | \\t\\t\\tawait provider.postStateToWebview()\\n1012 | \\t\\t\\tbreak\\n1013 | \\t\\tcase \\\"setHistoryPreviewCollapsed\\\": // Add the new case handler\\n1014 | \\t\\t\\tawait updateGlobalState(\\\"historyPreviewCollapsed\\\", message.bool ?? false)\\n1015 | \\t\\t\\t// No need to call postStateToWebview here as the UI already updated optimistically\\n1016 | \\t\\t\\tbreak\\n1017 | \\t\\tcase \\\"toggleApiConfigPin\\\":\\n1018 | \\t\\t\\tif (message.text) {\\n1019 | \\t\\t\\t\\tconst currentPinned = getGlobalState(\\\"pinnedApiConfigs\\\") ?? {}\\n1020 | \\t\\t\\t\\tconst updatedPinned: Record = { ...currentPinned }\\n1021 | \\n1022 | \\t\\t\\t\\tif (currentPinned[message.text]) {\\n1023 | \\t\\t\\t\\t\\tdelete updatedPinned[message.text]\\n1024 | \\t\\t\\t\\t} else {\\n1025 | \\t\\t\\t\\t\\tupdatedPinned[message.text] = true\\n1026 | \\t\\t\\t\\t}\\n1027 | \\n1028 | \\t\\t\\t\\tawait updateGlobalState(\\\"pinnedApiConfigs\\\", updatedPinned)\\n1029 | \\t\\t\\t\\tawait provider.postStateToWebview()\\n1030 | \\t\\t\\t}\\n1031 | \\t\\t\\tbreak\\n1032 | \\t\\tcase \\\"enhancementApiConfigId\\\":\\n1033 | \\t\\t\\tawait updateGlobalState(\\\"enhancementApiConfigId\\\", message.text)\\n1034 | \\t\\t\\tawait provider.postStateToWebview()\\n1035 | \\t\\t\\tbreak\\n1036 | \\t\\tcase \\\"condensingApiConfigId\\\":\\n1037 | \\t\\t\\tawait updateGlobalState(\\\"condensingApiConfigId\\\", message.text)\\n1038 | \\t\\t\\tawait provider.postStateToWebview()\\n1039 | \\t\\t\\tbreak\\n1040 | \\t\\tcase \\\"updateCondensingPrompt\\\":\\n1041 | \\t\\t\\tawait updateGlobalState(\\\"customCondensingPrompt\\\", message.text)\\n1042 | \\t\\t\\tawait provider.postStateToWebview()\\n1043 | \\t\\t\\tbreak\\n1044 | \\t\\tcase \\\"autoApprovalEnabled\\\":\\n1045 | \\t\\t\\tawait updateGlobalState(\\\"autoApprovalEnabled\\\", message.bool ?? false)\\n1046 | \\t\\t\\tawait provider.postStateToWebview()\\n1047 | \\t\\t\\tbreak\\n1048 | \\t\\tcase \\\"enhancePrompt\\\":\\n1049 | \\t\\t\\tif (message.text) {\\n1050 | \\t\\t\\t\\ttry {\\n1051 | \\t\\t\\t\\t\\tconst { apiConfiguration, customSupportPrompts, listApiConfigMeta, enhancementApiConfigId } =\\n1052 | \\t\\t\\t\\t\\t\\tawait provider.getState()\\n1053 | \\n1054 | \\t\\t\\t\\t\\t// Try to get enhancement config first, fall back to current config.\\n1055 | \\t\\t\\t\\t\\tlet configToUse: ProviderSettings = apiConfiguration\\n1056 | \\n1057 | \\t\\t\\t\\t\\tif (enhancementApiConfigId && !!listApiConfigMeta.find(({ id }) => id === enhancementApiConfigId)) {\\n1058 | \\t\\t\\t\\t\\t\\tconst { name: _, ...providerSettings } = await provider.providerSettingsManager.getProfile({\\n1059 | \\t\\t\\t\\t\\t\\t\\tid: enhancementApiConfigId,\\n1060 | \\t\\t\\t\\t\\t\\t})\\n1061 | \\n1062 | \\t\\t\\t\\t\\t\\tif (providerSettings.apiProvider) {\\n1063 | \\t\\t\\t\\t\\t\\t\\tconfigToUse = providerSettings\\n1064 | \\t\\t\\t\\t\\t\\t}\\n1065 | \\t\\t\\t\\t\\t}\\n1066 | \\n1067 | \\t\\t\\t\\t\\tconst enhancedPrompt = await singleCompletionHandler(\\n1068 | \\t\\t\\t\\t\\t\\tconfigToUse,\\n1069 | \\t\\t\\t\\t\\t\\tsupportPrompt.create(\\\"ENHANCE\\\", { userInput: message.text }, customSupportPrompts),\\n1070 | \\t\\t\\t\\t\\t)\\n1071 | \\n1072 | \\t\\t\\t\\t\\t// Capture telemetry for prompt enhancement.\\n1073 | \\t\\t\\t\\t\\tconst currentCline = provider.getCurrentCline()\\n1074 | \\t\\t\\t\\t\\tTelemetryService.instance.capturePromptEnhanced(currentCline?.taskId)\\n1075 | \\n1076 | \\t\\t\\t\\t\\tawait provider.postMessageToWebview({ type: \\\"enhancedPrompt\\\", text: enhancedPrompt })\\n1077 | \\t\\t\\t\\t} catch (error) {\\n1078 | \\t\\t\\t\\t\\tprovider.log(\\n1079 | \\t\\t\\t\\t\\t\\t`Error enhancing prompt: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`,\\n1080 | \\t\\t\\t\\t\\t)\\n1081 | \\n1082 | \\t\\t\\t\\t\\tvscode.window.showErrorMessage(t(\\\"common:errors.enhance_prompt\\\"))\\n1083 | \\t\\t\\t\\t\\tawait provider.postMessageToWebview({ type: \\\"enhancedPrompt\\\" })\\n1084 | \\t\\t\\t\\t}\\n1085 | \\t\\t\\t}\\n1086 | \\t\\t\\tbreak\\n1087 | \\t\\tcase \\\"getSystemPrompt\\\":\\n1088 | \\t\\t\\ttry {\\n1089 | \\t\\t\\t\\tconst systemPrompt = await generateSystemPrompt(provider, message)\\n1090 | \\n1091 | \\t\\t\\t\\tawait provider.postMessageToWebview({\\n1092 | \\t\\t\\t\\t\\ttype: \\\"systemPrompt\\\",\\n1093 | \\t\\t\\t\\t\\ttext: systemPrompt,\\n1094 | \\t\\t\\t\\t\\tmode: message.mode,\\n1095 | \\t\\t\\t\\t})\\n1096 | \\t\\t\\t} catch (error) {\\n1097 | \\t\\t\\t\\tprovider.log(\\n1098 | \\t\\t\\t\\t\\t`Error getting system prompt: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`,\\n1099 | \\t\\t\\t\\t)\\n1100 | \\t\\t\\t\\tvscode.window.showErrorMessage(t(\\\"common:errors.get_system_prompt\\\"))\\n1101 | \\t\\t\\t}\\n1102 | \\t\\t\\tbreak\\n1103 | \\t\\tcase \\\"copySystemPrompt\\\":\\n1104 | \\t\\t\\ttry {\\n1105 | \\t\\t\\t\\tconst systemPrompt = await generateSystemPrompt(provider, message)\\n1106 | \\n1107 | \\t\\t\\t\\tawait vscode.env.clipboard.writeText(systemPrompt)\\n1108 | \\t\\t\\t\\tawait vscode.window.showInformationMessage(t(\\\"common:info.clipboard_copy\\\"))\\n1109 | \\t\\t\\t} catch (error) {\\n1110 | \\t\\t\\t\\tprovider.log(\\n1111 | \\t\\t\\t\\t\\t`Error getting system prompt: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`,\\n1112 | \\t\\t\\t\\t)\\n1113 | \\t\\t\\t\\tvscode.window.showErrorMessage(t(\\\"common:errors.get_system_prompt\\\"))\\n1114 | \\t\\t\\t}\\n1115 | \\t\\t\\tbreak\\n1116 | \\t\\tcase \\\"searchCommits\\\": {\\n1117 | \\t\\t\\tconst cwd = provider.cwd\\n1118 | \\t\\t\\tif (cwd) {\\n1119 | \\t\\t\\t\\ttry {\\n1120 | \\t\\t\\t\\t\\tconst commits = await searchCommits(message.query || \\\"\\\", cwd)\\n1121 | \\t\\t\\t\\t\\tawait provider.postMessageToWebview({\\n1122 | \\t\\t\\t\\t\\t\\ttype: \\\"commitSearchResults\\\",\\n1123 | \\t\\t\\t\\t\\t\\tcommits,\\n1124 | \\t\\t\\t\\t\\t})\\n1125 | \\t\\t\\t\\t} catch (error) {\\n1126 | \\t\\t\\t\\t\\tprovider.log(\\n1127 | \\t\\t\\t\\t\\t\\t`Error searching commits: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`,\\n1128 | \\t\\t\\t\\t\\t)\\n1129 | \\t\\t\\t\\t\\tvscode.window.showErrorMessage(t(\\\"common:errors.search_commits\\\"))\\n1130 | \\t\\t\\t\\t}\\n1131 | \\t\\t\\t}\\n1132 | \\t\\t\\tbreak\\n1133 | \\t\\t}\\n1134 | \\t\\tcase \\\"searchFiles\\\": {\\n1135 | \\t\\t\\tconst workspacePath = getWorkspacePath()\\n1136 | \\n1137 | \\t\\t\\tif (!workspacePath) {\\n1138 | \\t\\t\\t\\t// Handle case where workspace path is not available\\n1139 | \\t\\t\\t\\tawait provider.postMessageToWebview({\\n1140 | \\t\\t\\t\\t\\ttype: \\\"fileSearchResults\\\",\\n1141 | \\t\\t\\t\\t\\tresults: [],\\n1142 | \\t\\t\\t\\t\\trequestId: message.requestId,\\n1143 | \\t\\t\\t\\t\\terror: \\\"No workspace path available\\\",\\n1144 | \\t\\t\\t\\t})\\n1145 | \\t\\t\\t\\tbreak\\n1146 | \\t\\t\\t}\\n1147 | \\t\\t\\ttry {\\n1148 | \\t\\t\\t\\t// Call file search service with query from message\\n1149 | \\t\\t\\t\\tconst results = await searchWorkspaceFiles(\\n1150 | \\t\\t\\t\\t\\tmessage.query || \\\"\\\",\\n1151 | \\t\\t\\t\\t\\tworkspacePath,\\n1152 | \\t\\t\\t\\t\\t20, // Use default limit, as filtering is now done in the backend\\n1153 | \\t\\t\\t\\t)\\n1154 | \\n1155 | \\t\\t\\t\\t// Send results back to webview\\n1156 | \\t\\t\\t\\tawait provider.postMessageToWebview({\\n1157 | \\t\\t\\t\\t\\ttype: \\\"fileSearchResults\\\",\\n1158 | \\t\\t\\t\\t\\tresults,\\n1159 | \\t\\t\\t\\t\\trequestId: message.requestId,\\n1160 | \\t\\t\\t\\t})\\n1161 | \\t\\t\\t} catch (error) {\\n1162 | \\t\\t\\t\\tconst errorMessage = error instanceof Error ? error.message : String(error)\\n1163 | \\n1164 | \\t\\t\\t\\t// Send error response to webview\\n1165 | \\t\\t\\t\\tawait provider.postMessageToWebview({\\n1166 | \\t\\t\\t\\t\\ttype: \\\"fileSearchResults\\\",\\n1167 | \\t\\t\\t\\t\\tresults: [],\\n1168 | \\t\\t\\t\\t\\terror: errorMessage,\\n1169 | \\t\\t\\t\\t\\trequestId: message.requestId,\\n1170 | \\t\\t\\t\\t})\\n1171 | \\t\\t\\t}\\n1172 | \\t\\t\\tbreak\\n1173 | \\t\\t}\\n1174 | \\t\\tcase \\\"saveApiConfiguration\\\":\\n1175 | \\t\\t\\tif (message.text && message.apiConfiguration) {\\n1176 | \\t\\t\\t\\ttry {\\n1177 | \\t\\t\\t\\t\\tawait provider.providerSettingsManager.saveConfig(message.text, message.apiConfiguration)\\n1178 | \\t\\t\\t\\t\\tconst listApiConfig = await provider.providerSettingsManager.listConfig()\\n1179 | \\t\\t\\t\\t\\tawait updateGlobalState(\\\"listApiConfigMeta\\\", listApiConfig)\\n1180 | \\t\\t\\t\\t} catch (error) {\\n1181 | \\t\\t\\t\\t\\tprovider.log(\\n1182 | \\t\\t\\t\\t\\t\\t`Error save api configuration: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`,\\n1183 | \\t\\t\\t\\t\\t)\\n1184 | \\t\\t\\t\\t\\tvscode.window.showErrorMessage(t(\\\"common:errors.save_api_config\\\"))\\n1185 | \\t\\t\\t\\t}\\n1186 | \\t\\t\\t}\\n1187 | \\t\\t\\tbreak\\n1188 | \\t\\tcase \\\"upsertApiConfiguration\\\":\\n1189 | \\t\\t\\tif (message.text && message.apiConfiguration) {\\n1190 | \\t\\t\\t\\tawait provider.upsertProviderProfile(message.text, message.apiConfiguration)\\n1191 | \\t\\t\\t}\\n1192 | \\t\\t\\tbreak\\n1193 | \\t\\tcase \\\"renameApiConfiguration\\\":\\n1194 | \\t\\t\\tif (message.values && message.apiConfiguration) {\\n1195 | \\t\\t\\t\\ttry {\\n1196 | \\t\\t\\t\\t\\tconst { oldName, newName } = message.values\\n1197 | \\n1198 | \\t\\t\\t\\t\\tif (oldName === newName) {\\n1199 | \\t\\t\\t\\t\\t\\tbreak\\n1200 | \\t\\t\\t\\t\\t}\\n1201 | \\n1202 | \\t\\t\\t\\t\\t// Load the old configuration to get its ID.\\n1203 | \\t\\t\\t\\t\\tconst { id } = await provider.providerSettingsManager.getProfile({ name: oldName })\\n1204 | \\n1205 | \\t\\t\\t\\t\\t// Create a new configuration with the new name and old ID.\\n1206 | \\t\\t\\t\\t\\tawait provider.providerSettingsManager.saveConfig(newName, { ...message.apiConfiguration, id })\\n1207 | \\n1208 | \\t\\t\\t\\t\\t// Delete the old configuration.\\n1209 | \\t\\t\\t\\t\\tawait provider.providerSettingsManager.deleteConfig(oldName)\\n1210 | \\n1211 | \\t\\t\\t\\t\\t// Re-activate to update the global settings related to the\\n1212 | \\t\\t\\t\\t\\t// currently activated provider profile.\\n1213 | \\t\\t\\t\\t\\tawait provider.activateProviderProfile({ name: newName })\\n1214 | \\t\\t\\t\\t} catch (error) {\\n1215 | \\t\\t\\t\\t\\tprovider.log(\\n1216 | \\t\\t\\t\\t\\t\\t`Error rename api configuration: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`,\\n1217 | \\t\\t\\t\\t\\t)\\n1218 | \\n1219 | \\t\\t\\t\\t\\tvscode.window.showErrorMessage(t(\\\"common:errors.rename_api_config\\\"))\\n1220 | \\t\\t\\t\\t}\\n1221 | \\t\\t\\t}\\n1222 | \\t\\t\\tbreak\\n1223 | \\t\\tcase \\\"loadApiConfiguration\\\":\\n1224 | \\t\\t\\tif (message.text) {\\n1225 | \\t\\t\\t\\ttry {\\n1226 | \\t\\t\\t\\t\\tawait provider.activateProviderProfile({ name: message.text })\\n1227 | \\t\\t\\t\\t} catch (error) {\\n1228 | \\t\\t\\t\\t\\tprovider.log(\\n1229 | \\t\\t\\t\\t\\t\\t`Error load api configuration: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`,\\n1230 | \\t\\t\\t\\t\\t)\\n1231 | \\t\\t\\t\\t\\tvscode.window.showErrorMessage(t(\\\"common:errors.load_api_config\\\"))\\n1232 | \\t\\t\\t\\t}\\n1233 | \\t\\t\\t}\\n1234 | \\t\\t\\tbreak\\n1235 | \\t\\tcase \\\"loadApiConfigurationById\\\":\\n1236 | \\t\\t\\tif (message.text) {\\n1237 | \\t\\t\\t\\ttry {\\n1238 | \\t\\t\\t\\t\\tawait provider.activateProviderProfile({ id: message.text })\\n1239 | \\t\\t\\t\\t} catch (error) {\\n1240 | \\t\\t\\t\\t\\tprovider.log(\\n1241 | \\t\\t\\t\\t\\t\\t`Error load api configuration by ID: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`,\\n1242 | \\t\\t\\t\\t\\t)\\n1243 | \\t\\t\\t\\t\\tvscode.window.showErrorMessage(t(\\\"common:errors.load_api_config\\\"))\\n1244 | \\t\\t\\t\\t}\\n1245 | \\t\\t\\t}\\n1246 | \\t\\t\\tbreak\\n1247 | \\t\\tcase \\\"deleteApiConfiguration\\\":\\n1248 | \\t\\t\\tif (message.text) {\\n1249 | \\t\\t\\t\\tconst answer = await vscode.window.showInformationMessage(\\n1250 | \\t\\t\\t\\t\\tt(\\\"common:confirmation.delete_config_profile\\\"),\\n1251 | \\t\\t\\t\\t\\t{ modal: true },\\n1252 | \\t\\t\\t\\t\\tt(\\\"common:answers.yes\\\"),\\n1253 | \\t\\t\\t\\t)\\n1254 | \\n1255 | \\t\\t\\t\\tif (answer !== t(\\\"common:answers.yes\\\")) {\\n1256 | \\t\\t\\t\\t\\tbreak\\n1257 | \\t\\t\\t\\t}\\n1258 | \\n1259 | \\t\\t\\t\\tconst oldName = message.text\\n1260 | \\n1261 | \\t\\t\\t\\tconst newName = (await provider.providerSettingsManager.listConfig()).filter(\\n1262 | \\t\\t\\t\\t\\t(c) => c.name !== oldName,\\n1263 | \\t\\t\\t\\t)[0]?.name\\n1264 | \\n1265 | \\t\\t\\t\\tif (!newName) {\\n1266 | \\t\\t\\t\\t\\tvscode.window.showErrorMessage(t(\\\"common:errors.delete_api_config\\\"))\\n1267 | \\t\\t\\t\\t\\treturn\\n1268 | \\t\\t\\t\\t}\\n1269 | \\n1270 | \\t\\t\\t\\ttry {\\n1271 | \\t\\t\\t\\t\\tawait provider.providerSettingsManager.deleteConfig(oldName)\\n1272 | \\t\\t\\t\\t\\tawait provider.activateProviderProfile({ name: newName })\\n1273 | \\t\\t\\t\\t} catch (error) {\\n1274 | \\t\\t\\t\\t\\tprovider.log(\\n1275 | \\t\\t\\t\\t\\t\\t`Error delete api configuration: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`,\\n1276 | \\t\\t\\t\\t\\t)\\n1277 | \\n1278 | \\t\\t\\t\\t\\tvscode.window.showErrorMessage(t(\\\"common:errors.delete_api_config\\\"))\\n1279 | \\t\\t\\t\\t}\\n1280 | \\t\\t\\t}\\n1281 | \\t\\t\\tbreak\\n1282 | \\t\\tcase \\\"getListApiConfiguration\\\":\\n1283 | \\t\\t\\ttry {\\n1284 | \\t\\t\\t\\tconst listApiConfig = await provider.providerSettingsManager.listConfig()\\n1285 | \\t\\t\\t\\tawait updateGlobalState(\\\"listApiConfigMeta\\\", listApiConfig)\\n1286 | \\t\\t\\t\\tprovider.postMessageToWebview({ type: \\\"listApiConfig\\\", listApiConfig })\\n1287 | \\t\\t\\t} catch (error) {\\n1288 | \\t\\t\\t\\tprovider.log(\\n1289 | \\t\\t\\t\\t\\t`Error get list api configuration: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`,\\n1290 | \\t\\t\\t\\t)\\n1291 | \\t\\t\\t\\tvscode.window.showErrorMessage(t(\\\"common:errors.list_api_config\\\"))\\n1292 | \\t\\t\\t}\\n1293 | \\t\\t\\tbreak\\n1294 | \\t\\tcase \\\"updateExperimental\\\": {\\n1295 | \\t\\t\\tif (!message.values) {\\n1296 | \\t\\t\\t\\tbreak\\n1297 | \\t\\t\\t}\\n1298 | \\n1299 | \\t\\t\\tconst updatedExperiments = {\\n1300 | \\t\\t\\t\\t...(getGlobalState(\\\"experiments\\\") ?? experimentDefault),\\n1301 | \\t\\t\\t\\t...message.values,\\n1302 | \\t\\t\\t}\\n1303 | \\n1304 | \\t\\t\\tawait updateGlobalState(\\\"experiments\\\", updatedExperiments)\\n1305 | \\n1306 | \\t\\t\\tawait provider.postStateToWebview()\\n1307 | \\t\\t\\tbreak\\n1308 | \\t\\t}\\n1309 | \\t\\tcase \\\"updateMcpTimeout\\\":\\n1310 | \\t\\t\\tif (message.serverName && typeof message.timeout === \\\"number\\\") {\\n1311 | \\t\\t\\t\\ttry {\\n1312 | \\t\\t\\t\\t\\tawait provider\\n1313 | \\t\\t\\t\\t\\t\\t.getMcpHub()\\n1314 | \\t\\t\\t\\t\\t\\t?.updateServerTimeout(\\n1315 | \\t\\t\\t\\t\\t\\t\\tmessage.serverName,\\n1316 | \\t\\t\\t\\t\\t\\t\\tmessage.timeout,\\n1317 | \\t\\t\\t\\t\\t\\t\\tmessage.source as \\\"global\\\" | \\\"project\\\",\\n1318 | \\t\\t\\t\\t\\t\\t)\\n1319 | \\t\\t\\t\\t} catch (error) {\\n1320 | \\t\\t\\t\\t\\tprovider.log(\\n1321 | \\t\\t\\t\\t\\t\\t`Failed to update timeout for ${message.serverName}: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`,\\n1322 | \\t\\t\\t\\t\\t)\\n1323 | \\t\\t\\t\\t\\tvscode.window.showErrorMessage(t(\\\"common:errors.update_server_timeout\\\"))\\n1324 | \\t\\t\\t\\t}\\n1325 | \\t\\t\\t}\\n1326 | \\t\\t\\tbreak\\n1327 | \\t\\tcase \\\"updateCustomMode\\\":\\n1328 | \\t\\t\\tif (message.modeConfig) {\\n1329 | \\t\\t\\t\\tawait provider.customModesManager.updateCustomMode(message.modeConfig.slug, message.modeConfig)\\n1330 | \\t\\t\\t\\t// Update state after saving the mode\\n1331 | \\t\\t\\t\\tconst customModes = await provider.customModesManager.getCustomModes()\\n1332 | \\t\\t\\t\\tawait updateGlobalState(\\\"customModes\\\", customModes)\\n1333 | \\t\\t\\t\\tawait updateGlobalState(\\\"mode\\\", message.modeConfig.slug)\\n1334 | \\t\\t\\t\\tawait provider.postStateToWebview()\\n1335 | \\t\\t\\t}\\n1336 | \\t\\t\\tbreak\\n1337 | \\t\\tcase \\\"deleteCustomMode\\\":\\n1338 | \\t\\t\\tif (message.slug) {\\n1339 | \\t\\t\\t\\tconst answer = await vscode.window.showInformationMessage(\\n1340 | \\t\\t\\t\\t\\tt(\\\"common:confirmation.delete_custom_mode\\\"),\\n1341 | \\t\\t\\t\\t\\t{ modal: true },\\n1342 | \\t\\t\\t\\t\\tt(\\\"common:answers.yes\\\"),\\n1343 | \\t\\t\\t\\t)\\n1344 | \\n1345 | \\t\\t\\t\\tif (answer !== t(\\\"common:answers.yes\\\")) {\\n1346 | \\t\\t\\t\\t\\tbreak\\n1347 | \\t\\t\\t\\t}\\n1348 | \\n1349 | \\t\\t\\t\\tawait provider.customModesManager.deleteCustomMode(message.slug)\\n1350 | \\t\\t\\t\\t// Switch back to default mode after deletion\\n1351 | \\t\\t\\t\\tawait updateGlobalState(\\\"mode\\\", defaultModeSlug)\\n1352 | \\t\\t\\t\\tawait provider.postStateToWebview()\\n1353 | \\t\\t\\t}\\n1354 | \\t\\t\\tbreak\\n1355 | \\t\\tcase \\\"humanRelayResponse\\\":\\n1356 | \\t\\t\\tif (message.requestId && message.text) {\\n1357 | \\t\\t\\t\\tvscode.commands.executeCommand(getCommand(\\\"handleHumanRelayResponse\\\"), {\\n1358 | \\t\\t\\t\\t\\trequestId: message.requestId,\\n1359 | \\t\\t\\t\\t\\ttext: message.text,\\n1360 | \\t\\t\\t\\t\\tcancelled: false,\\n1361 | \\t\\t\\t\\t})\\n1362 | \\t\\t\\t}\\n1363 | \\t\\t\\tbreak\\n1364 | \\n1365 | \\t\\tcase \\\"humanRelayCancel\\\":\\n1366 | \\t\\t\\tif (message.requestId) {\\n1367 | \\t\\t\\t\\tvscode.commands.executeCommand(getCommand(\\\"handleHumanRelayResponse\\\"), {\\n1368 | \\t\\t\\t\\t\\trequestId: message.requestId,\\n1369 | \\t\\t\\t\\t\\tcancelled: true,\\n1370 | \\t\\t\\t\\t})\\n1371 | \\t\\t\\t}\\n1372 | \\t\\t\\tbreak\\n1373 | \\n1374 | \\t\\tcase \\\"telemetrySetting\\\": {\\n1375 | \\t\\t\\tconst telemetrySetting = message.text as TelemetrySetting\\n1376 | \\t\\t\\tawait updateGlobalState(\\\"telemetrySetting\\\", telemetrySetting)\\n1377 | \\t\\t\\tconst isOptedIn = telemetrySetting === \\\"enabled\\\"\\n1378 | \\t\\t\\tTelemetryService.instance.updateTelemetryState(isOptedIn)\\n1379 | \\t\\t\\tawait provider.postStateToWebview()\\n1380 | \\t\\t\\tbreak\\n1381 | \\t\\t}\\n1382 | \\t\\tcase \\\"accountButtonClicked\\\": {\\n1383 | \\t\\t\\t// Navigate to the account tab.\\n1384 | \\t\\t\\tprovider.postMessageToWebview({ type: \\\"action\\\", action: \\\"accountButtonClicked\\\" })\\n1385 | \\t\\t\\tbreak\\n1386 | \\t\\t}\\n1387 | \\t\\tcase \\\"rooCloudSignIn\\\": {\\n1388 | \\t\\t\\ttry {\\n1389 | \\t\\t\\t\\tTelemetryService.instance.captureEvent(TelemetryEventName.AUTHENTICATION_INITIATED)\\n1390 | \\t\\t\\t\\tawait CloudService.instance.login()\\n1391 | \\t\\t\\t} catch (error) {\\n1392 | \\t\\t\\t\\tprovider.log(`AuthService#login failed: ${error}`)\\n1393 | \\t\\t\\t\\tvscode.window.showErrorMessage(\\\"Sign in failed.\\\")\\n1394 | \\t\\t\\t}\\n1395 | \\n1396 | \\t\\t\\tbreak\\n1397 | \\t\\t}\\n1398 | \\t\\tcase \\\"rooCloudSignOut\\\": {\\n1399 | \\t\\t\\ttry {\\n1400 | \\t\\t\\t\\tawait CloudService.instance.logout()\\n1401 | \\t\\t\\t\\tawait provider.postStateToWebview()\\n1402 | \\t\\t\\t\\tprovider.postMessageToWebview({ type: \\\"authenticatedUser\\\", userInfo: undefined })\\n1403 | \\t\\t\\t} catch (error) {\\n1404 | \\t\\t\\t\\tprovider.log(`AuthService#logout failed: ${error}`)\\n1405 | \\t\\t\\t\\tvscode.window.showErrorMessage(\\\"Sign out failed.\\\")\\n1406 | \\t\\t\\t}\\n1407 | \\n1408 | \\t\\t\\tbreak\\n1409 | \\t\\t}\\n1410 | \\t\\tcase \\\"codebaseIndexConfig\\\": {\\n1411 | \\t\\t\\tconst codebaseIndexConfig = message.values ?? {\\n1412 | \\t\\t\\t\\tcodebaseIndexEnabled: false,\\n1413 | \\t\\t\\t\\tcodebaseIndexQdrantUrl: \\\"http://localhost:6333\\\",\\n1414 | \\t\\t\\t\\tcodebaseIndexEmbedderProvider: \\\"openai\\\",\\n1415 | \\t\\t\\t\\tcodebaseIndexEmbedderBaseUrl: \\\"\\\",\\n1416 | \\t\\t\\t\\tcodebaseIndexEmbedderModelId: \\\"\\\",\\n1417 | \\t\\t\\t}\\n1418 | \\t\\t\\tawait updateGlobalState(\\\"codebaseIndexConfig\\\", codebaseIndexConfig)\\n1419 | \\n1420 | \\t\\t\\ttry {\\n1421 | \\t\\t\\t\\tif (provider.codeIndexManager) {\\n1422 | \\t\\t\\t\\t\\tawait provider.codeIndexManager.handleExternalSettingsChange()\\n1423 | \\n1424 | \\t\\t\\t\\t\\t// If now configured and enabled, start indexing automatically\\n1425 | \\t\\t\\t\\t\\tif (provider.codeIndexManager.isFeatureEnabled && provider.codeIndexManager.isFeatureConfigured) {\\n1426 | \\t\\t\\t\\t\\t\\tif (!provider.codeIndexManager.isInitialized) {\\n1427 | \\t\\t\\t\\t\\t\\t\\tawait provider.codeIndexManager.initialize(provider.contextProxy)\\n1428 | \\t\\t\\t\\t\\t\\t}\\n1429 | \\t\\t\\t\\t\\t\\t// Start indexing in background (no await)\\n1430 | \\t\\t\\t\\t\\t\\tprovider.codeIndexManager.startIndexing()\\n1431 | \\t\\t\\t\\t\\t}\\n1432 | \\t\\t\\t\\t}\\n1433 | \\t\\t\\t} catch (error) {\\n1434 | \\t\\t\\t\\tprovider.log(\\n1435 | \\t\\t\\t\\t\\t`[CodeIndexManager] Error during background CodeIndexManager configuration/indexing: ${error.message || error}`,\\n1436 | \\t\\t\\t\\t)\\n1437 | \\t\\t\\t}\\n1438 | \\n1439 | \\t\\t\\tawait provider.postStateToWebview()\\n1440 | \\t\\t\\tbreak\\n1441 | \\t\\t}\\n1442 | \\t\\tcase \\\"requestIndexingStatus\\\": {\\n1443 | \\t\\t\\tconst status = provider.codeIndexManager!.getCurrentStatus()\\n1444 | \\t\\t\\tprovider.postMessageToWebview({\\n1445 | \\t\\t\\t\\ttype: \\\"indexingStatusUpdate\\\",\\n1446 | \\t\\t\\t\\tvalues: status,\\n1447 | \\t\\t\\t})\\n1448 | \\t\\t\\tbreak\\n1449 | \\t\\t}\\n1450 | \\t\\tcase \\\"startIndexing\\\": {\\n1451 | \\t\\t\\ttry {\\n1452 | \\t\\t\\t\\tconst manager = provider.codeIndexManager!\\n1453 | \\t\\t\\t\\tif (manager.isFeatureEnabled && manager.isFeatureConfigured) {\\n1454 | \\t\\t\\t\\t\\tif (!manager.isInitialized) {\\n1455 | \\t\\t\\t\\t\\t\\tawait manager.initialize(provider.contextProxy)\\n1456 | \\t\\t\\t\\t\\t}\\n1457 | \\n1458 | \\t\\t\\t\\t\\tmanager.startIndexing()\\n1459 | \\t\\t\\t\\t}\\n1460 | \\t\\t\\t} catch (error) {\\n1461 | \\t\\t\\t\\tprovider.log(`Error starting indexing: ${error instanceof Error ? error.message : String(error)}`)\\n1462 | \\t\\t\\t}\\n1463 | \\t\\t\\tbreak\\n1464 | \\t\\t}\\n1465 | \\t\\tcase \\\"clearIndexData\\\": {\\n1466 | \\t\\t\\ttry {\\n1467 | \\t\\t\\t\\tconst manager = provider.codeIndexManager!\\n1468 | \\t\\t\\t\\tawait manager.clearIndexData()\\n1469 | \\t\\t\\t\\tprovider.postMessageToWebview({ type: \\\"indexCleared\\\", values: { success: true } })\\n1470 | \\t\\t\\t} catch (error) {\\n1471 | \\t\\t\\t\\tprovider.log(`Error clearing index data: ${error instanceof Error ? error.message : String(error)}`)\\n1472 | \\t\\t\\t\\tprovider.postMessageToWebview({\\n1473 | \\t\\t\\t\\t\\ttype: \\\"indexCleared\\\",\\n1474 | \\t\\t\\t\\t\\tvalues: {\\n1475 | \\t\\t\\t\\t\\t\\tsuccess: false,\\n1476 | \\t\\t\\t\\t\\t\\terror: error instanceof Error ? error.message : String(error),\\n1477 | \\t\\t\\t\\t\\t},\\n1478 | \\t\\t\\t\\t})\\n1479 | \\t\\t\\t}\\n1480 | \\t\\t\\tbreak\\n1481 | \\t\\t}\\n1482 | \\t\\tcase \\\"filterMarketplaceItems\\\": {\\n1483 | \\t\\t\\t// Check if marketplace is enabled before making API calls\\n1484 | \\t\\t\\tconst { experiments } = await provider.getState()\\n1485 | \\t\\t\\tif (!experiments.marketplace) {\\n1486 | \\t\\t\\t\\tconsole.log(\\\"Marketplace: Feature disabled, skipping API call\\\")\\n1487 | \\t\\t\\t\\tbreak\\n1488 | \\t\\t\\t}\\n1489 | \\n1490 | \\t\\t\\tif (marketplaceManager && message.filters) {\\n1491 | \\t\\t\\t\\ttry {\\n1492 | \\t\\t\\t\\t\\tawait marketplaceManager.updateWithFilteredItems({\\n1493 | \\t\\t\\t\\t\\t\\ttype: message.filters.type as MarketplaceItemType | undefined,\\n1494 | \\t\\t\\t\\t\\t\\tsearch: message.filters.search,\\n1495 | \\t\\t\\t\\t\\t\\ttags: message.filters.tags,\\n1496 | \\t\\t\\t\\t\\t})\\n1497 | \\t\\t\\t\\t\\tawait provider.postStateToWebview()\\n1498 | \\t\\t\\t\\t} catch (error) {\\n1499 | \\t\\t\\t\\t\\tconsole.error(\\\"Marketplace: Error filtering items:\\\", error)\\n1500 | \\t\\t\\t\\t\\tvscode.window.showErrorMessage(\\\"Failed to filter marketplace items\\\")\\n1501 | \\t\\t\\t\\t}\\n1502 | \\t\\t\\t}\\n1503 | \\t\\t\\tbreak\\n1504 | \\t\\t}\\n1505 | \\n1506 | \\t\\tcase \\\"installMarketplaceItem\\\": {\\n1507 | \\t\\t\\t// Check if marketplace is enabled before installing\\n1508 | \\t\\t\\tconst { experiments } = await provider.getState()\\n1509 | \\t\\t\\tif (!experiments.marketplace) {\\n1510 | \\t\\t\\t\\tconsole.log(\\\"Marketplace: Feature disabled, skipping installation\\\")\\n1511 | \\t\\t\\t\\tbreak\\n1512 | \\t\\t\\t}\\n1513 | \\n1514 | \\t\\t\\tif (marketplaceManager && message.mpItem && message.mpInstallOptions) {\\n1515 | \\t\\t\\t\\ttry {\\n1516 | \\t\\t\\t\\t\\tconst configFilePath = await marketplaceManager.installMarketplaceItem(\\n1517 | \\t\\t\\t\\t\\t\\tmessage.mpItem,\\n1518 | \\t\\t\\t\\t\\t\\tmessage.mpInstallOptions,\\n1519 | \\t\\t\\t\\t\\t)\\n1520 | \\t\\t\\t\\t\\tawait provider.postStateToWebview()\\n1521 | \\t\\t\\t\\t\\tconsole.log(`Marketplace item installed and config file opened: ${configFilePath}`)\\n1522 | \\t\\t\\t\\t\\t// Send success message to webview\\n1523 | \\t\\t\\t\\t\\tprovider.postMessageToWebview({\\n1524 | \\t\\t\\t\\t\\t\\ttype: \\\"marketplaceInstallResult\\\",\\n1525 | \\t\\t\\t\\t\\t\\tsuccess: true,\\n1526 | \\t\\t\\t\\t\\t\\tslug: message.mpItem.id,\\n1527 | \\t\\t\\t\\t\\t})\\n1528 | \\t\\t\\t\\t} catch (error) {\\n1529 | \\t\\t\\t\\t\\tconsole.error(`Error installing marketplace item: ${error}`)\\n1530 | \\t\\t\\t\\t\\t// Send error message to webview\\n1531 | \\t\\t\\t\\t\\tprovider.postMessageToWebview({\\n1532 | \\t\\t\\t\\t\\t\\ttype: \\\"marketplaceInstallResult\\\",\\n1533 | \\t\\t\\t\\t\\t\\tsuccess: false,\\n1534 | \\t\\t\\t\\t\\t\\terror: error instanceof Error ? error.message : String(error),\\n1535 | \\t\\t\\t\\t\\t\\tslug: message.mpItem.id,\\n1536 | \\t\\t\\t\\t\\t})\\n1537 | \\t\\t\\t\\t}\\n1538 | \\t\\t\\t}\\n1539 | \\t\\t\\tbreak\\n1540 | \\t\\t}\\n1541 | \\n1542 | \\t\\tcase \\\"removeInstalledMarketplaceItem\\\": {\\n1543 | \\t\\t\\t// Check if marketplace is enabled before removing\\n1544 | \\t\\t\\tconst { experiments } = await provider.getState()\\n1545 | \\t\\t\\tif (!experiments.marketplace) {\\n1546 | \\t\\t\\t\\tconsole.log(\\\"Marketplace: Feature disabled, skipping removal\\\")\\n1547 | \\t\\t\\t\\tbreak\\n1548 | \\t\\t\\t}\\n1549 | \\n1550 | \\t\\t\\tif (marketplaceManager && message.mpItem && message.mpInstallOptions) {\\n1551 | \\t\\t\\t\\ttry {\\n1552 | \\t\\t\\t\\t\\tawait marketplaceManager.removeInstalledMarketplaceItem(message.mpItem, message.mpInstallOptions)\\n1553 | \\t\\t\\t\\t\\tawait provider.postStateToWebview()\\n1554 | \\t\\t\\t\\t} catch (error) {\\n1555 | \\t\\t\\t\\t\\tconsole.error(`Error removing marketplace item: ${error}`)\\n1556 | \\t\\t\\t\\t}\\n1557 | \\t\\t\\t}\\n1558 | \\t\\t\\tbreak\\n1559 | \\t\\t}\\n1560 | \\n1561 | \\t\\tcase \\\"installMarketplaceItemWithParameters\\\": {\\n1562 | \\t\\t\\t// Check if marketplace is enabled before installing with parameters\\n1563 | \\t\\t\\tconst { experiments } = await provider.getState()\\n1564 | \\t\\t\\tif (!experiments.marketplace) {\\n1565 | \\t\\t\\t\\tconsole.log(\\\"Marketplace: Feature disabled, skipping installation with parameters\\\")\\n1566 | \\t\\t\\t\\tbreak\\n1567 | \\t\\t\\t}\\n1568 | \\n1569 | \\t\\t\\tif (marketplaceManager && message.payload && \\\"item\\\" in message.payload && \\\"parameters\\\" in message.payload) {\\n1570 | \\t\\t\\t\\ttry {\\n1571 | \\t\\t\\t\\t\\tconst configFilePath = await marketplaceManager.installMarketplaceItem(message.payload.item, {\\n1572 | \\t\\t\\t\\t\\t\\tparameters: message.payload.parameters,\\n1573 | \\t\\t\\t\\t\\t})\\n1574 | \\t\\t\\t\\t\\tawait provider.postStateToWebview()\\n1575 | \\t\\t\\t\\t\\tconsole.log(`Marketplace item with parameters installed and config file opened: ${configFilePath}`)\\n1576 | \\t\\t\\t\\t} catch (error) {\\n1577 | \\t\\t\\t\\t\\tconsole.error(`Error installing marketplace item with parameters: ${error}`)\\n1578 | \\t\\t\\t\\t\\tvscode.window.showErrorMessage(\\n1579 | \\t\\t\\t\\t\\t\\t`Failed to install marketplace item: ${error instanceof Error ? error.message : String(error)}`,\\n1580 | \\t\\t\\t\\t\\t)\\n1581 | \\t\\t\\t\\t}\\n1582 | \\t\\t\\t}\\n1583 | \\t\\t\\tbreak\\n1584 | \\t\\t}\\n1585 | \\n1586 | \\t\\tcase \\\"switchTab\\\": {\\n1587 | \\t\\t\\tif (message.tab) {\\n1588 | \\t\\t\\t\\t// Send a message to the webview to switch to the specified tab\\n1589 | \\t\\t\\t\\tawait provider.postMessageToWebview({ type: \\\"action\\\", action: \\\"switchTab\\\", tab: message.tab })\\n1590 | \\t\\t\\t}\\n1591 | \\t\\t\\tbreak\\n1592 | \\t\\t}\\n1593 | \\t}\\n1594 | }\\n\\n\\n\\n\\nLoading...\"}" +} +[2025-06-13T05:00:01.323Z] { + "ts": 1749790800308, + "type": "say", + "say": "text", + "text": "Now I can see the authentication implementation! I found the Clerk authentication callback handler and the CloudService integration. Let me search for more authentication-related files to understand the token refresh mechanism.", + "partial": false +} +[2025-06-13T05:00:01.942Z] { + "ts": 1749790801323, + "type": "ask", + "ask": "tool", + "text": "{\"tool\":\"searchFiles\",\"path\":\"Roo-Code-Cloud-Agents\",\"regex\":\"CloudService|refresh|token|auth\",\"filePattern\":\"*.ts\",\"content\":\"Showing first 300 of 300+ results. Use a more specific search if necessary.\\n\\n# apps/web-evals/src/lib/formatters.ts\\n 30 | \\n 31 | export const formatTokens = (tokens: number) => {\\n 32 | \\tif (tokens < 1000) {\\n 33 | \\t\\treturn tokens.toString()\\n 34 | \\t}\\n 35 | \\n 36 | \\tif (tokens < 1000000) {\\n 37 | \\t\\treturn `${(tokens / 1000).toFixed(1)}k`\\n 38 | \\t}\\n 39 | \\n 40 | \\tif (tokens < 1000000000) {\\n 41 | \\t\\treturn `${(tokens / 1000000).toFixed(1)}M`\\n 42 | \\t}\\n 43 | \\n 44 | \\treturn `${(tokens / 1000000000).toFixed(1)}B`\\n 45 | }\\n----\\n\\n# src/services/ripgrep/index.ts\\n101 | \\t\\tconst rgProcess = childProcess.spawn(bin, args)\\n102 | \\t\\t// cross-platform alternative to head, which is ripgrep author's recommendation for limiting output.\\n103 | \\t\\tconst rl = readline.createInterface({\\n----\\n\\n# apps/web-roo-code/src/lib/index.ts\\n 3 | export * from \\\"./format-score\\\"\\n 4 | export * from \\\"./format-tokens\\\"\\n----\\n\\n# apps/web-roo-code/src/lib/format-tokens.ts\\n 1 | export const formatTokens = (tokens: number, decimals = 0) => {\\n 2 | \\tif (tokens < 1000) {\\n 3 | \\t\\treturn tokens.toString()\\n 4 | \\t}\\n 5 | \\n 6 | \\tif (tokens < 1000000) {\\n 7 | \\t\\treturn `${(tokens / 1000).toFixed(decimals)}K`\\n 8 | \\t}\\n 9 | \\n 10 | \\tif (tokens < 1000000000) {\\n 11 | \\t\\treturn `${(tokens / 1000000).toFixed(decimals)}M`\\n 12 | \\t}\\n 13 | \\n 14 | \\treturn `${(tokens / 1000000000).toFixed(decimals)}B`\\n 15 | }\\n----\\n\\n# apps/web-roo-code/src/lib/hooks/use-open-router-models.ts\\n 19 | \\t\\t.object({\\n 20 | \\t\\t\\tmax_completion_tokens: z.number().nullish(),\\n 21 | \\t\\t})\\n----\\n 53 | \\t\\t\\tmodelInfo: {\\n 54 | \\t\\t\\t\\tmaxTokens: rawModel.top_provider?.max_completion_tokens ?? undefined,\\n 55 | \\t\\t\\t\\tcontextWindow: rawModel.context_length,\\n----\\n\\n# src/services/mcp/__tests__/McpHub.test.ts\\n 50 | \\t\\t\\tscheme: \\\"file\\\",\\n 51 | \\t\\t\\tauthority: \\\"\\\",\\n 52 | \\t\\t\\tpath: \\\"/test/path\\\",\\n----\\n\\n# src/services/mcp/McpHub.ts\\n1092 | \\n1093 | \\tpublic async refreshAllConnections(): Promise {\\n1094 | \\t\\tif (this.isConnecting) {\\n1095 | \\t\\t\\tvscode.window.showInformationMessage(t(\\\"mcp:info.already_refreshing\\\"))\\n1096 | \\t\\t\\treturn\\n----\\n1099 | \\t\\tthis.isConnecting = true\\n1100 | \\t\\tvscode.window.showInformationMessage(t(\\\"mcp:info.refreshing_all\\\"))\\n1101 | \\n----\\n1151 | \\n1152 | \\t\\t\\tvscode.window.showInformationMessage(t(\\\"mcp:info.all_refreshed\\\"))\\n1153 | \\t\\t} catch (error) {\\n1154 | \\t\\t\\tthis.showErrorMessage(\\\"Failed to refresh MCP servers\\\", error)\\n1155 | \\t\\t} finally {\\n----\\n1244 | \\n1245 | \\t\\t\\t\\t\\t// Only refresh capabilities if connected\\n1246 | \\t\\t\\t\\t\\tif (connection.server.status === \\\"connected\\\") {\\n----\\n1254 | \\t\\t\\t\\t} catch (error) {\\n1255 | \\t\\t\\t\\t\\tconsole.error(`Failed to refresh capabilities for ${serverName}:`, error)\\n1256 | \\t\\t\\t\\t}\\n----\\n\\n# src/services/code-index/embedders/openai.ts\\n 52 | \\t\\t\\t\\t\\tconsole.warn(\\n 53 | \\t\\t\\t\\t\\t\\t`Text at index ${i} exceeds maximum token limit (${itemTokens} > ${MAX_ITEM_TOKENS}). Skipping.`,\\n 54 | \\t\\t\\t\\t\\t)\\n----\\n108 | \\t\\t\\t\\t\\tusage: {\\n109 | \\t\\t\\t\\t\\t\\tpromptTokens: response.usage?.prompt_tokens || 0,\\n110 | \\t\\t\\t\\t\\t\\ttotalTokens: response.usage?.total_tokens || 0,\\n111 | \\t\\t\\t\\t\\t},\\n----\\n\\n# src/services/code-index/embedders/openai-compatible.ts\\n 18 | \\tusage?: {\\n 19 | \\t\\tprompt_tokens?: number\\n 20 | \\t\\ttotal_tokens?: number\\n 21 | \\t}\\n----\\n 34 | \\t * @param baseUrl The base URL for the OpenAI-compatible API endpoint\\n 35 | \\t * @param apiKey The API key for authentication\\n 36 | \\t * @param modelId Optional model identifier (defaults to \\\"text-embedding-3-small\\\")\\n----\\n 75 | \\t\\t\\t\\t\\tconsole.warn(\\n 76 | \\t\\t\\t\\t\\t\\t`Text at index ${i} exceeds maximum token limit (${itemTokens} > ${MAX_ITEM_TOKENS}). Skipping.`,\\n 77 | \\t\\t\\t\\t\\t)\\n----\\n156 | \\t\\t\\t\\t\\tusage: {\\n157 | \\t\\t\\t\\t\\t\\tpromptTokens: response.usage?.prompt_tokens || 0,\\n158 | \\t\\t\\t\\t\\t\\ttotalTokens: response.usage?.total_tokens || 0,\\n159 | \\t\\t\\t\\t\\t},\\n----\\n\\n# src/services/code-index/embedders/__tests__/openai-compatible.spec.ts\\n103 | \\t\\t\\t\\tdata: [{ embedding: [0.1, 0.2, 0.3] }],\\n104 | \\t\\t\\t\\tusage: { prompt_tokens: 10, total_tokens: 15 },\\n105 | \\t\\t\\t}\\n----\\n124 | \\t\\t\\t\\tdata: [{ embedding: [0.1, 0.2, 0.3] }, { embedding: [0.4, 0.5, 0.6] }],\\n125 | \\t\\t\\t\\tusage: { prompt_tokens: 20, total_tokens: 30 },\\n126 | \\t\\t\\t}\\n----\\n149 | \\t\\t\\t\\tdata: [{ embedding: [0.1, 0.2, 0.3] }],\\n150 | \\t\\t\\t\\tusage: { prompt_tokens: 10, total_tokens: 15 },\\n151 | \\t\\t\\t}\\n----\\n194 | \\t\\t\\t\\t\\tdata: [{ embedding: base64String }], // Base64 string instead of array\\n195 | \\t\\t\\t\\t\\tusage: { prompt_tokens: 10, total_tokens: 15 },\\n196 | \\t\\t\\t\\t}\\n----\\n226 | \\t\\t\\t\\t\\tdata: [{ embedding: base64String1 }, { embedding: base64String2 }],\\n227 | \\t\\t\\t\\t\\tusage: { prompt_tokens: 20, total_tokens: 30 },\\n228 | \\t\\t\\t\\t}\\n----\\n253 | \\t\\t\\t\\t\\t],\\n254 | \\t\\t\\t\\t\\tusage: { prompt_tokens: 20, total_tokens: 30 },\\n255 | \\t\\t\\t\\t}\\n----\\n270 | \\t\\t/**\\n271 | \\t\\t * Test batching logic when texts exceed token limits\\n272 | \\t\\t */\\n----\\n283 | \\t\\t\\t\\t\\t],\\n284 | \\t\\t\\t\\t\\tusage: { prompt_tokens: 10, total_tokens: 15 },\\n285 | \\t\\t\\t\\t})\\n----\\n299 | \\t\\t\\t\\t\\tdata: [{ embedding: [0.1, 0.2, 0.3] }, { embedding: [0.4, 0.5, 0.6] }],\\n300 | \\t\\t\\t\\t\\tusage: { prompt_tokens: 10, total_tokens: 15 },\\n301 | \\t\\t\\t\\t}\\n----\\n306 | \\t\\t\\t\\t// Should warn about oversized text\\n307 | \\t\\t\\t\\texpect(console.warn).toHaveBeenCalledWith(expect.stringContaining(\\\"exceeds maximum token limit\\\"))\\n308 | \\n----\\n317 | \\t\\t\\t\\t\\tdata: [{ embedding: [0.1, 0.2, 0.3] }, { embedding: [0.4, 0.5, 0.6] }],\\n318 | \\t\\t\\t\\t\\tusage: { prompt_tokens: 10, total_tokens: 15 },\\n319 | \\t\\t\\t\\t})\\n----\\n354 | \\t\\t\\t\\t\\t\\tdata: [{ embedding: base64String }],\\n355 | \\t\\t\\t\\t\\t\\tusage: { prompt_tokens: 10, total_tokens: 15 },\\n356 | \\t\\t\\t\\t\\t})\\n----\\n375 | \\t\\t\\t\\tconst testTexts = [\\\"Hello world\\\"]\\n376 | \\t\\t\\t\\tconst authError = new Error(\\\"Unauthorized\\\")\\n377 | \\t\\t\\t\\t;(authError as any).status = 401\\n378 | \\n379 | \\t\\t\\t\\tmockEmbeddingsCreate.mockRejectedValue(authError)\\n380 | \\n----\\n452 | \\t\\t\\t\\t\\tdata: null,\\n453 | \\t\\t\\t\\t\\tusage: { prompt_tokens: 10, total_tokens: 15 },\\n454 | \\t\\t\\t\\t}\\n----\\n494 | \\t\\t\\t\\t\\tusage: {\\n495 | \\t\\t\\t\\t\\t\\tprompt_tokens: 2,\\n496 | \\t\\t\\t\\t\\t\\ttotal_tokens: 2,\\n497 | \\t\\t\\t\\t\\t},\\n----\\n\\n# apps/web-evals/src/hooks/use-run-status.ts\\n 16 | \\ttasks: (Task & { taskMetrics: TaskMetrics | null })[] | undefined\\n 17 | \\ttokenUsage: Map\\n 18 | \\tusageUpdatedAt: number | undefined\\n----\\n 24 | \\n 25 | \\tconst tokenUsage = useRef>(new Map())\\n 26 | \\tconst startTimes = useRef>(new Map())\\n----\\n 79 | \\t\\t\\t\\tconst duration = startTime ? Date.now() - startTime : undefined\\n 80 | \\t\\t\\t\\ttokenUsage.current.set(taskId, { ...payload[1], duration })\\n 81 | \\t\\t\\t\\tsetUsageUpdatedAt(Date.now())\\n----\\n 97 | \\t\\ttasks,\\n 98 | \\t\\ttokenUsage: tokenUsage.current,\\n 99 | \\t\\tusageUpdatedAt,\\n----\\n\\n# src/activate/handleUri.ts\\n 2 | \\n 3 | import { CloudService } from \\\"@roo-code/cloud\\\"\\n 4 | \\n----\\n 37 | \\t\\t}\\n 38 | \\t\\tcase \\\"/auth/clerk/callback\\\": {\\n 39 | \\t\\t\\tconst code = query.get(\\\"code\\\")\\n 40 | \\t\\t\\tconst state = query.get(\\\"state\\\")\\n 41 | \\t\\t\\tawait CloudService.instance.handleAuthCallback(code, state)\\n 42 | \\t\\t\\tbreak\\n----\\n\\n# src/services/marketplace/types.ts\\n 20 | \\tdescription: string\\n 21 | \\tauthor?: string\\n 22 | \\tauthorUrl?: string\\n 23 | \\ttags?: string[]\\n----\\n 45 | \\tdescription: string\\n 46 | \\tauthor?: string\\n 47 | \\tauthorUrl?: string\\n 48 | \\turl: string // Required url field\\n----\\n 62 | \\ttype: MarketplaceItemType\\n 63 | \\tauthor?: string\\n 64 | \\tauthorUrl?: string\\n 65 | \\turl?: string // Optional - only MCPs have url\\n----\\n\\n# src/services/tree-sitter/__tests__/fixtures/sample-embedded_template.ts\\n 35 |
\\n 36 | <% if user.authenticated? %>\\n 37 |

Welcome, <%= user.name %>

\\n----\\n\\n# src/services/tree-sitter/__tests__/parseSourceCodeDefinitions.embedded_template.test.ts\\n 43 | \\tit(\\\"should detect control structures\\\", () => {\\n 44 | \\t\\texpect(parseResult).toMatch(/\\\\d+--\\\\d+ \\\\|\\\\s+<% if user\\\\.authenticated\\\\? %>/)\\n 45 | \\t\\texpect(parseResult).toMatch(/\\\\d+--\\\\d+ \\\\|\\\\s+<% user\\\\.posts\\\\.each do \\\\|post\\\\| %>/)\\n----\\n\\n# src/services/marketplace/schemas.ts\\n 35 | \\ttype: marketplaceItemTypeSchema,\\n 36 | \\tauthor: z.string().optional(),\\n 37 | \\tauthorUrl: z.string().url(\\\"Author URL must be a valid URL\\\").optional(),\\n 38 | \\ttags: z.array(z.string()).optional(),\\n----\\n 61 | \\tdescription: z.string(),\\n 62 | \\tauthor: z.string().optional(),\\n 63 | \\tauthorUrl: z.string().url().optional(),\\n 64 | \\ttags: z.array(z.string()).optional(),\\n----\\n 72 | \\tdescription: z.string(),\\n 73 | \\tauthor: z.string().optional(),\\n 74 | \\tauthorUrl: z.string().url().optional(),\\n 75 | \\turl: z.string().url(), // Required url field\\n----\\n\\n# src/extension.ts\\n 14 | \\n 15 | import { CloudService } from \\\"@roo-code/cloud\\\"\\n 16 | import { TelemetryService, PostHogTelemetryClient } from \\\"@roo-code/telemetry\\\"\\n----\\n 75 | \\t// Initialize Roo Code Cloud service.\\n 76 | \\tawait CloudService.createInstance(context, {\\n 77 | \\t\\tstateChanged: () => ClineProvider.getVisibleInstance()?.postStateToWebview(),\\n----\\n\\n# src/workers/countTokens.ts\\n 4 | \\n 5 | import { tiktoken } from \\\"../utils/tiktoken\\\"\\n 6 | \\n----\\n 10 | \\ttry {\\n 11 | \\t\\tconst count = await tiktoken(content)\\n 12 | \\t\\treturn { success: true, count }\\n----\\n\\n# src/api/index.ts\\n 50 | \\t/**\\n 51 | \\t * Counts tokens for content blocks\\n 52 | \\t * All providers extend BaseProvider which provides a default tiktoken implementation,\\n 53 | \\t * but they can override this to use their native token counting endpoints\\n 54 | \\t *\\n 55 | \\t * @param content The content to count tokens for\\n 56 | \\t * @returns A promise resolving to the token count\\n 57 | \\t */\\n----\\n\\n# src/extension/api.ts\\n219 | \\n220 | \\t\\t\\tcline.on(\\\"taskCompleted\\\", async (_, tokenUsage, toolUsage) => {\\n221 | \\t\\t\\t\\tthis.emit(RooCodeEventName.TaskCompleted, cline.taskId, tokenUsage, toolUsage)\\n222 | \\t\\t\\t\\tthis.taskMap.delete(cline.taskId)\\n----\\n224 | \\t\\t\\t\\tawait this.fileLog(\\n225 | \\t\\t\\t\\t\\t`[${new Date().toISOString()}] taskCompleted -> ${cline.taskId} | ${JSON.stringify(tokenUsage, null, 2)} | ${JSON.stringify(toolUsage, null, 2)}\\\\n`,\\n226 | \\t\\t\\t\\t)\\n----\\n\\n# src/api/providers/ollama.ts\\n 79 | \\t\\t\\t\\ttype: \\\"usage\\\",\\n 80 | \\t\\t\\t\\tinputTokens: lastUsage?.prompt_tokens || 0,\\n 81 | \\t\\t\\t\\toutputTokens: lastUsage?.completion_tokens || 0,\\n 82 | \\t\\t\\t}\\n----\\n\\n# src/core/checkpoints/index.ts\\n215 | \\t\\t\\t\\tJSON.stringify({\\n216 | \\t\\t\\t\\t\\ttokensIn: totalTokensIn,\\n217 | \\t\\t\\t\\t\\ttokensOut: totalTokensOut,\\n218 | \\t\\t\\t\\t\\tcacheWrites: totalCacheWrites,\\n----\\n\\n# src/utils/git.ts\\n 11 | \\tsubject: string\\n 12 | \\tauthor: string\\n 13 | \\tdate: string\\n----\\n 57 | \\t\\t\\tconst { stdout: hashStdout } = await execAsync(\\n 58 | \\t\\t\\t\\t`git log -n 10 --format=\\\"%H%n%h%n%s%n%an%n%ad\\\" --date=short ` + `--author-date-order ${query}`,\\n 59 | \\t\\t\\t\\t{ cwd },\\n----\\n 79 | \\t\\t\\t\\tsubject: lines[i + 2],\\n 80 | \\t\\t\\t\\tauthor: lines[i + 3],\\n 81 | \\t\\t\\t\\tdate: lines[i + 4],\\n----\\n107 | \\t\\t})\\n108 | \\t\\tconst [fullHash, shortHash, subject, author, date, body] = info.trim().split(\\\"\\\\n\\\")\\n109 | \\n----\\n115 | \\t\\t\\t`Commit: ${shortHash} (${fullHash})`,\\n116 | \\t\\t\\t`Author: ${author}`,\\n117 | \\t\\t\\t`Date: ${date}`,\\n----\\n\\n# src/api/providers/__tests__/bedrock-invokedModelId.spec.ts\\n176 | \\n177 | \\t\\t// The last usage event should have the token counts from the metadata\\n178 | \\t\\tconst lastUsageEvent = usageEvents[usageEvents.length - 1]\\n179 | \\t\\t// Expect the usage event to include all token information\\n180 | \\t\\texpect(lastUsageEvent).toMatchObject({\\n----\\n183 | \\t\\t\\toutputTokens: 200,\\n184 | \\t\\t\\t// Cache tokens may be present with default values\\n185 | \\t\\t\\tcacheReadTokens: expect.any(Number),\\n----\\n\\n# src/api/providers/__tests__/groq.spec.ts\\n101 | \\t\\t\\t\\t\\t\\t\\tdone: false,\\n102 | \\t\\t\\t\\t\\t\\t\\tvalue: { choices: [{ delta: {} }], usage: { prompt_tokens: 10, completion_tokens: 20 } },\\n103 | \\t\\t\\t\\t\\t\\t})\\n----\\n139 | \\t\\t\\t\\tmodel: modelId,\\n140 | \\t\\t\\t\\tmax_tokens: modelInfo.maxTokens,\\n141 | \\t\\t\\t\\ttemperature: 0.5,\\n----\\n\\n# src/api/providers/__tests__/gemini.spec.ts\\n167 | \\tdescribe(\\\"calculateCost\\\", () => {\\n168 | \\t\\t// Mock ModelInfo based on gemini-1.5-flash-latest pricing (per 1M tokens)\\n169 | \\t\\t// Removed 'id' and 'name' as they are not part of ModelInfo type directly\\n170 | \\t\\tconst mockInfo: ModelInfo = {\\n171 | \\t\\t\\tinputPrice: 0.125, // $/1M tokens\\n172 | \\t\\t\\toutputPrice: 0.375, // $/1M tokens\\n173 | \\t\\t\\tcacheWritesPrice: 0.125, // Assume same as input for test\\n----\\n179 | \\n180 | \\t\\tit(\\\"should calculate cost correctly based on input and output tokens\\\", () => {\\n181 | \\t\\t\\tconst inputTokens = 10000 // Use larger numbers for per-million pricing\\n----\\n190 | \\n191 | \\t\\tit(\\\"should return 0 if token counts are zero\\\", () => {\\n192 | \\t\\t\\t// Note: The method expects numbers, not undefined. Passing undefined would be a type error.\\n193 | \\t\\t\\t// The calculateCost method itself returns undefined if prices are missing, but 0 if tokens are 0 and prices exist.\\n194 | \\t\\t\\texpect(handler.calculateCost({ info: mockInfo, inputTokens: 0, outputTokens: 0 })).toBe(0)\\n----\\n196 | \\n197 | \\t\\tit(\\\"should handle only input tokens\\\", () => {\\n198 | \\t\\t\\tconst inputTokens = 5000\\n----\\n203 | \\n204 | \\t\\tit(\\\"should handle only output tokens\\\", () => {\\n205 | \\t\\t\\tconst outputTokens = 15000\\n----\\n210 | \\n211 | \\t\\tit(\\\"should calculate cost with cache write tokens\\\", () => {\\n212 | \\t\\t\\tconst inputTokens = 10000\\n----\\n227 | \\n228 | \\t\\tit(\\\"should calculate cost with cache read tokens\\\", () => {\\n229 | \\t\\t\\tconst inputTokens = 10000 // Total logical input\\n----\\n\\n# src/core/condense/index.ts\\n 55 | \\tcost: number // The cost of the summarization operation\\n 56 | \\tnewContextTokens?: number // The number of tokens in the context for the next API request\\n 57 | \\terror?: string // Populated iff the operation fails: error message shown to the user on failure (see Task.ts)\\n----\\n 63 | * @param {ApiMessage[]} messages - The conversation messages\\n 64 | * @param {ApiHandler} apiHandler - The API handler to use for token counting.\\n 65 | * @param {string} systemPrompt - The system prompt for API requests, which should be considered in the context token count\\n 66 | * @param {string} taskId - The task ID for the conversation, used for telemetry\\n----\\n 73 | * @param {ApiMessage[]} messages - The conversation messages\\n 74 | * @param {ApiHandler} apiHandler - The API handler to use for token counting (fallback if condensingApiHandler not provided)\\n 75 | * @param {string} systemPrompt - The system prompt for API requests (fallback if customCondensingPrompt not provided)\\n 76 | * @param {string} taskId - The task ID for the conversation, used for telemetry\\n 77 | * @param {number} prevContextTokens - The number of tokens currently in the context, used to ensure we don't grow the context\\n 78 | * @param {boolean} isAutomaticTrigger - Whether the summarization is triggered automatically\\n----\\n186 | \\n187 | \\t// Count the tokens in the context for the next API request\\n188 | \\t// We only estimate the tokens in summaryMesage if outputTokens is 0, otherwise we use outputTokens\\n189 | \\tconst systemPromptMessage: ApiMessage = { role: \\\"user\\\", content: systemPrompt }\\n----\\n\\n# src/api/providers/__tests__/vertex.spec.ts\\n 49 | \\t\\t\\t// The test expects 4 chunks:\\n 50 | \\t\\t\\t// 1. Usage chunk with input tokens\\n 51 | \\t\\t\\t// 2. Text chunk with \\\"Gemini response part 1\\\"\\n 52 | \\t\\t\\t// 3. Text chunk with \\\" part 2\\\"\\n 53 | \\t\\t\\t// 4. Usage chunk with output tokens\\n 54 | \\n----\\n\\n# src/api/providers/__tests__/ollama.spec.ts\\n 28 | \\t\\t\\t\\t\\t\\t\\t\\tusage: {\\n 29 | \\t\\t\\t\\t\\t\\t\\t\\t\\tprompt_tokens: 10,\\n 30 | \\t\\t\\t\\t\\t\\t\\t\\t\\tcompletion_tokens: 5,\\n 31 | \\t\\t\\t\\t\\t\\t\\t\\t\\ttotal_tokens: 15,\\n 32 | \\t\\t\\t\\t\\t\\t\\t\\t},\\n----\\n 54 | \\t\\t\\t\\t\\t\\t\\t\\t\\tusage: {\\n 55 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\tprompt_tokens: 10,\\n 56 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\tcompletion_tokens: 5,\\n 57 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\ttotal_tokens: 15,\\n 58 | \\t\\t\\t\\t\\t\\t\\t\\t\\t},\\n----\\n\\n# src/core/environment/getEnvironmentDetails.ts\\n192 | \\n193 | \\t// Add context tokens information.\\n194 | \\tconst { contextTokens, totalCost } = getApiMetrics(cline.clineMessages)\\n----\\n\\n# src/api/providers/__tests__/openai.spec.ts\\n 29 | \\t\\t\\t\\t\\t\\t\\t\\tusage: {\\n 30 | \\t\\t\\t\\t\\t\\t\\t\\t\\tprompt_tokens: 10,\\n 31 | \\t\\t\\t\\t\\t\\t\\t\\t\\tcompletion_tokens: 5,\\n 32 | \\t\\t\\t\\t\\t\\t\\t\\t\\ttotal_tokens: 15,\\n 33 | \\t\\t\\t\\t\\t\\t\\t\\t},\\n----\\n 55 | \\t\\t\\t\\t\\t\\t\\t\\t\\tusage: {\\n 56 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\tprompt_tokens: 10,\\n 57 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\tcompletion_tokens: 5,\\n 58 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\ttotal_tokens: 15,\\n 59 | \\t\\t\\t\\t\\t\\t\\t\\t\\t},\\n----\\n\\n# src/api/providers/__tests__/anthropic.spec.ts\\n 19 | \\t\\t\\t\\t\\t\\tusage: {\\n 20 | \\t\\t\\t\\t\\t\\t\\tinput_tokens: 10,\\n 21 | \\t\\t\\t\\t\\t\\t\\toutput_tokens: 5,\\n 22 | \\t\\t\\t\\t\\t\\t},\\n----\\n 30 | \\t\\t\\t\\t\\t\\t\\t\\tusage: {\\n 31 | \\t\\t\\t\\t\\t\\t\\t\\t\\tinput_tokens: 100,\\n 32 | \\t\\t\\t\\t\\t\\t\\t\\t\\toutput_tokens: 50,\\n 33 | \\t\\t\\t\\t\\t\\t\\t\\t\\tcache_creation_input_tokens: 20,\\n 34 | \\t\\t\\t\\t\\t\\t\\t\\t\\tcache_read_input_tokens: 10,\\n 35 | \\t\\t\\t\\t\\t\\t\\t\\t},\\n----\\n105 | \\n106 | \\t\\tit(\\\"use apiKey for passing token if anthropicUseAuthToken is not set\\\", () => {\\n107 | \\t\\t\\tconst handlerWithCustomUrl = new AnthropicHandler({\\n----\\n112 | \\t\\t\\texpect(mockAnthropicConstructor.mock.calls[0]![0]!.apiKey).toEqual(\\\"test-api-key\\\")\\n113 | \\t\\t\\texpect(mockAnthropicConstructor.mock.calls[0]![0]!.authToken).toBeUndefined()\\n114 | \\t\\t})\\n115 | \\n116 | \\t\\tit(\\\"use apiKey for passing token if anthropicUseAuthToken is set but custom base URL is not given\\\", () => {\\n117 | \\t\\t\\tconst handlerWithCustomUrl = new AnthropicHandler({\\n----\\n123 | \\t\\t\\texpect(mockAnthropicConstructor.mock.calls[0]![0]!.apiKey).toEqual(\\\"test-api-key\\\")\\n124 | \\t\\t\\texpect(mockAnthropicConstructor.mock.calls[0]![0]!.authToken).toBeUndefined()\\n125 | \\t\\t})\\n126 | \\n127 | \\t\\tit(\\\"use authToken for passing token if both of anthropicBaseUrl and anthropicUseAuthToken are set\\\", () => {\\n128 | \\t\\t\\tconst customBaseUrl = \\\"https://custom.anthropic.com\\\"\\n----\\n135 | \\t\\t\\texpect(mockAnthropicConstructor).toHaveBeenCalledTimes(1)\\n136 | \\t\\t\\texpect(mockAnthropicConstructor.mock.calls[0]![0]!.authToken).toEqual(\\\"test-api-key\\\")\\n137 | \\t\\t\\texpect(mockAnthropicConstructor.mock.calls[0]![0]!.apiKey).toBeUndefined()\\n----\\n190 | \\t\\t\\t\\tmessages: [{ role: \\\"user\\\", content: \\\"Test prompt\\\" }],\\n191 | \\t\\t\\t\\tmax_tokens: 8192,\\n192 | \\t\\t\\t\\ttemperature: 0,\\n----\\n\\n# src/api/providers/__tests__/xai.test.ts\\n228 | \\t\\t\\t\\t\\t\\t\\t\\tusage: {\\n229 | \\t\\t\\t\\t\\t\\t\\t\\t\\tprompt_tokens: 10,\\n230 | \\t\\t\\t\\t\\t\\t\\t\\t\\tcompletion_tokens: 20,\\n231 | \\t\\t\\t\\t\\t\\t\\t\\t\\tcache_read_input_tokens: 5,\\n232 | \\t\\t\\t\\t\\t\\t\\t\\t\\tcache_creation_input_tokens: 15,\\n233 | \\t\\t\\t\\t\\t\\t\\t\\t},\\n----\\n284 | \\t\\t\\t\\tmodel: modelId,\\n285 | \\t\\t\\t\\tmax_tokens: modelInfo.maxTokens,\\n286 | \\t\\t\\t\\ttemperature: 0,\\n----\\n\\n# src/api/providers/__tests__/openrouter.spec.ts\\n132 | \\t\\t\\t\\t\\t\\tchoices: [{ delta: {} }],\\n133 | \\t\\t\\t\\t\\t\\tusage: { prompt_tokens: 10, completion_tokens: 20, cost: 0.001 },\\n134 | \\t\\t\\t\\t\\t}\\n----\\n162 | \\t\\t\\t\\texpect.objectContaining({\\n163 | \\t\\t\\t\\t\\tmax_tokens: 8192,\\n164 | \\t\\t\\t\\t\\tmessages: [\\n----\\n285 | \\t\\t\\t\\tmodel: mockOptions.openRouterModelId,\\n286 | \\t\\t\\t\\tmax_tokens: 8192,\\n287 | \\t\\t\\t\\tthinking: undefined,\\n----\\n\\n# src/api/providers/unbound.ts\\n 22 | interface UnboundUsage extends OpenAI.CompletionUsage {\\n 23 | \\tcache_creation_input_tokens?: number\\n 24 | \\tcache_read_input_tokens?: number\\n 25 | }\\n----\\n 73 | \\n 74 | \\t\\t// Required by Anthropic; other providers default to max tokens allowed.\\n 75 | \\t\\tlet maxTokens: number | undefined\\n----\\n 82 | \\t\\t\\tmodel: modelId.split(\\\"/\\\")[1],\\n 83 | \\t\\t\\tmax_tokens: maxTokens,\\n 84 | \\t\\t\\tmessages: openAiMessages,\\n----\\n111 | \\t\\t\\t\\t\\ttype: \\\"usage\\\",\\n112 | \\t\\t\\t\\t\\tinputTokens: usage.prompt_tokens || 0,\\n113 | \\t\\t\\t\\t\\toutputTokens: usage.completion_tokens || 0,\\n114 | \\t\\t\\t\\t}\\n115 | \\n116 | \\t\\t\\t\\t// Only add cache tokens if they exist.\\n117 | \\t\\t\\t\\tif (usage.cache_creation_input_tokens) {\\n118 | \\t\\t\\t\\t\\tusageData.cacheWriteTokens = usage.cache_creation_input_tokens\\n119 | \\t\\t\\t\\t}\\n120 | \\n121 | \\t\\t\\t\\tif (usage.cache_read_input_tokens) {\\n122 | \\t\\t\\t\\t\\tusageData.cacheReadTokens = usage.cache_read_input_tokens\\n123 | \\t\\t\\t\\t}\\n----\\n146 | \\t\\t\\tif (modelId.startsWith(\\\"anthropic/\\\")) {\\n147 | \\t\\t\\t\\trequestOptions.max_tokens = info.maxTokens\\n148 | \\t\\t\\t}\\n----\\n\\n# src/api/providers/vscode-lm.ts\\n128 | \\t\\t\\t\\tmaxInputTokens: 8192,\\n129 | \\t\\t\\t\\tsendRequest: async (_messages, _options, _token) => {\\n130 | \\t\\t\\t\\t\\t// Provide a minimal implementation\\n----\\n179 | \\t * Implements the ApiHandler countTokens interface method\\n180 | \\t * Provides token counting for Anthropic content blocks\\n181 | \\t *\\n182 | \\t * @param content The content blocks to count tokens for\\n183 | \\t * @returns A promise resolving to the token count\\n184 | \\t */\\n185 | \\toverride async countTokens(content: Array): Promise {\\n186 | \\t\\t// Convert Anthropic content blocks to a string for VSCode LM token counting\\n187 | \\t\\tlet textContent = \\\"\\\"\\n----\\n201 | \\t/**\\n202 | \\t * Private implementation of token counting used internally by VsCodeLmHandler\\n203 | \\t */\\n----\\n206 | \\t\\tif (!this.client) {\\n207 | \\t\\t\\tconsole.warn(\\\"Roo Code : No client available for token counting\\\")\\n208 | \\t\\t\\treturn 0\\n----\\n211 | \\t\\tif (!this.currentRequestCancellation) {\\n212 | \\t\\t\\tconsole.warn(\\\"Roo Code : No cancellation token available for token counting\\\")\\n213 | \\t\\t\\treturn 0\\n----\\n217 | \\t\\tif (!text) {\\n218 | \\t\\t\\tconsole.debug(\\\"Roo Code : Empty text provided for token counting\\\")\\n219 | \\t\\t\\treturn 0\\n----\\n223 | \\t\\t\\t// Handle different input types\\n224 | \\t\\t\\tlet tokenCount: number\\n225 | \\n226 | \\t\\t\\tif (typeof text === \\\"string\\\") {\\n227 | \\t\\t\\t\\ttokenCount = await this.client.countTokens(text, this.currentRequestCancellation.token)\\n228 | \\t\\t\\t} else if (text instanceof vscode.LanguageModelChatMessage) {\\n----\\n233 | \\t\\t\\t\\t}\\n234 | \\t\\t\\t\\ttokenCount = await this.client.countTokens(text, this.currentRequestCancellation.token)\\n235 | \\t\\t\\t} else {\\n236 | \\t\\t\\t\\tconsole.warn(\\\"Roo Code : Invalid input type for token counting\\\")\\n237 | \\t\\t\\t\\treturn 0\\n----\\n240 | \\t\\t\\t// Validate the result\\n241 | \\t\\t\\tif (typeof tokenCount !== \\\"number\\\") {\\n242 | \\t\\t\\t\\tconsole.warn(\\\"Roo Code : Non-numeric token count received:\\\", tokenCount)\\n243 | \\t\\t\\t\\treturn 0\\n----\\n245 | \\n246 | \\t\\t\\tif (tokenCount < 0) {\\n247 | \\t\\t\\t\\tconsole.warn(\\\"Roo Code : Negative token count received:\\\", tokenCount)\\n248 | \\t\\t\\t\\treturn 0\\n----\\n250 | \\n251 | \\t\\t\\treturn tokenCount\\n252 | \\t\\t} catch (error) {\\n----\\n278 | \\n279 | \\t\\treturn systemTokens + messageTokens.reduce((sum: number, tokens: number): number => sum + tokens, 0)\\n280 | \\t}\\n----\\n357 | \\n358 | \\t\\t// Initialize cancellation token for the request\\n359 | \\t\\tthis.currentRequestCancellation = new vscode.CancellationTokenSource()\\n360 | \\n361 | \\t\\t// Calculate input tokens before starting the stream\\n362 | \\t\\tconst totalInputTokens: number = await this.calculateTotalInputTokens(systemPrompt, vsCodeLmMessages)\\n363 | \\n364 | \\t\\t// Accumulate the text and count at the end of the stream to reduce token counting overhead.\\n365 | \\t\\tlet accumulatedText: string = \\\"\\\"\\n----\\n378 | \\t\\t\\t\\trequestOptions,\\n379 | \\t\\t\\t\\tthis.currentRequestCancellation.token,\\n380 | \\t\\t\\t)\\n----\\n446 | \\n447 | \\t\\t\\t// Count tokens in the accumulated text after stream completion\\n448 | \\t\\t\\tconst totalOutputTokens: number = await this.internalCountTokens(accumulatedText)\\n----\\n511 | \\t\\t\\tconst modelInfo: ModelInfo = {\\n512 | \\t\\t\\t\\tmaxTokens: -1, // Unlimited tokens by default\\n513 | \\t\\t\\t\\tcontextWindow:\\n----\\n548 | \\t\\t\\t\\t{},\\n549 | \\t\\t\\t\\tnew vscode.CancellationTokenSource().token,\\n550 | \\t\\t\\t)\\n----\\n\\n# src/core/condense/__tests__/index.test.ts\\n203 | \\n204 | \\t\\t// Check the cost and token counts\\n205 | \\t\\texpect(result.cost).toBe(0.05)\\n206 | \\t\\texpect(result.summary).toBe(\\\"This is a summary\\\")\\n207 | \\t\\texpect(result.newContextTokens).toBe(250) // 150 output tokens + 100 from countTokens\\n208 | \\t\\texpect(result.error).toBeUndefined()\\n----\\n317 | \\t\\t// Check the newContextTokens calculation includes system prompt\\n318 | \\t\\texpect(result.newContextTokens).toBe(300) // 200 output tokens + 100 from countTokens\\n319 | \\t\\texpect(result.cost).toBe(0.06)\\n----\\n323 | \\n324 | \\tit(\\\"should return error when new context tokens >= previous context tokens\\\", async () => {\\n325 | \\t\\tconst messages: ApiMessage[] = [\\n----\\n336 | \\t\\tconst streamWithLargeTokens = (async function* () {\\n337 | \\t\\t\\tyield { type: \\\"text\\\" as const, text: \\\"This is a very long summary that uses many tokens\\\" }\\n338 | \\t\\t\\tyield { type: \\\"usage\\\" as const, totalCost: 0.08, outputTokens: 500 }\\n----\\n364 | \\n365 | \\tit(\\\"should successfully summarize when new context tokens < previous context tokens\\\", async () => {\\n366 | \\t\\tconst messages: ApiMessage[] = [\\n----\\n375 | \\n376 | \\t\\t// Create a stream that produces a summary with reasonable token count\\n377 | \\t\\tconst streamWithSmallTokens = (async function* () {\\n----\\n401 | \\t\\texpect(result.error).toBeUndefined()\\n402 | \\t\\texpect(result.newContextTokens).toBe(80) // 50 output tokens + 30 from countTokens\\n403 | \\t\\texpect(result.newContextTokens).toBeLessThan(prevContextTokens)\\n----\\n\\n# src/api/providers/__tests__/lmstudio.test.ts\\n 25 | \\t\\t\\t\\t\\t\\t\\t\\tusage: {\\n 26 | \\t\\t\\t\\t\\t\\t\\t\\t\\tprompt_tokens: 10,\\n 27 | \\t\\t\\t\\t\\t\\t\\t\\t\\tcompletion_tokens: 5,\\n 28 | \\t\\t\\t\\t\\t\\t\\t\\t\\ttotal_tokens: 15,\\n 29 | \\t\\t\\t\\t\\t\\t\\t\\t},\\n----\\n 51 | \\t\\t\\t\\t\\t\\t\\t\\t\\tusage: {\\n 52 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\tprompt_tokens: 10,\\n 53 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\tcompletion_tokens: 5,\\n 54 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\ttotal_tokens: 15,\\n 55 | \\t\\t\\t\\t\\t\\t\\t\\t\\t},\\n----\\n\\n# src/api/providers/__tests__/bedrock-reasoning.test.ts\\n 94 | \\t\\t\\t\\ttype: \\\"enabled\\\",\\n 95 | \\t\\t\\t\\tbudget_tokens: 4096, // Uses the full modelMaxThinkingTokens value\\n 96 | \\t\\t\\t})\\n----\\n143 | \\t\\t\\t\\ttype: \\\"enabled\\\",\\n144 | \\t\\t\\t\\tbudget_tokens: 8192,\\n145 | \\t\\t\\t})\\n----\\n266 | \\t\\t\\t\\ttype: \\\"enabled\\\",\\n267 | \\t\\t\\t\\tbudget_tokens: 4096,\\n268 | \\t\\t\\t})\\n----\\n\\n# src/api/providers/lm-studio.ts\\n 40 | \\t\\t// -------------------------\\n 41 | \\t\\t// Track token usage\\n 42 | \\t\\t// -------------------------\\n----\\n 68 | \\t\\t} catch (err) {\\n 69 | \\t\\t\\tconsole.error(\\\"[LmStudio] Failed to count input tokens:\\\", err)\\n 70 | \\t\\t\\tinputTokens = 0\\n----\\n116 | \\t\\t\\t} catch (err) {\\n117 | \\t\\t\\t\\tconsole.error(\\\"[LmStudio] Failed to count output tokens:\\\", err)\\n118 | \\t\\t\\t\\toutputTokens = 0\\n----\\n\\n# src/utils/countTokens.ts\\n 4 | import { countTokensResultSchema } from \\\"../workers/types\\\"\\n 5 | import { tiktoken } from \\\"./tiktoken\\\"\\n 6 | \\n----\\n 27 | \\tif (!useWorker || !pool) {\\n 28 | \\t\\treturn tiktoken(content)\\n 29 | \\t}\\n----\\n 42 | \\t\\tconsole.error(error)\\n 43 | \\t\\treturn tiktoken(content)\\n 44 | \\t}\\n----\\n\\n# src/api/providers/__tests__/requesty.spec.ts\\n124 | \\t\\t\\t\\t\\t\\tusage: {\\n125 | \\t\\t\\t\\t\\t\\t\\tprompt_tokens: 10,\\n126 | \\t\\t\\t\\t\\t\\t\\tcompletion_tokens: 20,\\n127 | \\t\\t\\t\\t\\t\\t\\tprompt_tokens_details: {\\n128 | \\t\\t\\t\\t\\t\\t\\t\\tcaching_tokens: 5,\\n129 | \\t\\t\\t\\t\\t\\t\\t\\tcached_tokens: 2,\\n130 | \\t\\t\\t\\t\\t\\t\\t},\\n----\\n162 | \\t\\t\\t\\texpect.objectContaining({\\n163 | \\t\\t\\t\\t\\tmax_tokens: 8192,\\n164 | \\t\\t\\t\\t\\tmessages: [\\n----\\n204 | \\t\\t\\t\\tmodel: mockOptions.requestyModelId,\\n205 | \\t\\t\\t\\tmax_tokens: 8192,\\n206 | \\t\\t\\t\\tmessages: [{ role: \\\"system\\\", content: \\\"test prompt\\\" }],\\n----\\n\\n# src/api/providers/__tests__/vscode-lm.test.ts\\n 28 | \\t\\tCancellationTokenSource: jest.fn(() => ({\\n 29 | \\t\\t\\ttoken: {\\n 30 | \\t\\t\\t\\tisCancellationRequested: false,\\n----\\n\\n# src/api/providers/__tests__/chutes.spec.ts\\n 49 | \\t\\t\\t\\t\\tusage: {\\n 50 | \\t\\t\\t\\t\\t\\tprompt_tokens: 10,\\n 51 | \\t\\t\\t\\t\\t\\tcompletion_tokens: 5,\\n 52 | \\t\\t\\t\\t\\t\\ttotal_tokens: 15,\\n 53 | \\t\\t\\t\\t\\t},\\n----\\n103 | \\t\\t\\t\\t\\t],\\n104 | \\t\\t\\t\\t\\tusage: { prompt_tokens: 10, completion_tokens: 5 },\\n105 | \\t\\t\\t\\t}\\n----\\n211 | \\t\\t\\t\\t\\t\\t\\tdone: false,\\n212 | \\t\\t\\t\\t\\t\\t\\tvalue: { choices: [{ delta: {} }], usage: { prompt_tokens: 10, completion_tokens: 20 } },\\n213 | \\t\\t\\t\\t\\t\\t})\\n----\\n284 | \\t\\t\\t\\tmodel: modelId,\\n285 | \\t\\t\\t\\tmax_tokens: modelInfo.maxTokens,\\n286 | \\t\\t\\t\\ttemperature: 0.5,\\n----\\n\\n# src/api/providers/__tests__/openai-native.spec.ts\\n 29 | \\t\\t\\t\\t\\t\\t\\t\\tusage: {\\n 30 | \\t\\t\\t\\t\\t\\t\\t\\t\\tprompt_tokens: 10,\\n 31 | \\t\\t\\t\\t\\t\\t\\t\\t\\tcompletion_tokens: 5,\\n 32 | \\t\\t\\t\\t\\t\\t\\t\\t\\ttotal_tokens: 15,\\n 33 | \\t\\t\\t\\t\\t\\t\\t\\t},\\n----\\n 55 | \\t\\t\\t\\t\\t\\t\\t\\t\\tusage: {\\n 56 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\tprompt_tokens: 10,\\n 57 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\tcompletion_tokens: 5,\\n 58 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\ttotal_tokens: 15,\\n 59 | \\t\\t\\t\\t\\t\\t\\t\\t\\t},\\n----\\n145 | \\t\\t\\t\\t\\t\\tusage: {\\n146 | \\t\\t\\t\\t\\t\\t\\tprompt_tokens: 0,\\n147 | \\t\\t\\t\\t\\t\\t\\tcompletion_tokens: 0,\\n148 | \\t\\t\\t\\t\\t\\t\\ttotal_tokens: 0,\\n149 | \\t\\t\\t\\t\\t\\t},\\n----\\n215 | \\t\\t\\t\\t{ choices: [{ delta: { content: \\\" there\\\" } }], usage: null },\\n216 | \\t\\t\\t\\t{ choices: [{ delta: { content: \\\"!\\\" } }], usage: { prompt_tokens: 10, completion_tokens: 5 } },\\n217 | \\t\\t\\t]\\n----\\n261 | \\t\\t\\t\\t{ choices: [{ delta: { content: null } }], usage: null },\\n262 | \\t\\t\\t\\t{ choices: [{ delta: { content: \\\"Hello\\\" } }], usage: { prompt_tokens: 10, completion_tokens: 5 } },\\n263 | \\t\\t\\t]\\n----\\n\\n# src/api/providers/__tests__/deepseek.test.ts\\n 28 | \\t\\t\\t\\t\\t\\t\\t\\tusage: {\\n 29 | \\t\\t\\t\\t\\t\\t\\t\\t\\tprompt_tokens: 10,\\n 30 | \\t\\t\\t\\t\\t\\t\\t\\t\\tcompletion_tokens: 5,\\n 31 | \\t\\t\\t\\t\\t\\t\\t\\t\\ttotal_tokens: 15,\\n 32 | \\t\\t\\t\\t\\t\\t\\t\\t\\tprompt_tokens_details: {\\n 33 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\tcache_miss_tokens: 8,\\n 34 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\tcached_tokens: 2,\\n 35 | \\t\\t\\t\\t\\t\\t\\t\\t\\t},\\n----\\n 59 | \\t\\t\\t\\t\\t\\t\\t\\t\\tusage: {\\n 60 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\tprompt_tokens: 10,\\n 61 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\tcompletion_tokens: 5,\\n 62 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\ttotal_tokens: 15,\\n 63 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\tprompt_tokens_details: {\\n 64 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\t\\tcache_miss_tokens: 8,\\n 65 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\t\\tcached_tokens: 2,\\n 66 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\t},\\n----\\n261 | \\t\\t\\tconst usage = {\\n262 | \\t\\t\\t\\tprompt_tokens: 100,\\n263 | \\t\\t\\t\\tcompletion_tokens: 50,\\n264 | \\t\\t\\t\\ttotal_tokens: 150,\\n265 | \\t\\t\\t\\tprompt_tokens_details: {\\n266 | \\t\\t\\t\\t\\tcache_miss_tokens: 80,\\n267 | \\t\\t\\t\\t\\tcached_tokens: 20,\\n268 | \\t\\t\\t\\t},\\n----\\n289 | \\t\\t\\tconst usage = {\\n290 | \\t\\t\\t\\tprompt_tokens: 100,\\n291 | \\t\\t\\t\\tcompletion_tokens: 50,\\n292 | \\t\\t\\t\\ttotal_tokens: 150,\\n293 | \\t\\t\\t\\t// No prompt_tokens_details\\n294 | \\t\\t\\t}\\n----\\n\\n# src/api/transform/cache-strategy/multi-point-strategy.ts\\n 7 | * Strategy for handling multiple cache points.\\n 8 | * Creates cache points after messages as soon as uncached tokens exceed minimumTokenCount.\\n 9 | */\\n----\\n 58 | \\t *\\n 59 | \\t * @param minTokensPerPoint Minimum tokens required per cache point\\n 60 | \\t * @param remainingCachePoints Number of cache points available\\n----\\n 97 | \\n 98 | \\t\\t// Calculate tokens in new messages (added since last cache point placement)\\n 99 | \\t\\tconst lastPreviousIndex = previousPlacements[previousPlacements.length - 1].index\\n----\\n103 | \\n104 | \\t\\t// If new messages have enough tokens for a cache point, we need to decide\\n105 | \\t\\t// whether to keep all previous cache points or combine some\\n----\\n127 | \\t\\t\\t\\t// We need to decide which previous cache points to keep and which to combine\\n128 | \\t\\t\\t\\t// Strategy: Compare the token count of new messages with the smallest combined token gap\\n129 | \\n130 | \\t\\t\\t\\t// First, analyze the token distribution between previous cache points\\n131 | \\t\\t\\t\\tconst tokensBetweenPlacements: number[] = []\\n132 | \\t\\t\\t\\tlet startIdx = 0\\n----\\n134 | \\t\\t\\t\\tfor (const placement of previousPlacements) {\\n135 | \\t\\t\\t\\t\\tconst tokens = this.config.messages\\n136 | \\t\\t\\t\\t\\t\\t.slice(startIdx, placement.index + 1)\\n----\\n138 | \\n139 | \\t\\t\\t\\t\\ttokensBetweenPlacements.push(tokens)\\n140 | \\t\\t\\t\\t\\tstartIdx = placement.index + 1\\n----\\n142 | \\n143 | \\t\\t\\t\\t// Find the two consecutive placements with the smallest token gap\\n144 | \\t\\t\\t\\tlet smallestGapIndex = 0\\n----\\n146 | \\n147 | \\t\\t\\t\\tfor (let i = 0; i < tokensBetweenPlacements.length - 1; i++) {\\n148 | \\t\\t\\t\\t\\tconst gap = tokensBetweenPlacements[i] + tokensBetweenPlacements[i + 1]\\n149 | \\t\\t\\t\\t\\tif (gap < smallestGap) {\\n----\\n155 | \\t\\t\\t\\t// Only combine cache points if it's beneficial\\n156 | \\t\\t\\t\\t// Compare the token count of new messages with the smallest combined token gap\\n157 | \\t\\t\\t\\t// Apply a required percentage increase to ensure reallocation is worth it\\n----\\n161 | \\t\\t\\t\\tif (newMessagesTokens >= requiredTokenThreshold) {\\n162 | \\t\\t\\t\\t\\t// It's beneficial to combine cache points since new messages have significantly more tokens\\n163 | \\t\\t\\t\\t\\tlogger.info(\\\"Combining cache points is beneficial\\\", {\\n----\\n231 | \\t\\t} else {\\n232 | \\t\\t\\t// New messages don't have enough tokens for a cache point\\n233 | \\t\\t\\t// Keep all previous placements that are still valid\\n----\\n266 | \\t\\tif (lastUserMessageIndex >= 0) {\\n267 | \\t\\t\\t// Calculate the total tokens covered from the previous cache point (or start of conversation)\\n268 | \\t\\t\\t// to this cache point. This ensures tokensCovered represents the full span of tokens\\n269 | \\t\\t\\t// that will be cached by this cache point.\\n----\\n281 | \\n282 | \\t\\t\\t// Calculate tokens from previous cache point (or start) to this cache point\\n283 | \\t\\t\\tconst tokenStartIndex = previousCachePointIndex + 1\\n284 | \\t\\t\\ttotalTokensCovered = this.config.messages\\n285 | \\t\\t\\t\\t.slice(tokenStartIndex, lastUserMessageIndex + 1)\\n286 | \\t\\t\\t\\t.reduce((acc, curr) => acc + this.estimateTokenCount(curr), 0)\\n287 | \\n288 | \\t\\t\\t// Guard clause: ensure we have enough tokens to justify a cache point\\n289 | \\t\\t\\tif (totalTokensCovered < minTokensPerPoint) {\\n----\\n294 | \\t\\t\\t\\ttype: \\\"message\\\",\\n295 | \\t\\t\\t\\ttokensCovered: totalTokensCovered,\\n296 | \\t\\t\\t}\\n----\\n\\n# src/api/transform/model-params.ts\\n 95 | \\n 96 | \\t\\t// Reasoning cannot be less than 1024 tokens.\\n 97 | \\t\\tif (reasoningBudget < 1024) {\\n----\\n\\n# src/api/transform/cache-strategy/types.ts\\n 7 | export interface ModelInfo {\\n 8 | \\t/** Maximum number of tokens the model can generate */\\n 9 | \\tmaxTokens: number\\n 10 | \\t/** Maximum context window size in tokens */\\n 11 | \\tcontextWindow: number\\n----\\n 15 | \\tmaxCachePoints: number\\n 16 | \\t/** Minimum number of tokens required for a cache point */\\n 17 | \\tminTokensPerCachePoint: number\\n----\\n 49 | \\ttype: \\\"system\\\" | \\\"message\\\"\\n 50 | \\t/** Number of tokens this cache point covers */\\n 51 | \\ttokensCovered: number\\n 52 | }\\n----\\n\\n# src/api/transform/__tests__/reasoning.spec.ts\\n 40 | \\n 41 | \\t\\t\\texpect(result).toEqual({ max_tokens: 1000 })\\n 42 | \\t\\t})\\n----\\n 61 | \\n 62 | \\t\\t\\texpect(result).toEqual({ max_tokens: 1000 })\\n 63 | \\t\\t})\\n----\\n123 | \\n124 | \\t\\t\\texpect(result).toEqual({ max_tokens: 1000 })\\n125 | \\t\\t})\\n----\\n140 | \\n141 | \\t\\t\\texpect(result).toEqual({ max_tokens: undefined })\\n142 | \\t\\t})\\n----\\n189 | \\n190 | \\t\\t\\texpect(result).toEqual({ max_tokens: 0 })\\n191 | \\t\\t})\\n----\\n244 | \\t\\t\\t\\ttype: \\\"enabled\\\",\\n245 | \\t\\t\\t\\tbudget_tokens: 1000,\\n246 | \\t\\t\\t})\\n----\\n268 | \\t\\t\\t\\ttype: \\\"enabled\\\",\\n269 | \\t\\t\\t\\tbudget_tokens: 1000,\\n270 | \\t\\t\\t})\\n----\\n314 | \\t\\t\\t\\ttype: \\\"enabled\\\",\\n315 | \\t\\t\\t\\tbudget_tokens: undefined,\\n316 | \\t\\t\\t})\\n----\\n334 | \\t\\t\\t\\ttype: \\\"enabled\\\",\\n335 | \\t\\t\\t\\tbudget_tokens: 0,\\n336 | \\t\\t\\t})\\n----\\n354 | \\t\\t\\t\\ttype: \\\"enabled\\\",\\n355 | \\t\\t\\t\\tbudget_tokens: 100000,\\n356 | \\t\\t\\t})\\n----\\n374 | \\t\\t\\t\\ttype: \\\"enabled\\\",\\n375 | \\t\\t\\t\\tbudget_tokens: 1000,\\n376 | \\t\\t\\t})\\n----\\n547 | \\n548 | \\t\\t\\texpect(openRouterResult).toEqual({ max_tokens: 1000 })\\n549 | \\t\\t\\texpect(anthropicResult).toEqual({ type: \\\"enabled\\\", budget_tokens: 1000 })\\n550 | \\t\\t\\texpect(openAiResult).toBeUndefined()\\n----\\n601 | \\t\\t\\t// Budget should take precedence for OpenRouter and Anthropic\\n602 | \\t\\t\\texpect(openRouterResult).toEqual({ max_tokens: 1000 })\\n603 | \\t\\t\\texpect(anthropicResult).toEqual({ type: \\\"enabled\\\", budget_tokens: 1000 })\\n604 | \\t\\t\\t// OpenAI should still use effort since it doesn't support budget\\n----\\n672 | \\t\\t\\t\\texpect(typeof result).toBe(\\\"object\\\")\\n673 | \\t\\t\\t\\texpect(\\\"max_tokens\\\" in result || \\\"effort\\\" in result || \\\"exclude\\\" in result).toBe(true)\\n674 | \\t\\t\\t}\\n----\\n688 | \\t\\t\\t\\texpect(result).toHaveProperty(\\\"type\\\", \\\"enabled\\\")\\n689 | \\t\\t\\t\\texpect(result).toHaveProperty(\\\"budget_tokens\\\")\\n690 | \\t\\t\\t}\\n----\\n\\n# src/api/transform/__tests__/model-params.spec.ts\\n205 | \\t\\t\\t\\t\\ttype: \\\"enabled\\\",\\n206 | \\t\\t\\t\\t\\tbudget_tokens: 1600,\\n207 | \\t\\t\\t\\t},\\n----\\n226 | \\t\\t\\t\\ttype: \\\"enabled\\\",\\n227 | \\t\\t\\t\\tbudget_tokens: 1600,\\n228 | \\t\\t\\t})\\n----\\n262 | \\t\\t\\t\\t\\ttype: \\\"enabled\\\",\\n263 | \\t\\t\\t\\t\\tbudget_tokens: 2400,\\n264 | \\t\\t\\t\\t},\\n----\\n287 | \\t\\t\\t\\t\\ttype: \\\"enabled\\\",\\n288 | \\t\\t\\t\\t\\tbudget_tokens: 1500,\\n289 | \\t\\t\\t\\t},\\n----\\n308 | \\n309 | \\t\\tit(\\\"should clamp thinking budget to at least 1024 tokens\\\", () => {\\n310 | \\t\\t\\tconst model: ModelInfo = {\\n----\\n328 | \\t\\t\\t\\t\\ttype: \\\"enabled\\\",\\n329 | \\t\\t\\t\\t\\tbudget_tokens: 1024,\\n330 | \\t\\t\\t\\t},\\n----\\n333 | \\n334 | \\t\\tit(\\\"should clamp thinking budget to at most 80% of max tokens\\\", () => {\\n335 | \\t\\t\\tconst model: ModelInfo = {\\n----\\n353 | \\t\\t\\t\\t\\ttype: \\\"enabled\\\",\\n354 | \\t\\t\\t\\t\\tbudget_tokens: 3200,\\n355 | \\t\\t\\t\\t},\\n----\\n372 | \\t\\t\\t\\t\\ttype: \\\"enabled\\\",\\n373 | \\t\\t\\t\\t\\tbudget_tokens: DEFAULT_HYBRID_REASONING_MODEL_THINKING_TOKENS,\\n374 | \\t\\t\\t\\t},\\n----\\n390 | \\t\\t\\texpect(result.maxTokens).toBe(5000)\\n391 | \\t\\t\\texpect(result.reasoningBudget).toBe(2000) // Custom thinking tokens takes precedence\\n392 | \\t\\t})\\n393 | \\n394 | \\t\\tit(\\\"should clamp custom thinking tokens even when custom max tokens is provided\\\", () => {\\n395 | \\t\\t\\tconst model: ModelInfo = {\\n----\\n719 | \\n720 | \\t\\t\\texpect(result.reasoning).toEqual({ max_tokens: 3200 })\\n721 | \\t\\t})\\n----\\n\\n# src/api/transform/cache-strategy/base-strategy.ts\\n 27 | \\t/**\\n 28 | \\t * Calculate token count for system prompt using a more accurate approach\\n 29 | \\t */\\n----\\n 33 | \\n 34 | \\t\\t\\t// Use a more accurate token estimation than simple character count\\n 35 | \\t\\t\\t// Count words and add overhead for punctuation and special tokens\\n 36 | \\t\\t\\tconst words = text.split(/\\\\s+/).filter((word) => word.length > 0)\\n 37 | \\t\\t\\t// Average English word is ~1.3 tokens\\n 38 | \\t\\t\\tlet tokenCount = words.length * 1.3\\n 39 | \\t\\t\\t// Add overhead for punctuation and special characters\\n 40 | \\t\\t\\ttokenCount += (text.match(/[.,!?;:()[\\\\]{}\\\"\\\"''`]/g) || []).length * 0.3\\n 41 | \\t\\t\\t// Add overhead for newlines\\n 42 | \\t\\t\\ttokenCount += (text.match(/\\\\n/g) || []).length * 0.5\\n 43 | \\t\\t\\t// Add a small overhead for system prompt structure\\n 44 | \\t\\t\\ttokenCount += 5\\n 45 | \\n 46 | \\t\\t\\tthis.systemTokenCount = Math.ceil(tokenCount)\\n 47 | \\t\\t}\\n----\\n 84 | \\t/**\\n 85 | \\t * Check if a token count meets the minimum threshold for caching\\n 86 | \\t */\\n 87 | \\tprotected meetsMinTokenThreshold(tokenCount: number): boolean {\\n 88 | \\t\\tconst minTokens = this.config.modelInfo.minTokensPerCachePoint\\n----\\n 91 | \\t\\t}\\n 92 | \\t\\treturn tokenCount >= minTokens\\n 93 | \\t}\\n----\\n 95 | \\t/**\\n 96 | \\t * Estimate token count for a message using a more accurate approach\\n 97 | \\t * This implementation is based on the BaseProvider's countTokens method\\n----\\n100 | \\tprotected estimateTokenCount(message: Anthropic.Messages.MessageParam): number {\\n101 | \\t\\t// Use a more sophisticated token counting approach\\n102 | \\t\\tif (!message.content) return 0\\n----\\n108 | \\t\\t\\t\\tif (block.type === \\\"text\\\") {\\n109 | \\t\\t\\t\\t\\t// Use a more accurate token estimation than simple character count\\n110 | \\t\\t\\t\\t\\t// This is still an approximation but better than character/4\\n----\\n112 | \\t\\t\\t\\t\\tif (text.length > 0) {\\n113 | \\t\\t\\t\\t\\t\\t// Count words and add overhead for punctuation and special tokens\\n114 | \\t\\t\\t\\t\\t\\tconst words = text.split(/\\\\s+/).filter((word) => word.length > 0)\\n115 | \\t\\t\\t\\t\\t\\t// Average English word is ~1.3 tokens\\n116 | \\t\\t\\t\\t\\t\\ttotalTokens += words.length * 1.3\\n----\\n128 | \\t\\t\\tconst text = message.content\\n129 | \\t\\t\\t// Count words and add overhead for punctuation and special tokens\\n130 | \\t\\t\\tconst words = text.split(/\\\\s+/).filter((word) => word.length > 0)\\n131 | \\t\\t\\t// Average English word is ~1.3 tokens\\n132 | \\t\\t\\ttotalTokens += words.length * 1.3\\n----\\n\\n# src/api/providers/__tests__/glama.spec.ts\\n 57 | \\t\\t\\t\\t\\t\\t\\t\\t\\tchoices: [{ delta: {}, index: 0 }],\\n 58 | \\t\\t\\t\\t\\t\\t\\t\\t\\tusage: { prompt_tokens: 10, completion_tokens: 5, total_tokens: 15 },\\n 59 | \\t\\t\\t\\t\\t\\t\\t\\t}\\n----\\n112 | \\t\\t\\tusage: {\\n113 | \\t\\t\\t\\tprompt_tokens: 10,\\n114 | \\t\\t\\t\\tcompletion_tokens: 5,\\n115 | \\t\\t\\t\\ttotal_tokens: 15,\\n116 | \\t\\t\\t},\\n----\\n176 | \\t\\t\\t\\t\\ttemperature: 0,\\n177 | \\t\\t\\t\\t\\tmax_tokens: 8192,\\n178 | \\t\\t\\t\\t}),\\n----\\n194 | \\n195 | \\t\\tit(\\\"should not set max_tokens for non-Anthropic models\\\", async () => {\\n196 | \\t\\t\\t// Reset mock to clear any previous calls\\n----\\n213 | \\t\\t\\t)\\n214 | \\t\\t\\texpect(mockCreate.mock.calls[0][0]).not.toHaveProperty(\\\"max_tokens\\\")\\n215 | \\t\\t})\\n----\\n\\n# src/api/transform/__tests__/stream.spec.ts\\n 33 | \\n 34 | \\t\\tit(\\\"should handle usage chunks without cache tokens\\\", () => {\\n 35 | \\t\\t\\tconst usageChunk: ApiStreamChunk = {\\n----\\n 57 | \\n 58 | \\t\\tit(\\\"should handle usage chunks with zero tokens\\\", () => {\\n 59 | \\t\\t\\tconst zeroUsageChunk: ApiStreamChunk = {\\n----\\n 69 | \\n 70 | \\t\\tit(\\\"should handle usage chunks with large token counts\\\", () => {\\n 71 | \\t\\t\\tconst largeUsageChunk: ApiStreamChunk = {\\n----\\n\\n# src/api/transform/cache-strategy/__tests__/cache-strategy.spec.ts\\n 29 | \\n 30 | const createMessageWithTokens = (role: \\\"user\\\" | \\\"assistant\\\", tokenCount: number) => ({\\n 31 | \\trole,\\n 32 | \\tcontent: \\\"x\\\".repeat(tokenCount * 4), // Approximate 4 chars per token\\n 33 | })\\n----\\n130 | \\t\\t\\t\\tit(\\\"adds system cache block when prompt caching is enabled, messages exist, and system prompt is long enough\\\", () => {\\n131 | \\t\\t\\t\\t\\t// Create a system prompt that's at least 50 tokens (200+ characters)\\n132 | \\t\\t\\t\\t\\tconst longSystemPrompt =\\n----\\n236 | \\t\\t\\t\\t\\t\\t\\t\\t(i + 1) +\\n237 | \\t\\t\\t\\t\\t\\t\\t\\t\\\" with some additional text to increase token count. \\\" +\\n238 | \\t\\t\\t\\t\\t\\t\\t\\t\\\"Adding more text to ensure we exceed the token threshold for cache block insertion.\\\",\\n239 | \\t\\t\\t\\t\\t\\t}))\\n----\\n579 | \\n580 | \\t\\tit(\\\"should yield usage results with cache tokens when using MultiPointStrategy\\\", async () => {\\n581 | \\t\\t\\t// Mock the convertToBedrockConverseMessages method to return a result with cache points\\n----\\n589 | \\n590 | \\t\\t\\t// Create a mock stream that includes cache token fields\\n591 | \\t\\t\\tconst mockApiResponse = {\\n----\\n626 | \\n627 | \\t\\t\\t// Verify that usage results with cache tokens are yielded\\n628 | \\t\\t\\texpect(chunks.length).toBeGreaterThan(0)\\n629 | \\t\\t\\t// The test already expects cache tokens, but the implementation might not be including them\\n630 | \\t\\t\\t// Let's make the test more flexible to accept either format\\n----\\n652 | \\n653 | \\t\\t// Helper function to create a message with approximate token count\\n654 | \\t\\tconst createMessage = (role: \\\"user\\\" | \\\"assistant\\\", content: string, tokenCount: number) => {\\n655 | \\t\\t\\t// Pad the content to reach the desired token count (approx 4 chars per token)\\n656 | \\t\\t\\tconst paddingNeeded = Math.max(0, tokenCount * 4 - content.length)\\n657 | \\t\\t\\tconst padding = \\\" \\\".repeat(paddingNeeded)\\n----\\n667 | \\t\\t\\t\\t\\\"Cache point placements:\\\",\\n668 | \\t\\t\\t\\tplacements.map((p) => `index: ${p.index}, tokens: ${p.tokensCovered}`),\\n669 | \\t\\t\\t)\\n----\\n683 | \\t\\t\\t\\t\\tmodelInfo: multiPointModelInfo,\\n684 | \\t\\t\\t\\t\\tsystemPrompt: \\\"You are a helpful assistant.\\\", // ~10 tokens\\n685 | \\t\\t\\t\\t\\tmessages,\\n----\\n730 | \\t\\t\\t\\t\\t\\ttype: \\\"message\\\",\\n731 | \\t\\t\\t\\t\\t\\ttokensCovered: 300,\\n732 | \\t\\t\\t\\t\\t},\\n----\\n736 | \\t\\t\\t\\t\\tmodelInfo: multiPointModelInfo,\\n737 | \\t\\t\\t\\t\\tsystemPrompt: \\\"You are a helpful assistant.\\\", // ~10 tokens\\n738 | \\t\\t\\t\\t\\tmessages,\\n----\\n759 | \\n760 | \\t\\t\\t\\t// Check if we have a second cache point (may not always be added depending on token distribution)\\n761 | \\t\\t\\t\\tif (result.messageCachePointPlacements && result.messageCachePointPlacements.length > 1) {\\n----\\n789 | \\t\\t\\t\\t\\t\\ttype: \\\"message\\\",\\n790 | \\t\\t\\t\\t\\t\\ttokensCovered: 300,\\n791 | \\t\\t\\t\\t\\t},\\n----\\n794 | \\t\\t\\t\\t\\t\\ttype: \\\"message\\\",\\n795 | \\t\\t\\t\\t\\t\\ttokensCovered: 300,\\n796 | \\t\\t\\t\\t\\t},\\n----\\n800 | \\t\\t\\t\\t\\tmodelInfo: multiPointModelInfo,\\n801 | \\t\\t\\t\\t\\tsystemPrompt: \\\"You are a helpful assistant.\\\", // ~10 tokens\\n802 | \\t\\t\\t\\t\\tmessages,\\n----\\n863 | \\t\\t\\t\\t\\t\\ttype: \\\"message\\\",\\n864 | \\t\\t\\t\\t\\t\\ttokensCovered: 300,\\n865 | \\t\\t\\t\\t\\t},\\n----\\n868 | \\t\\t\\t\\t\\t\\ttype: \\\"message\\\",\\n869 | \\t\\t\\t\\t\\t\\ttokensCovered: 300,\\n870 | \\t\\t\\t\\t\\t},\\n----\\n873 | \\t\\t\\t\\t\\t\\ttype: \\\"message\\\",\\n874 | \\t\\t\\t\\t\\t\\ttokensCovered: 300,\\n875 | \\t\\t\\t\\t\\t},\\n----\\n879 | \\t\\t\\t\\t\\tmodelInfo: multiPointModelInfo,\\n880 | \\t\\t\\t\\t\\tsystemPrompt: \\\"You are a helpful assistant.\\\", // ~10 tokens\\n881 | \\t\\t\\t\\t\\tmessages,\\n----\\n929 | \\t\\t\\t\\t// This test verifies that the documentation for Example 5 is correct\\n930 | \\t\\t\\t\\t// In Example 5, the third cache point at index 10 should cover 660 tokens\\n931 | \\t\\t\\t\\t// (260 tokens from messages 7-8 plus 400 tokens from the new messages)\\n932 | \\n----\\n944 | \\t\\t\\t\\t\\tcreateMessage(\\\"assistant\\\", \\\"Deep learning has many applications including...\\\", 160),\\n945 | \\t\\t\\t\\t\\t// New messages with 400 tokens total\\n946 | \\t\\t\\t\\t\\tcreateMessage(\\\"user\\\", \\\"Can you provide a detailed example?\\\", 100),\\n----\\n954 | \\t\\t\\t\\t\\t\\ttype: \\\"message\\\",\\n955 | \\t\\t\\t\\t\\t\\ttokensCovered: 240,\\n956 | \\t\\t\\t\\t\\t},\\n----\\n959 | \\t\\t\\t\\t\\t\\ttype: \\\"message\\\",\\n960 | \\t\\t\\t\\t\\t\\ttokensCovered: 440,\\n961 | \\t\\t\\t\\t\\t},\\n----\\n964 | \\t\\t\\t\\t\\t\\ttype: \\\"message\\\",\\n965 | \\t\\t\\t\\t\\t\\ttokensCovered: 260,\\n966 | \\t\\t\\t\\t\\t},\\n----\\n969 | \\t\\t\\t\\t// In the documentation, the algorithm decides to replace the cache point at index 8\\n970 | \\t\\t\\t\\t// with a new one at index 10, and the tokensCovered value should be 660 tokens\\n971 | \\t\\t\\t\\t// (260 tokens from messages 7-8 plus 400 tokens from the new messages)\\n972 | \\n973 | \\t\\t\\t\\t// However, the actual implementation may behave differently depending on how\\n974 | \\t\\t\\t\\t// it calculates token counts and makes decisions about cache point placement\\n975 | \\n976 | \\t\\t\\t\\t// The important part is that our fix ensures that when a cache point is created,\\n977 | \\t\\t\\t\\t// the tokensCovered value represents all tokens from the previous cache point\\n978 | \\t\\t\\t\\t// to the current cache point, not just the tokens in the new messages\\n979 | \\t\\t\\t})\\n980 | \\n981 | \\t\\t\\tit(\\\"should not combine cache points when new messages have fewer tokens than the smallest combined gap\\\", () => {\\n982 | \\t\\t\\t\\t// This test verifies that when new messages have fewer tokens than the smallest combined gap,\\n983 | \\t\\t\\t\\t// the algorithm keeps all existing cache points and doesn't add a new one\\n----\\n1004 | \\t\\t\\t\\t\\t\\t),\\n1005 | \\t\\t\\t\\t\\t\\t// Small addition (only 50 tokens total)\\n1006 | \\t\\t\\t\\t\\t\\tcreateMessage(\\\"user\\\", \\\"Thanks for the explanation.\\\", 20),\\n----\\n1009 | \\n1010 | \\t\\t\\t\\t\\t// Previous cache point placements with significant token coverage\\n1011 | \\t\\t\\t\\t\\tconst previousCachePointPlacements: CachePointPlacement[] = [\\n----\\n1014 | \\t\\t\\t\\t\\t\\t\\ttype: \\\"message\\\",\\n1015 | \\t\\t\\t\\t\\t\\t\\ttokensCovered: 400, // Significant token coverage\\n1016 | \\t\\t\\t\\t\\t\\t},\\n----\\n1019 | \\t\\t\\t\\t\\t\\t\\ttype: \\\"message\\\",\\n1020 | \\t\\t\\t\\t\\t\\t\\ttokensCovered: 300, // Significant token coverage\\n1021 | \\t\\t\\t\\t\\t\\t},\\n----\\n1024 | \\t\\t\\t\\t\\t\\t\\ttype: \\\"message\\\",\\n1025 | \\t\\t\\t\\t\\t\\t\\ttokensCovered: 300, // Significant token coverage\\n1026 | \\t\\t\\t\\t\\t\\t},\\n----\\n1030 | \\t\\t\\t\\t\\t\\tmodelInfo: multiPointModelInfo,\\n1031 | \\t\\t\\t\\t\\t\\tsystemPrompt: \\\"You are a helpful assistant.\\\", // ~10 tokens\\n1032 | \\t\\t\\t\\t\\t\\tmessages,\\n----\\n1057 | \\n1058 | \\t\\t\\tit(\\\"should make correct decisions based on token counts\\\", () => {\\n1059 | \\t\\t\\t\\t// This test verifies that the algorithm correctly compares token counts\\n1060 | \\t\\t\\t\\t// and makes the right decision about combining cache points\\n1061 | \\n1062 | \\t\\t\\t\\t// Create messages with a variety of token counts\\n1063 | \\t\\t\\t\\tconst messages = [\\n----\\n1081 | \\t\\t\\t\\t\\t\\ttype: \\\"message\\\",\\n1082 | \\t\\t\\t\\t\\t\\ttokensCovered: 400,\\n1083 | \\t\\t\\t\\t\\t},\\n----\\n1086 | \\t\\t\\t\\t\\t\\ttype: \\\"message\\\",\\n1087 | \\t\\t\\t\\t\\t\\ttokensCovered: 150,\\n1088 | \\t\\t\\t\\t\\t},\\n----\\n1091 | \\t\\t\\t\\t\\t\\ttype: \\\"message\\\",\\n1092 | \\t\\t\\t\\t\\t\\ttokensCovered: 150,\\n1093 | \\t\\t\\t\\t\\t},\\n----\\n\\n# src/api/providers/__tests__/openai-usage-tracking.spec.ts\\n 28 | \\t\\t\\t\\t\\t\\t\\t\\tusage: {\\n 29 | \\t\\t\\t\\t\\t\\t\\t\\t\\tprompt_tokens: 10,\\n 30 | \\t\\t\\t\\t\\t\\t\\t\\t\\tcompletion_tokens: 5,\\n 31 | \\t\\t\\t\\t\\t\\t\\t\\t\\ttotal_tokens: 15,\\n 32 | \\t\\t\\t\\t\\t\\t\\t\\t},\\n----\\n 47 | \\t\\t\\t\\t\\t\\t\\t\\t\\tusage: {\\n 48 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\tprompt_tokens: 10,\\n 49 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\tcompletion_tokens: 2,\\n 50 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\ttotal_tokens: 12,\\n 51 | \\t\\t\\t\\t\\t\\t\\t\\t\\t},\\n----\\n 62 | \\t\\t\\t\\t\\t\\t\\t\\t\\tusage: {\\n 63 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\tprompt_tokens: 10,\\n 64 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\tcompletion_tokens: 4,\\n 65 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\ttotal_tokens: 14,\\n 66 | \\t\\t\\t\\t\\t\\t\\t\\t\\t},\\n----\\n 77 | \\t\\t\\t\\t\\t\\t\\t\\t\\tusage: {\\n 78 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\tprompt_tokens: 10,\\n 79 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\tcompletion_tokens: 5,\\n 80 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\ttotal_tokens: 15,\\n 81 | \\t\\t\\t\\t\\t\\t\\t\\t\\t},\\n----\\n155 | \\t\\t\\t\\t\\t\\tchoices: [{ message: { role: \\\"assistant\\\", content: \\\"Test response\\\" } }],\\n156 | \\t\\t\\t\\t\\t\\tusage: { prompt_tokens: 10, completion_tokens: 5, total_tokens: 15 },\\n157 | \\t\\t\\t\\t\\t}\\n----\\n177 | \\t\\t\\t\\t\\t\\t\\tusage: {\\n178 | \\t\\t\\t\\t\\t\\t\\t\\tprompt_tokens: 10,\\n179 | \\t\\t\\t\\t\\t\\t\\t\\tcompletion_tokens: 5,\\n180 | \\t\\t\\t\\t\\t\\t\\t\\ttotal_tokens: 15,\\n181 | \\t\\t\\t\\t\\t\\t\\t},\\n----\\n\\n# src/api/providers/__tests__/unbound.spec.ts\\n 82 | \\t\\t\\t\\t\\t\\t\\t\\t\\tusage: {\\n 83 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\tprompt_tokens: 10,\\n 84 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\tcompletion_tokens: 5,\\n 85 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\ttotal_tokens: 15,\\n 86 | \\t\\t\\t\\t\\t\\t\\t\\t\\t},\\n----\\n 91 | \\t\\t\\t\\t\\t\\t\\t\\t\\tusage: {\\n 92 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\tprompt_tokens: 8,\\n 93 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\tcompletion_tokens: 4,\\n 94 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\ttotal_tokens: 12,\\n 95 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\tcache_creation_input_tokens: 3,\\n 96 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\tcache_read_input_tokens: 2,\\n 97 | \\t\\t\\t\\t\\t\\t\\t\\t\\t},\\n----\\n232 | \\t\\t\\t\\t\\ttemperature: 0,\\n233 | \\t\\t\\t\\t\\tmax_tokens: 8192,\\n234 | \\t\\t\\t\\t}),\\n----\\n253 | \\n254 | \\t\\tit(\\\"should not set max_tokens for non-Anthropic models\\\", async () => {\\n255 | \\t\\t\\tmockCreate.mockClear()\\n----\\n277 | \\n278 | \\t\\t\\texpect(mockCreate.mock.calls[0][0]).not.toHaveProperty(\\\"max_tokens\\\")\\n279 | \\t\\t})\\n----\\n\\n# src/api/providers/__tests__/anthropic-vertex.spec.ts\\n 21 | \\t\\t\\t\\t\\t\\tusage: {\\n 22 | \\t\\t\\t\\t\\t\\t\\tinput_tokens: 10,\\n 23 | \\t\\t\\t\\t\\t\\t\\toutput_tokens: 5,\\n 24 | \\t\\t\\t\\t\\t\\t},\\n----\\n 32 | \\t\\t\\t\\t\\t\\t\\t\\tusage: {\\n 33 | \\t\\t\\t\\t\\t\\t\\t\\t\\tinput_tokens: 10,\\n 34 | \\t\\t\\t\\t\\t\\t\\t\\t\\toutput_tokens: 5,\\n 35 | \\t\\t\\t\\t\\t\\t\\t\\t},\\n----\\n 95 | \\t\\t\\t\\t\\t\\tusage: {\\n 96 | \\t\\t\\t\\t\\t\\t\\tinput_tokens: 10,\\n 97 | \\t\\t\\t\\t\\t\\t\\toutput_tokens: 0,\\n 98 | \\t\\t\\t\\t\\t\\t},\\n----\\n118 | \\t\\t\\t\\t\\tusage: {\\n119 | \\t\\t\\t\\t\\t\\toutput_tokens: 5,\\n120 | \\t\\t\\t\\t\\t},\\n----\\n164 | \\t\\t\\t\\tmodel: \\\"claude-3-5-sonnet-v2@20241022\\\",\\n165 | \\t\\t\\t\\tmax_tokens: 8192,\\n166 | \\t\\t\\t\\ttemperature: 0,\\n----\\n284 | \\t\\t\\t\\t\\t\\tusage: {\\n285 | \\t\\t\\t\\t\\t\\t\\tinput_tokens: 10,\\n286 | \\t\\t\\t\\t\\t\\t\\toutput_tokens: 0,\\n287 | \\t\\t\\t\\t\\t\\t\\tcache_creation_input_tokens: 3,\\n288 | \\t\\t\\t\\t\\t\\t\\tcache_read_input_tokens: 2,\\n289 | \\t\\t\\t\\t\\t\\t},\\n----\\n309 | \\t\\t\\t\\t\\tusage: {\\n310 | \\t\\t\\t\\t\\t\\toutput_tokens: 5,\\n311 | \\t\\t\\t\\t\\t},\\n----\\n419 | \\t\\t\\t\\t\\t\\tusage: {\\n420 | \\t\\t\\t\\t\\t\\t\\tinput_tokens: 10,\\n421 | \\t\\t\\t\\t\\t\\t\\toutput_tokens: 0,\\n422 | \\t\\t\\t\\t\\t\\t\\tcache_creation_input_tokens: 5,\\n423 | \\t\\t\\t\\t\\t\\t\\tcache_read_input_tokens: 3,\\n424 | \\t\\t\\t\\t\\t\\t},\\n----\\n484 | \\t\\t\\t\\t\\t\\tusage: {\\n485 | \\t\\t\\t\\t\\t\\t\\tinput_tokens: 10,\\n486 | \\t\\t\\t\\t\\t\\t\\toutput_tokens: 0,\\n487 | \\t\\t\\t\\t\\t\\t},\\n----\\n618 | \\t\\t\\t\\tmodel: \\\"claude-3-5-sonnet-v2@20241022\\\",\\n619 | \\t\\t\\t\\tmax_tokens: 8192,\\n620 | \\t\\t\\t\\ttemperature: 0,\\n----\\n752 | \\n753 | \\t\\t\\t// Test with default thinking budget (80% of max tokens)\\n754 | \\t\\t\\tconst handlerWithDefaultBudget = new AnthropicVertexHandler({\\n----\\n767 | \\t\\t\\t\\tvertexRegion: \\\"us-central1\\\",\\n768 | \\t\\t\\t\\tmodelMaxTokens: 1000, // This would result in 800 tokens for thinking, but minimum is 1024\\n769 | \\t\\t\\t})\\n----\\n789 | \\t\\t\\t\\t\\t\\tmodel: options.model,\\n790 | \\t\\t\\t\\t\\t\\tusage: { input_tokens: 10, output_tokens: 5 },\\n791 | \\t\\t\\t\\t\\t}\\n----\\n794 | \\t\\t\\t\\t\\tasync *[Symbol.asyncIterator]() {\\n795 | \\t\\t\\t\\t\\t\\tyield { type: \\\"message_start\\\", message: { usage: { input_tokens: 10, output_tokens: 5 } } }\\n796 | \\t\\t\\t\\t\\t},\\n----\\n806 | \\t\\t\\t\\texpect.objectContaining({\\n807 | \\t\\t\\t\\t\\tthinking: { type: \\\"enabled\\\", budget_tokens: 4096 },\\n808 | \\t\\t\\t\\t\\ttemperature: 1.0, // Thinking requires temperature 1.0\\n----\\n\\n# src/core/prompts/tools/codebase-search.ts\\n 12 | \\n 13 | Example: Searching for functions related to user authentication\\n 14 | \\n----\\n\\n# src/core/task/__tests__/Task.test.ts\\n185 | \\t\\t\\t\\t\\t\\t\\t\\ttask: \\\"historical task\\\",\\n186 | \\t\\t\\t\\t\\t\\t\\t\\ttokensIn: 100,\\n187 | \\t\\t\\t\\t\\t\\t\\t\\ttokensOut: 200,\\n188 | \\t\\t\\t\\t\\t\\t\\t\\tcacheWrites: 0,\\n----\\n253 | \\t\\t\\t\\ttask: \\\"historical task\\\",\\n254 | \\t\\t\\t\\ttokensIn: 100,\\n255 | \\t\\t\\t\\ttokensOut: 200,\\n256 | \\t\\t\\t\\tcacheWrites: 0,\\n----\\n615 | \\t\\t\\t\\t\\t\\ttext: JSON.stringify({\\n616 | \\t\\t\\t\\t\\t\\t\\ttokensIn: 100,\\n617 | \\t\\t\\t\\t\\t\\t\\ttokensOut: 50,\\n618 | \\t\\t\\t\\t\\t\\t\\tcacheWrites: 0,\\n----\\n740 | \\t\\t\\t\\t\\t\\ttext: JSON.stringify({\\n741 | \\t\\t\\t\\t\\t\\t\\ttokensIn: 100,\\n742 | \\t\\t\\t\\t\\t\\t\\ttokensOut: 50,\\n743 | \\t\\t\\t\\t\\t\\t\\tcacheWrites: 0,\\n----\\n\\n# src/core/prompts/instructions/create-mcp-server.ts\\n 11 | \\n 12 | When creating MCP servers, it's important to understand that they operate in a non-interactive environment. The server cannot initiate OAuth flows, open browser windows, or prompt for user input during runtime. All credentials and authentication tokens must be provided upfront through environment variables in the MCP settings configuration. For example, Spotify's API uses OAuth to get a refresh token for the user, but the MCP server cannot initiate this flow. While you can walk the user through [truncated...]\\n 13 | \\n----\\n 76 | \\t\\t\\t\\t...\\n 77 | \\t\\t\\t\\t\\\"type\\\": \\\"module\\\", // added by default, uses ES module syntax (import/export) rather than CommonJS (require/module.exports) (Important to know if you create additional scripts in this server repository like a get-refresh-token.js script)\\n 78 | \\t\\t\\t\\t\\\"scripts\\\": {\\n----\\n\\n# src/core/task/Task.ts\\n 24 | import { TelemetryService } from \\\"@roo-code/telemetry\\\"\\n 25 | import { CloudService } from \\\"@roo-code/cloud\\\"\\n 26 | \\n----\\n 96 | \\ttaskSpawned: [taskId: string]\\n 97 | \\ttaskCompleted: [taskId: string, tokenUsage: TokenUsage, toolUsage: ToolUsage]\\n 98 | \\ttaskTokenUsageUpdated: [taskId: string, tokenUsage: TokenUsage]\\n 99 | \\ttaskToolFailed: [taskId: string, tool: ToolName, error: string]\\n----\\n346 | \\n347 | \\t\\tconst shouldCaptureMessage = message.partial !== true && CloudService.isEnabled()\\n348 | \\n349 | \\t\\tif (shouldCaptureMessage) {\\n350 | \\t\\t\\tCloudService.instance.captureEvent({\\n351 | \\t\\t\\t\\tevent: TelemetryEventName.TASK_MESSAGE,\\n----\\n366 | \\n367 | \\t\\tconst shouldCaptureMessage = message.partial !== true && CloudService.isEnabled()\\n368 | \\n369 | \\t\\tif (shouldCaptureMessage) {\\n370 | \\t\\t\\tCloudService.instance.captureEvent({\\n371 | \\t\\t\\t\\tevent: TelemetryEventName.TASK_MESSAGE,\\n----\\n384 | \\n385 | \\t\\t\\tconst { historyItem, tokenUsage } = await taskMetadata({\\n386 | \\t\\t\\t\\tmessages: this.clineMessages,\\n----\\n392 | \\n393 | \\t\\t\\tthis.emit(\\\"taskTokenUsageUpdated\\\", this.taskId, tokenUsage)\\n394 | \\n----\\n1246 | \\t\\t\\t\\t\\t...JSON.parse(this.clineMessages[lastApiReqIndex].text || \\\"{}\\\"),\\n1247 | \\t\\t\\t\\t\\ttokensIn: inputTokens,\\n1248 | \\t\\t\\t\\t\\ttokensOut: outputTokens,\\n1249 | \\t\\t\\t\\t\\tcacheWrites: cacheWriteTokens,\\n----\\n1678 | \\t\\tif (contextTokens) {\\n1679 | \\t\\t\\t// Default max tokens value for thinking models when no specific\\n1680 | \\t\\t\\t// value is set.\\n----\\n\\n# src/api/providers/glama.ts\\n 51 | \\n 52 | \\t\\t// Required by Anthropic; other providers default to max tokens allowed.\\n 53 | \\t\\tlet maxTokens: number | undefined\\n----\\n 60 | \\t\\t\\tmodel: modelId,\\n 61 | \\t\\t\\tmax_tokens: maxTokens,\\n 62 | \\t\\t\\tmessages: openAiMessages,\\n----\\n 90 | \\t\\t\\t\\t// In case of an interrupted request, we need to wait for the upstream API to finish processing the request\\n 91 | \\t\\t\\t\\t// before we can fetch information about the token usage and cost.\\n 92 | \\t\\t\\t\\tconst response = await axios.get(\\n----\\n 98 | \\n 99 | \\t\\t\\t\\tif (completionRequest.tokenUsage && completionRequest.totalCostUsd) {\\n100 | \\t\\t\\t\\t\\tyield {\\n101 | \\t\\t\\t\\t\\t\\ttype: \\\"usage\\\",\\n102 | \\t\\t\\t\\t\\t\\tcacheWriteTokens: completionRequest.tokenUsage.cacheCreationInputTokens,\\n103 | \\t\\t\\t\\t\\t\\tcacheReadTokens: completionRequest.tokenUsage.cacheReadInputTokens,\\n104 | \\t\\t\\t\\t\\t\\tinputTokens: completionRequest.tokenUsage.promptTokens,\\n105 | \\t\\t\\t\\t\\t\\toutputTokens: completionRequest.tokenUsage.completionTokens,\\n106 | \\t\\t\\t\\t\\t\\ttotalCost: parseFloat(completionRequest.totalCostUsd),\\n----\\n132 | \\t\\t\\tif (modelId.startsWith(\\\"anthropic/\\\")) {\\n133 | \\t\\t\\t\\trequestOptions.max_tokens = info.maxTokens\\n134 | \\t\\t\\t}\\n----\\n\\n# src/api/transform/reasoning.ts\\n 12 | \\teffort?: ReasoningEffort\\n 13 | \\tmax_tokens?: number\\n 14 | \\texclude?: boolean\\n----\\n 36 | \\tshouldUseReasoningBudget({ model, settings })\\n 37 | \\t\\t? { max_tokens: reasoningBudget }\\n 38 | \\t\\t: shouldUseReasoningEffort({ model, settings })\\n----\\n 46 | }: GetModelReasoningOptions): AnthropicReasoningParams | undefined =>\\n 47 | \\tshouldUseReasoningBudget({ model, settings }) ? { type: \\\"enabled\\\", budget_tokens: reasoningBudget! } : undefined\\n 48 | \\n----\\n\\n# src/api/providers/openrouter.ts\\n 32 | \\tinclude_reasoning?: boolean\\n 33 | \\t// https://openrouter.ai/docs/use-cases/reasoning-tokens\\n 34 | \\treasoning?: OpenRouterReasoningParams\\n----\\n 40 | interface CompletionUsage {\\n 41 | \\tcompletion_tokens?: number\\n 42 | \\tcompletion_tokens_details?: {\\n 43 | \\t\\treasoning_tokens?: number\\n 44 | \\t}\\n 45 | \\tprompt_tokens?: number\\n 46 | \\tprompt_tokens_details?: {\\n 47 | \\t\\tcached_tokens?: number\\n 48 | \\t}\\n 49 | \\ttotal_tokens?: number\\n 50 | \\tcost?: number\\n----\\n 81 | \\n 82 | \\t\\t// OpenRouter sends reasoning tokens by default for Gemini 2.5 Pro\\n 83 | \\t\\t// Preview even if you don't request them. This is not the default for\\n----\\n116 | \\t\\t\\tmodel: modelId,\\n117 | \\t\\t\\t...(maxTokens && maxTokens > 0 && { max_tokens: maxTokens }),\\n118 | \\t\\t\\ttemperature,\\n----\\n165 | \\t\\t\\t\\ttype: \\\"usage\\\",\\n166 | \\t\\t\\t\\tinputTokens: lastUsage.prompt_tokens || 0,\\n167 | \\t\\t\\t\\toutputTokens: lastUsage.completion_tokens || 0,\\n168 | \\t\\t\\t\\t// Waiting on OpenRouter to figure out what this represents in the Gemini case\\n169 | \\t\\t\\t\\t// and how to best support it.\\n170 | \\t\\t\\t\\t// cacheReadTokens: lastUsage.prompt_tokens_details?.cached_tokens,\\n171 | \\t\\t\\t\\treasoningTokens: lastUsage.completion_tokens_details?.reasoning_tokens,\\n172 | \\t\\t\\t\\ttotalCost: (lastUsage.is_byok ? BYOK_COST_MULTIPLIER : 1) * (lastUsage.cost || 0),\\n----\\n219 | \\t\\t\\tmodel: modelId,\\n220 | \\t\\t\\tmax_tokens: maxTokens,\\n221 | \\t\\t\\ttemperature,\\n----\\n\\n# src/utils/__tests__/cost.spec.ts\\n 13 | \\t\\t\\tsupportsPromptCache: true,\\n 14 | \\t\\t\\tinputPrice: 3.0, // $3 per million tokens\\n 15 | \\t\\t\\toutputPrice: 15.0, // $15 per million tokens\\n 16 | \\t\\t\\tcacheWritesPrice: 3.75, // $3.75 per million tokens\\n 17 | \\t\\t\\tcacheReadsPrice: 0.3, // $0.30 per million tokens\\n 18 | \\t\\t}\\n----\\n 70 | \\n 71 | \\t\\tit(\\\"should handle zero tokens\\\", () => {\\n 72 | \\t\\t\\tconst cost = calculateApiCostAnthropic(mockModelInfo, 0, 0, 0, 0)\\n----\\n106 | \\t\\t\\tsupportsPromptCache: true,\\n107 | \\t\\t\\tinputPrice: 3.0, // $3 per million tokens\\n108 | \\t\\t\\toutputPrice: 15.0, // $15 per million tokens\\n109 | \\t\\t\\tcacheWritesPrice: 3.75, // $3.75 per million tokens\\n110 | \\t\\t\\tcacheReadsPrice: 0.3, // $0.30 per million tokens\\n111 | \\t\\t}\\n----\\n163 | \\n164 | \\t\\tit(\\\"should handle zero tokens\\\", () => {\\n165 | \\t\\t\\tconst cost = calculateApiCostOpenAI(mockModelInfo, 0, 0, 0, 0)\\n----\\n\\n# src/utils/__tests__/git.spec.ts\\n 98 | \\t\\t\\t\\tsubject: \\\"fix: test commit\\\",\\n 99 | \\t\\t\\t\\tauthor: \\\"John Doe\\\",\\n100 | \\t\\t\\t\\tdate: \\\"2024-01-06\\\",\\n----\\n162 | \\t\\t\\t\\t[\\n163 | \\t\\t\\t\\t\\t'git log -n 10 --format=\\\"%H%n%h%n%s%n%an%n%ad\\\" --date=short --author-date-order abc123',\\n164 | \\t\\t\\t\\t\\t{ stdout: mockCommitData, stderr: \\\"\\\" },\\n----\\n185 | \\t\\t\\t\\tsubject: \\\"fix: test commit\\\",\\n186 | \\t\\t\\t\\tauthor: \\\"John Doe\\\",\\n187 | \\t\\t\\t\\tdate: \\\"2024-01-06\\\",\\n----\\n\\n# src/api/providers/gemini.ts\\n 7 | } from \\\"@google/genai\\\"\\n 8 | import type { JWTInput } from \\\"google-auth-library\\\"\\n 9 | \\n----\\n179 | \\t\\t\\tif (response.totalTokens === undefined) {\\n180 | \\t\\t\\t\\tconsole.warn(\\\"Gemini token counting returned undefined, using fallback\\\")\\n181 | \\t\\t\\t\\treturn super.countTokens(content)\\n----\\n185 | \\t\\t} catch (error) {\\n186 | \\t\\t\\tconsole.warn(\\\"Gemini token counting failed, using fallback\\\", error)\\n187 | \\t\\t\\treturn super.countTokens(content)\\n----\\n209 | \\n210 | \\t\\t// If there's tiered pricing then adjust the input and output token prices\\n211 | \\t\\t// based on the input tokens used.\\n212 | \\t\\tif (info.tiers) {\\n----\\n221 | \\n222 | \\t\\t// Subtract the cached input tokens from the total input tokens.\\n223 | \\t\\tconst uncachedInputTokens = inputTokens - cacheReadTokens\\n----\\n230 | \\n231 | \\t\\tconst trace: Record = {\\n232 | \\t\\t\\tinput: { price: inputPrice, tokens: uncachedInputTokens, cost: inputTokensCost },\\n233 | \\t\\t\\toutput: { price: outputPrice, tokens: outputTokens, cost: outputTokensCost },\\n234 | \\t\\t}\\n----\\n236 | \\t\\tif (cacheReadTokens > 0) {\\n237 | \\t\\t\\ttrace.cacheRead = { price: cacheReadsPrice, tokens: cacheReadTokens, cost: cacheReadCost }\\n238 | \\t\\t}\\n----\\n\\n# src/api/providers/lite-llm.ts\\n 46 | \\n 47 | \\t\\t// Required by some providers; others default to max tokens allowed\\n 48 | \\t\\tlet maxTokens: number | undefined = info.maxTokens ?? undefined\\n----\\n 51 | \\t\\t\\tmodel: modelId,\\n 52 | \\t\\t\\tmax_tokens: maxTokens,\\n 53 | \\t\\t\\tmessages: openAiMessages,\\n----\\n 84 | \\t\\t\\t\\t\\ttype: \\\"usage\\\",\\n 85 | \\t\\t\\t\\t\\tinputTokens: lastUsage.prompt_tokens || 0,\\n 86 | \\t\\t\\t\\t\\toutputTokens: lastUsage.completion_tokens || 0,\\n 87 | \\t\\t\\t\\t\\tcacheWriteTokens: lastUsage.cache_creation_input_tokens || 0,\\n 88 | \\t\\t\\t\\t\\tcacheReadTokens: lastUsage.prompt_tokens_details?.cached_tokens || 0,\\n 89 | \\t\\t\\t\\t}\\n----\\n121 | \\n122 | \\t\\t\\trequestOptions.max_tokens = info.maxTokens\\n123 | \\n----\\n136 | interface LiteLLMUsage extends OpenAI.CompletionUsage {\\n137 | \\tcache_creation_input_tokens?: number\\n138 | }\\n----\\n\\n# src/api/providers/requesty.ts\\n 19 | // Requesty usage includes an extra field for Anthropic use cases.\\n 20 | // Safely cast the prompt token details section to the appropriate structure.\\n 21 | interface RequestyUsage extends OpenAI.CompletionUsage {\\n 22 | \\tprompt_tokens_details?: {\\n 23 | \\t\\tcaching_tokens?: number\\n 24 | \\t\\tcached_tokens?: number\\n 25 | \\t}\\n----\\n 76 | \\t\\tconst requestyUsage = usage as RequestyUsage\\n 77 | \\t\\tconst inputTokens = requestyUsage?.prompt_tokens || 0\\n 78 | \\t\\tconst outputTokens = requestyUsage?.completion_tokens || 0\\n 79 | \\t\\tconst cacheWriteTokens = requestyUsage?.prompt_tokens_details?.caching_tokens || 0\\n 80 | \\t\\tconst cacheReadTokens = requestyUsage?.prompt_tokens_details?.cached_tokens || 0\\n 81 | \\t\\tconst totalCost = modelInfo\\n----\\n102 | \\t\\t\\tinfo,\\n103 | \\t\\t\\tmaxTokens: max_tokens,\\n104 | \\t\\t\\ttemperature,\\n----\\n116 | \\t\\t\\tmodel,\\n117 | \\t\\t\\tmax_tokens,\\n118 | \\t\\t\\ttemperature,\\n----\\n150 | \\tasync completePrompt(prompt: string): Promise {\\n151 | \\t\\tconst { id: model, maxTokens: max_tokens, temperature } = await this.fetchModel()\\n152 | \\n----\\n156 | \\t\\t\\tmodel,\\n157 | \\t\\t\\tmax_tokens,\\n158 | \\t\\t\\tmessages: openAiMessages,\\n----\\n\\n# src/api/providers/anthropic.ts\\n 29 | \\t\\tconst apiKeyFieldName =\\n 30 | \\t\\t\\tthis.options.anthropicBaseUrl && this.options.anthropicUseAuthToken ? \\\"authToken\\\" : \\\"apiKey\\\"\\n 31 | \\n----\\n 75 | \\t\\t\\t\\t\\t\\tmodel: modelId,\\n 76 | \\t\\t\\t\\t\\t\\tmax_tokens: maxTokens ?? ANTHROPIC_DEFAULT_MAX_TOKENS,\\n 77 | \\t\\t\\t\\t\\t\\ttemperature,\\n----\\n124 | \\t\\t\\t\\t\\tmodel: modelId,\\n125 | \\t\\t\\t\\t\\tmax_tokens: maxTokens ?? ANTHROPIC_DEFAULT_MAX_TOKENS,\\n126 | \\t\\t\\t\\t\\ttemperature,\\n----\\n142 | \\t\\t\\t\\t\\t\\ttype: \\\"usage\\\",\\n143 | \\t\\t\\t\\t\\t\\tinputTokens: usage.input_tokens || 0,\\n144 | \\t\\t\\t\\t\\t\\toutputTokens: usage.output_tokens || 0,\\n145 | \\t\\t\\t\\t\\t\\tcacheWriteTokens: usage.cache_creation_input_tokens || undefined,\\n146 | \\t\\t\\t\\t\\t\\tcacheReadTokens: usage.cache_read_input_tokens || undefined,\\n147 | \\t\\t\\t\\t\\t}\\n----\\n151 | \\t\\t\\t\\tcase \\\"message_delta\\\":\\n152 | \\t\\t\\t\\t\\t// Tells us stop_reason, stop_sequence, and output tokens\\n153 | \\t\\t\\t\\t\\t// along the way and at the end of the message.\\n----\\n156 | \\t\\t\\t\\t\\t\\tinputTokens: 0,\\n157 | \\t\\t\\t\\t\\t\\toutputTokens: chunk.usage.output_tokens || 0,\\n158 | \\t\\t\\t\\t\\t}\\n----\\n231 | \\t\\t\\tmodel,\\n232 | \\t\\t\\tmax_tokens: ANTHROPIC_DEFAULT_MAX_TOKENS,\\n233 | \\t\\t\\tthinking: undefined,\\n----\\n243 | \\t/**\\n244 | \\t * Counts tokens for the given content using Anthropic's API\\n245 | \\t *\\n246 | \\t * @param content The content blocks to count tokens for\\n247 | \\t * @returns A promise resolving to the token count\\n248 | \\t */\\n----\\n258 | \\n259 | \\t\\t\\treturn response.input_tokens\\n260 | \\t\\t} catch (error) {\\n261 | \\t\\t\\t// Log error but fallback to tiktoken estimation\\n262 | \\t\\t\\tconsole.warn(\\\"Anthropic token counting failed, using fallback\\\", error)\\n263 | \\n----\\n\\n# src/api/providers/deepseek.ts\\n 32 | \\t\\t\\ttype: \\\"usage\\\",\\n 33 | \\t\\t\\tinputTokens: usage?.prompt_tokens || 0,\\n 34 | \\t\\t\\toutputTokens: usage?.completion_tokens || 0,\\n 35 | \\t\\t\\tcacheWriteTokens: usage?.prompt_tokens_details?.cache_miss_tokens,\\n 36 | \\t\\t\\tcacheReadTokens: usage?.prompt_tokens_details?.cached_tokens,\\n 37 | \\t\\t}\\n----\\n\\n# src/api/providers/openai.ts\\n162 | \\t\\t\\tif (this.options.includeMaxTokens) {\\n163 | \\t\\t\\t\\trequestOptions.max_tokens = modelInfo.maxTokens\\n164 | \\t\\t\\t}\\n----\\n241 | \\t\\t\\ttype: \\\"usage\\\",\\n242 | \\t\\t\\tinputTokens: usage?.prompt_tokens || 0,\\n243 | \\t\\t\\toutputTokens: usage?.completion_tokens || 0,\\n244 | \\t\\t\\tcacheWriteTokens: usage?.cache_creation_input_tokens || undefined,\\n245 | \\t\\t\\tcacheReadTokens: usage?.cache_read_input_tokens || undefined,\\n246 | \\t\\t}\\n----\\n347 | \\t\\t\\t\\t\\ttype: \\\"usage\\\",\\n348 | \\t\\t\\t\\t\\tinputTokens: chunk.usage.prompt_tokens || 0,\\n349 | \\t\\t\\t\\t\\toutputTokens: chunk.usage.completion_tokens || 0,\\n350 | \\t\\t\\t\\t}\\n----\\n\\n# src/api/providers/xai.ts\\n 52 | \\t\\t\\tmodel: modelId,\\n 53 | \\t\\t\\tmax_tokens: modelInfo.maxTokens,\\n 54 | \\t\\t\\ttemperature: this.options.modelTemperature ?? XAI_DEFAULT_TEMPERATURE,\\n----\\n 78 | \\t\\t\\tif (chunk.usage) {\\n 79 | \\t\\t\\t\\t// Extract detailed token information if available\\n 80 | \\t\\t\\t\\t// First check for prompt_tokens_details structure (real API response)\\n 81 | \\t\\t\\t\\tconst promptDetails = \\\"prompt_tokens_details\\\" in chunk.usage ? chunk.usage.prompt_tokens_details : null;\\n 82 | \\t\\t\\t\\tconst cachedTokens = promptDetails && \\\"cached_tokens\\\" in promptDetails ? promptDetails.cached_tokens : 0;\\n 83 | \\n 84 | \\t\\t\\t\\t// Fall back to direct fields in usage (used in test mocks)\\n 85 | \\t\\t\\t\\tconst readTokens = cachedTokens || (\\\"cache_read_input_tokens\\\" in chunk.usage ? (chunk.usage as any).cache_read_input_tokens : 0);\\n 86 | \\t\\t\\t\\tconst writeTokens = \\\"cache_creation_input_tokens\\\" in chunk.usage ? (chunk.usage as any).cache_creation_input_tokens : 0;\\n 87 | \\n----\\n 89 | \\t\\t\\t\\t\\ttype: \\\"usage\\\",\\n 90 | \\t\\t\\t\\t\\tinputTokens: chunk.usage.prompt_tokens || 0,\\n 91 | \\t\\t\\t\\t\\toutputTokens: chunk.usage.completion_tokens || 0,\\n 92 | \\t\\t\\t\\t\\tcacheReadTokens: readTokens,\\n----\"}", + "partial": false +} +[2025-06-13T05:00:02.337Z] { + "ts": 1749790802303, + "type": "say", + "say": "api_req_started", + "text": "{\"request\":\"[search_files for 'CloudService|refresh|token|auth' in '*.ts'] Result:\\n\\nShowing first 300 of 300+ results. Use a more specific search if necessary.\\n\\n# apps/web-evals/src/lib/formatters.ts\\n 30 | \\n 31 | export const formatTokens = (tokens: number) => {\\n 32 | \\tif (tokens < 1000) {\\n 33 | \\t\\treturn tokens.toString()\\n 34 | \\t}\\n 35 | \\n 36 | \\tif (tokens < 1000000) {\\n 37 | \\t\\treturn `${(tokens / 1000).toFixed(1)}k`\\n 38 | \\t}\\n 39 | \\n 40 | \\tif (tokens < 1000000000) {\\n 41 | \\t\\treturn `${(tokens / 1000000).toFixed(1)}M`\\n 42 | \\t}\\n 43 | \\n 44 | \\treturn `${(tokens / 1000000000).toFixed(1)}B`\\n 45 | }\\n----\\n\\n# src/services/ripgrep/index.ts\\n101 | \\t\\tconst rgProcess = childProcess.spawn(bin, args)\\n102 | \\t\\t// cross-platform alternative to head, which is ripgrep author's recommendation for limiting output.\\n103 | \\t\\tconst rl = readline.createInterface({\\n----\\n\\n# apps/web-roo-code/src/lib/index.ts\\n 3 | export * from \\\"./format-score\\\"\\n 4 | export * from \\\"./format-tokens\\\"\\n----\\n\\n# apps/web-roo-code/src/lib/format-tokens.ts\\n 1 | export const formatTokens = (tokens: number, decimals = 0) => {\\n 2 | \\tif (tokens < 1000) {\\n 3 | \\t\\treturn tokens.toString()\\n 4 | \\t}\\n 5 | \\n 6 | \\tif (tokens < 1000000) {\\n 7 | \\t\\treturn `${(tokens / 1000).toFixed(decimals)}K`\\n 8 | \\t}\\n 9 | \\n 10 | \\tif (tokens < 1000000000) {\\n 11 | \\t\\treturn `${(tokens / 1000000).toFixed(decimals)}M`\\n 12 | \\t}\\n 13 | \\n 14 | \\treturn `${(tokens / 1000000000).toFixed(decimals)}B`\\n 15 | }\\n----\\n\\n# apps/web-roo-code/src/lib/hooks/use-open-router-models.ts\\n 19 | \\t\\t.object({\\n 20 | \\t\\t\\tmax_completion_tokens: z.number().nullish(),\\n 21 | \\t\\t})\\n----\\n 53 | \\t\\t\\tmodelInfo: {\\n 54 | \\t\\t\\t\\tmaxTokens: rawModel.top_provider?.max_completion_tokens ?? undefined,\\n 55 | \\t\\t\\t\\tcontextWindow: rawModel.context_length,\\n----\\n\\n# src/services/mcp/__tests__/McpHub.test.ts\\n 50 | \\t\\t\\tscheme: \\\"file\\\",\\n 51 | \\t\\t\\tauthority: \\\"\\\",\\n 52 | \\t\\t\\tpath: \\\"/test/path\\\",\\n----\\n\\n# src/services/mcp/McpHub.ts\\n1092 | \\n1093 | \\tpublic async refreshAllConnections(): Promise {\\n1094 | \\t\\tif (this.isConnecting) {\\n1095 | \\t\\t\\tvscode.window.showInformationMessage(t(\\\"mcp:info.already_refreshing\\\"))\\n1096 | \\t\\t\\treturn\\n----\\n1099 | \\t\\tthis.isConnecting = true\\n1100 | \\t\\tvscode.window.showInformationMessage(t(\\\"mcp:info.refreshing_all\\\"))\\n1101 | \\n----\\n1151 | \\n1152 | \\t\\t\\tvscode.window.showInformationMessage(t(\\\"mcp:info.all_refreshed\\\"))\\n1153 | \\t\\t} catch (error) {\\n1154 | \\t\\t\\tthis.showErrorMessage(\\\"Failed to refresh MCP servers\\\", error)\\n1155 | \\t\\t} finally {\\n----\\n1244 | \\n1245 | \\t\\t\\t\\t\\t// Only refresh capabilities if connected\\n1246 | \\t\\t\\t\\t\\tif (connection.server.status === \\\"connected\\\") {\\n----\\n1254 | \\t\\t\\t\\t} catch (error) {\\n1255 | \\t\\t\\t\\t\\tconsole.error(`Failed to refresh capabilities for ${serverName}:`, error)\\n1256 | \\t\\t\\t\\t}\\n----\\n\\n# src/services/code-index/embedders/openai.ts\\n 52 | \\t\\t\\t\\t\\tconsole.warn(\\n 53 | \\t\\t\\t\\t\\t\\t`Text at index ${i} exceeds maximum token limit (${itemTokens} > ${MAX_ITEM_TOKENS}). Skipping.`,\\n 54 | \\t\\t\\t\\t\\t)\\n----\\n108 | \\t\\t\\t\\t\\tusage: {\\n109 | \\t\\t\\t\\t\\t\\tpromptTokens: response.usage?.prompt_tokens || 0,\\n110 | \\t\\t\\t\\t\\t\\ttotalTokens: response.usage?.total_tokens || 0,\\n111 | \\t\\t\\t\\t\\t},\\n----\\n\\n# src/services/code-index/embedders/openai-compatible.ts\\n 18 | \\tusage?: {\\n 19 | \\t\\tprompt_tokens?: number\\n 20 | \\t\\ttotal_tokens?: number\\n 21 | \\t}\\n----\\n 34 | \\t * @param baseUrl The base URL for the OpenAI-compatible API endpoint\\n 35 | \\t * @param apiKey The API key for authentication\\n 36 | \\t * @param modelId Optional model identifier (defaults to \\\"text-embedding-3-small\\\")\\n----\\n 75 | \\t\\t\\t\\t\\tconsole.warn(\\n 76 | \\t\\t\\t\\t\\t\\t`Text at index ${i} exceeds maximum token limit (${itemTokens} > ${MAX_ITEM_TOKENS}). Skipping.`,\\n 77 | \\t\\t\\t\\t\\t)\\n----\\n156 | \\t\\t\\t\\t\\tusage: {\\n157 | \\t\\t\\t\\t\\t\\tpromptTokens: response.usage?.prompt_tokens || 0,\\n158 | \\t\\t\\t\\t\\t\\ttotalTokens: response.usage?.total_tokens || 0,\\n159 | \\t\\t\\t\\t\\t},\\n----\\n\\n# src/services/code-index/embedders/__tests__/openai-compatible.spec.ts\\n103 | \\t\\t\\t\\tdata: [{ embedding: [0.1, 0.2, 0.3] }],\\n104 | \\t\\t\\t\\tusage: { prompt_tokens: 10, total_tokens: 15 },\\n105 | \\t\\t\\t}\\n----\\n124 | \\t\\t\\t\\tdata: [{ embedding: [0.1, 0.2, 0.3] }, { embedding: [0.4, 0.5, 0.6] }],\\n125 | \\t\\t\\t\\tusage: { prompt_tokens: 20, total_tokens: 30 },\\n126 | \\t\\t\\t}\\n----\\n149 | \\t\\t\\t\\tdata: [{ embedding: [0.1, 0.2, 0.3] }],\\n150 | \\t\\t\\t\\tusage: { prompt_tokens: 10, total_tokens: 15 },\\n151 | \\t\\t\\t}\\n----\\n194 | \\t\\t\\t\\t\\tdata: [{ embedding: base64String }], // Base64 string instead of array\\n195 | \\t\\t\\t\\t\\tusage: { prompt_tokens: 10, total_tokens: 15 },\\n196 | \\t\\t\\t\\t}\\n----\\n226 | \\t\\t\\t\\t\\tdata: [{ embedding: base64String1 }, { embedding: base64String2 }],\\n227 | \\t\\t\\t\\t\\tusage: { prompt_tokens: 20, total_tokens: 30 },\\n228 | \\t\\t\\t\\t}\\n----\\n253 | \\t\\t\\t\\t\\t],\\n254 | \\t\\t\\t\\t\\tusage: { prompt_tokens: 20, total_tokens: 30 },\\n255 | \\t\\t\\t\\t}\\n----\\n270 | \\t\\t/**\\n271 | \\t\\t * Test batching logic when texts exceed token limits\\n272 | \\t\\t */\\n----\\n283 | \\t\\t\\t\\t\\t],\\n284 | \\t\\t\\t\\t\\tusage: { prompt_tokens: 10, total_tokens: 15 },\\n285 | \\t\\t\\t\\t})\\n----\\n299 | \\t\\t\\t\\t\\tdata: [{ embedding: [0.1, 0.2, 0.3] }, { embedding: [0.4, 0.5, 0.6] }],\\n300 | \\t\\t\\t\\t\\tusage: { prompt_tokens: 10, total_tokens: 15 },\\n301 | \\t\\t\\t\\t}\\n----\\n306 | \\t\\t\\t\\t// Should warn about oversized text\\n307 | \\t\\t\\t\\texpect(console.warn).toHaveBeenCalledWith(expect.stringContaining(\\\"exceeds maximum token limit\\\"))\\n308 | \\n----\\n317 | \\t\\t\\t\\t\\tdata: [{ embedding: [0.1, 0.2, 0.3] }, { embedding: [0.4, 0.5, 0.6] }],\\n318 | \\t\\t\\t\\t\\tusage: { prompt_tokens: 10, total_tokens: 15 },\\n319 | \\t\\t\\t\\t})\\n----\\n354 | \\t\\t\\t\\t\\t\\tdata: [{ embedding: base64String }],\\n355 | \\t\\t\\t\\t\\t\\tusage: { prompt_tokens: 10, total_tokens: 15 },\\n356 | \\t\\t\\t\\t\\t})\\n----\\n375 | \\t\\t\\t\\tconst testTexts = [\\\"Hello world\\\"]\\n376 | \\t\\t\\t\\tconst authError = new Error(\\\"Unauthorized\\\")\\n377 | \\t\\t\\t\\t;(authError as any).status = 401\\n378 | \\n379 | \\t\\t\\t\\tmockEmbeddingsCreate.mockRejectedValue(authError)\\n380 | \\n----\\n452 | \\t\\t\\t\\t\\tdata: null,\\n453 | \\t\\t\\t\\t\\tusage: { prompt_tokens: 10, total_tokens: 15 },\\n454 | \\t\\t\\t\\t}\\n----\\n494 | \\t\\t\\t\\t\\tusage: {\\n495 | \\t\\t\\t\\t\\t\\tprompt_tokens: 2,\\n496 | \\t\\t\\t\\t\\t\\ttotal_tokens: 2,\\n497 | \\t\\t\\t\\t\\t},\\n----\\n\\n# apps/web-evals/src/hooks/use-run-status.ts\\n 16 | \\ttasks: (Task & { taskMetrics: TaskMetrics | null })[] | undefined\\n 17 | \\ttokenUsage: Map\\n 18 | \\tusageUpdatedAt: number | undefined\\n----\\n 24 | \\n 25 | \\tconst tokenUsage = useRef>(new Map())\\n 26 | \\tconst startTimes = useRef>(new Map())\\n----\\n 79 | \\t\\t\\t\\tconst duration = startTime ? Date.now() - startTime : undefined\\n 80 | \\t\\t\\t\\ttokenUsage.current.set(taskId, { ...payload[1], duration })\\n 81 | \\t\\t\\t\\tsetUsageUpdatedAt(Date.now())\\n----\\n 97 | \\t\\ttasks,\\n 98 | \\t\\ttokenUsage: tokenUsage.current,\\n 99 | \\t\\tusageUpdatedAt,\\n----\\n\\n# src/activate/handleUri.ts\\n 2 | \\n 3 | import { CloudService } from \\\"@roo-code/cloud\\\"\\n 4 | \\n----\\n 37 | \\t\\t}\\n 38 | \\t\\tcase \\\"/auth/clerk/callback\\\": {\\n 39 | \\t\\t\\tconst code = query.get(\\\"code\\\")\\n 40 | \\t\\t\\tconst state = query.get(\\\"state\\\")\\n 41 | \\t\\t\\tawait CloudService.instance.handleAuthCallback(code, state)\\n 42 | \\t\\t\\tbreak\\n----\\n\\n# src/services/marketplace/types.ts\\n 20 | \\tdescription: string\\n 21 | \\tauthor?: string\\n 22 | \\tauthorUrl?: string\\n 23 | \\ttags?: string[]\\n----\\n 45 | \\tdescription: string\\n 46 | \\tauthor?: string\\n 47 | \\tauthorUrl?: string\\n 48 | \\turl: string // Required url field\\n----\\n 62 | \\ttype: MarketplaceItemType\\n 63 | \\tauthor?: string\\n 64 | \\tauthorUrl?: string\\n 65 | \\turl?: string // Optional - only MCPs have url\\n----\\n\\n# src/services/tree-sitter/__tests__/fixtures/sample-embedded_template.ts\\n 35 |
\\n 36 | <% if user.authenticated? %>\\n 37 |

Welcome, <%= user.name %>

\\n----\\n\\n# src/services/tree-sitter/__tests__/parseSourceCodeDefinitions.embedded_template.test.ts\\n 43 | \\tit(\\\"should detect control structures\\\", () => {\\n 44 | \\t\\texpect(parseResult).toMatch(/\\\\d+--\\\\d+ \\\\|\\\\s+<% if user\\\\.authenticated\\\\? %>/)\\n 45 | \\t\\texpect(parseResult).toMatch(/\\\\d+--\\\\d+ \\\\|\\\\s+<% user\\\\.posts\\\\.each do \\\\|post\\\\| %>/)\\n----\\n\\n# src/services/marketplace/schemas.ts\\n 35 | \\ttype: marketplaceItemTypeSchema,\\n 36 | \\tauthor: z.string().optional(),\\n 37 | \\tauthorUrl: z.string().url(\\\"Author URL must be a valid URL\\\").optional(),\\n 38 | \\ttags: z.array(z.string()).optional(),\\n----\\n 61 | \\tdescription: z.string(),\\n 62 | \\tauthor: z.string().optional(),\\n 63 | \\tauthorUrl: z.string().url().optional(),\\n 64 | \\ttags: z.array(z.string()).optional(),\\n----\\n 72 | \\tdescription: z.string(),\\n 73 | \\tauthor: z.string().optional(),\\n 74 | \\tauthorUrl: z.string().url().optional(),\\n 75 | \\turl: z.string().url(), // Required url field\\n----\\n\\n# src/extension.ts\\n 14 | \\n 15 | import { CloudService } from \\\"@roo-code/cloud\\\"\\n 16 | import { TelemetryService, PostHogTelemetryClient } from \\\"@roo-code/telemetry\\\"\\n----\\n 75 | \\t// Initialize Roo Code Cloud service.\\n 76 | \\tawait CloudService.createInstance(context, {\\n 77 | \\t\\tstateChanged: () => ClineProvider.getVisibleInstance()?.postStateToWebview(),\\n----\\n\\n# src/workers/countTokens.ts\\n 4 | \\n 5 | import { tiktoken } from \\\"../utils/tiktoken\\\"\\n 6 | \\n----\\n 10 | \\ttry {\\n 11 | \\t\\tconst count = await tiktoken(content)\\n 12 | \\t\\treturn { success: true, count }\\n----\\n\\n# src/api/index.ts\\n 50 | \\t/**\\n 51 | \\t * Counts tokens for content blocks\\n 52 | \\t * All providers extend BaseProvider which provides a default tiktoken implementation,\\n 53 | \\t * but they can override this to use their native token counting endpoints\\n 54 | \\t *\\n 55 | \\t * @param content The content to count tokens for\\n 56 | \\t * @returns A promise resolving to the token count\\n 57 | \\t */\\n----\\n\\n# src/extension/api.ts\\n219 | \\n220 | \\t\\t\\tcline.on(\\\"taskCompleted\\\", async (_, tokenUsage, toolUsage) => {\\n221 | \\t\\t\\t\\tthis.emit(RooCodeEventName.TaskCompleted, cline.taskId, tokenUsage, toolUsage)\\n222 | \\t\\t\\t\\tthis.taskMap.delete(cline.taskId)\\n----\\n224 | \\t\\t\\t\\tawait this.fileLog(\\n225 | \\t\\t\\t\\t\\t`[${new Date().toISOString()}] taskCompleted -> ${cline.taskId} | ${JSON.stringify(tokenUsage, null, 2)} | ${JSON.stringify(toolUsage, null, 2)}\\\\n`,\\n226 | \\t\\t\\t\\t)\\n----\\n\\n# src/api/providers/ollama.ts\\n 79 | \\t\\t\\t\\ttype: \\\"usage\\\",\\n 80 | \\t\\t\\t\\tinputTokens: lastUsage?.prompt_tokens || 0,\\n 81 | \\t\\t\\t\\toutputTokens: lastUsage?.completion_tokens || 0,\\n 82 | \\t\\t\\t}\\n----\\n\\n# src/core/checkpoints/index.ts\\n215 | \\t\\t\\t\\tJSON.stringify({\\n216 | \\t\\t\\t\\t\\ttokensIn: totalTokensIn,\\n217 | \\t\\t\\t\\t\\ttokensOut: totalTokensOut,\\n218 | \\t\\t\\t\\t\\tcacheWrites: totalCacheWrites,\\n----\\n\\n# src/utils/git.ts\\n 11 | \\tsubject: string\\n 12 | \\tauthor: string\\n 13 | \\tdate: string\\n----\\n 57 | \\t\\t\\tconst { stdout: hashStdout } = await execAsync(\\n 58 | \\t\\t\\t\\t`git log -n 10 --format=\\\"%H%n%h%n%s%n%an%n%ad\\\" --date=short ` + `--author-date-order ${query}`,\\n 59 | \\t\\t\\t\\t{ cwd },\\n----\\n 79 | \\t\\t\\t\\tsubject: lines[i + 2],\\n 80 | \\t\\t\\t\\tauthor: lines[i + 3],\\n 81 | \\t\\t\\t\\tdate: lines[i + 4],\\n----\\n107 | \\t\\t})\\n108 | \\t\\tconst [fullHash, shortHash, subject, author, date, body] = info.trim().split(\\\"\\\\n\\\")\\n109 | \\n----\\n115 | \\t\\t\\t`Commit: ${shortHash} (${fullHash})`,\\n116 | \\t\\t\\t`Author: ${author}`,\\n117 | \\t\\t\\t`Date: ${date}`,\\n----\\n\\n# src/api/providers/__tests__/bedrock-invokedModelId.spec.ts\\n176 | \\n177 | \\t\\t// The last usage event should have the token counts from the metadata\\n178 | \\t\\tconst lastUsageEvent = usageEvents[usageEvents.length - 1]\\n179 | \\t\\t// Expect the usage event to include all token information\\n180 | \\t\\texpect(lastUsageEvent).toMatchObject({\\n----\\n183 | \\t\\t\\toutputTokens: 200,\\n184 | \\t\\t\\t// Cache tokens may be present with default values\\n185 | \\t\\t\\tcacheReadTokens: expect.any(Number),\\n----\\n\\n# src/api/providers/__tests__/groq.spec.ts\\n101 | \\t\\t\\t\\t\\t\\t\\tdone: false,\\n102 | \\t\\t\\t\\t\\t\\t\\tvalue: { choices: [{ delta: {} }], usage: { prompt_tokens: 10, completion_tokens: 20 } },\\n103 | \\t\\t\\t\\t\\t\\t})\\n----\\n139 | \\t\\t\\t\\tmodel: modelId,\\n140 | \\t\\t\\t\\tmax_tokens: modelInfo.maxTokens,\\n141 | \\t\\t\\t\\ttemperature: 0.5,\\n----\\n\\n# src/api/providers/__tests__/gemini.spec.ts\\n167 | \\tdescribe(\\\"calculateCost\\\", () => {\\n168 | \\t\\t// Mock ModelInfo based on gemini-1.5-flash-latest pricing (per 1M tokens)\\n169 | \\t\\t// Removed 'id' and 'name' as they are not part of ModelInfo type directly\\n170 | \\t\\tconst mockInfo: ModelInfo = {\\n171 | \\t\\t\\tinputPrice: 0.125, // $/1M tokens\\n172 | \\t\\t\\toutputPrice: 0.375, // $/1M tokens\\n173 | \\t\\t\\tcacheWritesPrice: 0.125, // Assume same as input for test\\n----\\n179 | \\n180 | \\t\\tit(\\\"should calculate cost correctly based on input and output tokens\\\", () => {\\n181 | \\t\\t\\tconst inputTokens = 10000 // Use larger numbers for per-million pricing\\n----\\n190 | \\n191 | \\t\\tit(\\\"should return 0 if token counts are zero\\\", () => {\\n192 | \\t\\t\\t// Note: The method expects numbers, not undefined. Passing undefined would be a type error.\\n193 | \\t\\t\\t// The calculateCost method itself returns undefined if prices are missing, but 0 if tokens are 0 and prices exist.\\n194 | \\t\\t\\texpect(handler.calculateCost({ info: mockInfo, inputTokens: 0, outputTokens: 0 })).toBe(0)\\n----\\n196 | \\n197 | \\t\\tit(\\\"should handle only input tokens\\\", () => {\\n198 | \\t\\t\\tconst inputTokens = 5000\\n----\\n203 | \\n204 | \\t\\tit(\\\"should handle only output tokens\\\", () => {\\n205 | \\t\\t\\tconst outputTokens = 15000\\n----\\n210 | \\n211 | \\t\\tit(\\\"should calculate cost with cache write tokens\\\", () => {\\n212 | \\t\\t\\tconst inputTokens = 10000\\n----\\n227 | \\n228 | \\t\\tit(\\\"should calculate cost with cache read tokens\\\", () => {\\n229 | \\t\\t\\tconst inputTokens = 10000 // Total logical input\\n----\\n\\n# src/core/condense/index.ts\\n 55 | \\tcost: number // The cost of the summarization operation\\n 56 | \\tnewContextTokens?: number // The number of tokens in the context for the next API request\\n 57 | \\terror?: string // Populated iff the operation fails: error message shown to the user on failure (see Task.ts)\\n----\\n 63 | * @param {ApiMessage[]} messages - The conversation messages\\n 64 | * @param {ApiHandler} apiHandler - The API handler to use for token counting.\\n 65 | * @param {string} systemPrompt - The system prompt for API requests, which should be considered in the context token count\\n 66 | * @param {string} taskId - The task ID for the conversation, used for telemetry\\n----\\n 73 | * @param {ApiMessage[]} messages - The conversation messages\\n 74 | * @param {ApiHandler} apiHandler - The API handler to use for token counting (fallback if condensingApiHandler not provided)\\n 75 | * @param {string} systemPrompt - The system prompt for API requests (fallback if customCondensingPrompt not provided)\\n 76 | * @param {string} taskId - The task ID for the conversation, used for telemetry\\n 77 | * @param {number} prevContextTokens - The number of tokens currently in the context, used to ensure we don't grow the context\\n 78 | * @param {boolean} isAutomaticTrigger - Whether the summarization is triggered automatically\\n----\\n186 | \\n187 | \\t// Count the tokens in the context for the next API request\\n188 | \\t// We only estimate the tokens in summaryMesage if outputTokens is 0, otherwise we use outputTokens\\n189 | \\tconst systemPromptMessage: ApiMessage = { role: \\\"user\\\", content: systemPrompt }\\n----\\n\\n# src/api/providers/__tests__/vertex.spec.ts\\n 49 | \\t\\t\\t// The test expects 4 chunks:\\n 50 | \\t\\t\\t// 1. Usage chunk with input tokens\\n 51 | \\t\\t\\t// 2. Text chunk with \\\"Gemini response part 1\\\"\\n 52 | \\t\\t\\t// 3. Text chunk with \\\" part 2\\\"\\n 53 | \\t\\t\\t// 4. Usage chunk with output tokens\\n 54 | \\n----\\n\\n# src/api/providers/__tests__/ollama.spec.ts\\n 28 | \\t\\t\\t\\t\\t\\t\\t\\tusage: {\\n 29 | \\t\\t\\t\\t\\t\\t\\t\\t\\tprompt_tokens: 10,\\n 30 | \\t\\t\\t\\t\\t\\t\\t\\t\\tcompletion_tokens: 5,\\n 31 | \\t\\t\\t\\t\\t\\t\\t\\t\\ttotal_tokens: 15,\\n 32 | \\t\\t\\t\\t\\t\\t\\t\\t},\\n----\\n 54 | \\t\\t\\t\\t\\t\\t\\t\\t\\tusage: {\\n 55 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\tprompt_tokens: 10,\\n 56 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\tcompletion_tokens: 5,\\n 57 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\ttotal_tokens: 15,\\n 58 | \\t\\t\\t\\t\\t\\t\\t\\t\\t},\\n----\\n\\n# src/core/environment/getEnvironmentDetails.ts\\n192 | \\n193 | \\t// Add context tokens information.\\n194 | \\tconst { contextTokens, totalCost } = getApiMetrics(cline.clineMessages)\\n----\\n\\n# src/api/providers/__tests__/openai.spec.ts\\n 29 | \\t\\t\\t\\t\\t\\t\\t\\tusage: {\\n 30 | \\t\\t\\t\\t\\t\\t\\t\\t\\tprompt_tokens: 10,\\n 31 | \\t\\t\\t\\t\\t\\t\\t\\t\\tcompletion_tokens: 5,\\n 32 | \\t\\t\\t\\t\\t\\t\\t\\t\\ttotal_tokens: 15,\\n 33 | \\t\\t\\t\\t\\t\\t\\t\\t},\\n----\\n 55 | \\t\\t\\t\\t\\t\\t\\t\\t\\tusage: {\\n 56 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\tprompt_tokens: 10,\\n 57 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\tcompletion_tokens: 5,\\n 58 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\ttotal_tokens: 15,\\n 59 | \\t\\t\\t\\t\\t\\t\\t\\t\\t},\\n----\\n\\n# src/api/providers/__tests__/anthropic.spec.ts\\n 19 | \\t\\t\\t\\t\\t\\tusage: {\\n 20 | \\t\\t\\t\\t\\t\\t\\tinput_tokens: 10,\\n 21 | \\t\\t\\t\\t\\t\\t\\toutput_tokens: 5,\\n 22 | \\t\\t\\t\\t\\t\\t},\\n----\\n 30 | \\t\\t\\t\\t\\t\\t\\t\\tusage: {\\n 31 | \\t\\t\\t\\t\\t\\t\\t\\t\\tinput_tokens: 100,\\n 32 | \\t\\t\\t\\t\\t\\t\\t\\t\\toutput_tokens: 50,\\n 33 | \\t\\t\\t\\t\\t\\t\\t\\t\\tcache_creation_input_tokens: 20,\\n 34 | \\t\\t\\t\\t\\t\\t\\t\\t\\tcache_read_input_tokens: 10,\\n 35 | \\t\\t\\t\\t\\t\\t\\t\\t},\\n----\\n105 | \\n106 | \\t\\tit(\\\"use apiKey for passing token if anthropicUseAuthToken is not set\\\", () => {\\n107 | \\t\\t\\tconst handlerWithCustomUrl = new AnthropicHandler({\\n----\\n112 | \\t\\t\\texpect(mockAnthropicConstructor.mock.calls[0]![0]!.apiKey).toEqual(\\\"test-api-key\\\")\\n113 | \\t\\t\\texpect(mockAnthropicConstructor.mock.calls[0]![0]!.authToken).toBeUndefined()\\n114 | \\t\\t})\\n115 | \\n116 | \\t\\tit(\\\"use apiKey for passing token if anthropicUseAuthToken is set but custom base URL is not given\\\", () => {\\n117 | \\t\\t\\tconst handlerWithCustomUrl = new AnthropicHandler({\\n----\\n123 | \\t\\t\\texpect(mockAnthropicConstructor.mock.calls[0]![0]!.apiKey).toEqual(\\\"test-api-key\\\")\\n124 | \\t\\t\\texpect(mockAnthropicConstructor.mock.calls[0]![0]!.authToken).toBeUndefined()\\n125 | \\t\\t})\\n126 | \\n127 | \\t\\tit(\\\"use authToken for passing token if both of anthropicBaseUrl and anthropicUseAuthToken are set\\\", () => {\\n128 | \\t\\t\\tconst customBaseUrl = \\\"https://custom.anthropic.com\\\"\\n----\\n135 | \\t\\t\\texpect(mockAnthropicConstructor).toHaveBeenCalledTimes(1)\\n136 | \\t\\t\\texpect(mockAnthropicConstructor.mock.calls[0]![0]!.authToken).toEqual(\\\"test-api-key\\\")\\n137 | \\t\\t\\texpect(mockAnthropicConstructor.mock.calls[0]![0]!.apiKey).toBeUndefined()\\n----\\n190 | \\t\\t\\t\\tmessages: [{ role: \\\"user\\\", content: \\\"Test prompt\\\" }],\\n191 | \\t\\t\\t\\tmax_tokens: 8192,\\n192 | \\t\\t\\t\\ttemperature: 0,\\n----\\n\\n# src/api/providers/__tests__/xai.test.ts\\n228 | \\t\\t\\t\\t\\t\\t\\t\\tusage: {\\n229 | \\t\\t\\t\\t\\t\\t\\t\\t\\tprompt_tokens: 10,\\n230 | \\t\\t\\t\\t\\t\\t\\t\\t\\tcompletion_tokens: 20,\\n231 | \\t\\t\\t\\t\\t\\t\\t\\t\\tcache_read_input_tokens: 5,\\n232 | \\t\\t\\t\\t\\t\\t\\t\\t\\tcache_creation_input_tokens: 15,\\n233 | \\t\\t\\t\\t\\t\\t\\t\\t},\\n----\\n284 | \\t\\t\\t\\tmodel: modelId,\\n285 | \\t\\t\\t\\tmax_tokens: modelInfo.maxTokens,\\n286 | \\t\\t\\t\\ttemperature: 0,\\n----\\n\\n# src/api/providers/__tests__/openrouter.spec.ts\\n132 | \\t\\t\\t\\t\\t\\tchoices: [{ delta: {} }],\\n133 | \\t\\t\\t\\t\\t\\tusage: { prompt_tokens: 10, completion_tokens: 20, cost: 0.001 },\\n134 | \\t\\t\\t\\t\\t}\\n----\\n162 | \\t\\t\\t\\texpect.objectContaining({\\n163 | \\t\\t\\t\\t\\tmax_tokens: 8192,\\n164 | \\t\\t\\t\\t\\tmessages: [\\n----\\n285 | \\t\\t\\t\\tmodel: mockOptions.openRouterModelId,\\n286 | \\t\\t\\t\\tmax_tokens: 8192,\\n287 | \\t\\t\\t\\tthinking: undefined,\\n----\\n\\n# src/api/providers/unbound.ts\\n 22 | interface UnboundUsage extends OpenAI.CompletionUsage {\\n 23 | \\tcache_creation_input_tokens?: number\\n 24 | \\tcache_read_input_tokens?: number\\n 25 | }\\n----\\n 73 | \\n 74 | \\t\\t// Required by Anthropic; other providers default to max tokens allowed.\\n 75 | \\t\\tlet maxTokens: number | undefined\\n----\\n 82 | \\t\\t\\tmodel: modelId.split(\\\"/\\\")[1],\\n 83 | \\t\\t\\tmax_tokens: maxTokens,\\n 84 | \\t\\t\\tmessages: openAiMessages,\\n----\\n111 | \\t\\t\\t\\t\\ttype: \\\"usage\\\",\\n112 | \\t\\t\\t\\t\\tinputTokens: usage.prompt_tokens || 0,\\n113 | \\t\\t\\t\\t\\toutputTokens: usage.completion_tokens || 0,\\n114 | \\t\\t\\t\\t}\\n115 | \\n116 | \\t\\t\\t\\t// Only add cache tokens if they exist.\\n117 | \\t\\t\\t\\tif (usage.cache_creation_input_tokens) {\\n118 | \\t\\t\\t\\t\\tusageData.cacheWriteTokens = usage.cache_creation_input_tokens\\n119 | \\t\\t\\t\\t}\\n120 | \\n121 | \\t\\t\\t\\tif (usage.cache_read_input_tokens) {\\n122 | \\t\\t\\t\\t\\tusageData.cacheReadTokens = usage.cache_read_input_tokens\\n123 | \\t\\t\\t\\t}\\n----\\n146 | \\t\\t\\tif (modelId.startsWith(\\\"anthropic/\\\")) {\\n147 | \\t\\t\\t\\trequestOptions.max_tokens = info.maxTokens\\n148 | \\t\\t\\t}\\n----\\n\\n# src/api/providers/vscode-lm.ts\\n128 | \\t\\t\\t\\tmaxInputTokens: 8192,\\n129 | \\t\\t\\t\\tsendRequest: async (_messages, _options, _token) => {\\n130 | \\t\\t\\t\\t\\t// Provide a minimal implementation\\n----\\n179 | \\t * Implements the ApiHandler countTokens interface method\\n180 | \\t * Provides token counting for Anthropic content blocks\\n181 | \\t *\\n182 | \\t * @param content The content blocks to count tokens for\\n183 | \\t * @returns A promise resolving to the token count\\n184 | \\t */\\n185 | \\toverride async countTokens(content: Array): Promise {\\n186 | \\t\\t// Convert Anthropic content blocks to a string for VSCode LM token counting\\n187 | \\t\\tlet textContent = \\\"\\\"\\n----\\n201 | \\t/**\\n202 | \\t * Private implementation of token counting used internally by VsCodeLmHandler\\n203 | \\t */\\n----\\n206 | \\t\\tif (!this.client) {\\n207 | \\t\\t\\tconsole.warn(\\\"Roo Code : No client available for token counting\\\")\\n208 | \\t\\t\\treturn 0\\n----\\n211 | \\t\\tif (!this.currentRequestCancellation) {\\n212 | \\t\\t\\tconsole.warn(\\\"Roo Code : No cancellation token available for token counting\\\")\\n213 | \\t\\t\\treturn 0\\n----\\n217 | \\t\\tif (!text) {\\n218 | \\t\\t\\tconsole.debug(\\\"Roo Code : Empty text provided for token counting\\\")\\n219 | \\t\\t\\treturn 0\\n----\\n223 | \\t\\t\\t// Handle different input types\\n224 | \\t\\t\\tlet tokenCount: number\\n225 | \\n226 | \\t\\t\\tif (typeof text === \\\"string\\\") {\\n227 | \\t\\t\\t\\ttokenCount = await this.client.countTokens(text, this.currentRequestCancellation.token)\\n228 | \\t\\t\\t} else if (text instanceof vscode.LanguageModelChatMessage) {\\n----\\n233 | \\t\\t\\t\\t}\\n234 | \\t\\t\\t\\ttokenCount = await this.client.countTokens(text, this.currentRequestCancellation.token)\\n235 | \\t\\t\\t} else {\\n236 | \\t\\t\\t\\tconsole.warn(\\\"Roo Code : Invalid input type for token counting\\\")\\n237 | \\t\\t\\t\\treturn 0\\n----\\n240 | \\t\\t\\t// Validate the result\\n241 | \\t\\t\\tif (typeof tokenCount !== \\\"number\\\") {\\n242 | \\t\\t\\t\\tconsole.warn(\\\"Roo Code : Non-numeric token count received:\\\", tokenCount)\\n243 | \\t\\t\\t\\treturn 0\\n----\\n245 | \\n246 | \\t\\t\\tif (tokenCount < 0) {\\n247 | \\t\\t\\t\\tconsole.warn(\\\"Roo Code : Negative token count received:\\\", tokenCount)\\n248 | \\t\\t\\t\\treturn 0\\n----\\n250 | \\n251 | \\t\\t\\treturn tokenCount\\n252 | \\t\\t} catch (error) {\\n----\\n278 | \\n279 | \\t\\treturn systemTokens + messageTokens.reduce((sum: number, tokens: number): number => sum + tokens, 0)\\n280 | \\t}\\n----\\n357 | \\n358 | \\t\\t// Initialize cancellation token for the request\\n359 | \\t\\tthis.currentRequestCancellation = new vscode.CancellationTokenSource()\\n360 | \\n361 | \\t\\t// Calculate input tokens before starting the stream\\n362 | \\t\\tconst totalInputTokens: number = await this.calculateTotalInputTokens(systemPrompt, vsCodeLmMessages)\\n363 | \\n364 | \\t\\t// Accumulate the text and count at the end of the stream to reduce token counting overhead.\\n365 | \\t\\tlet accumulatedText: string = \\\"\\\"\\n----\\n378 | \\t\\t\\t\\trequestOptions,\\n379 | \\t\\t\\t\\tthis.currentRequestCancellation.token,\\n380 | \\t\\t\\t)\\n----\\n446 | \\n447 | \\t\\t\\t// Count tokens in the accumulated text after stream completion\\n448 | \\t\\t\\tconst totalOutputTokens: number = await this.internalCountTokens(accumulatedText)\\n----\\n511 | \\t\\t\\tconst modelInfo: ModelInfo = {\\n512 | \\t\\t\\t\\tmaxTokens: -1, // Unlimited tokens by default\\n513 | \\t\\t\\t\\tcontextWindow:\\n----\\n548 | \\t\\t\\t\\t{},\\n549 | \\t\\t\\t\\tnew vscode.CancellationTokenSource().token,\\n550 | \\t\\t\\t)\\n----\\n\\n# src/core/condense/__tests__/index.test.ts\\n203 | \\n204 | \\t\\t// Check the cost and token counts\\n205 | \\t\\texpect(result.cost).toBe(0.05)\\n206 | \\t\\texpect(result.summary).toBe(\\\"This is a summary\\\")\\n207 | \\t\\texpect(result.newContextTokens).toBe(250) // 150 output tokens + 100 from countTokens\\n208 | \\t\\texpect(result.error).toBeUndefined()\\n----\\n317 | \\t\\t// Check the newContextTokens calculation includes system prompt\\n318 | \\t\\texpect(result.newContextTokens).toBe(300) // 200 output tokens + 100 from countTokens\\n319 | \\t\\texpect(result.cost).toBe(0.06)\\n----\\n323 | \\n324 | \\tit(\\\"should return error when new context tokens >= previous context tokens\\\", async () => {\\n325 | \\t\\tconst messages: ApiMessage[] = [\\n----\\n336 | \\t\\tconst streamWithLargeTokens = (async function* () {\\n337 | \\t\\t\\tyield { type: \\\"text\\\" as const, text: \\\"This is a very long summary that uses many tokens\\\" }\\n338 | \\t\\t\\tyield { type: \\\"usage\\\" as const, totalCost: 0.08, outputTokens: 500 }\\n----\\n364 | \\n365 | \\tit(\\\"should successfully summarize when new context tokens < previous context tokens\\\", async () => {\\n366 | \\t\\tconst messages: ApiMessage[] = [\\n----\\n375 | \\n376 | \\t\\t// Create a stream that produces a summary with reasonable token count\\n377 | \\t\\tconst streamWithSmallTokens = (async function* () {\\n----\\n401 | \\t\\texpect(result.error).toBeUndefined()\\n402 | \\t\\texpect(result.newContextTokens).toBe(80) // 50 output tokens + 30 from countTokens\\n403 | \\t\\texpect(result.newContextTokens).toBeLessThan(prevContextTokens)\\n----\\n\\n# src/api/providers/__tests__/lmstudio.test.ts\\n 25 | \\t\\t\\t\\t\\t\\t\\t\\tusage: {\\n 26 | \\t\\t\\t\\t\\t\\t\\t\\t\\tprompt_tokens: 10,\\n 27 | \\t\\t\\t\\t\\t\\t\\t\\t\\tcompletion_tokens: 5,\\n 28 | \\t\\t\\t\\t\\t\\t\\t\\t\\ttotal_tokens: 15,\\n 29 | \\t\\t\\t\\t\\t\\t\\t\\t},\\n----\\n 51 | \\t\\t\\t\\t\\t\\t\\t\\t\\tusage: {\\n 52 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\tprompt_tokens: 10,\\n 53 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\tcompletion_tokens: 5,\\n 54 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\ttotal_tokens: 15,\\n 55 | \\t\\t\\t\\t\\t\\t\\t\\t\\t},\\n----\\n\\n# src/api/providers/__tests__/bedrock-reasoning.test.ts\\n 94 | \\t\\t\\t\\ttype: \\\"enabled\\\",\\n 95 | \\t\\t\\t\\tbudget_tokens: 4096, // Uses the full modelMaxThinkingTokens value\\n 96 | \\t\\t\\t})\\n----\\n143 | \\t\\t\\t\\ttype: \\\"enabled\\\",\\n144 | \\t\\t\\t\\tbudget_tokens: 8192,\\n145 | \\t\\t\\t})\\n----\\n266 | \\t\\t\\t\\ttype: \\\"enabled\\\",\\n267 | \\t\\t\\t\\tbudget_tokens: 4096,\\n268 | \\t\\t\\t})\\n----\\n\\n# src/api/providers/lm-studio.ts\\n 40 | \\t\\t// -------------------------\\n 41 | \\t\\t// Track token usage\\n 42 | \\t\\t// -------------------------\\n----\\n 68 | \\t\\t} catch (err) {\\n 69 | \\t\\t\\tconsole.error(\\\"[LmStudio] Failed to count input tokens:\\\", err)\\n 70 | \\t\\t\\tinputTokens = 0\\n----\\n116 | \\t\\t\\t} catch (err) {\\n117 | \\t\\t\\t\\tconsole.error(\\\"[LmStudio] Failed to count output tokens:\\\", err)\\n118 | \\t\\t\\t\\toutputTokens = 0\\n----\\n\\n# src/utils/countTokens.ts\\n 4 | import { countTokensResultSchema } from \\\"../workers/types\\\"\\n 5 | import { tiktoken } from \\\"./tiktoken\\\"\\n 6 | \\n----\\n 27 | \\tif (!useWorker || !pool) {\\n 28 | \\t\\treturn tiktoken(content)\\n 29 | \\t}\\n----\\n 42 | \\t\\tconsole.error(error)\\n 43 | \\t\\treturn tiktoken(content)\\n 44 | \\t}\\n----\\n\\n# src/api/providers/__tests__/requesty.spec.ts\\n124 | \\t\\t\\t\\t\\t\\tusage: {\\n125 | \\t\\t\\t\\t\\t\\t\\tprompt_tokens: 10,\\n126 | \\t\\t\\t\\t\\t\\t\\tcompletion_tokens: 20,\\n127 | \\t\\t\\t\\t\\t\\t\\tprompt_tokens_details: {\\n128 | \\t\\t\\t\\t\\t\\t\\t\\tcaching_tokens: 5,\\n129 | \\t\\t\\t\\t\\t\\t\\t\\tcached_tokens: 2,\\n130 | \\t\\t\\t\\t\\t\\t\\t},\\n----\\n162 | \\t\\t\\t\\texpect.objectContaining({\\n163 | \\t\\t\\t\\t\\tmax_tokens: 8192,\\n164 | \\t\\t\\t\\t\\tmessages: [\\n----\\n204 | \\t\\t\\t\\tmodel: mockOptions.requestyModelId,\\n205 | \\t\\t\\t\\tmax_tokens: 8192,\\n206 | \\t\\t\\t\\tmessages: [{ role: \\\"system\\\", content: \\\"test prompt\\\" }],\\n----\\n\\n# src/api/providers/__tests__/vscode-lm.test.ts\\n 28 | \\t\\tCancellationTokenSource: jest.fn(() => ({\\n 29 | \\t\\t\\ttoken: {\\n 30 | \\t\\t\\t\\tisCancellationRequested: false,\\n----\\n\\n# src/api/providers/__tests__/chutes.spec.ts\\n 49 | \\t\\t\\t\\t\\tusage: {\\n 50 | \\t\\t\\t\\t\\t\\tprompt_tokens: 10,\\n 51 | \\t\\t\\t\\t\\t\\tcompletion_tokens: 5,\\n 52 | \\t\\t\\t\\t\\t\\ttotal_tokens: 15,\\n 53 | \\t\\t\\t\\t\\t},\\n----\\n103 | \\t\\t\\t\\t\\t],\\n104 | \\t\\t\\t\\t\\tusage: { prompt_tokens: 10, completion_tokens: 5 },\\n105 | \\t\\t\\t\\t}\\n----\\n211 | \\t\\t\\t\\t\\t\\t\\tdone: false,\\n212 | \\t\\t\\t\\t\\t\\t\\tvalue: { choices: [{ delta: {} }], usage: { prompt_tokens: 10, completion_tokens: 20 } },\\n213 | \\t\\t\\t\\t\\t\\t})\\n----\\n284 | \\t\\t\\t\\tmodel: modelId,\\n285 | \\t\\t\\t\\tmax_tokens: modelInfo.maxTokens,\\n286 | \\t\\t\\t\\ttemperature: 0.5,\\n----\\n\\n# src/api/providers/__tests__/openai-native.spec.ts\\n 29 | \\t\\t\\t\\t\\t\\t\\t\\tusage: {\\n 30 | \\t\\t\\t\\t\\t\\t\\t\\t\\tprompt_tokens: 10,\\n 31 | \\t\\t\\t\\t\\t\\t\\t\\t\\tcompletion_tokens: 5,\\n 32 | \\t\\t\\t\\t\\t\\t\\t\\t\\ttotal_tokens: 15,\\n 33 | \\t\\t\\t\\t\\t\\t\\t\\t},\\n----\\n 55 | \\t\\t\\t\\t\\t\\t\\t\\t\\tusage: {\\n 56 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\tprompt_tokens: 10,\\n 57 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\tcompletion_tokens: 5,\\n 58 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\ttotal_tokens: 15,\\n 59 | \\t\\t\\t\\t\\t\\t\\t\\t\\t},\\n----\\n145 | \\t\\t\\t\\t\\t\\tusage: {\\n146 | \\t\\t\\t\\t\\t\\t\\tprompt_tokens: 0,\\n147 | \\t\\t\\t\\t\\t\\t\\tcompletion_tokens: 0,\\n148 | \\t\\t\\t\\t\\t\\t\\ttotal_tokens: 0,\\n149 | \\t\\t\\t\\t\\t\\t},\\n----\\n215 | \\t\\t\\t\\t{ choices: [{ delta: { content: \\\" there\\\" } }], usage: null },\\n216 | \\t\\t\\t\\t{ choices: [{ delta: { content: \\\"!\\\" } }], usage: { prompt_tokens: 10, completion_tokens: 5 } },\\n217 | \\t\\t\\t]\\n----\\n261 | \\t\\t\\t\\t{ choices: [{ delta: { content: null } }], usage: null },\\n262 | \\t\\t\\t\\t{ choices: [{ delta: { content: \\\"Hello\\\" } }], usage: { prompt_tokens: 10, completion_tokens: 5 } },\\n263 | \\t\\t\\t]\\n----\\n\\n# src/api/providers/__tests__/deepseek.test.ts\\n 28 | \\t\\t\\t\\t\\t\\t\\t\\tusage: {\\n 29 | \\t\\t\\t\\t\\t\\t\\t\\t\\tprompt_tokens: 10,\\n 30 | \\t\\t\\t\\t\\t\\t\\t\\t\\tcompletion_tokens: 5,\\n 31 | \\t\\t\\t\\t\\t\\t\\t\\t\\ttotal_tokens: 15,\\n 32 | \\t\\t\\t\\t\\t\\t\\t\\t\\tprompt_tokens_details: {\\n 33 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\tcache_miss_tokens: 8,\\n 34 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\tcached_tokens: 2,\\n 35 | \\t\\t\\t\\t\\t\\t\\t\\t\\t},\\n----\\n 59 | \\t\\t\\t\\t\\t\\t\\t\\t\\tusage: {\\n 60 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\tprompt_tokens: 10,\\n 61 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\tcompletion_tokens: 5,\\n 62 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\ttotal_tokens: 15,\\n 63 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\tprompt_tokens_details: {\\n 64 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\t\\tcache_miss_tokens: 8,\\n 65 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\t\\tcached_tokens: 2,\\n 66 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\t},\\n----\\n261 | \\t\\t\\tconst usage = {\\n262 | \\t\\t\\t\\tprompt_tokens: 100,\\n263 | \\t\\t\\t\\tcompletion_tokens: 50,\\n264 | \\t\\t\\t\\ttotal_tokens: 150,\\n265 | \\t\\t\\t\\tprompt_tokens_details: {\\n266 | \\t\\t\\t\\t\\tcache_miss_tokens: 80,\\n267 | \\t\\t\\t\\t\\tcached_tokens: 20,\\n268 | \\t\\t\\t\\t},\\n----\\n289 | \\t\\t\\tconst usage = {\\n290 | \\t\\t\\t\\tprompt_tokens: 100,\\n291 | \\t\\t\\t\\tcompletion_tokens: 50,\\n292 | \\t\\t\\t\\ttotal_tokens: 150,\\n293 | \\t\\t\\t\\t// No prompt_tokens_details\\n294 | \\t\\t\\t}\\n----\\n\\n# src/api/transform/cache-strategy/multi-point-strategy.ts\\n 7 | * Strategy for handling multiple cache points.\\n 8 | * Creates cache points after messages as soon as uncached tokens exceed minimumTokenCount.\\n 9 | */\\n----\\n 58 | \\t *\\n 59 | \\t * @param minTokensPerPoint Minimum tokens required per cache point\\n 60 | \\t * @param remainingCachePoints Number of cache points available\\n----\\n 97 | \\n 98 | \\t\\t// Calculate tokens in new messages (added since last cache point placement)\\n 99 | \\t\\tconst lastPreviousIndex = previousPlacements[previousPlacements.length - 1].index\\n----\\n103 | \\n104 | \\t\\t// If new messages have enough tokens for a cache point, we need to decide\\n105 | \\t\\t// whether to keep all previous cache points or combine some\\n----\\n127 | \\t\\t\\t\\t// We need to decide which previous cache points to keep and which to combine\\n128 | \\t\\t\\t\\t// Strategy: Compare the token count of new messages with the smallest combined token gap\\n129 | \\n130 | \\t\\t\\t\\t// First, analyze the token distribution between previous cache points\\n131 | \\t\\t\\t\\tconst tokensBetweenPlacements: number[] = []\\n132 | \\t\\t\\t\\tlet startIdx = 0\\n----\\n134 | \\t\\t\\t\\tfor (const placement of previousPlacements) {\\n135 | \\t\\t\\t\\t\\tconst tokens = this.config.messages\\n136 | \\t\\t\\t\\t\\t\\t.slice(startIdx, placement.index + 1)\\n----\\n138 | \\n139 | \\t\\t\\t\\t\\ttokensBetweenPlacements.push(tokens)\\n140 | \\t\\t\\t\\t\\tstartIdx = placement.index + 1\\n----\\n142 | \\n143 | \\t\\t\\t\\t// Find the two consecutive placements with the smallest token gap\\n144 | \\t\\t\\t\\tlet smallestGapIndex = 0\\n----\\n146 | \\n147 | \\t\\t\\t\\tfor (let i = 0; i < tokensBetweenPlacements.length - 1; i++) {\\n148 | \\t\\t\\t\\t\\tconst gap = tokensBetweenPlacements[i] + tokensBetweenPlacements[i + 1]\\n149 | \\t\\t\\t\\t\\tif (gap < smallestGap) {\\n----\\n155 | \\t\\t\\t\\t// Only combine cache points if it's beneficial\\n156 | \\t\\t\\t\\t// Compare the token count of new messages with the smallest combined token gap\\n157 | \\t\\t\\t\\t// Apply a required percentage increase to ensure reallocation is worth it\\n----\\n161 | \\t\\t\\t\\tif (newMessagesTokens >= requiredTokenThreshold) {\\n162 | \\t\\t\\t\\t\\t// It's beneficial to combine cache points since new messages have significantly more tokens\\n163 | \\t\\t\\t\\t\\tlogger.info(\\\"Combining cache points is beneficial\\\", {\\n----\\n231 | \\t\\t} else {\\n232 | \\t\\t\\t// New messages don't have enough tokens for a cache point\\n233 | \\t\\t\\t// Keep all previous placements that are still valid\\n----\\n266 | \\t\\tif (lastUserMessageIndex >= 0) {\\n267 | \\t\\t\\t// Calculate the total tokens covered from the previous cache point (or start of conversation)\\n268 | \\t\\t\\t// to this cache point. This ensures tokensCovered represents the full span of tokens\\n269 | \\t\\t\\t// that will be cached by this cache point.\\n----\\n281 | \\n282 | \\t\\t\\t// Calculate tokens from previous cache point (or start) to this cache point\\n283 | \\t\\t\\tconst tokenStartIndex = previousCachePointIndex + 1\\n284 | \\t\\t\\ttotalTokensCovered = this.config.messages\\n285 | \\t\\t\\t\\t.slice(tokenStartIndex, lastUserMessageIndex + 1)\\n286 | \\t\\t\\t\\t.reduce((acc, curr) => acc + this.estimateTokenCount(curr), 0)\\n287 | \\n288 | \\t\\t\\t// Guard clause: ensure we have enough tokens to justify a cache point\\n289 | \\t\\t\\tif (totalTokensCovered < minTokensPerPoint) {\\n----\\n294 | \\t\\t\\t\\ttype: \\\"message\\\",\\n295 | \\t\\t\\t\\ttokensCovered: totalTokensCovered,\\n296 | \\t\\t\\t}\\n----\\n\\n# src/api/transform/model-params.ts\\n 95 | \\n 96 | \\t\\t// Reasoning cannot be less than 1024 tokens.\\n 97 | \\t\\tif (reasoningBudget < 1024) {\\n----\\n\\n# src/api/transform/cache-strategy/types.ts\\n 7 | export interface ModelInfo {\\n 8 | \\t/** Maximum number of tokens the model can generate */\\n 9 | \\tmaxTokens: number\\n 10 | \\t/** Maximum context window size in tokens */\\n 11 | \\tcontextWindow: number\\n----\\n 15 | \\tmaxCachePoints: number\\n 16 | \\t/** Minimum number of tokens required for a cache point */\\n 17 | \\tminTokensPerCachePoint: number\\n----\\n 49 | \\ttype: \\\"system\\\" | \\\"message\\\"\\n 50 | \\t/** Number of tokens this cache point covers */\\n 51 | \\ttokensCovered: number\\n 52 | }\\n----\\n\\n# src/api/transform/__tests__/reasoning.spec.ts\\n 40 | \\n 41 | \\t\\t\\texpect(result).toEqual({ max_tokens: 1000 })\\n 42 | \\t\\t})\\n----\\n 61 | \\n 62 | \\t\\t\\texpect(result).toEqual({ max_tokens: 1000 })\\n 63 | \\t\\t})\\n----\\n123 | \\n124 | \\t\\t\\texpect(result).toEqual({ max_tokens: 1000 })\\n125 | \\t\\t})\\n----\\n140 | \\n141 | \\t\\t\\texpect(result).toEqual({ max_tokens: undefined })\\n142 | \\t\\t})\\n----\\n189 | \\n190 | \\t\\t\\texpect(result).toEqual({ max_tokens: 0 })\\n191 | \\t\\t})\\n----\\n244 | \\t\\t\\t\\ttype: \\\"enabled\\\",\\n245 | \\t\\t\\t\\tbudget_tokens: 1000,\\n246 | \\t\\t\\t})\\n----\\n268 | \\t\\t\\t\\ttype: \\\"enabled\\\",\\n269 | \\t\\t\\t\\tbudget_tokens: 1000,\\n270 | \\t\\t\\t})\\n----\\n314 | \\t\\t\\t\\ttype: \\\"enabled\\\",\\n315 | \\t\\t\\t\\tbudget_tokens: undefined,\\n316 | \\t\\t\\t})\\n----\\n334 | \\t\\t\\t\\ttype: \\\"enabled\\\",\\n335 | \\t\\t\\t\\tbudget_tokens: 0,\\n336 | \\t\\t\\t})\\n----\\n354 | \\t\\t\\t\\ttype: \\\"enabled\\\",\\n355 | \\t\\t\\t\\tbudget_tokens: 100000,\\n356 | \\t\\t\\t})\\n----\\n374 | \\t\\t\\t\\ttype: \\\"enabled\\\",\\n375 | \\t\\t\\t\\tbudget_tokens: 1000,\\n376 | \\t\\t\\t})\\n----\\n547 | \\n548 | \\t\\t\\texpect(openRouterResult).toEqual({ max_tokens: 1000 })\\n549 | \\t\\t\\texpect(anthropicResult).toEqual({ type: \\\"enabled\\\", budget_tokens: 1000 })\\n550 | \\t\\t\\texpect(openAiResult).toBeUndefined()\\n----\\n601 | \\t\\t\\t// Budget should take precedence for OpenRouter and Anthropic\\n602 | \\t\\t\\texpect(openRouterResult).toEqual({ max_tokens: 1000 })\\n603 | \\t\\t\\texpect(anthropicResult).toEqual({ type: \\\"enabled\\\", budget_tokens: 1000 })\\n604 | \\t\\t\\t// OpenAI should still use effort since it doesn't support budget\\n----\\n672 | \\t\\t\\t\\texpect(typeof result).toBe(\\\"object\\\")\\n673 | \\t\\t\\t\\texpect(\\\"max_tokens\\\" in result || \\\"effort\\\" in result || \\\"exclude\\\" in result).toBe(true)\\n674 | \\t\\t\\t}\\n----\\n688 | \\t\\t\\t\\texpect(result).toHaveProperty(\\\"type\\\", \\\"enabled\\\")\\n689 | \\t\\t\\t\\texpect(result).toHaveProperty(\\\"budget_tokens\\\")\\n690 | \\t\\t\\t}\\n----\\n\\n# src/api/transform/__tests__/model-params.spec.ts\\n205 | \\t\\t\\t\\t\\ttype: \\\"enabled\\\",\\n206 | \\t\\t\\t\\t\\tbudget_tokens: 1600,\\n207 | \\t\\t\\t\\t},\\n----\\n226 | \\t\\t\\t\\ttype: \\\"enabled\\\",\\n227 | \\t\\t\\t\\tbudget_tokens: 1600,\\n228 | \\t\\t\\t})\\n----\\n262 | \\t\\t\\t\\t\\ttype: \\\"enabled\\\",\\n263 | \\t\\t\\t\\t\\tbudget_tokens: 2400,\\n264 | \\t\\t\\t\\t},\\n----\\n287 | \\t\\t\\t\\t\\ttype: \\\"enabled\\\",\\n288 | \\t\\t\\t\\t\\tbudget_tokens: 1500,\\n289 | \\t\\t\\t\\t},\\n----\\n308 | \\n309 | \\t\\tit(\\\"should clamp thinking budget to at least 1024 tokens\\\", () => {\\n310 | \\t\\t\\tconst model: ModelInfo = {\\n----\\n328 | \\t\\t\\t\\t\\ttype: \\\"enabled\\\",\\n329 | \\t\\t\\t\\t\\tbudget_tokens: 1024,\\n330 | \\t\\t\\t\\t},\\n----\\n333 | \\n334 | \\t\\tit(\\\"should clamp thinking budget to at most 80% of max tokens\\\", () => {\\n335 | \\t\\t\\tconst model: ModelInfo = {\\n----\\n353 | \\t\\t\\t\\t\\ttype: \\\"enabled\\\",\\n354 | \\t\\t\\t\\t\\tbudget_tokens: 3200,\\n355 | \\t\\t\\t\\t},\\n----\\n372 | \\t\\t\\t\\t\\ttype: \\\"enabled\\\",\\n373 | \\t\\t\\t\\t\\tbudget_tokens: DEFAULT_HYBRID_REASONING_MODEL_THINKING_TOKENS,\\n374 | \\t\\t\\t\\t},\\n----\\n390 | \\t\\t\\texpect(result.maxTokens).toBe(5000)\\n391 | \\t\\t\\texpect(result.reasoningBudget).toBe(2000) // Custom thinking tokens takes precedence\\n392 | \\t\\t})\\n393 | \\n394 | \\t\\tit(\\\"should clamp custom thinking tokens even when custom max tokens is provided\\\", () => {\\n395 | \\t\\t\\tconst model: ModelInfo = {\\n----\\n719 | \\n720 | \\t\\t\\texpect(result.reasoning).toEqual({ max_tokens: 3200 })\\n721 | \\t\\t})\\n----\\n\\n# src/api/transform/cache-strategy/base-strategy.ts\\n 27 | \\t/**\\n 28 | \\t * Calculate token count for system prompt using a more accurate approach\\n 29 | \\t */\\n----\\n 33 | \\n 34 | \\t\\t\\t// Use a more accurate token estimation than simple character count\\n 35 | \\t\\t\\t// Count words and add overhead for punctuation and special tokens\\n 36 | \\t\\t\\tconst words = text.split(/\\\\s+/).filter((word) => word.length > 0)\\n 37 | \\t\\t\\t// Average English word is ~1.3 tokens\\n 38 | \\t\\t\\tlet tokenCount = words.length * 1.3\\n 39 | \\t\\t\\t// Add overhead for punctuation and special characters\\n 40 | \\t\\t\\ttokenCount += (text.match(/[.,!?;:()[\\\\]{}\\\"\\\"''`]/g) || []).length * 0.3\\n 41 | \\t\\t\\t// Add overhead for newlines\\n 42 | \\t\\t\\ttokenCount += (text.match(/\\\\n/g) || []).length * 0.5\\n 43 | \\t\\t\\t// Add a small overhead for system prompt structure\\n 44 | \\t\\t\\ttokenCount += 5\\n 45 | \\n 46 | \\t\\t\\tthis.systemTokenCount = Math.ceil(tokenCount)\\n 47 | \\t\\t}\\n----\\n 84 | \\t/**\\n 85 | \\t * Check if a token count meets the minimum threshold for caching\\n 86 | \\t */\\n 87 | \\tprotected meetsMinTokenThreshold(tokenCount: number): boolean {\\n 88 | \\t\\tconst minTokens = this.config.modelInfo.minTokensPerCachePoint\\n----\\n 91 | \\t\\t}\\n 92 | \\t\\treturn tokenCount >= minTokens\\n 93 | \\t}\\n----\\n 95 | \\t/**\\n 96 | \\t * Estimate token count for a message using a more accurate approach\\n 97 | \\t * This implementation is based on the BaseProvider's countTokens method\\n----\\n100 | \\tprotected estimateTokenCount(message: Anthropic.Messages.MessageParam): number {\\n101 | \\t\\t// Use a more sophisticated token counting approach\\n102 | \\t\\tif (!message.content) return 0\\n----\\n108 | \\t\\t\\t\\tif (block.type === \\\"text\\\") {\\n109 | \\t\\t\\t\\t\\t// Use a more accurate token estimation than simple character count\\n110 | \\t\\t\\t\\t\\t// This is still an approximation but better than character/4\\n----\\n112 | \\t\\t\\t\\t\\tif (text.length > 0) {\\n113 | \\t\\t\\t\\t\\t\\t// Count words and add overhead for punctuation and special tokens\\n114 | \\t\\t\\t\\t\\t\\tconst words = text.split(/\\\\s+/).filter((word) => word.length > 0)\\n115 | \\t\\t\\t\\t\\t\\t// Average English word is ~1.3 tokens\\n116 | \\t\\t\\t\\t\\t\\ttotalTokens += words.length * 1.3\\n----\\n128 | \\t\\t\\tconst text = message.content\\n129 | \\t\\t\\t// Count words and add overhead for punctuation and special tokens\\n130 | \\t\\t\\tconst words = text.split(/\\\\s+/).filter((word) => word.length > 0)\\n131 | \\t\\t\\t// Average English word is ~1.3 tokens\\n132 | \\t\\t\\ttotalTokens += words.length * 1.3\\n----\\n\\n# src/api/providers/__tests__/glama.spec.ts\\n 57 | \\t\\t\\t\\t\\t\\t\\t\\t\\tchoices: [{ delta: {}, index: 0 }],\\n 58 | \\t\\t\\t\\t\\t\\t\\t\\t\\tusage: { prompt_tokens: 10, completion_tokens: 5, total_tokens: 15 },\\n 59 | \\t\\t\\t\\t\\t\\t\\t\\t}\\n----\\n112 | \\t\\t\\tusage: {\\n113 | \\t\\t\\t\\tprompt_tokens: 10,\\n114 | \\t\\t\\t\\tcompletion_tokens: 5,\\n115 | \\t\\t\\t\\ttotal_tokens: 15,\\n116 | \\t\\t\\t},\\n----\\n176 | \\t\\t\\t\\t\\ttemperature: 0,\\n177 | \\t\\t\\t\\t\\tmax_tokens: 8192,\\n178 | \\t\\t\\t\\t}),\\n----\\n194 | \\n195 | \\t\\tit(\\\"should not set max_tokens for non-Anthropic models\\\", async () => {\\n196 | \\t\\t\\t// Reset mock to clear any previous calls\\n----\\n213 | \\t\\t\\t)\\n214 | \\t\\t\\texpect(mockCreate.mock.calls[0][0]).not.toHaveProperty(\\\"max_tokens\\\")\\n215 | \\t\\t})\\n----\\n\\n# src/api/transform/__tests__/stream.spec.ts\\n 33 | \\n 34 | \\t\\tit(\\\"should handle usage chunks without cache tokens\\\", () => {\\n 35 | \\t\\t\\tconst usageChunk: ApiStreamChunk = {\\n----\\n 57 | \\n 58 | \\t\\tit(\\\"should handle usage chunks with zero tokens\\\", () => {\\n 59 | \\t\\t\\tconst zeroUsageChunk: ApiStreamChunk = {\\n----\\n 69 | \\n 70 | \\t\\tit(\\\"should handle usage chunks with large token counts\\\", () => {\\n 71 | \\t\\t\\tconst largeUsageChunk: ApiStreamChunk = {\\n----\\n\\n# src/api/transform/cache-strategy/__tests__/cache-strategy.spec.ts\\n 29 | \\n 30 | const createMessageWithTokens = (role: \\\"user\\\" | \\\"assistant\\\", tokenCount: number) => ({\\n 31 | \\trole,\\n 32 | \\tcontent: \\\"x\\\".repeat(tokenCount * 4), // Approximate 4 chars per token\\n 33 | })\\n----\\n130 | \\t\\t\\t\\tit(\\\"adds system cache block when prompt caching is enabled, messages exist, and system prompt is long enough\\\", () => {\\n131 | \\t\\t\\t\\t\\t// Create a system prompt that's at least 50 tokens (200+ characters)\\n132 | \\t\\t\\t\\t\\tconst longSystemPrompt =\\n----\\n236 | \\t\\t\\t\\t\\t\\t\\t\\t(i + 1) +\\n237 | \\t\\t\\t\\t\\t\\t\\t\\t\\\" with some additional text to increase token count. \\\" +\\n238 | \\t\\t\\t\\t\\t\\t\\t\\t\\\"Adding more text to ensure we exceed the token threshold for cache block insertion.\\\",\\n239 | \\t\\t\\t\\t\\t\\t}))\\n----\\n579 | \\n580 | \\t\\tit(\\\"should yield usage results with cache tokens when using MultiPointStrategy\\\", async () => {\\n581 | \\t\\t\\t// Mock the convertToBedrockConverseMessages method to return a result with cache points\\n----\\n589 | \\n590 | \\t\\t\\t// Create a mock stream that includes cache token fields\\n591 | \\t\\t\\tconst mockApiResponse = {\\n----\\n626 | \\n627 | \\t\\t\\t// Verify that usage results with cache tokens are yielded\\n628 | \\t\\t\\texpect(chunks.length).toBeGreaterThan(0)\\n629 | \\t\\t\\t// The test already expects cache tokens, but the implementation might not be including them\\n630 | \\t\\t\\t// Let's make the test more flexible to accept either format\\n----\\n652 | \\n653 | \\t\\t// Helper function to create a message with approximate token count\\n654 | \\t\\tconst createMessage = (role: \\\"user\\\" | \\\"assistant\\\", content: string, tokenCount: number) => {\\n655 | \\t\\t\\t// Pad the content to reach the desired token count (approx 4 chars per token)\\n656 | \\t\\t\\tconst paddingNeeded = Math.max(0, tokenCount * 4 - content.length)\\n657 | \\t\\t\\tconst padding = \\\" \\\".repeat(paddingNeeded)\\n----\\n667 | \\t\\t\\t\\t\\\"Cache point placements:\\\",\\n668 | \\t\\t\\t\\tplacements.map((p) => `index: ${p.index}, tokens: ${p.tokensCovered}`),\\n669 | \\t\\t\\t)\\n----\\n683 | \\t\\t\\t\\t\\tmodelInfo: multiPointModelInfo,\\n684 | \\t\\t\\t\\t\\tsystemPrompt: \\\"You are a helpful assistant.\\\", // ~10 tokens\\n685 | \\t\\t\\t\\t\\tmessages,\\n----\\n730 | \\t\\t\\t\\t\\t\\ttype: \\\"message\\\",\\n731 | \\t\\t\\t\\t\\t\\ttokensCovered: 300,\\n732 | \\t\\t\\t\\t\\t},\\n----\\n736 | \\t\\t\\t\\t\\tmodelInfo: multiPointModelInfo,\\n737 | \\t\\t\\t\\t\\tsystemPrompt: \\\"You are a helpful assistant.\\\", // ~10 tokens\\n738 | \\t\\t\\t\\t\\tmessages,\\n----\\n759 | \\n760 | \\t\\t\\t\\t// Check if we have a second cache point (may not always be added depending on token distribution)\\n761 | \\t\\t\\t\\tif (result.messageCachePointPlacements && result.messageCachePointPlacements.length > 1) {\\n----\\n789 | \\t\\t\\t\\t\\t\\ttype: \\\"message\\\",\\n790 | \\t\\t\\t\\t\\t\\ttokensCovered: 300,\\n791 | \\t\\t\\t\\t\\t},\\n----\\n794 | \\t\\t\\t\\t\\t\\ttype: \\\"message\\\",\\n795 | \\t\\t\\t\\t\\t\\ttokensCovered: 300,\\n796 | \\t\\t\\t\\t\\t},\\n----\\n800 | \\t\\t\\t\\t\\tmodelInfo: multiPointModelInfo,\\n801 | \\t\\t\\t\\t\\tsystemPrompt: \\\"You are a helpful assistant.\\\", // ~10 tokens\\n802 | \\t\\t\\t\\t\\tmessages,\\n----\\n863 | \\t\\t\\t\\t\\t\\ttype: \\\"message\\\",\\n864 | \\t\\t\\t\\t\\t\\ttokensCovered: 300,\\n865 | \\t\\t\\t\\t\\t},\\n----\\n868 | \\t\\t\\t\\t\\t\\ttype: \\\"message\\\",\\n869 | \\t\\t\\t\\t\\t\\ttokensCovered: 300,\\n870 | \\t\\t\\t\\t\\t},\\n----\\n873 | \\t\\t\\t\\t\\t\\ttype: \\\"message\\\",\\n874 | \\t\\t\\t\\t\\t\\ttokensCovered: 300,\\n875 | \\t\\t\\t\\t\\t},\\n----\\n879 | \\t\\t\\t\\t\\tmodelInfo: multiPointModelInfo,\\n880 | \\t\\t\\t\\t\\tsystemPrompt: \\\"You are a helpful assistant.\\\", // ~10 tokens\\n881 | \\t\\t\\t\\t\\tmessages,\\n----\\n929 | \\t\\t\\t\\t// This test verifies that the documentation for Example 5 is correct\\n930 | \\t\\t\\t\\t// In Example 5, the third cache point at index 10 should cover 660 tokens\\n931 | \\t\\t\\t\\t// (260 tokens from messages 7-8 plus 400 tokens from the new messages)\\n932 | \\n----\\n944 | \\t\\t\\t\\t\\tcreateMessage(\\\"assistant\\\", \\\"Deep learning has many applications including...\\\", 160),\\n945 | \\t\\t\\t\\t\\t// New messages with 400 tokens total\\n946 | \\t\\t\\t\\t\\tcreateMessage(\\\"user\\\", \\\"Can you provide a detailed example?\\\", 100),\\n----\\n954 | \\t\\t\\t\\t\\t\\ttype: \\\"message\\\",\\n955 | \\t\\t\\t\\t\\t\\ttokensCovered: 240,\\n956 | \\t\\t\\t\\t\\t},\\n----\\n959 | \\t\\t\\t\\t\\t\\ttype: \\\"message\\\",\\n960 | \\t\\t\\t\\t\\t\\ttokensCovered: 440,\\n961 | \\t\\t\\t\\t\\t},\\n----\\n964 | \\t\\t\\t\\t\\t\\ttype: \\\"message\\\",\\n965 | \\t\\t\\t\\t\\t\\ttokensCovered: 260,\\n966 | \\t\\t\\t\\t\\t},\\n----\\n969 | \\t\\t\\t\\t// In the documentation, the algorithm decides to replace the cache point at index 8\\n970 | \\t\\t\\t\\t// with a new one at index 10, and the tokensCovered value should be 660 tokens\\n971 | \\t\\t\\t\\t// (260 tokens from messages 7-8 plus 400 tokens from the new messages)\\n972 | \\n973 | \\t\\t\\t\\t// However, the actual implementation may behave differently depending on how\\n974 | \\t\\t\\t\\t// it calculates token counts and makes decisions about cache point placement\\n975 | \\n976 | \\t\\t\\t\\t// The important part is that our fix ensures that when a cache point is created,\\n977 | \\t\\t\\t\\t// the tokensCovered value represents all tokens from the previous cache point\\n978 | \\t\\t\\t\\t// to the current cache point, not just the tokens in the new messages\\n979 | \\t\\t\\t})\\n980 | \\n981 | \\t\\t\\tit(\\\"should not combine cache points when new messages have fewer tokens than the smallest combined gap\\\", () => {\\n982 | \\t\\t\\t\\t// This test verifies that when new messages have fewer tokens than the smallest combined gap,\\n983 | \\t\\t\\t\\t// the algorithm keeps all existing cache points and doesn't add a new one\\n----\\n1004 | \\t\\t\\t\\t\\t\\t),\\n1005 | \\t\\t\\t\\t\\t\\t// Small addition (only 50 tokens total)\\n1006 | \\t\\t\\t\\t\\t\\tcreateMessage(\\\"user\\\", \\\"Thanks for the explanation.\\\", 20),\\n----\\n1009 | \\n1010 | \\t\\t\\t\\t\\t// Previous cache point placements with significant token coverage\\n1011 | \\t\\t\\t\\t\\tconst previousCachePointPlacements: CachePointPlacement[] = [\\n----\\n1014 | \\t\\t\\t\\t\\t\\t\\ttype: \\\"message\\\",\\n1015 | \\t\\t\\t\\t\\t\\t\\ttokensCovered: 400, // Significant token coverage\\n1016 | \\t\\t\\t\\t\\t\\t},\\n----\\n1019 | \\t\\t\\t\\t\\t\\t\\ttype: \\\"message\\\",\\n1020 | \\t\\t\\t\\t\\t\\t\\ttokensCovered: 300, // Significant token coverage\\n1021 | \\t\\t\\t\\t\\t\\t},\\n----\\n1024 | \\t\\t\\t\\t\\t\\t\\ttype: \\\"message\\\",\\n1025 | \\t\\t\\t\\t\\t\\t\\ttokensCovered: 300, // Significant token coverage\\n1026 | \\t\\t\\t\\t\\t\\t},\\n----\\n1030 | \\t\\t\\t\\t\\t\\tmodelInfo: multiPointModelInfo,\\n1031 | \\t\\t\\t\\t\\t\\tsystemPrompt: \\\"You are a helpful assistant.\\\", // ~10 tokens\\n1032 | \\t\\t\\t\\t\\t\\tmessages,\\n----\\n1057 | \\n1058 | \\t\\t\\tit(\\\"should make correct decisions based on token counts\\\", () => {\\n1059 | \\t\\t\\t\\t// This test verifies that the algorithm correctly compares token counts\\n1060 | \\t\\t\\t\\t// and makes the right decision about combining cache points\\n1061 | \\n1062 | \\t\\t\\t\\t// Create messages with a variety of token counts\\n1063 | \\t\\t\\t\\tconst messages = [\\n----\\n1081 | \\t\\t\\t\\t\\t\\ttype: \\\"message\\\",\\n1082 | \\t\\t\\t\\t\\t\\ttokensCovered: 400,\\n1083 | \\t\\t\\t\\t\\t},\\n----\\n1086 | \\t\\t\\t\\t\\t\\ttype: \\\"message\\\",\\n1087 | \\t\\t\\t\\t\\t\\ttokensCovered: 150,\\n1088 | \\t\\t\\t\\t\\t},\\n----\\n1091 | \\t\\t\\t\\t\\t\\ttype: \\\"message\\\",\\n1092 | \\t\\t\\t\\t\\t\\ttokensCovered: 150,\\n1093 | \\t\\t\\t\\t\\t},\\n----\\n\\n# src/api/providers/__tests__/openai-usage-tracking.spec.ts\\n 28 | \\t\\t\\t\\t\\t\\t\\t\\tusage: {\\n 29 | \\t\\t\\t\\t\\t\\t\\t\\t\\tprompt_tokens: 10,\\n 30 | \\t\\t\\t\\t\\t\\t\\t\\t\\tcompletion_tokens: 5,\\n 31 | \\t\\t\\t\\t\\t\\t\\t\\t\\ttotal_tokens: 15,\\n 32 | \\t\\t\\t\\t\\t\\t\\t\\t},\\n----\\n 47 | \\t\\t\\t\\t\\t\\t\\t\\t\\tusage: {\\n 48 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\tprompt_tokens: 10,\\n 49 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\tcompletion_tokens: 2,\\n 50 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\ttotal_tokens: 12,\\n 51 | \\t\\t\\t\\t\\t\\t\\t\\t\\t},\\n----\\n 62 | \\t\\t\\t\\t\\t\\t\\t\\t\\tusage: {\\n 63 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\tprompt_tokens: 10,\\n 64 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\tcompletion_tokens: 4,\\n 65 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\ttotal_tokens: 14,\\n 66 | \\t\\t\\t\\t\\t\\t\\t\\t\\t},\\n----\\n 77 | \\t\\t\\t\\t\\t\\t\\t\\t\\tusage: {\\n 78 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\tprompt_tokens: 10,\\n 79 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\tcompletion_tokens: 5,\\n 80 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\ttotal_tokens: 15,\\n 81 | \\t\\t\\t\\t\\t\\t\\t\\t\\t},\\n----\\n155 | \\t\\t\\t\\t\\t\\tchoices: [{ message: { role: \\\"assistant\\\", content: \\\"Test response\\\" } }],\\n156 | \\t\\t\\t\\t\\t\\tusage: { prompt_tokens: 10, completion_tokens: 5, total_tokens: 15 },\\n157 | \\t\\t\\t\\t\\t}\\n----\\n177 | \\t\\t\\t\\t\\t\\t\\tusage: {\\n178 | \\t\\t\\t\\t\\t\\t\\t\\tprompt_tokens: 10,\\n179 | \\t\\t\\t\\t\\t\\t\\t\\tcompletion_tokens: 5,\\n180 | \\t\\t\\t\\t\\t\\t\\t\\ttotal_tokens: 15,\\n181 | \\t\\t\\t\\t\\t\\t\\t},\\n----\\n\\n# src/api/providers/__tests__/unbound.spec.ts\\n 82 | \\t\\t\\t\\t\\t\\t\\t\\t\\tusage: {\\n 83 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\tprompt_tokens: 10,\\n 84 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\tcompletion_tokens: 5,\\n 85 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\ttotal_tokens: 15,\\n 86 | \\t\\t\\t\\t\\t\\t\\t\\t\\t},\\n----\\n 91 | \\t\\t\\t\\t\\t\\t\\t\\t\\tusage: {\\n 92 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\tprompt_tokens: 8,\\n 93 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\tcompletion_tokens: 4,\\n 94 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\ttotal_tokens: 12,\\n 95 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\tcache_creation_input_tokens: 3,\\n 96 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\tcache_read_input_tokens: 2,\\n 97 | \\t\\t\\t\\t\\t\\t\\t\\t\\t},\\n----\\n232 | \\t\\t\\t\\t\\ttemperature: 0,\\n233 | \\t\\t\\t\\t\\tmax_tokens: 8192,\\n234 | \\t\\t\\t\\t}),\\n----\\n253 | \\n254 | \\t\\tit(\\\"should not set max_tokens for non-Anthropic models\\\", async () => {\\n255 | \\t\\t\\tmockCreate.mockClear()\\n----\\n277 | \\n278 | \\t\\t\\texpect(mockCreate.mock.calls[0][0]).not.toHaveProperty(\\\"max_tokens\\\")\\n279 | \\t\\t})\\n----\\n\\n# src/api/providers/__tests__/anthropic-vertex.spec.ts\\n 21 | \\t\\t\\t\\t\\t\\tusage: {\\n 22 | \\t\\t\\t\\t\\t\\t\\tinput_tokens: 10,\\n 23 | \\t\\t\\t\\t\\t\\t\\toutput_tokens: 5,\\n 24 | \\t\\t\\t\\t\\t\\t},\\n----\\n 32 | \\t\\t\\t\\t\\t\\t\\t\\tusage: {\\n 33 | \\t\\t\\t\\t\\t\\t\\t\\t\\tinput_tokens: 10,\\n 34 | \\t\\t\\t\\t\\t\\t\\t\\t\\toutput_tokens: 5,\\n 35 | \\t\\t\\t\\t\\t\\t\\t\\t},\\n----\\n 95 | \\t\\t\\t\\t\\t\\tusage: {\\n 96 | \\t\\t\\t\\t\\t\\t\\tinput_tokens: 10,\\n 97 | \\t\\t\\t\\t\\t\\t\\toutput_tokens: 0,\\n 98 | \\t\\t\\t\\t\\t\\t},\\n----\\n118 | \\t\\t\\t\\t\\tusage: {\\n119 | \\t\\t\\t\\t\\t\\toutput_tokens: 5,\\n120 | \\t\\t\\t\\t\\t},\\n----\\n164 | \\t\\t\\t\\tmodel: \\\"claude-3-5-sonnet-v2@20241022\\\",\\n165 | \\t\\t\\t\\tmax_tokens: 8192,\\n166 | \\t\\t\\t\\ttemperature: 0,\\n----\\n284 | \\t\\t\\t\\t\\t\\tusage: {\\n285 | \\t\\t\\t\\t\\t\\t\\tinput_tokens: 10,\\n286 | \\t\\t\\t\\t\\t\\t\\toutput_tokens: 0,\\n287 | \\t\\t\\t\\t\\t\\t\\tcache_creation_input_tokens: 3,\\n288 | \\t\\t\\t\\t\\t\\t\\tcache_read_input_tokens: 2,\\n289 | \\t\\t\\t\\t\\t\\t},\\n----\\n309 | \\t\\t\\t\\t\\tusage: {\\n310 | \\t\\t\\t\\t\\t\\toutput_tokens: 5,\\n311 | \\t\\t\\t\\t\\t},\\n----\\n419 | \\t\\t\\t\\t\\t\\tusage: {\\n420 | \\t\\t\\t\\t\\t\\t\\tinput_tokens: 10,\\n421 | \\t\\t\\t\\t\\t\\t\\toutput_tokens: 0,\\n422 | \\t\\t\\t\\t\\t\\t\\tcache_creation_input_tokens: 5,\\n423 | \\t\\t\\t\\t\\t\\t\\tcache_read_input_tokens: 3,\\n424 | \\t\\t\\t\\t\\t\\t},\\n----\\n484 | \\t\\t\\t\\t\\t\\tusage: {\\n485 | \\t\\t\\t\\t\\t\\t\\tinput_tokens: 10,\\n486 | \\t\\t\\t\\t\\t\\t\\toutput_tokens: 0,\\n487 | \\t\\t\\t\\t\\t\\t},\\n----\\n618 | \\t\\t\\t\\tmodel: \\\"claude-3-5-sonnet-v2@20241022\\\",\\n619 | \\t\\t\\t\\tmax_tokens: 8192,\\n620 | \\t\\t\\t\\ttemperature: 0,\\n----\\n752 | \\n753 | \\t\\t\\t// Test with default thinking budget (80% of max tokens)\\n754 | \\t\\t\\tconst handlerWithDefaultBudget = new AnthropicVertexHandler({\\n----\\n767 | \\t\\t\\t\\tvertexRegion: \\\"us-central1\\\",\\n768 | \\t\\t\\t\\tmodelMaxTokens: 1000, // This would result in 800 tokens for thinking, but minimum is 1024\\n769 | \\t\\t\\t})\\n----\\n789 | \\t\\t\\t\\t\\t\\tmodel: options.model,\\n790 | \\t\\t\\t\\t\\t\\tusage: { input_tokens: 10, output_tokens: 5 },\\n791 | \\t\\t\\t\\t\\t}\\n----\\n794 | \\t\\t\\t\\t\\tasync *[Symbol.asyncIterator]() {\\n795 | \\t\\t\\t\\t\\t\\tyield { type: \\\"message_start\\\", message: { usage: { input_tokens: 10, output_tokens: 5 } } }\\n796 | \\t\\t\\t\\t\\t},\\n----\\n806 | \\t\\t\\t\\texpect.objectContaining({\\n807 | \\t\\t\\t\\t\\tthinking: { type: \\\"enabled\\\", budget_tokens: 4096 },\\n808 | \\t\\t\\t\\t\\ttemperature: 1.0, // Thinking requires temperature 1.0\\n----\\n\\n# src/core/prompts/tools/codebase-search.ts\\n 12 | \\n 13 | Example: Searching for functions related to user authentication\\n 14 | \\n----\\n\\n# src/core/task/__tests__/Task.test.ts\\n185 | \\t\\t\\t\\t\\t\\t\\t\\ttask: \\\"historical task\\\",\\n186 | \\t\\t\\t\\t\\t\\t\\t\\ttokensIn: 100,\\n187 | \\t\\t\\t\\t\\t\\t\\t\\ttokensOut: 200,\\n188 | \\t\\t\\t\\t\\t\\t\\t\\tcacheWrites: 0,\\n----\\n253 | \\t\\t\\t\\ttask: \\\"historical task\\\",\\n254 | \\t\\t\\t\\ttokensIn: 100,\\n255 | \\t\\t\\t\\ttokensOut: 200,\\n256 | \\t\\t\\t\\tcacheWrites: 0,\\n----\\n615 | \\t\\t\\t\\t\\t\\ttext: JSON.stringify({\\n616 | \\t\\t\\t\\t\\t\\t\\ttokensIn: 100,\\n617 | \\t\\t\\t\\t\\t\\t\\ttokensOut: 50,\\n618 | \\t\\t\\t\\t\\t\\t\\tcacheWrites: 0,\\n----\\n740 | \\t\\t\\t\\t\\t\\ttext: JSON.stringify({\\n741 | \\t\\t\\t\\t\\t\\t\\ttokensIn: 100,\\n742 | \\t\\t\\t\\t\\t\\t\\ttokensOut: 50,\\n743 | \\t\\t\\t\\t\\t\\t\\tcacheWrites: 0,\\n----\\n\\n# src/core/prompts/instructions/create-mcp-server.ts\\n 11 | \\n 12 | When creating MCP servers, it's important to understand that they operate in a non-interactive environment. The server cannot initiate OAuth flows, open browser windows, or prompt for user input during runtime. All credentials and authentication tokens must be provided upfront through environment variables in the MCP settings configuration. For example, Spotify's API uses OAuth to get a refresh token for the user, but the MCP server cannot initiate this flow. While you can walk the user through [truncated...]\\n 13 | \\n----\\n 76 | \\t\\t\\t\\t...\\n 77 | \\t\\t\\t\\t\\\"type\\\": \\\"module\\\", // added by default, uses ES module syntax (import/export) rather than CommonJS (require/module.exports) (Important to know if you create additional scripts in this server repository like a get-refresh-token.js script)\\n 78 | \\t\\t\\t\\t\\\"scripts\\\": {\\n----\\n\\n# src/core/task/Task.ts\\n 24 | import { TelemetryService } from \\\"@roo-code/telemetry\\\"\\n 25 | import { CloudService } from \\\"@roo-code/cloud\\\"\\n 26 | \\n----\\n 96 | \\ttaskSpawned: [taskId: string]\\n 97 | \\ttaskCompleted: [taskId: string, tokenUsage: TokenUsage, toolUsage: ToolUsage]\\n 98 | \\ttaskTokenUsageUpdated: [taskId: string, tokenUsage: TokenUsage]\\n 99 | \\ttaskToolFailed: [taskId: string, tool: ToolName, error: string]\\n----\\n346 | \\n347 | \\t\\tconst shouldCaptureMessage = message.partial !== true && CloudService.isEnabled()\\n348 | \\n349 | \\t\\tif (shouldCaptureMessage) {\\n350 | \\t\\t\\tCloudService.instance.captureEvent({\\n351 | \\t\\t\\t\\tevent: TelemetryEventName.TASK_MESSAGE,\\n----\\n366 | \\n367 | \\t\\tconst shouldCaptureMessage = message.partial !== true && CloudService.isEnabled()\\n368 | \\n369 | \\t\\tif (shouldCaptureMessage) {\\n370 | \\t\\t\\tCloudService.instance.captureEvent({\\n371 | \\t\\t\\t\\tevent: TelemetryEventName.TASK_MESSAGE,\\n----\\n384 | \\n385 | \\t\\t\\tconst { historyItem, tokenUsage } = await taskMetadata({\\n386 | \\t\\t\\t\\tmessages: this.clineMessages,\\n----\\n392 | \\n393 | \\t\\t\\tthis.emit(\\\"taskTokenUsageUpdated\\\", this.taskId, tokenUsage)\\n394 | \\n----\\n1246 | \\t\\t\\t\\t\\t...JSON.parse(this.clineMessages[lastApiReqIndex].text || \\\"{}\\\"),\\n1247 | \\t\\t\\t\\t\\ttokensIn: inputTokens,\\n1248 | \\t\\t\\t\\t\\ttokensOut: outputTokens,\\n1249 | \\t\\t\\t\\t\\tcacheWrites: cacheWriteTokens,\\n----\\n1678 | \\t\\tif (contextTokens) {\\n1679 | \\t\\t\\t// Default max tokens value for thinking models when no specific\\n1680 | \\t\\t\\t// value is set.\\n----\\n\\n# src/api/providers/glama.ts\\n 51 | \\n 52 | \\t\\t// Required by Anthropic; other providers default to max tokens allowed.\\n 53 | \\t\\tlet maxTokens: number | undefined\\n----\\n 60 | \\t\\t\\tmodel: modelId,\\n 61 | \\t\\t\\tmax_tokens: maxTokens,\\n 62 | \\t\\t\\tmessages: openAiMessages,\\n----\\n 90 | \\t\\t\\t\\t// In case of an interrupted request, we need to wait for the upstream API to finish processing the request\\n 91 | \\t\\t\\t\\t// before we can fetch information about the token usage and cost.\\n 92 | \\t\\t\\t\\tconst response = await axios.get(\\n----\\n 98 | \\n 99 | \\t\\t\\t\\tif (completionRequest.tokenUsage && completionRequest.totalCostUsd) {\\n100 | \\t\\t\\t\\t\\tyield {\\n101 | \\t\\t\\t\\t\\t\\ttype: \\\"usage\\\",\\n102 | \\t\\t\\t\\t\\t\\tcacheWriteTokens: completionRequest.tokenUsage.cacheCreationInputTokens,\\n103 | \\t\\t\\t\\t\\t\\tcacheReadTokens: completionRequest.tokenUsage.cacheReadInputTokens,\\n104 | \\t\\t\\t\\t\\t\\tinputTokens: completionRequest.tokenUsage.promptTokens,\\n105 | \\t\\t\\t\\t\\t\\toutputTokens: completionRequest.tokenUsage.completionTokens,\\n106 | \\t\\t\\t\\t\\t\\ttotalCost: parseFloat(completionRequest.totalCostUsd),\\n----\\n132 | \\t\\t\\tif (modelId.startsWith(\\\"anthropic/\\\")) {\\n133 | \\t\\t\\t\\trequestOptions.max_tokens = info.maxTokens\\n134 | \\t\\t\\t}\\n----\\n\\n# src/api/transform/reasoning.ts\\n 12 | \\teffort?: ReasoningEffort\\n 13 | \\tmax_tokens?: number\\n 14 | \\texclude?: boolean\\n----\\n 36 | \\tshouldUseReasoningBudget({ model, settings })\\n 37 | \\t\\t? { max_tokens: reasoningBudget }\\n 38 | \\t\\t: shouldUseReasoningEffort({ model, settings })\\n----\\n 46 | }: GetModelReasoningOptions): AnthropicReasoningParams | undefined =>\\n 47 | \\tshouldUseReasoningBudget({ model, settings }) ? { type: \\\"enabled\\\", budget_tokens: reasoningBudget! } : undefined\\n 48 | \\n----\\n\\n# src/api/providers/openrouter.ts\\n 32 | \\tinclude_reasoning?: boolean\\n 33 | \\t// https://openrouter.ai/docs/use-cases/reasoning-tokens\\n 34 | \\treasoning?: OpenRouterReasoningParams\\n----\\n 40 | interface CompletionUsage {\\n 41 | \\tcompletion_tokens?: number\\n 42 | \\tcompletion_tokens_details?: {\\n 43 | \\t\\treasoning_tokens?: number\\n 44 | \\t}\\n 45 | \\tprompt_tokens?: number\\n 46 | \\tprompt_tokens_details?: {\\n 47 | \\t\\tcached_tokens?: number\\n 48 | \\t}\\n 49 | \\ttotal_tokens?: number\\n 50 | \\tcost?: number\\n----\\n 81 | \\n 82 | \\t\\t// OpenRouter sends reasoning tokens by default for Gemini 2.5 Pro\\n 83 | \\t\\t// Preview even if you don't request them. This is not the default for\\n----\\n116 | \\t\\t\\tmodel: modelId,\\n117 | \\t\\t\\t...(maxTokens && maxTokens > 0 && { max_tokens: maxTokens }),\\n118 | \\t\\t\\ttemperature,\\n----\\n165 | \\t\\t\\t\\ttype: \\\"usage\\\",\\n166 | \\t\\t\\t\\tinputTokens: lastUsage.prompt_tokens || 0,\\n167 | \\t\\t\\t\\toutputTokens: lastUsage.completion_tokens || 0,\\n168 | \\t\\t\\t\\t// Waiting on OpenRouter to figure out what this represents in the Gemini case\\n169 | \\t\\t\\t\\t// and how to best support it.\\n170 | \\t\\t\\t\\t// cacheReadTokens: lastUsage.prompt_tokens_details?.cached_tokens,\\n171 | \\t\\t\\t\\treasoningTokens: lastUsage.completion_tokens_details?.reasoning_tokens,\\n172 | \\t\\t\\t\\ttotalCost: (lastUsage.is_byok ? BYOK_COST_MULTIPLIER : 1) * (lastUsage.cost || 0),\\n----\\n219 | \\t\\t\\tmodel: modelId,\\n220 | \\t\\t\\tmax_tokens: maxTokens,\\n221 | \\t\\t\\ttemperature,\\n----\\n\\n# src/utils/__tests__/cost.spec.ts\\n 13 | \\t\\t\\tsupportsPromptCache: true,\\n 14 | \\t\\t\\tinputPrice: 3.0, // $3 per million tokens\\n 15 | \\t\\t\\toutputPrice: 15.0, // $15 per million tokens\\n 16 | \\t\\t\\tcacheWritesPrice: 3.75, // $3.75 per million tokens\\n 17 | \\t\\t\\tcacheReadsPrice: 0.3, // $0.30 per million tokens\\n 18 | \\t\\t}\\n----\\n 70 | \\n 71 | \\t\\tit(\\\"should handle zero tokens\\\", () => {\\n 72 | \\t\\t\\tconst cost = calculateApiCostAnthropic(mockModelInfo, 0, 0, 0, 0)\\n----\\n106 | \\t\\t\\tsupportsPromptCache: true,\\n107 | \\t\\t\\tinputPrice: 3.0, // $3 per million tokens\\n108 | \\t\\t\\toutputPrice: 15.0, // $15 per million tokens\\n109 | \\t\\t\\tcacheWritesPrice: 3.75, // $3.75 per million tokens\\n110 | \\t\\t\\tcacheReadsPrice: 0.3, // $0.30 per million tokens\\n111 | \\t\\t}\\n----\\n163 | \\n164 | \\t\\tit(\\\"should handle zero tokens\\\", () => {\\n165 | \\t\\t\\tconst cost = calculateApiCostOpenAI(mockModelInfo, 0, 0, 0, 0)\\n----\\n\\n# src/utils/__tests__/git.spec.ts\\n 98 | \\t\\t\\t\\tsubject: \\\"fix: test commit\\\",\\n 99 | \\t\\t\\t\\tauthor: \\\"John Doe\\\",\\n100 | \\t\\t\\t\\tdate: \\\"2024-01-06\\\",\\n----\\n162 | \\t\\t\\t\\t[\\n163 | \\t\\t\\t\\t\\t'git log -n 10 --format=\\\"%H%n%h%n%s%n%an%n%ad\\\" --date=short --author-date-order abc123',\\n164 | \\t\\t\\t\\t\\t{ stdout: mockCommitData, stderr: \\\"\\\" },\\n----\\n185 | \\t\\t\\t\\tsubject: \\\"fix: test commit\\\",\\n186 | \\t\\t\\t\\tauthor: \\\"John Doe\\\",\\n187 | \\t\\t\\t\\tdate: \\\"2024-01-06\\\",\\n----\\n\\n# src/api/providers/gemini.ts\\n 7 | } from \\\"@google/genai\\\"\\n 8 | import type { JWTInput } from \\\"google-auth-library\\\"\\n 9 | \\n----\\n179 | \\t\\t\\tif (response.totalTokens === undefined) {\\n180 | \\t\\t\\t\\tconsole.warn(\\\"Gemini token counting returned undefined, using fallback\\\")\\n181 | \\t\\t\\t\\treturn super.countTokens(content)\\n----\\n185 | \\t\\t} catch (error) {\\n186 | \\t\\t\\tconsole.warn(\\\"Gemini token counting failed, using fallback\\\", error)\\n187 | \\t\\t\\treturn super.countTokens(content)\\n----\\n209 | \\n210 | \\t\\t// If there's tiered pricing then adjust the input and output token prices\\n211 | \\t\\t// based on the input tokens used.\\n212 | \\t\\tif (info.tiers) {\\n----\\n221 | \\n222 | \\t\\t// Subtract the cached input tokens from the total input tokens.\\n223 | \\t\\tconst uncachedInputTokens = inputTokens - cacheReadTokens\\n----\\n230 | \\n231 | \\t\\tconst trace: Record = {\\n232 | \\t\\t\\tinput: { price: inputPrice, tokens: uncachedInputTokens, cost: inputTokensCost },\\n233 | \\t\\t\\toutput: { price: outputPrice, tokens: outputTokens, cost: outputTokensCost },\\n234 | \\t\\t}\\n----\\n236 | \\t\\tif (cacheReadTokens > 0) {\\n237 | \\t\\t\\ttrace.cacheRead = { price: cacheReadsPrice, tokens: cacheReadTokens, cost: cacheReadCost }\\n238 | \\t\\t}\\n----\\n\\n# src/api/providers/lite-llm.ts\\n 46 | \\n 47 | \\t\\t// Required by some providers; others default to max tokens allowed\\n 48 | \\t\\tlet maxTokens: number | undefined = info.maxTokens ?? undefined\\n----\\n 51 | \\t\\t\\tmodel: modelId,\\n 52 | \\t\\t\\tmax_tokens: maxTokens,\\n 53 | \\t\\t\\tmessages: openAiMessages,\\n----\\n 84 | \\t\\t\\t\\t\\ttype: \\\"usage\\\",\\n 85 | \\t\\t\\t\\t\\tinputTokens: lastUsage.prompt_tokens || 0,\\n 86 | \\t\\t\\t\\t\\toutputTokens: lastUsage.completion_tokens || 0,\\n 87 | \\t\\t\\t\\t\\tcacheWriteTokens: lastUsage.cache_creation_input_tokens || 0,\\n 88 | \\t\\t\\t\\t\\tcacheReadTokens: lastUsage.prompt_tokens_details?.cached_tokens || 0,\\n 89 | \\t\\t\\t\\t}\\n----\\n121 | \\n122 | \\t\\t\\trequestOptions.max_tokens = info.maxTokens\\n123 | \\n----\\n136 | interface LiteLLMUsage extends OpenAI.CompletionUsage {\\n137 | \\tcache_creation_input_tokens?: number\\n138 | }\\n----\\n\\n# src/api/providers/requesty.ts\\n 19 | // Requesty usage includes an extra field for Anthropic use cases.\\n 20 | // Safely cast the prompt token details section to the appropriate structure.\\n 21 | interface RequestyUsage extends OpenAI.CompletionUsage {\\n 22 | \\tprompt_tokens_details?: {\\n 23 | \\t\\tcaching_tokens?: number\\n 24 | \\t\\tcached_tokens?: number\\n 25 | \\t}\\n----\\n 76 | \\t\\tconst requestyUsage = usage as RequestyUsage\\n 77 | \\t\\tconst inputTokens = requestyUsage?.prompt_tokens || 0\\n 78 | \\t\\tconst outputTokens = requestyUsage?.completion_tokens || 0\\n 79 | \\t\\tconst cacheWriteTokens = requestyUsage?.prompt_tokens_details?.caching_tokens || 0\\n 80 | \\t\\tconst cacheReadTokens = requestyUsage?.prompt_tokens_details?.cached_tokens || 0\\n 81 | \\t\\tconst totalCost = modelInfo\\n----\\n102 | \\t\\t\\tinfo,\\n103 | \\t\\t\\tmaxTokens: max_tokens,\\n104 | \\t\\t\\ttemperature,\\n----\\n116 | \\t\\t\\tmodel,\\n117 | \\t\\t\\tmax_tokens,\\n118 | \\t\\t\\ttemperature,\\n----\\n150 | \\tasync completePrompt(prompt: string): Promise {\\n151 | \\t\\tconst { id: model, maxTokens: max_tokens, temperature } = await this.fetchModel()\\n152 | \\n----\\n156 | \\t\\t\\tmodel,\\n157 | \\t\\t\\tmax_tokens,\\n158 | \\t\\t\\tmessages: openAiMessages,\\n----\\n\\n# src/api/providers/anthropic.ts\\n 29 | \\t\\tconst apiKeyFieldName =\\n 30 | \\t\\t\\tthis.options.anthropicBaseUrl && this.options.anthropicUseAuthToken ? \\\"authToken\\\" : \\\"apiKey\\\"\\n 31 | \\n----\\n 75 | \\t\\t\\t\\t\\t\\tmodel: modelId,\\n 76 | \\t\\t\\t\\t\\t\\tmax_tokens: maxTokens ?? ANTHROPIC_DEFAULT_MAX_TOKENS,\\n 77 | \\t\\t\\t\\t\\t\\ttemperature,\\n----\\n124 | \\t\\t\\t\\t\\tmodel: modelId,\\n125 | \\t\\t\\t\\t\\tmax_tokens: maxTokens ?? ANTHROPIC_DEFAULT_MAX_TOKENS,\\n126 | \\t\\t\\t\\t\\ttemperature,\\n----\\n142 | \\t\\t\\t\\t\\t\\ttype: \\\"usage\\\",\\n143 | \\t\\t\\t\\t\\t\\tinputTokens: usage.input_tokens || 0,\\n144 | \\t\\t\\t\\t\\t\\toutputTokens: usage.output_tokens || 0,\\n145 | \\t\\t\\t\\t\\t\\tcacheWriteTokens: usage.cache_creation_input_tokens || undefined,\\n146 | \\t\\t\\t\\t\\t\\tcacheReadTokens: usage.cache_read_input_tokens || undefined,\\n147 | \\t\\t\\t\\t\\t}\\n----\\n151 | \\t\\t\\t\\tcase \\\"message_delta\\\":\\n152 | \\t\\t\\t\\t\\t// Tells us stop_reason, stop_sequence, and output tokens\\n153 | \\t\\t\\t\\t\\t// along the way and at the end of the message.\\n----\\n156 | \\t\\t\\t\\t\\t\\tinputTokens: 0,\\n157 | \\t\\t\\t\\t\\t\\toutputTokens: chunk.usage.output_tokens || 0,\\n158 | \\t\\t\\t\\t\\t}\\n----\\n231 | \\t\\t\\tmodel,\\n232 | \\t\\t\\tmax_tokens: ANTHROPIC_DEFAULT_MAX_TOKENS,\\n233 | \\t\\t\\tthinking: undefined,\\n----\\n243 | \\t/**\\n244 | \\t * Counts tokens for the given content using Anthropic's API\\n245 | \\t *\\n246 | \\t * @param content The content blocks to count tokens for\\n247 | \\t * @returns A promise resolving to the token count\\n248 | \\t */\\n----\\n258 | \\n259 | \\t\\t\\treturn response.input_tokens\\n260 | \\t\\t} catch (error) {\\n261 | \\t\\t\\t// Log error but fallback to tiktoken estimation\\n262 | \\t\\t\\tconsole.warn(\\\"Anthropic token counting failed, using fallback\\\", error)\\n263 | \\n----\\n\\n# src/api/providers/deepseek.ts\\n 32 | \\t\\t\\ttype: \\\"usage\\\",\\n 33 | \\t\\t\\tinputTokens: usage?.prompt_tokens || 0,\\n 34 | \\t\\t\\toutputTokens: usage?.completion_tokens || 0,\\n 35 | \\t\\t\\tcacheWriteTokens: usage?.prompt_tokens_details?.cache_miss_tokens,\\n 36 | \\t\\t\\tcacheReadTokens: usage?.prompt_tokens_details?.cached_tokens,\\n 37 | \\t\\t}\\n----\\n\\n# src/api/providers/openai.ts\\n162 | \\t\\t\\tif (this.options.includeMaxTokens) {\\n163 | \\t\\t\\t\\trequestOptions.max_tokens = modelInfo.maxTokens\\n164 | \\t\\t\\t}\\n----\\n241 | \\t\\t\\ttype: \\\"usage\\\",\\n242 | \\t\\t\\tinputTokens: usage?.prompt_tokens || 0,\\n243 | \\t\\t\\toutputTokens: usage?.completion_tokens || 0,\\n244 | \\t\\t\\tcacheWriteTokens: usage?.cache_creation_input_tokens || undefined,\\n245 | \\t\\t\\tcacheReadTokens: usage?.cache_read_input_tokens || undefined,\\n246 | \\t\\t}\\n----\\n347 | \\t\\t\\t\\t\\ttype: \\\"usage\\\",\\n348 | \\t\\t\\t\\t\\tinputTokens: chunk.usage.prompt_tokens || 0,\\n349 | \\t\\t\\t\\t\\toutputTokens: chunk.usage.completion_tokens || 0,\\n350 | \\t\\t\\t\\t}\\n----\\n\\n# src/api/providers/xai.ts\\n 52 | \\t\\t\\tmodel: modelId,\\n 53 | \\t\\t\\tmax_tokens: modelInfo.maxTokens,\\n 54 | \\t\\t\\ttemperature: this.options.modelTemperature ?? XAI_DEFAULT_TEMPERATURE,\\n----\\n 78 | \\t\\t\\tif (chunk.usage) {\\n 79 | \\t\\t\\t\\t// Extract detailed token information if available\\n 80 | \\t\\t\\t\\t// First check for prompt_tokens_details structure (real API response)\\n 81 | \\t\\t\\t\\tconst promptDetails = \\\"prompt_tokens_details\\\" in chunk.usage ? chunk.usage.prompt_tokens_details : null;\\n 82 | \\t\\t\\t\\tconst cachedTokens = promptDetails && \\\"cached_tokens\\\" in promptDetails ? promptDetails.cached_tokens : 0;\\n 83 | \\n 84 | \\t\\t\\t\\t// Fall back to direct fields in usage (used in test mocks)\\n 85 | \\t\\t\\t\\tconst readTokens = cachedTokens || (\\\"cache_read_input_tokens\\\" in chunk.usage ? (chunk.usage as any).cache_read_input_tokens : 0);\\n 86 | \\t\\t\\t\\tconst writeTokens = \\\"cache_creation_input_tokens\\\" in chunk.usage ? (chunk.usage as any).cache_creation_input_tokens : 0;\\n 87 | \\n----\\n 89 | \\t\\t\\t\\t\\ttype: \\\"usage\\\",\\n 90 | \\t\\t\\t\\t\\tinputTokens: chunk.usage.prompt_tokens || 0,\\n 91 | \\t\\t\\t\\t\\toutputTokens: chunk.usage.completion_tokens || 0,\\n 92 | \\t\\t\\t\\t\\tcacheReadTokens: readTokens,\\n----\\n\\nLoading...\"}" +} +[2025-06-13T05:00:03.473Z] { + "ts": 1749790803442, + "type": "ask", + "ask": "resume_task" +} +[2025-06-13T05:00:14.579Z] { + "ts": 1749790814550, + "type": "say", + "say": "text", + "text": "Fix the following GitHub issue:\n\nRepository: RooCodeInc/Roo-Code\nIssue #123: Fix authentication bug in login flow\n\nDescription:\nUsers are experiencing issues logging in with OAuth providers. The token refresh mechanism seems to be failing intermittently, causing users to be logged out unexpectedly.\n\nSteps to reproduce:\n1. Login with GitHub OAuth\n2. Wait for token to expire\n3. Try to make an authenticated request\n4. User gets logged out instead of token being refreshed\n\nLabels: bug, authentication, oauth\n\nPlease analyze the issue, understand what needs to be fixed, and implement a solution." +} +[2025-06-13T05:00:14.629Z] taskStarted -> 895eb50e-d7d0-4aad-9b96-f37f457e15ad +[2025-06-13T05:00:14.659Z] { + "ts": 1749790814630, + "type": "say", + "say": "api_req_started", + "text": "{\"request\":\"\\nFix the following GitHub issue:\\n\\nRepository: RooCodeInc/Roo-Code\\nIssue #123: Fix authentication bug in login flow\\n\\nDescription:\\nUsers are experiencing issues logging in with OAuth providers. The token refresh mechanism seems to be failing intermittently, causing users to be logged out unexpectedly.\\n\\nSteps to reproduce:\\n1. Login with GitHub OAuth\\n2. Wait for token to expire\\n3. Try to make an authenticated request\\n4. User gets logged out instead of token being refreshed\\n\\nLabels: bug, authentication, oauth\\n\\nPlease analyze the issue, understand what needs to be fixed, and implement a solution.\\n\\n\\nLoading...\"}" +} From 6ae6029512074a93c3f6a6abe766948a348322f2 Mon Sep 17 00:00:00 2001 From: cte Date: Thu, 12 Jun 2025 23:33:46 -0700 Subject: [PATCH 06/19] Better script --- apps/cloud-agents/scripts/enqueue-test-job.sh | 81 ++++++++++++++++--- 1 file changed, 71 insertions(+), 10 deletions(-) diff --git a/apps/cloud-agents/scripts/enqueue-test-job.sh b/apps/cloud-agents/scripts/enqueue-test-job.sh index a39ded3a2c..6133fdb32d 100755 --- a/apps/cloud-agents/scripts/enqueue-test-job.sh +++ b/apps/cloud-agents/scripts/enqueue-test-job.sh @@ -1,18 +1,79 @@ #!/bin/bash +REPO="RooCodeInc/Roo-Code" +ISSUE_NUMBER="$1" BASE_URL="http://localhost:3001" JOBS_ENDPOINT="$BASE_URL/api/jobs" +if [ -z "$ISSUE_NUMBER" ]; then + echo "Usage: $0 [repo]" + echo "" + echo "Examples:" + echo " $0 4567 # Fetch issue #4567 from RooCodeInc/Roo-Code" + echo " $0 123 owner/repo # Fetch issue #123 from owner/repo" + echo "" + echo "This script fetches real GitHub issue data and enqueues it as a job." + exit 1 +fi + +if [ -n "$2" ]; then + REPO="$2" +fi + +if ! command -v gh &> /dev/null; then + echo "Error: GitHub CLI (gh) is not installed. Please install it first." + echo "Visit: https://cli.github.com/" + exit 1 +fi + +if ! gh auth status &> /dev/null; then + echo "Error: Not authenticated with GitHub CLI. Please run 'gh auth login' first." + exit 1 +fi + +if ! command -v jq &> /dev/null; then + echo "Error: jq is not installed. Please install it first." + echo "Visit: https://jqlang.github.io/jq/download/" + exit 1 +fi + +echo "Fetching issue #${ISSUE_NUMBER} from ${REPO}..." + +ISSUE_DATA=$(gh issue view ${ISSUE_NUMBER} --repo ${REPO} --json title,body,labels 2>/dev/null) + +if [ $? -ne 0 ]; then + echo "Error: Failed to fetch issue #${ISSUE_NUMBER} from ${REPO}" + echo "Please check that the issue exists and you have access to the repository." + exit 1 +fi + +TITLE=$(echo "$ISSUE_DATA" | jq -r '.title') +BODY=$(echo "$ISSUE_DATA" | jq -r '.body // ""') +LABELS=$(echo "$ISSUE_DATA" | jq -r '[.labels[].name] | @json') + +TITLE_ESCAPED=$(printf '%s' "$TITLE" | sed 's/"/\\"/g' | sed 's/\n/\\n/g') +BODY_ESCAPED=$(printf '%s' "$BODY" | sed 's/"/\\"/g' | awk '{printf "%s\\n", $0}' | sed 's/\\n$//') + +echo "Issue Title: $TITLE" +echo "Issue Labels: $LABELS" +echo "" +echo "Enqueueing job..." + +JSON_PAYLOAD=$(cat < Date: Fri, 13 Jun 2025 09:12:39 -0700 Subject: [PATCH 07/19] More progress --- apps/cloud-agents/package.json | 3 +- apps/cloud-agents/src/app/api/jobs/route.ts | 2 +- .../src/app/api/webhooks/github/route.ts | 45 +++++----- apps/cloud-agents/src/db/schema.ts | 22 +++-- apps/cloud-agents/src/lib/job.ts | 32 +++---- apps/cloud-agents/src/lib/queue.ts | 10 +-- apps/cloud-agents/src/types/index.ts | 43 ++++++++-- packages/types/src/global-settings.ts | 84 +------------------ pnpm-lock.yaml | 30 ++++++- 9 files changed, 115 insertions(+), 156 deletions(-) diff --git a/apps/cloud-agents/package.json b/apps/cloud-agents/package.json index 011e717596..fc6af0d1ea 100644 --- a/apps/cloud-agents/package.json +++ b/apps/cloud-agents/package.json @@ -5,7 +5,7 @@ "scripts": { "lint": "next lint", "check-types": "tsc --noEmit", - "dev": "next dev --port 3001", + "dev": "concurrently \"next dev --port 3001\" \"ngrok http 3001 --domain cte.ngrok.dev\"", "build": "next build", "start": "next start --port 3001", "clean": "rimraf .next .turbo", @@ -45,6 +45,7 @@ "@types/node": "^22.15.20", "@types/react": "^18.3.23", "@types/react-dom": "^18.3.7", + "concurrently": "^9.1.0", "drizzle-kit": "^0.31.1", "tsx": "^4.19.3" } diff --git a/apps/cloud-agents/src/app/api/jobs/route.ts b/apps/cloud-agents/src/app/api/jobs/route.ts index 28186042ee..a96212ed34 100644 --- a/apps/cloud-agents/src/app/api/jobs/route.ts +++ b/apps/cloud-agents/src/app/api/jobs/route.ts @@ -20,7 +20,7 @@ export async function POST(request: NextRequest) { switch (values.type) { case "github.issue.fix": - await enqueue("github.issue.fix", values.payload, job.id) + await enqueue({ jobId: job.id, type: "github.issue.fix", payload: values.payload }) break default: throw new Error(`Unknown job type: ${values.type}`) diff --git a/apps/cloud-agents/src/app/api/webhooks/github/route.ts b/apps/cloud-agents/src/app/api/webhooks/github/route.ts index 81eab584ad..4f11b6736c 100644 --- a/apps/cloud-agents/src/app/api/webhooks/github/route.ts +++ b/apps/cloud-agents/src/app/api/webhooks/github/route.ts @@ -2,24 +2,12 @@ import { NextRequest, NextResponse } from "next/server" import { createHmac } from "crypto" import { z } from "zod" +import { type JobType, type JobStatus, type JobPayload, githubWebhookSchema } from "@/types" import { db, cloudJobs } from "@/db" import { enqueue } from "@/lib" -const githubWebhookSchema = z.object({ - action: z.string(), - issue: z.object({ - number: z.number(), - title: z.string(), - body: z.string().nullable(), - labels: z.array(z.object({ name: z.string() })), - }), - repository: z.object({ - full_name: z.string(), - }), -}) - -function verifySignature(payload: string, signature: string, secret: string): boolean { - const expectedSignature = createHmac("sha256", secret).update(payload, "utf8").digest("hex") +function verifySignature(body: string, signature: string, secret: string): boolean { + const expectedSignature = createHmac("sha256", secret).update(body, "utf8").digest("hex") const receivedSignature = signature.replace("sha256=", "") return expectedSignature === receivedSignature } @@ -33,23 +21,31 @@ export async function POST(request: NextRequest) { return NextResponse.json({ error: "Missing signature" }, { status: 400 }) } - const payload = await request.text() + const body = await request.text() - if (!verifySignature(payload, signature, process.env.GITHUB_WEBHOOK_SECRET!)) { + if (!verifySignature(body, signature, process.env.GITHUB_WEBHOOK_SECRET!)) { return NextResponse.json({ error: "Invalid signature" }, { status: 401 }) } + console.log("✅ Signature verified") + console.log("📋 Event ->", event) + if (event !== "issues") { - return NextResponse.json({ message: "Event ignored" }) + return NextResponse.json({ message: "event_ignored" }) } - const data = githubWebhookSchema.parse(JSON.parse(payload)) + const data = githubWebhookSchema.parse(JSON.parse(body)) + + console.log("🗄️ Data ->", data) if (data.action !== "opened") { - return NextResponse.json({ message: "Action ignored" }) + return NextResponse.json({ message: "action_ignored" }) } - const jobPayload = { + const type: JobType = "github.issue.fix" + const status: JobStatus = "pending" + + const payload: JobPayload = { repo: data.repository.full_name, issue: data.issue.number, title: data.issue.title, @@ -57,16 +53,13 @@ export async function POST(request: NextRequest) { labels: data.issue.labels.map((label) => label.name), } - const [job] = await db - .insert(cloudJobs) - .values({ type: "github.issue.fix", status: "pending", payload: jobPayload }) - .returning() + const [job] = await db.insert(cloudJobs).values({ type, status, payload }).returning() if (!job) { throw new Error("Failed to create job") } - await enqueue("github.issue.fix", jobPayload, job.id) + await enqueue({ jobId: job.id, type, payload }) return NextResponse.json({ message: "Job created successfully", jobId: job.id }) } catch (error) { console.error("GitHub webhook error:", error) diff --git a/apps/cloud-agents/src/db/schema.ts b/apps/cloud-agents/src/db/schema.ts index d8962575c7..7c38516ace 100644 --- a/apps/cloud-agents/src/db/schema.ts +++ b/apps/cloud-agents/src/db/schema.ts @@ -1,20 +1,22 @@ import { pgTable, text, timestamp, integer, jsonb } from "drizzle-orm/pg-core" import { relations } from "drizzle-orm" +import type { JobType, JobStatus, JobPayload } from "@/types" + /** * cloudJobs */ export const cloudJobs = pgTable("cloud_jobs", { id: integer().primaryKey().generatedAlwaysAsIdentity(), - type: text().notNull(), // e.g., 'github.issue.fix' - status: text().notNull().default("pending"), // 'pending', 'processing', 'completed', 'failed' - payload: jsonb().notNull(), // job-specific data - result: jsonb(), // job output - error: text(), // error message if failed - createdAt: timestamp("created_at").notNull().defaultNow(), + type: text().notNull().$type(), + status: text().notNull().default("pending").$type(), + payload: jsonb().notNull().$type(), + result: jsonb(), + error: text(), startedAt: timestamp("started_at"), completedAt: timestamp("completed_at"), + createdAt: timestamp("created_at").notNull().defaultNow(), }) export const cloudJobsRelations = relations(cloudJobs, ({ many }) => ({ @@ -22,7 +24,9 @@ export const cloudJobsRelations = relations(cloudJobs, ({ many }) => ({ })) export type CloudJob = typeof cloudJobs.$inferSelect + export type InsertCloudJob = typeof cloudJobs.$inferInsert + export type UpdateCloudJob = Partial> /** @@ -34,8 +38,8 @@ export const cloudTasks = pgTable("cloud_tasks", { jobId: integer("job_id") .references(() => cloudJobs.id) .notNull(), - taskId: integer("task_id"), // references tasks from evals database - containerId: text("container_id"), // Docker container ID + taskId: integer("task_id"), + containerId: text("container_id"), createdAt: timestamp("created_at").notNull().defaultNow(), }) @@ -44,7 +48,9 @@ export const cloudTasksRelations = relations(cloudTasks, ({ one }) => ({ })) export type CloudTask = typeof cloudTasks.$inferSelect + export type InsertCloudTask = typeof cloudTasks.$inferInsert + export type UpdateCloudTask = Partial> /** diff --git a/apps/cloud-agents/src/lib/job.ts b/apps/cloud-agents/src/lib/job.ts index 3745b9ecb9..5fa44805e7 100644 --- a/apps/cloud-agents/src/lib/job.ts +++ b/apps/cloud-agents/src/lib/job.ts @@ -1,14 +1,13 @@ import { eq } from "drizzle-orm" import { Job } from "bullmq" -import { db, cloudJobs } from "@/db" -import type { JobTypes, CloudJobData } from "@/types" +import { db, cloudJobs, type UpdateCloudJob } from "@/db" +import type { JobTypes, JobType, JobStatus, JobParams } from "@/types" import { fixGitHubIssue } from "./jobs/fixGitHubIssue" -export async function processJob(job: Job) { - const { type, payload, jobId } = job.data - console.log(`Processing job ${jobId} of type ${type}`) +export async function processJob({ data: { type, payload, jobId }, ...job }: Job>) { + console.log(`[${job.name} | ${job.id}] Processing job ${jobId} of type ${type}`) try { await updateJobStatus(jobId, "processing") @@ -23,36 +22,31 @@ export async function processJob(job: Job) { } await updateJobStatus(jobId, "completed", result) - console.log(`Job ${jobId} completed successfully`) + console.log(`[${job.name} | ${job.id}] Job ${jobId} completed successfully`) } catch (error) { - console.error(`Job ${jobId} failed:`, error) + console.error(`[${job.name} | ${job.id}] Job ${jobId} failed:`, error) const errorMessage = error instanceof Error ? error.message : String(error) await updateJobStatus(jobId, "failed", undefined, errorMessage) throw error // Re-throw to mark job as failed in BullMQ. } } -async function updateJobStatus( - jobId: number, - status: "processing" | "completed" | "failed", - result?: unknown, - error?: string, -) { - const updates: Record = { status } +async function updateJobStatus(jobId: number, status: JobStatus, result?: unknown, error?: string) { + const values: UpdateCloudJob = { status } if (status === "processing") { - updates.startedAt = new Date() + values.startedAt = new Date() } else if (status === "completed" || status === "failed") { - updates.completedAt = new Date() + values.completedAt = new Date() if (result) { - updates.result = result + values.result = result } if (error) { - updates.error = error + values.error = error } } - await db.update(cloudJobs).set(updates).where(eq(cloudJobs.id, jobId)) + await db.update(cloudJobs).set(values).where(eq(cloudJobs.id, jobId)) } diff --git a/apps/cloud-agents/src/lib/queue.ts b/apps/cloud-agents/src/lib/queue.ts index c4bd777c11..923e9fe34a 100644 --- a/apps/cloud-agents/src/lib/queue.ts +++ b/apps/cloud-agents/src/lib/queue.ts @@ -1,6 +1,6 @@ import { Queue, Job } from "bullmq" -import type { JobTypes, CloudJobData } from "@/types" +import type { JobTypes, JobPayload, JobParams } from "@/types" import { redis } from "./redis" @@ -14,10 +14,6 @@ const queue = new Queue("cloud-agents", { }, }) -export async function enqueue( - type: T, - payload: JobTypes[T], - jobId: number, -): Promise>> { - return queue.add(type, { type, payload, jobId }, { jobId: `${type}-${jobId}` }) +export async function enqueue(params: JobParams): Promise>> { + return queue.add(params.type, params, { jobId: `${params.type}-${params.jobId}` }) } diff --git a/apps/cloud-agents/src/types/index.ts b/apps/cloud-agents/src/types/index.ts index 36dde79890..4b0a2b86d2 100644 --- a/apps/cloud-agents/src/types/index.ts +++ b/apps/cloud-agents/src/types/index.ts @@ -2,22 +2,30 @@ import { z } from "zod" export interface JobTypes { "github.issue.fix": { - repo: string // e.g., "RooCodeInc/Roo-Code" - issue: number // Issue number - title: string // Issue title - body: string // Issue description - labels?: string[] // Issue labels + repo: string + issue: number + title: string + body: string + labels?: string[] } } +export type JobType = keyof JobTypes + export type JobStatus = "pending" | "processing" | "completed" | "failed" -export type CloudJobData = { - type: T - payload: JobTypes[T] +export type JobPayload = JobTypes[T] + +export type JobParams = { jobId: number + type: T + payload: JobPayload } +/** + * CreateJob + */ + export const createJobSchema = z.discriminatedUnion("type", [ z.object({ type: z.literal("github.issue.fix"), @@ -32,3 +40,22 @@ export const createJobSchema = z.discriminatedUnion("type", [ ]) export type CreateJob = z.infer + +/** + * GitHubWebhook + */ + +export const githubWebhookSchema = z.object({ + action: z.string(), + issue: z.object({ + number: z.number(), + title: z.string(), + body: z.string().nullable(), + labels: z.array(z.object({ name: z.string() })), + }), + repository: z.object({ + full_name: z.string(), + }), +}) + +export type GitHubWebhook = z.infer diff --git a/packages/types/src/global-settings.ts b/packages/types/src/global-settings.ts index e0eeb70a33..295201a62a 100644 --- a/packages/types/src/global-settings.ts +++ b/packages/types/src/global-settings.ts @@ -106,89 +106,7 @@ export const globalSettingsSchema = z.object({ export type GlobalSettings = z.infer -export const GLOBAL_SETTINGS_KEYS = keysOf()([ - "currentApiConfigName", - "listApiConfigMeta", - "pinnedApiConfigs", - - "lastShownAnnouncementId", - "customInstructions", - "taskHistory", - - "condensingApiConfigId", - "customCondensingPrompt", - - "autoApprovalEnabled", - "alwaysAllowReadOnly", - "alwaysAllowReadOnlyOutsideWorkspace", - "alwaysAllowWrite", - "alwaysAllowWriteOutsideWorkspace", - "writeDelayMs", - "alwaysAllowBrowser", - "alwaysApproveResubmit", - "requestDelaySeconds", - "alwaysAllowMcp", - "alwaysAllowModeSwitch", - "alwaysAllowSubtasks", - "alwaysAllowExecute", - "allowedCommands", - "allowedMaxRequests", - "autoCondenseContext", - "autoCondenseContextPercent", - "maxConcurrentFileReads", - - "browserToolEnabled", - "browserViewportSize", - "screenshotQuality", - "remoteBrowserEnabled", - "remoteBrowserHost", - - "enableCheckpoints", - - "ttsEnabled", - "ttsSpeed", - "soundEnabled", - "soundVolume", - - "maxOpenTabsContext", - "maxWorkspaceFiles", - "showRooIgnoredFiles", - "maxReadFileLine", - - "terminalOutputLineLimit", - "terminalShellIntegrationTimeout", - "terminalShellIntegrationDisabled", - "terminalCommandDelay", - "terminalPowershellCounter", - "terminalZshClearEolMark", - "terminalZshOhMy", - "terminalZshP10k", - "terminalZdotdir", - "terminalCompressProgressBar", - - "rateLimitSeconds", - "diffEnabled", - "fuzzyMatchThreshold", - "experiments", - - "codebaseIndexModels", - "codebaseIndexConfig", - - "language", - - "telemetrySetting", - "mcpEnabled", - "enableMcpServerCreation", - - "mode", - "modeApiConfigs", - "customModes", - "customModePrompts", - "customSupportPrompts", - "enhancementApiConfigId", - "cachedChromeHostUrl", - "historyPreviewCollapsed", -]) +export const GLOBAL_SETTINGS_KEYS = globalSettingsSchema.keyof().options /** * RooCodeSettings diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 29cede13ba..2debdde51c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -123,6 +123,9 @@ importers: '@types/react-dom': specifier: ^18.3.7 version: 18.3.7(@types/react@18.3.23) + concurrently: + specifier: ^9.1.0 + version: 9.1.2 drizzle-kit: specifier: ^0.31.1 version: 0.31.1 @@ -5318,6 +5321,11 @@ packages: concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + concurrently@9.1.2: + resolution: {integrity: sha512-H9MWcoPsYddwbOGM6difjVwVZHl63nwMEwDJG/L7VGtuaJhb12h2caPG2tVPWs7emuYix252iGfqOyrz1GczTQ==} + engines: {node: '>=18'} + hasBin: true + confbox@0.1.8: resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==} @@ -9179,6 +9187,9 @@ packages: rw@1.3.3: resolution: {integrity: sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==} + rxjs@7.8.2: + resolution: {integrity: sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==} + safe-array-concat@1.1.3: resolution: {integrity: sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==} engines: {node: '>=0.4'} @@ -14476,7 +14487,7 @@ snapshots: '@types/yauzl@2.10.3': dependencies: - '@types/node': 20.17.57 + '@types/node': 22.15.29 optional: true '@typescript-eslint/eslint-plugin@8.32.1(@typescript-eslint/parser@8.32.1(eslint@9.27.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.27.0(jiti@2.4.2))(typescript@5.8.3)': @@ -15513,6 +15524,16 @@ snapshots: concat-map@0.0.1: {} + concurrently@9.1.2: + dependencies: + chalk: 4.1.2 + lodash: 4.17.21 + rxjs: 7.8.2 + shell-quote: 1.8.3 + supports-color: 8.1.1 + tree-kill: 1.2.2 + yargs: 17.7.2 + confbox@0.1.8: {} confbox@0.2.2: {} @@ -20291,6 +20312,10 @@ snapshots: rw@1.3.3: {} + rxjs@7.8.2: + dependencies: + tslib: 2.8.1 + safe-array-concat@1.1.3: dependencies: call-bind: 1.0.8 @@ -20446,8 +20471,7 @@ snapshots: shell-quote@1.8.2: {} - shell-quote@1.8.3: - optional: true + shell-quote@1.8.3: {} shiki@3.4.1: dependencies: From 05dac0e09aef4fb8639e1fda5c8821d2a961a203 Mon Sep 17 00:00:00 2001 From: cte Date: Fri, 13 Jun 2025 09:41:27 -0700 Subject: [PATCH 08/19] More progress --- apps/cloud-agents/scripts/enqueue-test-job.sh | 14 ++---- apps/cloud-agents/src/app/api/jobs/route.ts | 15 ++++--- .../src/app/api/webhooks/github/route.ts | 43 +++++++++---------- .../src/lib/jobs/fixGitHubIssue.ts | 4 ++ apps/cloud-agents/src/lib/runTask.ts | 4 +- 5 files changed, 39 insertions(+), 41 deletions(-) diff --git a/apps/cloud-agents/scripts/enqueue-test-job.sh b/apps/cloud-agents/scripts/enqueue-test-job.sh index 6133fdb32d..09f32fb2dc 100755 --- a/apps/cloud-agents/scripts/enqueue-test-job.sh +++ b/apps/cloud-agents/scripts/enqueue-test-job.sh @@ -9,8 +9,8 @@ if [ -z "$ISSUE_NUMBER" ]; then echo "Usage: $0 [repo]" echo "" echo "Examples:" - echo " $0 4567 # Fetch issue #4567 from RooCodeInc/Roo-Code" - echo " $0 123 owner/repo # Fetch issue #123 from owner/repo" + echo " $0 4567 # Fetch issue #4567 from RooCodeInc/Roo-Code" + echo " $0 123 owner/repo # Fetch issue #123 from owner/repo" echo "" echo "This script fetches real GitHub issue data and enqueues it as a job." exit 1 @@ -54,11 +54,6 @@ LABELS=$(echo "$ISSUE_DATA" | jq -r '[.labels[].name] | @json') TITLE_ESCAPED=$(printf '%s' "$TITLE" | sed 's/"/\\"/g' | sed 's/\n/\\n/g') BODY_ESCAPED=$(printf '%s' "$BODY" | sed 's/"/\\"/g' | awk '{printf "%s\\n", $0}' | sed 's/\\n$//') -echo "Issue Title: $TITLE" -echo "Issue Labels: $LABELS" -echo "" -echo "Enqueueing job..." - JSON_PAYLOAD=$(cat <", event) + const event = request.headers.get("x-github-event") if (event !== "issues") { return NextResponse.json({ message: "event_ignored" }) } const data = githubWebhookSchema.parse(JSON.parse(body)) + console.log("🗄️ Webhook ->", data) + const { action, repository, issue } = data - console.log("🗄️ Data ->", data) - - if (data.action !== "opened") { + if (action !== "opened") { return NextResponse.json({ message: "action_ignored" }) } const type: JobType = "github.issue.fix" - const status: JobStatus = "pending" const payload: JobPayload = { - repo: data.repository.full_name, - issue: data.issue.number, - title: data.issue.title, - body: data.issue.body || "", - labels: data.issue.labels.map((label) => label.name), + repo: repository.full_name, + issue: issue.number, + title: issue.title, + body: issue.body || "", + labels: issue.labels.map(({ name }) => name), } - const [job] = await db.insert(cloudJobs).values({ type, status, payload }).returning() + const [job] = await db.insert(cloudJobs).values({ type, payload, status: "pending" }).returning() if (!job) { - throw new Error("Failed to create job") + throw new Error("Failed to create `cloudJobs` record.") } - await enqueue({ jobId: job.id, type, payload }) - return NextResponse.json({ message: "Job created successfully", jobId: job.id }) + const enqueuedJob = await enqueue({ jobId: job.id, type, payload }) + console.log("🔗 Enqueued job ->", enqueuedJob) + + return NextResponse.json({ message: "job_enqueued", jobId: job.id, enqueuedJobId: enqueuedJob.id }) } catch (error) { - console.error("GitHub webhook error:", error) + console.error("GitHub Webhook Error:", error) if (error instanceof z.ZodError) { - return NextResponse.json({ error: "Invalid webhook payload", details: error.errors }, { status: 400 }) + return NextResponse.json({ error: "bad_request", details: error.errors }, { status: 400 }) } - return NextResponse.json({ error: "Internal server error" }, { status: 500 }) + return NextResponse.json({ error: "internal_server_error" }, { status: 500 }) } } diff --git a/apps/cloud-agents/src/lib/jobs/fixGitHubIssue.ts b/apps/cloud-agents/src/lib/jobs/fixGitHubIssue.ts index a8a1496a9a..9da4e287ef 100644 --- a/apps/cloud-agents/src/lib/jobs/fixGitHubIssue.ts +++ b/apps/cloud-agents/src/lib/jobs/fixGitHubIssue.ts @@ -26,6 +26,10 @@ ${payload.body} ${payload.labels && payload.labels.length > 0 ? `Labels: ${payload.labels.join(", ")}` : ""} Please analyze the issue, understand what needs to be fixed, and implement a solution. +If you're reasonably satisfied with the solution then create and submit a pull request using the "gh" command line tool. +You'll first need to create a new branch for the pull request. + +Make sure to reference the issue in the pull request description. `.trim() const result = await runTask({ diff --git a/apps/cloud-agents/src/lib/runTask.ts b/apps/cloud-agents/src/lib/runTask.ts index 363f8279a1..ef74a1feaf 100644 --- a/apps/cloud-agents/src/lib/runTask.ts +++ b/apps/cloud-agents/src/lib/runTask.ts @@ -9,7 +9,7 @@ import { type TaskEvent, TaskCommandName, RooCodeEventName, IpcMessageType, EVAL import { IpcClient } from "@roo-code/ipc" import { Logger } from "./logger" -import { isDockerContainer, findGitRoot } from "./utils" +import { isDockerContainer } from "./utils" const TIMEOUT = 30 * 60 * 1_000 @@ -27,7 +27,7 @@ type RunTaskOptions = { } export const runTask = async ({ prompt, publish, logger }: RunTaskOptions) => { - const workspacePath = findGitRoot(process.cwd()) + const workspacePath = "/Users/cte/Documents/Roomote-Control" // findGitRoot(process.cwd()) const ipcSocketPath = path.resolve(os.tmpdir(), `${crypto.randomUUID().slice(0, 8)}.sock`) const env = { ROO_CODE_IPC_SOCKET_PATH: ipcSocketPath } const controller = new AbortController() From d29ab5a6a7bb0c99952078e6c5d3cdf216413a64 Mon Sep 17 00:00:00 2001 From: cte Date: Fri, 13 Jun 2025 13:01:06 -0700 Subject: [PATCH 09/19] More progress --- apps/cloud-agents/.env.example | 1 + apps/cloud-agents/src/lib/job.ts | 4 +- .../src/lib/jobs/fixGitHubIssue.ts | 38 ++-- apps/cloud-agents/src/lib/runTask.ts | 38 +++- apps/cloud-agents/src/lib/slack.ts | 170 ++++++++++++++++++ 5 files changed, 228 insertions(+), 23 deletions(-) create mode 100644 apps/cloud-agents/src/lib/slack.ts diff --git a/apps/cloud-agents/.env.example b/apps/cloud-agents/.env.example index 95fc37b06d..8ffa54f697 100644 --- a/apps/cloud-agents/.env.example +++ b/apps/cloud-agents/.env.example @@ -2,3 +2,4 @@ DATABASE_URL=postgresql://postgres:password@localhost:5433/cloud_agents REDIS_URL=redis://localhost:6380 GITHUB_WEBHOOK_SECRET=your-webhook-secret-here OPENROUTER_API_KEY=your-openrouter-api-key +SLACK_API_TOKEN=xoxb-... diff --git a/apps/cloud-agents/src/lib/job.ts b/apps/cloud-agents/src/lib/job.ts index 5fa44805e7..5ec52c44f3 100644 --- a/apps/cloud-agents/src/lib/job.ts +++ b/apps/cloud-agents/src/lib/job.ts @@ -2,7 +2,7 @@ import { eq } from "drizzle-orm" import { Job } from "bullmq" import { db, cloudJobs, type UpdateCloudJob } from "@/db" -import type { JobTypes, JobType, JobStatus, JobParams } from "@/types" +import type { JobType, JobStatus, JobParams } from "@/types" import { fixGitHubIssue } from "./jobs/fixGitHubIssue" @@ -15,7 +15,7 @@ export async function processJob({ data: { type, payload, job switch (type) { case "github.issue.fix": - result = await fixGitHubIssue(jobId, payload as JobTypes["github.issue.fix"]) + result = await fixGitHubIssue(payload) break default: throw new Error(`Unknown job type: ${type}`) diff --git a/apps/cloud-agents/src/lib/jobs/fixGitHubIssue.ts b/apps/cloud-agents/src/lib/jobs/fixGitHubIssue.ts index 9da4e287ef..daad07e671 100644 --- a/apps/cloud-agents/src/lib/jobs/fixGitHubIssue.ts +++ b/apps/cloud-agents/src/lib/jobs/fixGitHubIssue.ts @@ -1,15 +1,16 @@ import * as path from "path" import * as os from "node:os" -import type { JobTypes } from "@/types" +import type { JobType, JobPayload } from "@/types" import { runTask } from "../runTask" import { Logger } from "../logger" -export async function fixGitHubIssue( - jobId: number, - payload: JobTypes["github.issue.fix"], -): Promise<{ +const jobType: JobType = "github.issue.fix" + +type FixGitHubIssueJobPayload = JobPayload<"github.issue.fix"> + +export async function fixGitHubIssue(jobPayload: FixGitHubIssueJobPayload): Promise<{ repo: string issue: number result: unknown @@ -17,30 +18,31 @@ export async function fixGitHubIssue( const prompt = ` Fix the following GitHub issue: -Repository: ${payload.repo} -Issue #${payload.issue}: ${payload.title} +Repository: ${jobPayload.repo} +Issue #${jobPayload.issue}: ${jobPayload.title} Description: -${payload.body} +${jobPayload.body} -${payload.labels && payload.labels.length > 0 ? `Labels: ${payload.labels.join(", ")}` : ""} +${jobPayload.labels && jobPayload.labels.length > 0 ? `Labels: ${jobPayload.labels.join(", ")}` : ""} Please analyze the issue, understand what needs to be fixed, and implement a solution. -If you're reasonably satisfied with the solution then create and submit a pull request using the "gh" command line tool. -You'll first need to create a new branch for the pull request. -Make sure to reference the issue in the pull request description. +If you're reasonably satisfied with the solution then create and submit a pull request using the "gh" command line tool: +gh pr create --title "Fixes #${jobPayload.issue}\n\n[Your PR description here.]" --fill --template "pull_request_template.md" + +You'll first need to create a new branch for the pull request. `.trim() + const { repo, issue } = jobPayload + const result = await runTask({ + jobType, + jobPayload, prompt, publish: async () => {}, - logger: new Logger({ - logDir: path.resolve(os.tmpdir(), "logs"), - filename: "worker.log", - tag: "worker", - }), + logger: new Logger({ logDir: path.resolve(os.tmpdir(), "logs"), filename: "worker.log", tag: "worker" }), }) - return { repo: payload.repo, issue: payload.issue, result } + return { repo, issue, result } } diff --git a/apps/cloud-agents/src/lib/runTask.ts b/apps/cloud-agents/src/lib/runTask.ts index ef74a1feaf..c146db0319 100644 --- a/apps/cloud-agents/src/lib/runTask.ts +++ b/apps/cloud-agents/src/lib/runTask.ts @@ -8,8 +8,11 @@ import { execa } from "execa" import { type TaskEvent, TaskCommandName, RooCodeEventName, IpcMessageType, EVALS_SETTINGS } from "@roo-code/types" import { IpcClient } from "@roo-code/ipc" +import type { JobPayload, JobType } from "@/types" + import { Logger } from "./logger" import { isDockerContainer } from "./utils" +import { SlackNotifier } from "./slack" const TIMEOUT = 30 * 60 * 1_000 @@ -20,13 +23,21 @@ class SubprocessTimeoutError extends Error { } } -type RunTaskOptions = { +type RunTaskOptions = { + jobType: T + jobPayload: JobPayload prompt: string publish: (taskEvent: TaskEvent) => Promise logger: Logger } -export const runTask = async ({ prompt, publish, logger }: RunTaskOptions) => { +export const runTask = async ({ + jobType, + jobPayload, + prompt, + publish, + logger, +}: RunTaskOptions) => { const workspacePath = "/Users/cte/Documents/Roomote-Control" // findGitRoot(process.cwd()) const ipcSocketPath = path.resolve(os.tmpdir(), `${crypto.randomUUID().slice(0, 8)}.sock`) const env = { ROO_CODE_IPC_SOCKET_PATH: ipcSocketPath } @@ -73,13 +84,16 @@ export const runTask = async ({ prompt, publish, logger }: RunTaskOptions) => { } } - let taskStartedAt = Date.now() // eslint-disable-line @typescript-eslint/no-unused-vars + let taskStartedAt = Date.now() let taskFinishedAt: number | undefined let taskAbortedAt: number | undefined let taskTimedOut: boolean = false let rooTaskId: string | undefined let isClientDisconnected = false + const slackNotifier = new SlackNotifier(logger) + let slackThreadTs: string | null = null + const ignoreEvents: Record<"broadcast" | "log", RooCodeEventName[]> = { broadcast: [RooCodeEventName.Message], log: [RooCodeEventName.TaskTokenUsageUpdated, RooCodeEventName.TaskAskResponded], @@ -105,14 +119,23 @@ export const runTask = async ({ prompt, publish, logger }: RunTaskOptions) => { if (eventName === RooCodeEventName.TaskStarted) { taskStartedAt = Date.now() rooTaskId = payload[0] + slackThreadTs = await slackNotifier.postTaskStarted({ jobType, jobPayload, rooTaskId }) } if (eventName === RooCodeEventName.TaskAborted) { taskAbortedAt = Date.now() + + if (slackThreadTs) { + await slackNotifier.postTaskUpdated(slackThreadTs, "Task was aborted", "warning") + } } if (eventName === RooCodeEventName.TaskCompleted) { taskFinishedAt = Date.now() + + if (slackThreadTs) { + await slackNotifier.postTaskCompleted(slackThreadTs, true, taskFinishedAt - taskStartedAt, rooTaskId) + } } }) @@ -142,6 +165,10 @@ export const runTask = async ({ prompt, publish, logger }: RunTaskOptions) => { taskTimedOut = true logger.error("time limit reached") + if (slackThreadTs) { + await slackNotifier.postTaskUpdated(slackThreadTs, "Task timed out after 30 minutes", "error") + } + if (rooTaskId && !isClientDisconnected) { logger.info("cancelling task") client.sendCommand({ commandName: TaskCommandName.CancelTask, data: rooTaskId }) @@ -153,6 +180,11 @@ export const runTask = async ({ prompt, publish, logger }: RunTaskOptions) => { if (!taskFinishedAt && !taskTimedOut) { logger.error("client disconnected before task finished") + + if (slackThreadTs) { + await slackNotifier.postTaskUpdated(slackThreadTs, "Client disconnected before task completion", "error") + } + throw new Error("Client disconnected before task completion.") } diff --git a/apps/cloud-agents/src/lib/slack.ts b/apps/cloud-agents/src/lib/slack.ts new file mode 100644 index 0000000000..6cad11c890 --- /dev/null +++ b/apps/cloud-agents/src/lib/slack.ts @@ -0,0 +1,170 @@ +import { JobPayload, JobType } from "@/types" +import { Logger } from "./logger" + +export interface SlackMessage { + text: string + blocks?: unknown[] + attachments?: unknown[] + thread_ts?: string + channel?: string +} + +// Example response: +// { +// "ok": true, +// "channel": "C123ABC456", +// "ts": "1503435956.000247", +// "message": { +// "text": "Here's a message for you", +// "username": "ecto1", +// "bot_id": "B123ABC456", +// "attachments": [ +// { +// "text": "This is an attachment", +// "id": 1, +// "fallback": "This is an attachment's fallback" +// } +// ], +// "type": "message", +// "subtype": "bot_message", +// "ts": "1503435956.000247" +// } +// } + +export interface SlackResponse { + ok: boolean + channel?: string + ts?: string + error?: string + message?: Record +} + +export class SlackNotifier { + private readonly logger: Logger + private readonly token: string + + constructor(logger: Logger, token: string = process.env.SLACK_API_TOKEN!) { + this.logger = logger + this.token = token + } + + private async postMessage(message: SlackMessage): Promise { + try { + const messageWithChannel = { ...message, channel: message.channel || "#roomote-control" } + + const response = await fetch("https://slack.com/api/chat.postMessage", { + method: "POST", + headers: { "Content-Type": "application/json", Authorization: `Bearer ${this.token}` }, + body: JSON.stringify(messageWithChannel), + }) + + if (!response.ok) { + this.logger.error(`Slack API failed: ${response.status} ${response.statusText}`) + return null + } + + const result: SlackResponse = await response.json() + + if (!result.ok) { + if (result.error === "not_in_channel") { + this.logger.error( + `Slack bot is not a member of channel "${messageWithChannel.channel}". ` + + `Please add the bot to the channel or ensure the channel exists. ` + + `Error: ${result.error}`, + ) + } else { + this.logger.error(`Slack API error: ${result.error}`) + } + return null + } + + return result.ts ?? null + } catch (error) { + this.logger.error("Failed to send Slack message:", error) + return null + } + } + + public async postTaskStarted({ + jobType, + jobPayload, + rooTaskId, + }: { + jobType: T + jobPayload: JobPayload + rooTaskId: string + }) { + switch (jobType) { + case "github.issue.fix": + return await this.postMessage({ + text: `🚀 Task Started`, + blocks: [ + { + type: "header", + text: { type: "plain_text", text: "🚀 Roo Code Task Started" }, + }, + { + type: "section", + text: { + type: "mrkdwn", + text: `Creating a pull request for `, + }, + }, + { + type: "context", + elements: [ + { + type: "mrkdwn", + text: `jobType: ${jobType}, rooTaskId: ${rooTaskId}`, + }, + ], + }, + ], + }) + default: + throw new Error(`Unknown job type: ${jobType}`) + } + } + + public async postTaskUpdated( + threadTs: string, + text: string, + status?: "info" | "success" | "warning" | "error", + ): Promise { + const emoji = { info: "ℹ️", success: "✅", warning: "⚠️", error: "❌" }[status || "info"] + await this.postMessage({ text: `${emoji} ${text}`, thread_ts: threadTs }) + } + + public async postTaskCompleted( + threadTs: string, + success: boolean, + duration: number, + taskId?: string, + ): Promise { + const status = success ? "✅ Completed" : "❌ Failed" + const durationText = `${Math.round(duration / 1000)}s` + + await this.postMessage({ + text: `${status} Task finished in ${durationText}`, + blocks: [ + { + type: "section", + text: { + type: "mrkdwn", + text: `*${status}*\n*Task ID:* ${taskId || "Unknown"}\n*Duration:* ${durationText}`, + }, + }, + { + type: "context", + elements: [ + { + type: "mrkdwn", + text: `Finished at: `, + }, + ], + }, + ], + thread_ts: threadTs, + }) + } +} From 11d4058391e0a3264545b15ccdf8fd33c504fe0f Mon Sep 17 00:00:00 2001 From: cte Date: Fri, 13 Jun 2025 15:18:10 -0700 Subject: [PATCH 10/19] More progress --- apps/cloud-agents/.env.example | 8 +- apps/cloud-agents/docker-compose.yml | 37 ++------ apps/cloud-agents/docker/Dockerfile.runner | 88 ------------------- apps/cloud-agents/docker/Dockerfile.worker | 60 +++++++++++++ apps/cloud-agents/package.json | 2 +- .../src/app/api/webhooks/github/route.ts | 2 +- apps/cloud-agents/src/lib/slack.ts | 39 +------- 7 files changed, 77 insertions(+), 159 deletions(-) delete mode 100644 apps/cloud-agents/docker/Dockerfile.runner create mode 100644 apps/cloud-agents/docker/Dockerfile.worker diff --git a/apps/cloud-agents/.env.example b/apps/cloud-agents/.env.example index 8ffa54f697..0e1eb3ae79 100644 --- a/apps/cloud-agents/.env.example +++ b/apps/cloud-agents/.env.example @@ -1,5 +1,9 @@ DATABASE_URL=postgresql://postgres:password@localhost:5433/cloud_agents REDIS_URL=redis://localhost:6380 -GITHUB_WEBHOOK_SECRET=your-webhook-secret-here -OPENROUTER_API_KEY=your-openrouter-api-key + +GH_WEBHOOK_SECRET=your-webhook-secret-here +GH_TOKEN=your-token-here + +OPENROUTER_API_KEY=sk-or-v1-... + SLACK_API_TOKEN=xoxb-... diff --git a/apps/cloud-agents/docker-compose.yml b/apps/cloud-agents/docker-compose.yml index 9cdce01568..feb7e16b79 100644 --- a/apps/cloud-agents/docker-compose.yml +++ b/apps/cloud-agents/docker-compose.yml @@ -16,8 +16,6 @@ services: timeout: 5s retries: 5 start_period: 30s - profiles: - - server redis: container_name: cloud-agents-redis @@ -27,8 +25,6 @@ services: volumes: - ./docker/redis-data:/data command: redis-server --appendonly yes - profiles: - - server dashboard: container_name: cloud-agents-dashboard @@ -41,9 +37,8 @@ services: - REDIS_URL=redis://redis:6379 - NODE_ENV=production depends_on: - - redis - profiles: - - server + redis: + condition: service_started api: build: @@ -62,31 +57,15 @@ services: condition: service_healthy redis: condition: service_started - profiles: - - server worker: build: context: ../../ - dockerfile: apps/cloud-agents/docker/Dockerfile.app - command: pnpm --filter @roo-code/cloud-agents worker:dev - environment: - - DATABASE_URL=postgresql://postgres:password@db:5432/cloud_agents - - REDIS_URL=redis://redis:6379 - - NODE_ENV=production - volumes: - - /var/run/docker.sock:/var/run/docker.sock - - /tmp/cloud-agents:/var/log/cloud-agents - depends_on: - - db - - redis - profiles: - - server - - runner: - build: - context: ../../ - dockerfile: apps/cloud-agents/docker/Dockerfile.runner + dockerfile: apps/cloud-agents/docker/Dockerfile.worker + args: + - GH_TOKEN=${GH_TOKEN} + env_file: + - .env environment: - HOST_EXECUTION_METHOD=docker volumes: @@ -94,8 +73,6 @@ services: - /tmp/cloud-agents:/var/log/cloud-agents stdin_open: true tty: true - profiles: - - runner networks: default: diff --git a/apps/cloud-agents/docker/Dockerfile.runner b/apps/cloud-agents/docker/Dockerfile.runner deleted file mode 100644 index e6cdb0fc81..0000000000 --- a/apps/cloud-agents/docker/Dockerfile.runner +++ /dev/null @@ -1,88 +0,0 @@ -FROM node:20-slim AS base - -# Install pnpm -ENV PNPM_HOME="/pnpm" -ENV PATH="$PNPM_HOME:$PATH" -RUN corepack enable - -# Install system packages -RUN apt update && \ - apt install -y \ - curl \ - git \ - vim \ - jq \ - netcat-openbsd \ - apt-transport-https \ - ca-certificates \ - gnupg \ - lsb-release \ - wget \ - gpg \ - xvfb \ - && rm -rf /var/lib/apt/lists/* - -# Install Docker cli -RUN curl -fsSL https://download.docker.com/linux/debian/gpg | gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg \ - && echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/debian $(lsb_release -cs) stable" | tee /etc/apt/sources.list.d/docker.list > /dev/null \ - && apt update && apt install -y docker-ce-cli \ - && rm -rf /var/lib/apt/lists/* - -# Install VS Code -RUN wget -qO- https://packages.microsoft.com/keys/microsoft.asc | gpg --dearmor > packages.microsoft.gpg \ - && install -D -o root -g root -m 644 packages.microsoft.gpg /etc/apt/keyrings/packages.microsoft.gpg \ - && echo "deb [arch=amd64,arm64,armhf signed-by=/etc/apt/keyrings/packages.microsoft.gpg] https://packages.microsoft.com/repos/code stable main" | tee /etc/apt/sources.list.d/vscode.list > /dev/null \ - && rm -f packages.microsoft.gpg \ - && apt update && apt install -y code \ - && rm -rf /var/lib/apt/lists/* - -WORKDIR /roo - -# Create VS Code template directory and install basic extensions -RUN mkdir -p /roo/.vscode-template \ - && code --no-sandbox --user-data-dir /roo/.vscode-template --install-extension ms-vscode.vscode-eslint - -WORKDIR /roo/repo - -# Install npm packages -RUN mkdir -p \ - scripts \ - packages/build \ - packages/config-eslint \ - packages/config-typescript \ - packages/ipc \ - packages/types \ - src \ - webview-ui - -# Copy package files for dependency installation -COPY ./package.json ./ -COPY ./pnpm-lock.yaml ./ -COPY ./pnpm-workspace.yaml ./ -COPY ./scripts/bootstrap.mjs ./scripts/ -COPY ./packages/build/package.json ./packages/build/ -COPY ./packages/config-eslint/package.json ./packages/config-eslint/ -COPY ./packages/config-typescript/package.json ./packages/config-typescript/ -COPY ./packages/ipc/package.json ./packages/ipc/ -COPY ./packages/types/package.json ./packages/types/ -COPY ./src/package.json ./src/ -COPY ./webview-ui/package.json ./webview-ui/ - -RUN pnpm install - -# Copy source code -COPY . ./ - -# Copy the pre-installed VS Code extensions -RUN cp -r /roo/.vscode-template /roo/.vscode - -# Build the Roo Code extension -RUN pnpm vsix -- --out ../bin/roo-code.vsix \ - && code --no-sandbox --user-data-dir /roo/.vscode --install-extension bin/roo-code.vsix - -# Set environment variables -ENV DATABASE_URL=postgresql://postgres:password@db:5432/cloud_agents -ENV REDIS_URL=redis://redis:6379 - -# Default command -CMD ["bash"] \ No newline at end of file diff --git a/apps/cloud-agents/docker/Dockerfile.worker b/apps/cloud-agents/docker/Dockerfile.worker new file mode 100644 index 0000000000..378951a556 --- /dev/null +++ b/apps/cloud-agents/docker/Dockerfile.worker @@ -0,0 +1,60 @@ +# docker compose build worker + +FROM node:20-slim AS base + +# Install pnpm +ENV PNPM_HOME="/pnpm" +ENV PATH="$PNPM_HOME:$PATH" +RUN corepack enable + +# Install system packages +RUN apt update && \ + apt install -y \ + curl \ + git \ + vim \ + jq \ + netcat-openbsd \ + apt-transport-https \ + ca-certificates \ + gnupg \ + lsb-release \ + wget \ + gpg \ + xvfb \ + gh \ + && rm -rf /var/lib/apt/lists/* + +# Install Docker cli +RUN curl -fsSL https://download.docker.com/linux/debian/gpg | gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg \ + && echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/debian $(lsb_release -cs) stable" | tee /etc/apt/sources.list.d/docker.list > /dev/null \ + && apt update && apt install -y docker-ce-cli \ + && rm -rf /var/lib/apt/lists/* + +# Install VS Code +RUN wget -qO- https://packages.microsoft.com/keys/microsoft.asc | gpg --dearmor > packages.microsoft.gpg \ + && install -D -o root -g root -m 644 packages.microsoft.gpg /etc/apt/keyrings/packages.microsoft.gpg \ + && echo "deb [arch=amd64,arm64,armhf signed-by=/etc/apt/keyrings/packages.microsoft.gpg] https://packages.microsoft.com/repos/code stable main" | tee /etc/apt/sources.list.d/vscode.list > /dev/null \ + && rm -f packages.microsoft.gpg \ + && apt update && apt install -y code \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /roo + +# Install extensions +RUN mkdir -p /roo/.vscode \ + && code --no-sandbox --user-data-dir /roo/.vscode --install-extension dbaeumer.vscode-eslint \ + && code --no-sandbox --user-data-dir /roo/.vscode --install-extension esbenp.prettier-vscode \ + && code --no-sandbox --user-data-dir /roo/.vscode --install-extension csstools.postcss \ + && code --no-sandbox --user-data-dir /roo/.vscode --install-extension RooVeterinaryInc.roo-cline + +# Clone repo +ARG GH_TOKEN +ENV GH_TOKEN=$GH_TOKEN +RUN gh repo clone RooCodeInc/Roo-Code + +WORKDIR /roo/Roo-Code +RUN pnpm install + +WORKDIR /roo +CMD ["bash"] diff --git a/apps/cloud-agents/package.json b/apps/cloud-agents/package.json index fc6af0d1ea..2412e36e01 100644 --- a/apps/cloud-agents/package.json +++ b/apps/cloud-agents/package.json @@ -16,7 +16,7 @@ "db:check": "pnpm drizzle-kit check", "db:studio": "pnpm drizzle-kit studio", "services:start": "docker compose up -d db redis dashboard", - "services:stop": "docker compose down", + "services:stop": "docker compose down dashboard redis db", "worker": "dotenvx run -f .env -- tsx src/lib/worker.ts", "dashboard": "tsx scripts/dashboard.ts" }, diff --git a/apps/cloud-agents/src/app/api/webhooks/github/route.ts b/apps/cloud-agents/src/app/api/webhooks/github/route.ts index 09d2e092e1..1f88747a53 100644 --- a/apps/cloud-agents/src/app/api/webhooks/github/route.ts +++ b/apps/cloud-agents/src/app/api/webhooks/github/route.ts @@ -22,7 +22,7 @@ export async function POST(request: NextRequest) { const body = await request.text() - if (!verifySignature(body, signature, process.env.GITHUB_WEBHOOK_SECRET!)) { + if (!verifySignature(body, signature, process.env.GH_WEBHOOK_SECRET!)) { return NextResponse.json({ error: "invalid_signature" }, { status: 401 }) } diff --git a/apps/cloud-agents/src/lib/slack.ts b/apps/cloud-agents/src/lib/slack.ts index 6cad11c890..b34f477d24 100644 --- a/apps/cloud-agents/src/lib/slack.ts +++ b/apps/cloud-agents/src/lib/slack.ts @@ -9,28 +9,6 @@ export interface SlackMessage { channel?: string } -// Example response: -// { -// "ok": true, -// "channel": "C123ABC456", -// "ts": "1503435956.000247", -// "message": { -// "text": "Here's a message for you", -// "username": "ecto1", -// "bot_id": "B123ABC456", -// "attachments": [ -// { -// "text": "This is an attachment", -// "id": 1, -// "fallback": "This is an attachment's fallback" -// } -// ], -// "type": "message", -// "subtype": "bot_message", -// "ts": "1503435956.000247" -// } -// } - export interface SlackResponse { ok: boolean channel?: string @@ -66,16 +44,7 @@ export class SlackNotifier { const result: SlackResponse = await response.json() if (!result.ok) { - if (result.error === "not_in_channel") { - this.logger.error( - `Slack bot is not a member of channel "${messageWithChannel.channel}". ` + - `Please add the bot to the channel or ensure the channel exists. ` + - `Error: ${result.error}`, - ) - } else { - this.logger.error(`Slack API error: ${result.error}`) - } - return null + this.logger.error(`Slack API error: ${result.error}`) } return result.ts ?? null @@ -99,15 +68,11 @@ export class SlackNotifier { return await this.postMessage({ text: `🚀 Task Started`, blocks: [ - { - type: "header", - text: { type: "plain_text", text: "🚀 Roo Code Task Started" }, - }, { type: "section", text: { type: "mrkdwn", - text: `Creating a pull request for `, + text: `🚀 *Task Started*\nCreating a pull request for `, }, }, { From 8dab4a7220899572441e9712d68999221a63be02 Mon Sep 17 00:00:00 2001 From: cte Date: Fri, 13 Jun 2025 15:20:38 -0700 Subject: [PATCH 11/19] Rename --- .dockerignore | 2 +- apps/{cloud-agents => roomote}/.env.example | 0 apps/{cloud-agents => roomote}/.gitignore | 0 .../{cloud-agents => roomote}/docker-compose.yml | 16 ++++++++-------- .../docker/Dockerfile.app | 8 ++++---- .../docker/Dockerfile.dashboard | 8 ++++---- .../docker/Dockerfile.worker | 0 apps/{cloud-agents => roomote}/drizzle.config.ts | 0 .../drizzle/0000_cuddly_luke_cage.sql | 0 .../drizzle/meta/0000_snapshot.json | 0 .../drizzle/meta/_journal.json | 0 apps/{cloud-agents => roomote}/eslint.config.mjs | 0 apps/{cloud-agents => roomote}/next-env.d.ts | 0 apps/{cloud-agents => roomote}/next.config.ts | 0 apps/{cloud-agents => roomote}/package.json | 2 +- .../scripts/dashboard.ts | 2 +- .../scripts/enqueue-test-job.sh | 0 .../src/app/api/health/route.ts | 0 .../src/app/api/jobs/[id]/route.ts | 0 .../src/app/api/jobs/route.ts | 0 .../src/app/api/webhooks/github/route.ts | 0 .../{cloud-agents => roomote}/src/app/layout.tsx | 0 apps/{cloud-agents => roomote}/src/app/page.tsx | 0 apps/{cloud-agents => roomote}/src/db/index.ts | 0 apps/{cloud-agents => roomote}/src/db/schema.ts | 0 apps/{cloud-agents => roomote}/src/lib/index.ts | 0 apps/{cloud-agents => roomote}/src/lib/job.ts | 0 .../src/lib/jobs/fixGitHubIssue.ts | 0 apps/{cloud-agents => roomote}/src/lib/logger.ts | 0 apps/{cloud-agents => roomote}/src/lib/queue.ts | 2 +- apps/{cloud-agents => roomote}/src/lib/redis.ts | 0 .../{cloud-agents => roomote}/src/lib/runTask.ts | 0 apps/{cloud-agents => roomote}/src/lib/slack.ts | 0 apps/{cloud-agents => roomote}/src/lib/utils.ts | 0 apps/{cloud-agents => roomote}/src/lib/worker.ts | 2 +- .../{cloud-agents => roomote}/src/types/index.ts | 0 apps/{cloud-agents => roomote}/tsconfig.json | 0 pnpm-lock.yaml | 2 +- 38 files changed, 22 insertions(+), 22 deletions(-) rename apps/{cloud-agents => roomote}/.env.example (100%) rename apps/{cloud-agents => roomote}/.gitignore (100%) rename apps/{cloud-agents => roomote}/docker-compose.yml (81%) rename apps/{cloud-agents => roomote}/docker/Dockerfile.app (88%) rename apps/{cloud-agents => roomote}/docker/Dockerfile.dashboard (76%) rename apps/{cloud-agents => roomote}/docker/Dockerfile.worker (100%) rename apps/{cloud-agents => roomote}/drizzle.config.ts (100%) rename apps/{cloud-agents => roomote}/drizzle/0000_cuddly_luke_cage.sql (100%) rename apps/{cloud-agents => roomote}/drizzle/meta/0000_snapshot.json (100%) rename apps/{cloud-agents => roomote}/drizzle/meta/_journal.json (100%) rename apps/{cloud-agents => roomote}/eslint.config.mjs (100%) rename apps/{cloud-agents => roomote}/next-env.d.ts (100%) rename apps/{cloud-agents => roomote}/next.config.ts (100%) rename apps/{cloud-agents => roomote}/package.json (97%) rename apps/{cloud-agents => roomote}/scripts/dashboard.ts (93%) rename apps/{cloud-agents => roomote}/scripts/enqueue-test-job.sh (100%) rename apps/{cloud-agents => roomote}/src/app/api/health/route.ts (100%) rename apps/{cloud-agents => roomote}/src/app/api/jobs/[id]/route.ts (100%) rename apps/{cloud-agents => roomote}/src/app/api/jobs/route.ts (100%) rename apps/{cloud-agents => roomote}/src/app/api/webhooks/github/route.ts (100%) rename apps/{cloud-agents => roomote}/src/app/layout.tsx (100%) rename apps/{cloud-agents => roomote}/src/app/page.tsx (100%) rename apps/{cloud-agents => roomote}/src/db/index.ts (100%) rename apps/{cloud-agents => roomote}/src/db/schema.ts (100%) rename apps/{cloud-agents => roomote}/src/lib/index.ts (100%) rename apps/{cloud-agents => roomote}/src/lib/job.ts (100%) rename apps/{cloud-agents => roomote}/src/lib/jobs/fixGitHubIssue.ts (100%) rename apps/{cloud-agents => roomote}/src/lib/logger.ts (100%) rename apps/{cloud-agents => roomote}/src/lib/queue.ts (92%) rename apps/{cloud-agents => roomote}/src/lib/redis.ts (100%) rename apps/{cloud-agents => roomote}/src/lib/runTask.ts (100%) rename apps/{cloud-agents => roomote}/src/lib/slack.ts (100%) rename apps/{cloud-agents => roomote}/src/lib/utils.ts (100%) rename apps/{cloud-agents => roomote}/src/lib/worker.ts (92%) rename apps/{cloud-agents => roomote}/src/types/index.ts (100%) rename apps/{cloud-agents => roomote}/tsconfig.json (100%) diff --git a/.dockerignore b/.dockerignore index fd3b383272..3ad0f8d8e4 100644 --- a/.dockerignore +++ b/.dockerignore @@ -77,7 +77,7 @@ src/node_modules !pnpm-workspace.yaml !scripts/bootstrap.mjs !apps/web-evals/ -!apps/cloud-agents/ +!apps/roomote/ !src/ !webview-ui/ !packages/evals/.docker/entrypoints/runner.sh diff --git a/apps/cloud-agents/.env.example b/apps/roomote/.env.example similarity index 100% rename from apps/cloud-agents/.env.example rename to apps/roomote/.env.example diff --git a/apps/cloud-agents/.gitignore b/apps/roomote/.gitignore similarity index 100% rename from apps/cloud-agents/.gitignore rename to apps/roomote/.gitignore diff --git a/apps/cloud-agents/docker-compose.yml b/apps/roomote/docker-compose.yml similarity index 81% rename from apps/cloud-agents/docker-compose.yml rename to apps/roomote/docker-compose.yml index feb7e16b79..b67de802e0 100644 --- a/apps/cloud-agents/docker-compose.yml +++ b/apps/roomote/docker-compose.yml @@ -1,6 +1,6 @@ services: db: - container_name: cloud-agents-db + container_name: roomote-db image: postgres:17.5 ports: - "5433:5432" @@ -18,7 +18,7 @@ services: start_period: 30s redis: - container_name: cloud-agents-redis + container_name: roomote-redis image: redis:7-alpine ports: - "6380:6379" @@ -27,10 +27,10 @@ services: command: redis-server --appendonly yes dashboard: - container_name: cloud-agents-dashboard + container_name: roomote-dashboard build: context: ../../ - dockerfile: apps/cloud-agents/docker/Dockerfile.dashboard + dockerfile: apps/roomote/docker/Dockerfile.dashboard ports: - "3002:3002" environment: @@ -43,7 +43,7 @@ services: api: build: context: ../../ - dockerfile: apps/cloud-agents/docker/Dockerfile.app + dockerfile: apps/roomote/docker/Dockerfile.app ports: - "3001:3001" environment: @@ -61,7 +61,7 @@ services: worker: build: context: ../../ - dockerfile: apps/cloud-agents/docker/Dockerfile.worker + dockerfile: apps/roomote/docker/Dockerfile.worker args: - GH_TOKEN=${GH_TOKEN} env_file: @@ -70,11 +70,11 @@ services: - HOST_EXECUTION_METHOD=docker volumes: - /var/run/docker.sock:/var/run/docker.sock - - /tmp/cloud-agents:/var/log/cloud-agents + - /tmp/roomote:/var/log/roomote stdin_open: true tty: true networks: default: - name: cloud-agents_default + name: roomote_default driver: bridge diff --git a/apps/cloud-agents/docker/Dockerfile.app b/apps/roomote/docker/Dockerfile.app similarity index 88% rename from apps/cloud-agents/docker/Dockerfile.app rename to apps/roomote/docker/Dockerfile.app index 1269e4e25d..051646ab54 100644 --- a/apps/cloud-agents/docker/Dockerfile.app +++ b/apps/roomote/docker/Dockerfile.app @@ -26,19 +26,19 @@ COPY scripts/bootstrap.mjs ./scripts/ COPY packages/types/package.json ./packages/types/ COPY packages/config-eslint/package.json ./packages/config-eslint/ COPY packages/config-typescript/package.json ./packages/config-typescript/ -COPY apps/cloud-agents/package.json ./apps/cloud-agents/ +COPY apps/roomote/package.json ./apps/roomote/ # Install dependencies -RUN pnpm install --filter @roo-code/cloud-agents +RUN pnpm install --filter @roo-code/roomote # Copy source code COPY packages/types ./packages/types/ COPY packages/config-eslint ./packages/config-eslint/ COPY packages/config-typescript ./packages/config-typescript/ -COPY apps/cloud-agents ./apps/cloud-agents/ +COPY apps/roomote ./apps/roomote/ # Set working directory to the app -WORKDIR /app/apps/cloud-agents +WORKDIR /app/apps/roomote # Build the application RUN pnpm build diff --git a/apps/cloud-agents/docker/Dockerfile.dashboard b/apps/roomote/docker/Dockerfile.dashboard similarity index 76% rename from apps/cloud-agents/docker/Dockerfile.dashboard rename to apps/roomote/docker/Dockerfile.dashboard index f5bbd57a03..5e674fcf85 100644 --- a/apps/cloud-agents/docker/Dockerfile.dashboard +++ b/apps/roomote/docker/Dockerfile.dashboard @@ -11,16 +11,16 @@ COPY scripts/bootstrap.mjs ./scripts/ COPY packages/types/package.json ./packages/types/ COPY packages/config-eslint/package.json ./packages/config-eslint/ COPY packages/config-typescript/package.json ./packages/config-typescript/ -COPY apps/cloud-agents/package.json ./apps/cloud-agents/ +COPY apps/roomote/package.json ./apps/roomote/ -RUN pnpm install --filter @roo-code/cloud-agents +RUN pnpm install --filter @roo-code/roomote COPY packages/types ./packages/types/ COPY packages/config-eslint ./packages/config-eslint/ COPY packages/config-typescript ./packages/config-typescript/ -COPY apps/cloud-agents ./apps/cloud-agents/ +COPY apps/roomote ./apps/roomote/ -WORKDIR /app/apps/cloud-agents +WORKDIR /app/apps/roomote EXPOSE 3002 diff --git a/apps/cloud-agents/docker/Dockerfile.worker b/apps/roomote/docker/Dockerfile.worker similarity index 100% rename from apps/cloud-agents/docker/Dockerfile.worker rename to apps/roomote/docker/Dockerfile.worker diff --git a/apps/cloud-agents/drizzle.config.ts b/apps/roomote/drizzle.config.ts similarity index 100% rename from apps/cloud-agents/drizzle.config.ts rename to apps/roomote/drizzle.config.ts diff --git a/apps/cloud-agents/drizzle/0000_cuddly_luke_cage.sql b/apps/roomote/drizzle/0000_cuddly_luke_cage.sql similarity index 100% rename from apps/cloud-agents/drizzle/0000_cuddly_luke_cage.sql rename to apps/roomote/drizzle/0000_cuddly_luke_cage.sql diff --git a/apps/cloud-agents/drizzle/meta/0000_snapshot.json b/apps/roomote/drizzle/meta/0000_snapshot.json similarity index 100% rename from apps/cloud-agents/drizzle/meta/0000_snapshot.json rename to apps/roomote/drizzle/meta/0000_snapshot.json diff --git a/apps/cloud-agents/drizzle/meta/_journal.json b/apps/roomote/drizzle/meta/_journal.json similarity index 100% rename from apps/cloud-agents/drizzle/meta/_journal.json rename to apps/roomote/drizzle/meta/_journal.json diff --git a/apps/cloud-agents/eslint.config.mjs b/apps/roomote/eslint.config.mjs similarity index 100% rename from apps/cloud-agents/eslint.config.mjs rename to apps/roomote/eslint.config.mjs diff --git a/apps/cloud-agents/next-env.d.ts b/apps/roomote/next-env.d.ts similarity index 100% rename from apps/cloud-agents/next-env.d.ts rename to apps/roomote/next-env.d.ts diff --git a/apps/cloud-agents/next.config.ts b/apps/roomote/next.config.ts similarity index 100% rename from apps/cloud-agents/next.config.ts rename to apps/roomote/next.config.ts diff --git a/apps/cloud-agents/package.json b/apps/roomote/package.json similarity index 97% rename from apps/cloud-agents/package.json rename to apps/roomote/package.json index 2412e36e01..a50196aa2b 100644 --- a/apps/cloud-agents/package.json +++ b/apps/roomote/package.json @@ -1,5 +1,5 @@ { - "name": "@roo-code/cloud-agents", + "name": "@roo-code/roomote", "version": "0.0.0", "type": "module", "scripts": { diff --git a/apps/cloud-agents/scripts/dashboard.ts b/apps/roomote/scripts/dashboard.ts similarity index 93% rename from apps/cloud-agents/scripts/dashboard.ts rename to apps/roomote/scripts/dashboard.ts index 24acc39f96..f43ba904b4 100644 --- a/apps/cloud-agents/scripts/dashboard.ts +++ b/apps/roomote/scripts/dashboard.ts @@ -7,7 +7,7 @@ import express from "express" import type { Express, Request, Response } from "express" const redis = new IORedis(process.env.REDIS_URL || "redis://localhost:6380", { maxRetriesPerRequest: null }) -const queue = new Queue("cloud-agents", { connection: redis }) +const queue = new Queue("roomote", { connection: redis }) const serverAdapter = new ExpressAdapter() serverAdapter.setBasePath("/admin/queues") diff --git a/apps/cloud-agents/scripts/enqueue-test-job.sh b/apps/roomote/scripts/enqueue-test-job.sh similarity index 100% rename from apps/cloud-agents/scripts/enqueue-test-job.sh rename to apps/roomote/scripts/enqueue-test-job.sh diff --git a/apps/cloud-agents/src/app/api/health/route.ts b/apps/roomote/src/app/api/health/route.ts similarity index 100% rename from apps/cloud-agents/src/app/api/health/route.ts rename to apps/roomote/src/app/api/health/route.ts diff --git a/apps/cloud-agents/src/app/api/jobs/[id]/route.ts b/apps/roomote/src/app/api/jobs/[id]/route.ts similarity index 100% rename from apps/cloud-agents/src/app/api/jobs/[id]/route.ts rename to apps/roomote/src/app/api/jobs/[id]/route.ts diff --git a/apps/cloud-agents/src/app/api/jobs/route.ts b/apps/roomote/src/app/api/jobs/route.ts similarity index 100% rename from apps/cloud-agents/src/app/api/jobs/route.ts rename to apps/roomote/src/app/api/jobs/route.ts diff --git a/apps/cloud-agents/src/app/api/webhooks/github/route.ts b/apps/roomote/src/app/api/webhooks/github/route.ts similarity index 100% rename from apps/cloud-agents/src/app/api/webhooks/github/route.ts rename to apps/roomote/src/app/api/webhooks/github/route.ts diff --git a/apps/cloud-agents/src/app/layout.tsx b/apps/roomote/src/app/layout.tsx similarity index 100% rename from apps/cloud-agents/src/app/layout.tsx rename to apps/roomote/src/app/layout.tsx diff --git a/apps/cloud-agents/src/app/page.tsx b/apps/roomote/src/app/page.tsx similarity index 100% rename from apps/cloud-agents/src/app/page.tsx rename to apps/roomote/src/app/page.tsx diff --git a/apps/cloud-agents/src/db/index.ts b/apps/roomote/src/db/index.ts similarity index 100% rename from apps/cloud-agents/src/db/index.ts rename to apps/roomote/src/db/index.ts diff --git a/apps/cloud-agents/src/db/schema.ts b/apps/roomote/src/db/schema.ts similarity index 100% rename from apps/cloud-agents/src/db/schema.ts rename to apps/roomote/src/db/schema.ts diff --git a/apps/cloud-agents/src/lib/index.ts b/apps/roomote/src/lib/index.ts similarity index 100% rename from apps/cloud-agents/src/lib/index.ts rename to apps/roomote/src/lib/index.ts diff --git a/apps/cloud-agents/src/lib/job.ts b/apps/roomote/src/lib/job.ts similarity index 100% rename from apps/cloud-agents/src/lib/job.ts rename to apps/roomote/src/lib/job.ts diff --git a/apps/cloud-agents/src/lib/jobs/fixGitHubIssue.ts b/apps/roomote/src/lib/jobs/fixGitHubIssue.ts similarity index 100% rename from apps/cloud-agents/src/lib/jobs/fixGitHubIssue.ts rename to apps/roomote/src/lib/jobs/fixGitHubIssue.ts diff --git a/apps/cloud-agents/src/lib/logger.ts b/apps/roomote/src/lib/logger.ts similarity index 100% rename from apps/cloud-agents/src/lib/logger.ts rename to apps/roomote/src/lib/logger.ts diff --git a/apps/cloud-agents/src/lib/queue.ts b/apps/roomote/src/lib/queue.ts similarity index 92% rename from apps/cloud-agents/src/lib/queue.ts rename to apps/roomote/src/lib/queue.ts index 923e9fe34a..2afef68a12 100644 --- a/apps/cloud-agents/src/lib/queue.ts +++ b/apps/roomote/src/lib/queue.ts @@ -4,7 +4,7 @@ import type { JobTypes, JobPayload, JobParams } from "@/types" import { redis } from "./redis" -const queue = new Queue("cloud-agents", { +const queue = new Queue("roomote", { connection: redis, defaultJobOptions: { removeOnComplete: 100, diff --git a/apps/cloud-agents/src/lib/redis.ts b/apps/roomote/src/lib/redis.ts similarity index 100% rename from apps/cloud-agents/src/lib/redis.ts rename to apps/roomote/src/lib/redis.ts diff --git a/apps/cloud-agents/src/lib/runTask.ts b/apps/roomote/src/lib/runTask.ts similarity index 100% rename from apps/cloud-agents/src/lib/runTask.ts rename to apps/roomote/src/lib/runTask.ts diff --git a/apps/cloud-agents/src/lib/slack.ts b/apps/roomote/src/lib/slack.ts similarity index 100% rename from apps/cloud-agents/src/lib/slack.ts rename to apps/roomote/src/lib/slack.ts diff --git a/apps/cloud-agents/src/lib/utils.ts b/apps/roomote/src/lib/utils.ts similarity index 100% rename from apps/cloud-agents/src/lib/utils.ts rename to apps/roomote/src/lib/utils.ts diff --git a/apps/cloud-agents/src/lib/worker.ts b/apps/roomote/src/lib/worker.ts similarity index 92% rename from apps/cloud-agents/src/lib/worker.ts rename to apps/roomote/src/lib/worker.ts index d30ca4cfe4..e124d6aa37 100644 --- a/apps/cloud-agents/src/lib/worker.ts +++ b/apps/roomote/src/lib/worker.ts @@ -3,7 +3,7 @@ import { Worker } from "bullmq" import { redis } from "./redis" import { processJob } from "./job" -export const worker = new Worker("cloud-agents", processJob, { +export const worker = new Worker("roomote", processJob, { connection: redis, concurrency: 1, removeOnComplete: { count: 100 }, diff --git a/apps/cloud-agents/src/types/index.ts b/apps/roomote/src/types/index.ts similarity index 100% rename from apps/cloud-agents/src/types/index.ts rename to apps/roomote/src/types/index.ts diff --git a/apps/cloud-agents/tsconfig.json b/apps/roomote/tsconfig.json similarity index 100% rename from apps/cloud-agents/tsconfig.json rename to apps/roomote/tsconfig.json diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2debdde51c..75408bb214 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -54,7 +54,7 @@ importers: specifier: ^5.4.5 version: 5.8.3 - apps/cloud-agents: + apps/roomote: dependencies: '@bull-board/api': specifier: ^6.10.1 From 0a158e31142f1bae994be58d1659c10d8510931f Mon Sep 17 00:00:00 2001 From: cte Date: Fri, 13 Jun 2025 15:29:08 -0700 Subject: [PATCH 12/19] Docker tweaks --- apps/roomote/.gitignore | 8 ++++++-- apps/roomote/{docker => }/Dockerfile.app | 0 apps/roomote/{docker => }/Dockerfile.dashboard | 0 apps/roomote/{docker => }/Dockerfile.worker | 0 apps/roomote/docker-compose.yml | 10 +++++----- apps/roomote/drizzle.config.ts | 2 +- 6 files changed, 12 insertions(+), 8 deletions(-) rename apps/roomote/{docker => }/Dockerfile.app (100%) rename apps/roomote/{docker => }/Dockerfile.dashboard (100%) rename apps/roomote/{docker => }/Dockerfile.worker (100%) diff --git a/apps/roomote/.gitignore b/apps/roomote/.gitignore index 63fb868ec2..9ef9b815c5 100644 --- a/apps/roomote/.gitignore +++ b/apps/roomote/.gitignore @@ -1,6 +1,10 @@ +# Next.js .next -docker/postgres-data -docker/redis-data +# docker +.docker/* +!.docker/scripts +!.docker/entrypoints +# ENV !.env.example diff --git a/apps/roomote/docker/Dockerfile.app b/apps/roomote/Dockerfile.app similarity index 100% rename from apps/roomote/docker/Dockerfile.app rename to apps/roomote/Dockerfile.app diff --git a/apps/roomote/docker/Dockerfile.dashboard b/apps/roomote/Dockerfile.dashboard similarity index 100% rename from apps/roomote/docker/Dockerfile.dashboard rename to apps/roomote/Dockerfile.dashboard diff --git a/apps/roomote/docker/Dockerfile.worker b/apps/roomote/Dockerfile.worker similarity index 100% rename from apps/roomote/docker/Dockerfile.worker rename to apps/roomote/Dockerfile.worker diff --git a/apps/roomote/docker-compose.yml b/apps/roomote/docker-compose.yml index b67de802e0..d49fc7f9f8 100644 --- a/apps/roomote/docker-compose.yml +++ b/apps/roomote/docker-compose.yml @@ -5,7 +5,7 @@ services: ports: - "5433:5432" volumes: - - ./docker/postgres-data:/var/lib/postgresql/data + - ./.docker/postgres:/var/lib/postgresql/data environment: - POSTGRES_USER=postgres - POSTGRES_PASSWORD=password @@ -23,14 +23,14 @@ services: ports: - "6380:6379" volumes: - - ./docker/redis-data:/data + - ./.docker/redis:/data command: redis-server --appendonly yes dashboard: container_name: roomote-dashboard build: context: ../../ - dockerfile: apps/roomote/docker/Dockerfile.dashboard + dockerfile: apps/roomote/Dockerfile.dashboard ports: - "3002:3002" environment: @@ -43,7 +43,7 @@ services: api: build: context: ../../ - dockerfile: apps/roomote/docker/Dockerfile.app + dockerfile: apps/roomote/Dockerfile.app ports: - "3001:3001" environment: @@ -61,7 +61,7 @@ services: worker: build: context: ../../ - dockerfile: apps/roomote/docker/Dockerfile.worker + dockerfile: apps/roomote/Dockerfile.worker args: - GH_TOKEN=${GH_TOKEN} env_file: diff --git a/apps/roomote/drizzle.config.ts b/apps/roomote/drizzle.config.ts index 446af92fa4..a5e5683fba 100644 --- a/apps/roomote/drizzle.config.ts +++ b/apps/roomote/drizzle.config.ts @@ -1,7 +1,7 @@ import { defineConfig } from "drizzle-kit" export default defineConfig({ - schema: "./src/lib/db/schema.ts", + schema: "./src/db/schema.ts", out: "./drizzle", dialect: "postgresql", dbCredentials: { From 2e46444f137612a10d07cdb23790e0a816a9ea4f Mon Sep 17 00:00:00 2001 From: cte Date: Sat, 14 Jun 2025 11:24:33 -0700 Subject: [PATCH 13/19] More progres --- apps/roomote/Dockerfile.api | 27 +++ apps/roomote/Dockerfile.app | 50 ------ apps/roomote/Dockerfile.base | 31 ++++ apps/roomote/Dockerfile.controller | 25 +++ apps/roomote/Dockerfile.dashboard | 22 ++- apps/roomote/Dockerfile.worker | 29 +--- apps/roomote/docker-compose.yml | 42 ++++- apps/roomote/package.json | 9 +- .../src/lib/__tests__/controller.test.ts | 95 +++++++++++ apps/roomote/src/lib/controller.ts | 156 ++++++++++++++++++ apps/roomote/src/lib/worker.ts | 43 +++-- apps/roomote/vitest.config.ts | 15 ++ 12 files changed, 440 insertions(+), 104 deletions(-) create mode 100644 apps/roomote/Dockerfile.api delete mode 100644 apps/roomote/Dockerfile.app create mode 100644 apps/roomote/Dockerfile.base create mode 100644 apps/roomote/Dockerfile.controller create mode 100644 apps/roomote/src/lib/__tests__/controller.test.ts create mode 100644 apps/roomote/src/lib/controller.ts create mode 100644 apps/roomote/vitest.config.ts diff --git a/apps/roomote/Dockerfile.api b/apps/roomote/Dockerfile.api new file mode 100644 index 0000000000..bf48892d54 --- /dev/null +++ b/apps/roomote/Dockerfile.api @@ -0,0 +1,27 @@ +# docker compose build api + +FROM roomote-base AS base + +WORKDIR /roo + +COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./ +COPY packages/config-eslint/package.json ./packages/config-eslint/ +COPY packages/config-typescript/package.json ./packages/config-typescript/ +COPY packages/types/package.json ./packages/types/ +COPY packages/ipc/package.json ./packages/ipc/ +COPY apps/roomote/package.json ./apps/roomote/ + +COPY scripts/bootstrap.mjs ./scripts/ +RUN pnpm install --filter @roo-code/roomote + +COPY apps/roomote ./apps/roomote/ +COPY packages/config-eslint ./packages/config-eslint/ +COPY packages/config-typescript ./packages/config-typescript/ +COPY packages/types ./packages/types/ +COPY packages/ipc ./packages/ipc/ + +WORKDIR /roo/apps/roomote +RUN pnpm build +ENV NODE_ENV=production +EXPOSE 3001 +CMD ["pnpm", "start"] diff --git a/apps/roomote/Dockerfile.app b/apps/roomote/Dockerfile.app deleted file mode 100644 index 051646ab54..0000000000 --- a/apps/roomote/Dockerfile.app +++ /dev/null @@ -1,50 +0,0 @@ -FROM node:20-slim AS base - -# Install pnpm -ENV PNPM_HOME="/pnpm" -ENV PATH="$PNPM_HOME:$PATH" -RUN corepack enable - -# Install Docker CLI for container management -RUN apt update && \ - apt install -y \ - curl \ - apt-transport-https \ - ca-certificates \ - gnupg \ - lsb-release \ - && curl -fsSL https://download.docker.com/linux/debian/gpg | gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg \ - && echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/debian $(lsb_release -cs) stable" | tee /etc/apt/sources.list.d/docker.list > /dev/null \ - && apt update && apt install -y docker-ce-cli \ - && rm -rf /var/lib/apt/lists/* - -WORKDIR /app - -# Copy package files -COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./ -COPY scripts/bootstrap.mjs ./scripts/ -COPY packages/types/package.json ./packages/types/ -COPY packages/config-eslint/package.json ./packages/config-eslint/ -COPY packages/config-typescript/package.json ./packages/config-typescript/ -COPY apps/roomote/package.json ./apps/roomote/ - -# Install dependencies -RUN pnpm install --filter @roo-code/roomote - -# Copy source code -COPY packages/types ./packages/types/ -COPY packages/config-eslint ./packages/config-eslint/ -COPY packages/config-typescript ./packages/config-typescript/ -COPY apps/roomote ./apps/roomote/ - -# Set working directory to the app -WORKDIR /app/apps/roomote - -# Build the application -RUN pnpm build - -# Expose port -EXPOSE 3001 - -# Start the application -CMD ["pnpm", "start"] \ No newline at end of file diff --git a/apps/roomote/Dockerfile.base b/apps/roomote/Dockerfile.base new file mode 100644 index 0000000000..50590693d0 --- /dev/null +++ b/apps/roomote/Dockerfile.base @@ -0,0 +1,31 @@ +# Shared base image for roomote controller and worker + +FROM node:20-slim AS base + +# Install pnpm +ENV PNPM_HOME="/pnpm" +ENV PATH="$PNPM_HOME:$PATH" +RUN corepack enable + +# Install common system packages +RUN apt update && \ + apt install -y \ + curl \ + git \ + vim \ + jq \ + netcat-openbsd \ + apt-transport-https \ + ca-certificates \ + gnupg \ + lsb-release \ + wget \ + gpg \ + gh \ + && rm -rf /var/lib/apt/lists/* + +# Install Docker cli +RUN curl -fsSL https://download.docker.com/linux/debian/gpg | gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg \ + && echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/debian $(lsb_release -cs) stable" | tee /etc/apt/sources.list.d/docker.list > /dev/null \ + && apt update && apt install -y docker-ce-cli \ + && rm -rf /var/lib/apt/lists/* diff --git a/apps/roomote/Dockerfile.controller b/apps/roomote/Dockerfile.controller new file mode 100644 index 0000000000..d236468a64 --- /dev/null +++ b/apps/roomote/Dockerfile.controller @@ -0,0 +1,25 @@ +# docker compose build base controller + +FROM roomote-base AS base + +WORKDIR /roo + +COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./ +COPY packages/config-eslint/package.json ./packages/config-eslint/ +COPY packages/config-typescript/package.json ./packages/config-typescript/ +COPY packages/types/package.json ./packages/types/ +COPY packages/ipc/package.json ./packages/ipc/ +COPY apps/roomote/package.json ./apps/roomote/ + +COPY scripts/bootstrap.mjs ./scripts/ +RUN pnpm install --filter @roo-code/roomote + +COPY apps/roomote ./apps/roomote/ +COPY packages/config-eslint ./packages/config-eslint/ +COPY packages/config-typescript ./packages/config-typescript/ +COPY packages/types ./packages/types/ +COPY packages/ipc ./packages/ipc/ + +WORKDIR /roo/apps/roomote +ENV NODE_ENV=production +CMD ["pnpm", "controller"] diff --git a/apps/roomote/Dockerfile.dashboard b/apps/roomote/Dockerfile.dashboard index 5e674fcf85..a4913a40de 100644 --- a/apps/roomote/Dockerfile.dashboard +++ b/apps/roomote/Dockerfile.dashboard @@ -1,27 +1,25 @@ -FROM node:20-slim AS base +# docker compose build dashboard -ENV PNPM_HOME="/pnpm" -ENV PATH="$PNPM_HOME:$PATH" -RUN corepack enable +FROM roomote-base AS base -WORKDIR /app +WORKDIR /roo COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./ -COPY scripts/bootstrap.mjs ./scripts/ -COPY packages/types/package.json ./packages/types/ COPY packages/config-eslint/package.json ./packages/config-eslint/ COPY packages/config-typescript/package.json ./packages/config-typescript/ +COPY packages/types/package.json ./packages/types/ +COPY packages/ipc/package.json ./packages/ipc/ COPY apps/roomote/package.json ./apps/roomote/ +COPY scripts/bootstrap.mjs ./scripts/ RUN pnpm install --filter @roo-code/roomote -COPY packages/types ./packages/types/ +COPY apps/roomote ./apps/roomote/ COPY packages/config-eslint ./packages/config-eslint/ COPY packages/config-typescript ./packages/config-typescript/ -COPY apps/roomote ./apps/roomote/ - -WORKDIR /app/apps/roomote +COPY packages/types ./packages/types/ +COPY packages/ipc ./packages/ipc/ +WORKDIR /roo/apps/roomote EXPOSE 3002 - CMD ["pnpm", "dashboard"] diff --git a/apps/roomote/Dockerfile.worker b/apps/roomote/Dockerfile.worker index 378951a556..d59719f775 100644 --- a/apps/roomote/Dockerfile.worker +++ b/apps/roomote/Dockerfile.worker @@ -1,34 +1,11 @@ # docker compose build worker -FROM node:20-slim AS base +FROM roomote-base AS base -# Install pnpm -ENV PNPM_HOME="/pnpm" -ENV PATH="$PNPM_HOME:$PATH" -RUN corepack enable - -# Install system packages +# Install additional worker-specific packages RUN apt update && \ apt install -y \ - curl \ - git \ - vim \ - jq \ - netcat-openbsd \ - apt-transport-https \ - ca-certificates \ - gnupg \ - lsb-release \ - wget \ - gpg \ xvfb \ - gh \ - && rm -rf /var/lib/apt/lists/* - -# Install Docker cli -RUN curl -fsSL https://download.docker.com/linux/debian/gpg | gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg \ - && echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/debian $(lsb_release -cs) stable" | tee /etc/apt/sources.list.d/docker.list > /dev/null \ - && apt update && apt install -y docker-ce-cli \ && rm -rf /var/lib/apt/lists/* # Install VS Code @@ -54,7 +31,7 @@ ENV GH_TOKEN=$GH_TOKEN RUN gh repo clone RooCodeInc/Roo-Code WORKDIR /roo/Roo-Code +RUN git checkout cte/cloud-agents RUN pnpm install -WORKDIR /roo CMD ["bash"] diff --git a/apps/roomote/docker-compose.yml b/apps/roomote/docker-compose.yml index d49fc7f9f8..e6772458e3 100644 --- a/apps/roomote/docker-compose.yml +++ b/apps/roomote/docker-compose.yml @@ -1,4 +1,10 @@ services: + base: + build: + context: ../../ + dockerfile: apps/roomote/Dockerfile.base + image: roomote-base + db: container_name: roomote-db image: postgres:17.5 @@ -37,13 +43,15 @@ services: - REDIS_URL=redis://redis:6379 - NODE_ENV=production depends_on: + base: + condition: service_completed_successfully redis: condition: service_started api: build: context: ../../ - dockerfile: apps/roomote/Dockerfile.app + dockerfile: apps/roomote/Dockerfile.api ports: - "3001:3001" environment: @@ -53,6 +61,8 @@ services: volumes: - /var/run/docker.sock:/var/run/docker.sock depends_on: + base: + condition: service_completed_successfully db: condition: service_healthy redis: @@ -73,6 +83,36 @@ services: - /tmp/roomote:/var/log/roomote stdin_open: true tty: true + depends_on: + base: + condition: service_completed_successfully + db: + condition: service_healthy + redis: + condition: service_started + + controller: + build: + context: ../../ + dockerfile: apps/roomote/Dockerfile.controller + env_file: + - .env + environment: + - DATABASE_URL=postgresql://postgres:password@db:5432/cloud_agents + - REDIS_URL=redis://redis:6379 + - NODE_ENV=production + - HOST_EXECUTION_METHOD=docker + volumes: + - /var/run/docker.sock:/var/run/docker.sock + - /tmp/roomote:/var/log/roomote + depends_on: + base: + condition: service_completed_successfully + db: + condition: service_healthy + redis: + condition: service_started + restart: unless-stopped networks: default: diff --git a/apps/roomote/package.json b/apps/roomote/package.json index a50196aa2b..6d1f5fa84e 100644 --- a/apps/roomote/package.json +++ b/apps/roomote/package.json @@ -5,6 +5,7 @@ "scripts": { "lint": "next lint", "check-types": "tsc --noEmit", + "test": "vitest", "dev": "concurrently \"next dev --port 3001\" \"ngrok http 3001 --domain cte.ngrok.dev\"", "build": "next build", "start": "next start --port 3001", @@ -15,9 +16,10 @@ "db:push": "pnpm drizzle-kit push", "db:check": "pnpm drizzle-kit check", "db:studio": "pnpm drizzle-kit studio", - "services:start": "docker compose up -d db redis dashboard", - "services:stop": "docker compose down dashboard redis db", + "services:start": "docker compose up -d db redis dashboard controller", + "services:stop": "docker compose down controller dashboard redis db", "worker": "dotenvx run -f .env -- tsx src/lib/worker.ts", + "controller": "dotenvx run -f .env -- tsx src/lib/controller.ts", "dashboard": "tsx scripts/dashboard.ts" }, "dependencies": { @@ -47,6 +49,7 @@ "@types/react-dom": "^18.3.7", "concurrently": "^9.1.0", "drizzle-kit": "^0.31.1", - "tsx": "^4.19.3" + "tsx": "^4.19.3", + "vitest": "^2.1.8" } } diff --git a/apps/roomote/src/lib/__tests__/controller.test.ts b/apps/roomote/src/lib/__tests__/controller.test.ts new file mode 100644 index 0000000000..8131f15c19 --- /dev/null +++ b/apps/roomote/src/lib/__tests__/controller.test.ts @@ -0,0 +1,95 @@ +// npx vitest src/lib/__tests__/controller.test.ts + +import { describe, it, expect, vi, beforeEach, afterEach } from "vitest" + +const mockQueue = { + getWaiting: vi.fn(() => Promise.resolve([])), + getActive: vi.fn(() => Promise.resolve([])), + close: vi.fn(() => Promise.resolve()), + on: vi.fn(), +} + +const mockSpawn = vi.fn(() => ({ + stdout: { pipe: vi.fn() }, + stderr: { pipe: vi.fn() }, + on: vi.fn(), + unref: vi.fn(), +})) + +const mockCreateWriteStream = vi.fn(() => ({ + end: vi.fn(), +})) + +vi.mock("../redis", () => ({ + redis: { host: "localhost", port: 6379 }, +})) + +vi.mock("child_process", () => ({ + spawn: mockSpawn, +})) + +vi.mock("fs", () => ({ + default: { + existsSync: vi.fn(() => false), + createWriteStream: mockCreateWriteStream, + }, +})) + +const mockQueueConstructor = vi.fn(() => mockQueue) + +vi.mock("bullmq", () => ({ + Queue: mockQueueConstructor, +})) + +describe("WorkerController", () => { + let WorkerController: typeof import("../controller").WorkerController + + beforeEach(async () => { + vi.clearAllMocks() + const controllerModule = await import("../controller") + WorkerController = controllerModule.WorkerController + }) + + afterEach(() => { + vi.restoreAllMocks() + }) + + it("should create a Queue instance with correct configuration", () => { + const controller = new WorkerController() + + expect(mockQueueConstructor).toHaveBeenCalledWith("roomote", { + connection: { host: "localhost", port: 6379 }, + }) + + expect(controller).toBeDefined() + }) + + it("should handle queue monitoring without errors", async () => { + const controller = new WorkerController() + + await controller.start() + expect(controller).toBeDefined() + + await controller.stop() + expect(mockQueue.close).toHaveBeenCalled() + }) + + it("should handle worker spawning logic", () => { + const controller = new WorkerController() + expect(mockSpawn).toBeDefined() + expect(mockCreateWriteStream).toBeDefined() + expect(controller).toBeDefined() + }) + + it("should track running state correctly", async () => { + const controller = new WorkerController() + + expect(controller.isRunning).toBeFalsy() + + await controller.start() + expect(controller.isRunning).toBeTruthy() + + await controller.stop() + expect(controller.isRunning).toBeFalsy() + }) +}) diff --git a/apps/roomote/src/lib/controller.ts b/apps/roomote/src/lib/controller.ts new file mode 100644 index 0000000000..92066a2290 --- /dev/null +++ b/apps/roomote/src/lib/controller.ts @@ -0,0 +1,156 @@ +import { spawn } from "child_process" +import fs from "fs" +import { Queue } from "bullmq" + +import { redis } from "./redis" + +export class WorkerController { + private queue: Queue + public isRunning = false + private pollingInterval: NodeJS.Timeout | null = null + private readonly POLL_INTERVAL_MS = 5000 + private activeWorkers = new Set() + + constructor() { + this.queue = new Queue("roomote", { connection: redis }) + } + + async start() { + if (this.isRunning) { + console.log("Controller is already running") + return + } + + this.isRunning = true + console.log("Worker controller started") + + await this.checkAndSpawnWorker() + + this.pollingInterval = setInterval(async () => { + await this.checkAndSpawnWorker() + }, this.POLL_INTERVAL_MS) + } + + async stop() { + if (!this.isRunning) { + return + } + + this.isRunning = false + console.log("Stopping worker controller...") + + if (this.pollingInterval) { + clearInterval(this.pollingInterval) + this.pollingInterval = null + } + + await this.queue.close() + console.log("Worker controller stopped") + } + + private async checkAndSpawnWorker() { + try { + const waiting = await this.queue.getWaiting() + const active = await this.queue.getActive() + + const waitingCount = waiting.length + const activeCount = active.length + + console.log( + `Queue status: ${waitingCount} waiting, ${activeCount} active, ${this.activeWorkers.size} spawned workers`, + ) + + if (waitingCount > 0 && activeCount === 0 && this.activeWorkers.size === 0) { + await this.spawnWorker() + } + } catch (error) { + console.error("Error checking queue status:", error) + } + } + + private async spawnWorker() { + const workerId = `worker-${Date.now()}-${Math.random().toString(36).substr(2, 9)}` + + try { + console.log(`Spawning worker: ${workerId}`) + + const isRunningInDocker = fs.existsSync("/.dockerenv") + + const dockerArgs = [ + `--name roomote-worker-${workerId}`, + "--rm", + "--network roomote_default", + "--env-file /roo/Roo-Code/apps/roomote/.env", + "-e NODE_ENV=production", + "-e HOST_EXECUTION_METHOD=docker", + "-v /var/run/docker.sock:/var/run/docker.sock", + "-v /tmp/roomote:/var/log/roomote", + ] + + const cliCommand = "cd /roo/Roo-Code/apps/roomote && pnpm worker" + + const command = isRunningInDocker + ? `docker run ${dockerArgs.join(" ")} roomote-worker sh -c "${cliCommand}"` + : cliCommand + + console.log("Spawning worker with command:", command) + + const childProcess = spawn("sh", ["-c", command], { + detached: true, + stdio: ["ignore", "pipe", "pipe"], + }) + + const logStream = fs.createWriteStream("/tmp/roomote-worker.log", { flags: "a" }) + + if (childProcess.stdout) { + childProcess.stdout.pipe(logStream) + } + + if (childProcess.stderr) { + childProcess.stderr.pipe(logStream) + } + + this.activeWorkers.add(workerId) + + childProcess.on("exit", (code) => { + console.log(`Worker ${workerId} exited with code ${code}`) + this.activeWorkers.delete(workerId) + logStream.end() + }) + + childProcess.on("error", (error) => { + console.error(`Worker ${workerId} error:`, error) + this.activeWorkers.delete(workerId) + logStream.end() + }) + + // Detach the process so it can run independently. + childProcess.unref() + } catch (error) { + console.error(`Failed to spawn worker ${workerId}:`, error) + this.activeWorkers.delete(workerId) + } + } +} + +// Only run if this file is executed directly (not imported). +if (import.meta.url === `file://${process.argv[1]}`) { + const controller = new WorkerController() + + process.on("SIGTERM", async () => { + console.log("SIGTERM -> shutting down controller gracefully...") + await controller.stop() + process.exit(0) + }) + + process.on("SIGINT", async () => { + console.log("SIGINT -> shutting down controller gracefully...") + await controller.stop() + process.exit(0) + }) + + controller.start().catch((error) => { + console.error("Failed to start controller:", error) + process.exit(1) + }) +} diff --git a/apps/roomote/src/lib/worker.ts b/apps/roomote/src/lib/worker.ts index e124d6aa37..36f9d5f05a 100644 --- a/apps/roomote/src/lib/worker.ts +++ b/apps/roomote/src/lib/worker.ts @@ -3,27 +3,46 @@ import { Worker } from "bullmq" import { redis } from "./redis" import { processJob } from "./job" -export const worker = new Worker("roomote", processJob, { - connection: redis, - concurrency: 1, - removeOnComplete: { count: 100 }, - removeOnFail: { count: 50 }, -}) +async function processSingleJob() { + const worker = new Worker("roomote", processJob, { connection: redis }) + + try { + console.log("Looking for a job to process...") + const job = await worker.getNextJob("worker-token") + + if (!job) { + console.log("No jobs available, exiting...") + await worker.close() + process.exit(0) + } + + console.log(`Processing job ${job.id}...`) -worker.on("completed", (job) => console.log(`Job ${job.id} completed`)) -worker.on("failed", (job, err) => console.error(`Job ${job?.id} failed:`, err)) -worker.on("error", (err) => console.error("Worker error:", err)) + try { + await processJob(job) + await job.moveToCompleted(undefined, "worker-token") + console.log(`Job ${job.id} completed successfully`) + } catch (error) { + await job.moveToFailed(error as Error, "worker-token") + console.error(`Job ${job.id} failed:`, error) + } + } catch (error) { + console.error("Error processing job:", error) + } finally { + await worker.close() + process.exit(0) + } +} process.on("SIGTERM", async () => { console.log("SIGTERM -> shutting down gracefully...") - await worker.close() process.exit(0) }) process.on("SIGINT", async () => { console.log("SIGINT -> shutting down gracefully...") - await worker.close() process.exit(0) }) -console.log("Worker process started") +console.log("Single job worker started") +processSingleJob() diff --git a/apps/roomote/vitest.config.ts b/apps/roomote/vitest.config.ts new file mode 100644 index 0000000000..82d55b4ba9 --- /dev/null +++ b/apps/roomote/vitest.config.ts @@ -0,0 +1,15 @@ +import { defineConfig } from "vitest/config" +import path from "path" + +export default defineConfig({ + test: { + environment: "node", + globals: true, + watch: false, + }, + resolve: { + alias: { + "@": path.resolve(__dirname, "./src"), + }, + }, +}) From 402bfb3f314835587540e2d71cf6cab58baf3bd5 Mon Sep 17 00:00:00 2001 From: cte Date: Sun, 15 Jun 2025 18:33:25 -0700 Subject: [PATCH 14/19] More progress --- apps/roomote/Dockerfile.api | 4 +- apps/roomote/Dockerfile.base | 2 +- apps/roomote/Dockerfile.controller | 2 +- apps/roomote/Dockerfile.dashboard | 4 +- apps/roomote/docker-compose.yml | 4 + apps/roomote/scripts/build.sh | 38 +++ apps/roomote/src/lib/runTask.ts | 5 +- pnpm-lock.yaml | 446 +++++++++++++++++++++++++++++ 8 files changed, 498 insertions(+), 7 deletions(-) create mode 100755 apps/roomote/scripts/build.sh diff --git a/apps/roomote/Dockerfile.api b/apps/roomote/Dockerfile.api index bf48892d54..6566096a71 100644 --- a/apps/roomote/Dockerfile.api +++ b/apps/roomote/Dockerfile.api @@ -1,4 +1,4 @@ -# docker compose build api +# docker compose build base api FROM roomote-base AS base @@ -12,7 +12,7 @@ COPY packages/ipc/package.json ./packages/ipc/ COPY apps/roomote/package.json ./apps/roomote/ COPY scripts/bootstrap.mjs ./scripts/ -RUN pnpm install --filter @roo-code/roomote +RUN pnpm install COPY apps/roomote ./apps/roomote/ COPY packages/config-eslint ./packages/config-eslint/ diff --git a/apps/roomote/Dockerfile.base b/apps/roomote/Dockerfile.base index 50590693d0..3320b0b6a5 100644 --- a/apps/roomote/Dockerfile.base +++ b/apps/roomote/Dockerfile.base @@ -1,4 +1,4 @@ -# Shared base image for roomote controller and worker +# docker compose build base FROM node:20-slim AS base diff --git a/apps/roomote/Dockerfile.controller b/apps/roomote/Dockerfile.controller index d236468a64..a2a2e00203 100644 --- a/apps/roomote/Dockerfile.controller +++ b/apps/roomote/Dockerfile.controller @@ -12,7 +12,7 @@ COPY packages/ipc/package.json ./packages/ipc/ COPY apps/roomote/package.json ./apps/roomote/ COPY scripts/bootstrap.mjs ./scripts/ -RUN pnpm install --filter @roo-code/roomote +RUN pnpm install COPY apps/roomote ./apps/roomote/ COPY packages/config-eslint ./packages/config-eslint/ diff --git a/apps/roomote/Dockerfile.dashboard b/apps/roomote/Dockerfile.dashboard index a4913a40de..f04160d9d2 100644 --- a/apps/roomote/Dockerfile.dashboard +++ b/apps/roomote/Dockerfile.dashboard @@ -1,4 +1,4 @@ -# docker compose build dashboard +# docker compose build base dashboard FROM roomote-base AS base @@ -12,7 +12,7 @@ COPY packages/ipc/package.json ./packages/ipc/ COPY apps/roomote/package.json ./apps/roomote/ COPY scripts/bootstrap.mjs ./scripts/ -RUN pnpm install --filter @roo-code/roomote +RUN pnpm install COPY apps/roomote ./apps/roomote/ COPY packages/config-eslint ./packages/config-eslint/ diff --git a/apps/roomote/docker-compose.yml b/apps/roomote/docker-compose.yml index e6772458e3..8311ae2487 100644 --- a/apps/roomote/docker-compose.yml +++ b/apps/roomote/docker-compose.yml @@ -37,6 +37,7 @@ services: build: context: ../../ dockerfile: apps/roomote/Dockerfile.dashboard + image: roomote-dashboard ports: - "3002:3002" environment: @@ -52,6 +53,7 @@ services: build: context: ../../ dockerfile: apps/roomote/Dockerfile.api + image: roomote-api ports: - "3001:3001" environment: @@ -74,6 +76,7 @@ services: dockerfile: apps/roomote/Dockerfile.worker args: - GH_TOKEN=${GH_TOKEN} + image: roomote-worker env_file: - .env environment: @@ -95,6 +98,7 @@ services: build: context: ../../ dockerfile: apps/roomote/Dockerfile.controller + image: roomote-controller env_file: - .env environment: diff --git a/apps/roomote/scripts/build.sh b/apps/roomote/scripts/build.sh new file mode 100755 index 0000000000..1bc54d1e26 --- /dev/null +++ b/apps/roomote/scripts/build.sh @@ -0,0 +1,38 @@ +#!/bin/bash + +# Build script for roomote services. +# This ensures the base image is built before dependent services. + +set -e + +build_service() { + local service=$1 + + case $service in + "dashboard"|"api"|"worker"|"controller") + echo "Building base image first..." + docker compose build base + echo "Building $service..." + docker compose build $service + ;; + "base") + echo "Building base image..." + docker compose build base + ;; + *) + echo "Building $service..." + docker compose build $service + ;; + esac +} + +if [ $# -eq 0 ]; then + echo "Usage: $0 " + echo "Available services: base, dashboard, api, worker, controller, db, redis" + echo "Example: $0 dashboard" + exit 1 +fi + +build_service $1 + +echo "Build completed successfully!" diff --git a/apps/roomote/src/lib/runTask.ts b/apps/roomote/src/lib/runTask.ts index c146db0319..5f5aeaca21 100644 --- a/apps/roomote/src/lib/runTask.ts +++ b/apps/roomote/src/lib/runTask.ts @@ -119,7 +119,10 @@ export const runTask = async ({ if (eventName === RooCodeEventName.TaskStarted) { taskStartedAt = Date.now() rooTaskId = payload[0] - slackThreadTs = await slackNotifier.postTaskStarted({ jobType, jobPayload, rooTaskId }) + + if (rooTaskId) { + slackThreadTs = await slackNotifier.postTaskStarted({ jobType, jobPayload, rooTaskId }) + } } if (eventName === RooCodeEventName.TaskAborted) { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 75408bb214..b349dd16e2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -132,6 +132,9 @@ importers: tsx: specifier: ^4.19.3 version: 4.19.4 + vitest: + specifier: ^2.1.8 + version: 2.1.9(@types/node@22.15.29)(jsdom@20.0.3)(lightningcss@1.30.1) apps/vscode-e2e: devDependencies: @@ -1713,6 +1716,12 @@ packages: resolution: {integrity: sha512-FxEMIkJKnodyA1OaCUoEvbYRkoZlLZ4d/eXFu9Fh8CbBBgP5EmZxrfTRyN0qpXZ4vOvqnE5YdRdcrmUUXuU+dA==} deprecated: 'Merged into tsx: https://tsx.is' + '@esbuild/aix-ppc64@0.21.5': + resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [aix] + '@esbuild/aix-ppc64@0.25.4': resolution: {integrity: sha512-1VCICWypeQKhVbE9oW/sJaAmjLxhVqacdkvPLEjwlttjfwENRSClS8EjBz0KzRyFSCPDIkuXW34Je/vk7zdB7Q==} engines: {node: '>=18'} @@ -1731,6 +1740,12 @@ packages: cpu: [arm64] os: [android] + '@esbuild/android-arm64@0.21.5': + resolution: {integrity: sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + '@esbuild/android-arm64@0.25.4': resolution: {integrity: sha512-bBy69pgfhMGtCnwpC/x5QhfxAz/cBgQ9enbtwjf6V9lnPI/hMyT9iWpR1arm0l3kttTr4L0KSLpKmLp/ilKS9A==} engines: {node: '>=18'} @@ -1749,6 +1764,12 @@ packages: cpu: [arm] os: [android] + '@esbuild/android-arm@0.21.5': + resolution: {integrity: sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + '@esbuild/android-arm@0.25.4': resolution: {integrity: sha512-QNdQEps7DfFwE3hXiU4BZeOV68HHzYwGd0Nthhd3uCkkEKK7/R6MTgM0P7H7FAs5pU/DIWsviMmEGxEoxIZ+ZQ==} engines: {node: '>=18'} @@ -1767,6 +1788,12 @@ packages: cpu: [x64] os: [android] + '@esbuild/android-x64@0.21.5': + resolution: {integrity: sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + '@esbuild/android-x64@0.25.4': resolution: {integrity: sha512-TVhdVtQIFuVpIIR282btcGC2oGQoSfZfmBdTip2anCaVYcqWlZXGcdcKIUklfX2wj0JklNYgz39OBqh2cqXvcQ==} engines: {node: '>=18'} @@ -1785,6 +1812,12 @@ packages: cpu: [arm64] os: [darwin] + '@esbuild/darwin-arm64@0.21.5': + resolution: {integrity: sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + '@esbuild/darwin-arm64@0.25.4': resolution: {integrity: sha512-Y1giCfM4nlHDWEfSckMzeWNdQS31BQGs9/rouw6Ub91tkK79aIMTH3q9xHvzH8d0wDru5Ci0kWB8b3up/nl16g==} engines: {node: '>=18'} @@ -1803,6 +1836,12 @@ packages: cpu: [x64] os: [darwin] + '@esbuild/darwin-x64@0.21.5': + resolution: {integrity: sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + '@esbuild/darwin-x64@0.25.4': resolution: {integrity: sha512-CJsry8ZGM5VFVeyUYB3cdKpd/H69PYez4eJh1W/t38vzutdjEjtP7hB6eLKBoOdxcAlCtEYHzQ/PJ/oU9I4u0A==} engines: {node: '>=18'} @@ -1821,6 +1860,12 @@ packages: cpu: [arm64] os: [freebsd] + '@esbuild/freebsd-arm64@0.21.5': + resolution: {integrity: sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + '@esbuild/freebsd-arm64@0.25.4': resolution: {integrity: sha512-yYq+39NlTRzU2XmoPW4l5Ifpl9fqSk0nAJYM/V/WUGPEFfek1epLHJIkTQM6bBs1swApjO5nWgvr843g6TjxuQ==} engines: {node: '>=18'} @@ -1839,6 +1884,12 @@ packages: cpu: [x64] os: [freebsd] + '@esbuild/freebsd-x64@0.21.5': + resolution: {integrity: sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + '@esbuild/freebsd-x64@0.25.4': resolution: {integrity: sha512-0FgvOJ6UUMflsHSPLzdfDnnBBVoCDtBTVyn/MrWloUNvq/5SFmh13l3dvgRPkDihRxb77Y17MbqbCAa2strMQQ==} engines: {node: '>=18'} @@ -1857,6 +1908,12 @@ packages: cpu: [arm64] os: [linux] + '@esbuild/linux-arm64@0.21.5': + resolution: {integrity: sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + '@esbuild/linux-arm64@0.25.4': resolution: {integrity: sha512-+89UsQTfXdmjIvZS6nUnOOLoXnkUTB9hR5QAeLrQdzOSWZvNSAXAtcRDHWtqAUtAmv7ZM1WPOOeSxDzzzMogiQ==} engines: {node: '>=18'} @@ -1875,6 +1932,12 @@ packages: cpu: [arm] os: [linux] + '@esbuild/linux-arm@0.21.5': + resolution: {integrity: sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + '@esbuild/linux-arm@0.25.4': resolution: {integrity: sha512-kro4c0P85GMfFYqW4TWOpvmF8rFShbWGnrLqlzp4X1TNWjRY3JMYUfDCtOxPKOIY8B0WC8HN51hGP4I4hz4AaQ==} engines: {node: '>=18'} @@ -1893,6 +1956,12 @@ packages: cpu: [ia32] os: [linux] + '@esbuild/linux-ia32@0.21.5': + resolution: {integrity: sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + '@esbuild/linux-ia32@0.25.4': resolution: {integrity: sha512-yTEjoapy8UP3rv8dB0ip3AfMpRbyhSN3+hY8mo/i4QXFeDxmiYbEKp3ZRjBKcOP862Ua4b1PDfwlvbuwY7hIGQ==} engines: {node: '>=18'} @@ -1911,6 +1980,12 @@ packages: cpu: [loong64] os: [linux] + '@esbuild/linux-loong64@0.21.5': + resolution: {integrity: sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + '@esbuild/linux-loong64@0.25.4': resolution: {integrity: sha512-NeqqYkrcGzFwi6CGRGNMOjWGGSYOpqwCjS9fvaUlX5s3zwOtn1qwg1s2iE2svBe4Q/YOG1q6875lcAoQK/F4VA==} engines: {node: '>=18'} @@ -1929,6 +2004,12 @@ packages: cpu: [mips64el] os: [linux] + '@esbuild/linux-mips64el@0.21.5': + resolution: {integrity: sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + '@esbuild/linux-mips64el@0.25.4': resolution: {integrity: sha512-IcvTlF9dtLrfL/M8WgNI/qJYBENP3ekgsHbYUIzEzq5XJzzVEV/fXY9WFPfEEXmu3ck2qJP8LG/p3Q8f7Zc2Xg==} engines: {node: '>=18'} @@ -1947,6 +2028,12 @@ packages: cpu: [ppc64] os: [linux] + '@esbuild/linux-ppc64@0.21.5': + resolution: {integrity: sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + '@esbuild/linux-ppc64@0.25.4': resolution: {integrity: sha512-HOy0aLTJTVtoTeGZh4HSXaO6M95qu4k5lJcH4gxv56iaycfz1S8GO/5Jh6X4Y1YiI0h7cRyLi+HixMR+88swag==} engines: {node: '>=18'} @@ -1965,6 +2052,12 @@ packages: cpu: [riscv64] os: [linux] + '@esbuild/linux-riscv64@0.21.5': + resolution: {integrity: sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + '@esbuild/linux-riscv64@0.25.4': resolution: {integrity: sha512-i8JUDAufpz9jOzo4yIShCTcXzS07vEgWzyX3NH2G7LEFVgrLEhjwL3ajFE4fZI3I4ZgiM7JH3GQ7ReObROvSUA==} engines: {node: '>=18'} @@ -1983,6 +2076,12 @@ packages: cpu: [s390x] os: [linux] + '@esbuild/linux-s390x@0.21.5': + resolution: {integrity: sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + '@esbuild/linux-s390x@0.25.4': resolution: {integrity: sha512-jFnu+6UbLlzIjPQpWCNh5QtrcNfMLjgIavnwPQAfoGx4q17ocOU9MsQ2QVvFxwQoWpZT8DvTLooTvmOQXkO51g==} engines: {node: '>=18'} @@ -2001,6 +2100,12 @@ packages: cpu: [x64] os: [linux] + '@esbuild/linux-x64@0.21.5': + resolution: {integrity: sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + '@esbuild/linux-x64@0.25.4': resolution: {integrity: sha512-6e0cvXwzOnVWJHq+mskP8DNSrKBr1bULBvnFLpc1KY+d+irZSgZ02TGse5FsafKS5jg2e4pbvK6TPXaF/A6+CA==} engines: {node: '>=18'} @@ -2031,6 +2136,12 @@ packages: cpu: [x64] os: [netbsd] + '@esbuild/netbsd-x64@0.21.5': + resolution: {integrity: sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + '@esbuild/netbsd-x64@0.25.4': resolution: {integrity: sha512-XAg8pIQn5CzhOB8odIcAm42QsOfa98SBeKUdo4xa8OvX8LbMZqEtgeWE9P/Wxt7MlG2QqvjGths+nq48TrUiKw==} engines: {node: '>=18'} @@ -2061,6 +2172,12 @@ packages: cpu: [x64] os: [openbsd] + '@esbuild/openbsd-x64@0.21.5': + resolution: {integrity: sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + '@esbuild/openbsd-x64@0.25.4': resolution: {integrity: sha512-xAGGhyOQ9Otm1Xu8NT1ifGLnA6M3sJxZ6ixylb+vIUVzvvd6GOALpwQrYrtlPouMqd/vSbgehz6HaVk4+7Afhw==} engines: {node: '>=18'} @@ -2079,6 +2196,12 @@ packages: cpu: [x64] os: [sunos] + '@esbuild/sunos-x64@0.21.5': + resolution: {integrity: sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + '@esbuild/sunos-x64@0.25.4': resolution: {integrity: sha512-Mw+tzy4pp6wZEK0+Lwr76pWLjrtjmJyUB23tHKqEDP74R3q95luY/bXqXZeYl4NYlvwOqoRKlInQialgCKy67Q==} engines: {node: '>=18'} @@ -2097,6 +2220,12 @@ packages: cpu: [arm64] os: [win32] + '@esbuild/win32-arm64@0.21.5': + resolution: {integrity: sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + '@esbuild/win32-arm64@0.25.4': resolution: {integrity: sha512-AVUP428VQTSddguz9dO9ngb+E5aScyg7nOeJDrF1HPYu555gmza3bDGMPhmVXL8svDSoqPCsCPjb265yG/kLKQ==} engines: {node: '>=18'} @@ -2115,6 +2244,12 @@ packages: cpu: [ia32] os: [win32] + '@esbuild/win32-ia32@0.21.5': + resolution: {integrity: sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + '@esbuild/win32-ia32@0.25.4': resolution: {integrity: sha512-i1sW+1i+oWvQzSgfRcxxG2k4I9n3O9NRqy8U+uugaT2Dy7kLO9Y7wI72haOahxceMX8hZAzgGou1FhndRldxRg==} engines: {node: '>=18'} @@ -2133,6 +2268,12 @@ packages: cpu: [x64] os: [win32] + '@esbuild/win32-x64@0.21.5': + resolution: {integrity: sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + '@esbuild/win32-x64@0.25.4': resolution: {integrity: sha512-nOT2vZNw6hJ+z43oP1SPea/G/6AbN6X+bGNhNuq8NtRHy4wsMhw765IKLNmnjek7GvjWBYQ8Q5VBoYTFg9y1UQ==} engines: {node: '>=18'} @@ -4530,6 +4671,9 @@ packages: peerDependencies: vite: ^4.2.0 || ^5.0.0 || ^6.0.0 + '@vitest/expect@2.1.9': + resolution: {integrity: sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==} + '@vitest/expect@3.1.3': resolution: {integrity: sha512-7FTQQuuLKmN1Ig/h+h/GO+44Q1IlglPlR2es4ab7Yvfx+Uk5xsv+Ykk+MEt/M2Yn/xGmzaLKxGw2lgy2bwuYqg==} @@ -4539,6 +4683,17 @@ packages: '@vitest/expect@3.2.1': resolution: {integrity: sha512-FqS/BnDOzV6+IpxrTg5GQRyLOCtcJqkwMwcS8qGCI2IyRVDwPAtutztaf1CjtPHlZlWtl1yUPCd7HM0cNiDOYw==} + '@vitest/mocker@2.1.9': + resolution: {integrity: sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg==} + peerDependencies: + msw: ^2.4.9 + vite: ^5.0.0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + '@vitest/mocker@3.1.3': resolution: {integrity: sha512-PJbLjonJK82uCWHjzgBJZuR7zmAOrSvKk1QBxrennDIgtH4uK0TB1PvYmc0XBCigxxtiAVPfWtAdy4lpz8SQGQ==} peerDependencies: @@ -4572,6 +4727,9 @@ packages: vite: optional: true + '@vitest/pretty-format@2.1.9': + resolution: {integrity: sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==} + '@vitest/pretty-format@3.1.3': resolution: {integrity: sha512-i6FDiBeJUGLDKADw2Gb01UtUNb12yyXAqC/mmRWuYl+m/U9GS7s8us5ONmGkGpUUo7/iAYzI2ePVfOZTYvUifA==} @@ -4581,6 +4739,9 @@ packages: '@vitest/pretty-format@3.2.1': resolution: {integrity: sha512-xBh1X2GPlOGBupp6E1RcUQWIxw0w/hRLd3XyBS6H+dMdKTAqHDNsIR2AnJwPA3yYe9DFy3VUKTe3VRTrAiQ01g==} + '@vitest/runner@2.1.9': + resolution: {integrity: sha512-ZXSSqTFIrzduD63btIfEyOmNcBmQvgOVsPNPe0jYtESiXkhd8u2erDLnMxmGrDCwHCCHE7hxwRDCT3pt0esT4g==} + '@vitest/runner@3.1.3': resolution: {integrity: sha512-Tae+ogtlNfFei5DggOsSUvkIaSuVywujMj6HzR97AHK6XK8i3BuVyIifWAm/sE3a15lF5RH9yQIrbXYuo0IFyA==} @@ -4590,6 +4751,9 @@ packages: '@vitest/runner@3.2.1': resolution: {integrity: sha512-kygXhNTu/wkMYbwYpS3z/9tBe0O8qpdBuC3dD/AW9sWa0LE/DAZEjnHtWA9sIad7lpD4nFW1yQ+zN7mEKNH3yA==} + '@vitest/snapshot@2.1.9': + resolution: {integrity: sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ==} + '@vitest/snapshot@3.1.3': resolution: {integrity: sha512-XVa5OPNTYUsyqG9skuUkFzAeFnEzDp8hQu7kZ0N25B1+6KjGm4hWLtURyBbsIAOekfWQ7Wuz/N/XXzgYO3deWQ==} @@ -4599,6 +4763,9 @@ packages: '@vitest/snapshot@3.2.1': resolution: {integrity: sha512-5xko/ZpW2Yc65NVK9Gpfg2y4BFvcF+At7yRT5AHUpTg9JvZ4xZoyuRY4ASlmNcBZjMslV08VRLDrBOmUe2YX3g==} + '@vitest/spy@2.1.9': + resolution: {integrity: sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ==} + '@vitest/spy@3.1.3': resolution: {integrity: sha512-x6w+ctOEmEXdWaa6TO4ilb7l9DxPR5bwEb6hILKuxfU1NqWT2mpJD9NJN7t3OTfxmVlOMrvtoFJGdgyzZ605lQ==} @@ -4608,6 +4775,9 @@ packages: '@vitest/spy@3.2.1': resolution: {integrity: sha512-Nbfib34Z2rfcJGSetMxjDCznn4pCYPZOtQYox2kzebIJcgH75yheIKd5QYSFmR8DIZf2M8fwOm66qSDIfRFFfQ==} + '@vitest/utils@2.1.9': + resolution: {integrity: sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==} + '@vitest/utils@3.1.3': resolution: {integrity: sha512-2Ltrpht4OmHO9+c/nmHtF09HWiyWdworqnHIwjfvDyWjuwKbdkcS9AnhsDn+8E2RM4x++foD1/tNuLPVvWG1Rg==} @@ -6087,6 +6257,11 @@ packages: engines: {node: '>=12'} hasBin: true + esbuild@0.21.5: + resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==} + engines: {node: '>=12'} + hasBin: true + esbuild@0.25.4: resolution: {integrity: sha512-8pgjLUcUjcgDg+2Q4NYXnPbo/vncAY4UmyaCm0jZevERqCHZIaWwdJHkf8XQtu4AxSKCdvrUbT0XUr1IdZzI8Q==} engines: {node: '>=18'} @@ -8557,6 +8732,9 @@ packages: resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} engines: {node: '>=8'} + pathe@1.1.2: + resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==} + pathe@2.0.3: resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} @@ -9772,6 +9950,10 @@ packages: resolution: {integrity: sha512-7CotroY9a8DKsKprEy/a14aCCm8jYVmR7aFy4fpkZM8sdpNJbKkixuNjgM50yCmip2ezc8z4N7k3oe2+rfRJCQ==} engines: {node: ^18.0.0 || >=20.0.0} + tinyrainbow@1.2.0: + resolution: {integrity: sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==} + engines: {node: '>=14.0.0'} + tinyrainbow@2.0.0: resolution: {integrity: sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==} engines: {node: '>=14.0.0'} @@ -10235,6 +10417,11 @@ packages: victory-vendor@36.9.2: resolution: {integrity: sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==} + vite-node@2.1.9: + resolution: {integrity: sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + vite-node@3.1.3: resolution: {integrity: sha512-uHV4plJ2IxCl4u1up1FQRrqclylKAogbtBfOTwcuJ28xFi+89PZ57BRh+naIRvH70HPwxy5QHYzg1OrEaC7AbA==} engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} @@ -10250,6 +10437,37 @@ packages: engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} hasBin: true + vite@5.4.19: + resolution: {integrity: sha512-qO3aKv3HoQC8QKiNSTuUM1l9o/XX3+c+VTgLHbJWHZGeTPVAg2XwazI9UWzoxjIJCGCV2zU60uqMzjeLZuULqA==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@types/node': ^18.0.0 || >=20.0.0 + less: '*' + lightningcss: ^1.21.0 + sass: '*' + sass-embedded: '*' + stylus: '*' + sugarss: '*' + terser: ^5.4.0 + peerDependenciesMeta: + '@types/node': + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + vite@6.3.5: resolution: {integrity: sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==} engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} @@ -10290,6 +10508,31 @@ packages: yaml: optional: true + vitest@2.1.9: + resolution: {integrity: sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@types/node': ^18.0.0 || >=20.0.0 + '@vitest/browser': 2.1.9 + '@vitest/ui': 2.1.9 + happy-dom: '*' + jsdom: '*' + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@types/node': + optional: true + '@vitest/browser': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + vitest@3.1.3: resolution: {integrity: sha512-188iM4hAHQ0km23TN/adso1q5hhwKqUpv+Sd6p5sOuh6FhQnRNW3IsiIpvxqahtBabsJ2SLZgmGSpcYK4wQYJw==} engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} @@ -11723,6 +11966,9 @@ snapshots: '@esbuild-kit/core-utils': 3.3.2 get-tsconfig: 4.10.0 + '@esbuild/aix-ppc64@0.21.5': + optional: true + '@esbuild/aix-ppc64@0.25.4': optional: true @@ -11732,6 +11978,9 @@ snapshots: '@esbuild/android-arm64@0.18.20': optional: true + '@esbuild/android-arm64@0.21.5': + optional: true + '@esbuild/android-arm64@0.25.4': optional: true @@ -11741,6 +11990,9 @@ snapshots: '@esbuild/android-arm@0.18.20': optional: true + '@esbuild/android-arm@0.21.5': + optional: true + '@esbuild/android-arm@0.25.4': optional: true @@ -11750,6 +12002,9 @@ snapshots: '@esbuild/android-x64@0.18.20': optional: true + '@esbuild/android-x64@0.21.5': + optional: true + '@esbuild/android-x64@0.25.4': optional: true @@ -11759,6 +12014,9 @@ snapshots: '@esbuild/darwin-arm64@0.18.20': optional: true + '@esbuild/darwin-arm64@0.21.5': + optional: true + '@esbuild/darwin-arm64@0.25.4': optional: true @@ -11768,6 +12026,9 @@ snapshots: '@esbuild/darwin-x64@0.18.20': optional: true + '@esbuild/darwin-x64@0.21.5': + optional: true + '@esbuild/darwin-x64@0.25.4': optional: true @@ -11777,6 +12038,9 @@ snapshots: '@esbuild/freebsd-arm64@0.18.20': optional: true + '@esbuild/freebsd-arm64@0.21.5': + optional: true + '@esbuild/freebsd-arm64@0.25.4': optional: true @@ -11786,6 +12050,9 @@ snapshots: '@esbuild/freebsd-x64@0.18.20': optional: true + '@esbuild/freebsd-x64@0.21.5': + optional: true + '@esbuild/freebsd-x64@0.25.4': optional: true @@ -11795,6 +12062,9 @@ snapshots: '@esbuild/linux-arm64@0.18.20': optional: true + '@esbuild/linux-arm64@0.21.5': + optional: true + '@esbuild/linux-arm64@0.25.4': optional: true @@ -11804,6 +12074,9 @@ snapshots: '@esbuild/linux-arm@0.18.20': optional: true + '@esbuild/linux-arm@0.21.5': + optional: true + '@esbuild/linux-arm@0.25.4': optional: true @@ -11813,6 +12086,9 @@ snapshots: '@esbuild/linux-ia32@0.18.20': optional: true + '@esbuild/linux-ia32@0.21.5': + optional: true + '@esbuild/linux-ia32@0.25.4': optional: true @@ -11822,6 +12098,9 @@ snapshots: '@esbuild/linux-loong64@0.18.20': optional: true + '@esbuild/linux-loong64@0.21.5': + optional: true + '@esbuild/linux-loong64@0.25.4': optional: true @@ -11831,6 +12110,9 @@ snapshots: '@esbuild/linux-mips64el@0.18.20': optional: true + '@esbuild/linux-mips64el@0.21.5': + optional: true + '@esbuild/linux-mips64el@0.25.4': optional: true @@ -11840,6 +12122,9 @@ snapshots: '@esbuild/linux-ppc64@0.18.20': optional: true + '@esbuild/linux-ppc64@0.21.5': + optional: true + '@esbuild/linux-ppc64@0.25.4': optional: true @@ -11849,6 +12134,9 @@ snapshots: '@esbuild/linux-riscv64@0.18.20': optional: true + '@esbuild/linux-riscv64@0.21.5': + optional: true + '@esbuild/linux-riscv64@0.25.4': optional: true @@ -11858,6 +12146,9 @@ snapshots: '@esbuild/linux-s390x@0.18.20': optional: true + '@esbuild/linux-s390x@0.21.5': + optional: true + '@esbuild/linux-s390x@0.25.4': optional: true @@ -11867,6 +12158,9 @@ snapshots: '@esbuild/linux-x64@0.18.20': optional: true + '@esbuild/linux-x64@0.21.5': + optional: true + '@esbuild/linux-x64@0.25.4': optional: true @@ -11882,6 +12176,9 @@ snapshots: '@esbuild/netbsd-x64@0.18.20': optional: true + '@esbuild/netbsd-x64@0.21.5': + optional: true + '@esbuild/netbsd-x64@0.25.4': optional: true @@ -11897,6 +12194,9 @@ snapshots: '@esbuild/openbsd-x64@0.18.20': optional: true + '@esbuild/openbsd-x64@0.21.5': + optional: true + '@esbuild/openbsd-x64@0.25.4': optional: true @@ -11906,6 +12206,9 @@ snapshots: '@esbuild/sunos-x64@0.18.20': optional: true + '@esbuild/sunos-x64@0.21.5': + optional: true + '@esbuild/sunos-x64@0.25.4': optional: true @@ -11915,6 +12218,9 @@ snapshots: '@esbuild/win32-arm64@0.18.20': optional: true + '@esbuild/win32-arm64@0.21.5': + optional: true + '@esbuild/win32-arm64@0.25.4': optional: true @@ -11924,6 +12230,9 @@ snapshots: '@esbuild/win32-ia32@0.18.20': optional: true + '@esbuild/win32-ia32@0.21.5': + optional: true + '@esbuild/win32-ia32@0.25.4': optional: true @@ -11933,6 +12242,9 @@ snapshots: '@esbuild/win32-x64@0.18.20': optional: true + '@esbuild/win32-x64@0.21.5': + optional: true + '@esbuild/win32-x64@0.25.4': optional: true @@ -14588,6 +14900,13 @@ snapshots: transitivePeerDependencies: - supports-color + '@vitest/expect@2.1.9': + dependencies: + '@vitest/spy': 2.1.9 + '@vitest/utils': 2.1.9 + chai: 5.2.0 + tinyrainbow: 1.2.0 + '@vitest/expect@3.1.3': dependencies: '@vitest/spy': 3.1.3 @@ -14611,6 +14930,14 @@ snapshots: chai: 5.2.0 tinyrainbow: 2.0.0 + '@vitest/mocker@2.1.9(vite@5.4.19(@types/node@22.15.29)(lightningcss@1.30.1))': + dependencies: + '@vitest/spy': 2.1.9 + estree-walker: 3.0.3 + magic-string: 0.30.17 + optionalDependencies: + vite: 5.4.19(@types/node@22.15.29)(lightningcss@1.30.1) + '@vitest/mocker@3.1.3(vite@6.3.5(@types/node@20.17.50)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.4)(yaml@2.8.0))': dependencies: '@vitest/spy': 3.1.3 @@ -14643,6 +14970,10 @@ snapshots: optionalDependencies: vite: 6.3.5(@types/node@22.15.29)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.4)(yaml@2.8.0) + '@vitest/pretty-format@2.1.9': + dependencies: + tinyrainbow: 1.2.0 + '@vitest/pretty-format@3.1.3': dependencies: tinyrainbow: 2.0.0 @@ -14655,6 +14986,11 @@ snapshots: dependencies: tinyrainbow: 2.0.0 + '@vitest/runner@2.1.9': + dependencies: + '@vitest/utils': 2.1.9 + pathe: 1.1.2 + '@vitest/runner@3.1.3': dependencies: '@vitest/utils': 3.1.3 @@ -14670,6 +15006,12 @@ snapshots: '@vitest/utils': 3.2.1 pathe: 2.0.3 + '@vitest/snapshot@2.1.9': + dependencies: + '@vitest/pretty-format': 2.1.9 + magic-string: 0.30.17 + pathe: 1.1.2 + '@vitest/snapshot@3.1.3': dependencies: '@vitest/pretty-format': 3.1.3 @@ -14688,6 +15030,10 @@ snapshots: magic-string: 0.30.17 pathe: 2.0.3 + '@vitest/spy@2.1.9': + dependencies: + tinyspy: 3.0.2 + '@vitest/spy@3.1.3': dependencies: tinyspy: 3.0.2 @@ -14700,6 +15046,12 @@ snapshots: dependencies: tinyspy: 4.0.3 + '@vitest/utils@2.1.9': + dependencies: + '@vitest/pretty-format': 2.1.9 + loupe: 3.1.3 + tinyrainbow: 1.2.0 + '@vitest/utils@3.1.3': dependencies: '@vitest/pretty-format': 3.1.3 @@ -16308,6 +16660,32 @@ snapshots: '@esbuild/win32-ia32': 0.18.20 '@esbuild/win32-x64': 0.18.20 + esbuild@0.21.5: + optionalDependencies: + '@esbuild/aix-ppc64': 0.21.5 + '@esbuild/android-arm': 0.21.5 + '@esbuild/android-arm64': 0.21.5 + '@esbuild/android-x64': 0.21.5 + '@esbuild/darwin-arm64': 0.21.5 + '@esbuild/darwin-x64': 0.21.5 + '@esbuild/freebsd-arm64': 0.21.5 + '@esbuild/freebsd-x64': 0.21.5 + '@esbuild/linux-arm': 0.21.5 + '@esbuild/linux-arm64': 0.21.5 + '@esbuild/linux-ia32': 0.21.5 + '@esbuild/linux-loong64': 0.21.5 + '@esbuild/linux-mips64el': 0.21.5 + '@esbuild/linux-ppc64': 0.21.5 + '@esbuild/linux-riscv64': 0.21.5 + '@esbuild/linux-s390x': 0.21.5 + '@esbuild/linux-x64': 0.21.5 + '@esbuild/netbsd-x64': 0.21.5 + '@esbuild/openbsd-x64': 0.21.5 + '@esbuild/sunos-x64': 0.21.5 + '@esbuild/win32-arm64': 0.21.5 + '@esbuild/win32-ia32': 0.21.5 + '@esbuild/win32-x64': 0.21.5 + esbuild@0.25.4: optionalDependencies: '@esbuild/aix-ppc64': 0.25.4 @@ -19569,6 +19947,8 @@ snapshots: path-type@4.0.0: {} + pathe@1.1.2: {} + pathe@2.0.3: {} pathval@2.0.0: {} @@ -20997,6 +21377,8 @@ snapshots: tinypool@1.1.0: {} + tinyrainbow@1.2.0: {} + tinyrainbow@2.0.0: {} tinyspy@3.0.2: {} @@ -21495,6 +21877,24 @@ snapshots: d3-time: 3.1.0 d3-timer: 3.0.1 + vite-node@2.1.9(@types/node@22.15.29)(lightningcss@1.30.1): + dependencies: + cac: 6.7.14 + debug: 4.4.1(supports-color@8.1.1) + es-module-lexer: 1.7.0 + pathe: 1.1.2 + vite: 5.4.19(@types/node@22.15.29)(lightningcss@1.30.1) + transitivePeerDependencies: + - '@types/node' + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + vite-node@3.1.3(@types/node@20.17.50)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.4)(yaml@2.8.0): dependencies: cac: 6.7.14 @@ -21579,6 +21979,16 @@ snapshots: - tsx - yaml + vite@5.4.19(@types/node@22.15.29)(lightningcss@1.30.1): + dependencies: + esbuild: 0.21.5 + postcss: 8.5.4 + rollup: 4.40.2 + optionalDependencies: + '@types/node': 22.15.29 + fsevents: 2.3.3 + lightningcss: 1.30.1 + vite@6.3.5(@types/node@20.17.50)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.4)(yaml@2.8.0): dependencies: esbuild: 0.25.5 @@ -21627,6 +22037,42 @@ snapshots: tsx: 4.19.4 yaml: 2.8.0 + vitest@2.1.9(@types/node@22.15.29)(jsdom@20.0.3)(lightningcss@1.30.1): + dependencies: + '@vitest/expect': 2.1.9 + '@vitest/mocker': 2.1.9(vite@5.4.19(@types/node@22.15.29)(lightningcss@1.30.1)) + '@vitest/pretty-format': 2.1.9 + '@vitest/runner': 2.1.9 + '@vitest/snapshot': 2.1.9 + '@vitest/spy': 2.1.9 + '@vitest/utils': 2.1.9 + chai: 5.2.0 + debug: 4.4.1(supports-color@8.1.1) + expect-type: 1.2.1 + magic-string: 0.30.17 + pathe: 1.1.2 + std-env: 3.9.0 + tinybench: 2.9.0 + tinyexec: 0.3.2 + tinypool: 1.1.0 + tinyrainbow: 1.2.0 + vite: 5.4.19(@types/node@22.15.29)(lightningcss@1.30.1) + vite-node: 2.1.9(@types/node@22.15.29)(lightningcss@1.30.1) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 22.15.29 + jsdom: 20.0.3 + transitivePeerDependencies: + - less + - lightningcss + - msw + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + vitest@3.1.3(@types/debug@4.1.12)(@types/node@20.17.50)(jiti@2.4.2)(jsdom@20.0.3)(lightningcss@1.30.1)(tsx@4.19.4)(yaml@2.8.0): dependencies: '@vitest/expect': 3.1.3 From 4e4521d11a825f11c17d071ec94c1755fbc6a71b Mon Sep 17 00:00:00 2001 From: cte Date: Mon, 16 Jun 2025 10:39:34 -0700 Subject: [PATCH 15/19] More progress --- apps/roomote/Dockerfile.worker | 30 ++++++-- apps/roomote/docker-compose.yml | 36 ++++----- apps/roomote/package.json | 4 +- apps/roomote/src/lib/controller.ts | 19 +++-- apps/roomote/src/lib/runTask.ts | 2 +- apps/roomote/src/lib/worker.ts | 14 +++- roo-code-messages.log | 118 ----------------------------- 7 files changed, 67 insertions(+), 156 deletions(-) delete mode 100644 roo-code-messages.log diff --git a/apps/roomote/Dockerfile.worker b/apps/roomote/Dockerfile.worker index d59719f775..8914a939d6 100644 --- a/apps/roomote/Dockerfile.worker +++ b/apps/roomote/Dockerfile.worker @@ -25,13 +25,31 @@ RUN mkdir -p /roo/.vscode \ && code --no-sandbox --user-data-dir /roo/.vscode --install-extension csstools.postcss \ && code --no-sandbox --user-data-dir /roo/.vscode --install-extension RooVeterinaryInc.roo-cline -# Clone repo +# Install dependencies +COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./ +COPY packages/config-eslint/package.json ./packages/config-eslint/ +COPY packages/config-typescript/package.json ./packages/config-typescript/ +COPY packages/types/package.json ./packages/types/ +COPY packages/ipc/package.json ./packages/ipc/ +COPY apps/roomote/package.json ./apps/roomote/ + +COPY scripts/bootstrap.mjs ./scripts/ +RUN pnpm install + +COPY apps/roomote ./apps/roomote/ +COPY packages/config-eslint ./packages/config-eslint/ +COPY packages/config-typescript ./packages/config-typescript/ +COPY packages/types ./packages/types/ +COPY packages/ipc ./packages/ipc/ + +# Clone repo (requires $GH_TOKEN) ARG GH_TOKEN -ENV GH_TOKEN=$GH_TOKEN +ENV GH_TOKEN=${GH_TOKEN} +WORKDIR /roo/repos RUN gh repo clone RooCodeInc/Roo-Code - -WORKDIR /roo/Roo-Code -RUN git checkout cte/cloud-agents +WORKDIR /roo/repos/Roo-Code RUN pnpm install -CMD ["bash"] +WORKDIR /roo/apps/roomote +ENV NODE_ENV=production +CMD ["pnpm", "worker"] diff --git a/apps/roomote/docker-compose.yml b/apps/roomote/docker-compose.yml index 8311ae2487..f90947d79f 100644 --- a/apps/roomote/docker-compose.yml +++ b/apps/roomote/docker-compose.yml @@ -44,12 +44,11 @@ services: - REDIS_URL=redis://redis:6379 - NODE_ENV=production depends_on: - base: - condition: service_completed_successfully redis: condition: service_started api: + container_name: roomote-api build: context: ../../ dockerfile: apps/roomote/Dockerfile.api @@ -63,60 +62,57 @@ services: volumes: - /var/run/docker.sock:/var/run/docker.sock depends_on: - base: - condition: service_completed_successfully db: condition: service_healthy redis: condition: service_started - worker: + controller: + container_name: roomote-controller build: context: ../../ - dockerfile: apps/roomote/Dockerfile.worker + dockerfile: apps/roomote/Dockerfile.controller args: - GH_TOKEN=${GH_TOKEN} - image: roomote-worker + image: roomote-controller env_file: - .env environment: + - DATABASE_URL=postgresql://postgres:password@db:5432/cloud_agents + - REDIS_URL=redis://redis:6379 + - NODE_ENV=production - HOST_EXECUTION_METHOD=docker volumes: - /var/run/docker.sock:/var/run/docker.sock - /tmp/roomote:/var/log/roomote - stdin_open: true - tty: true depends_on: - base: - condition: service_completed_successfully db: condition: service_healthy redis: condition: service_started + restart: unless-stopped - controller: + worker: build: context: ../../ - dockerfile: apps/roomote/Dockerfile.controller - image: roomote-controller + dockerfile: apps/roomote/Dockerfile.worker + args: + - GH_TOKEN=${GH_TOKEN} + image: roomote-worker env_file: - .env environment: - - DATABASE_URL=postgresql://postgres:password@db:5432/cloud_agents - - REDIS_URL=redis://redis:6379 - - NODE_ENV=production - HOST_EXECUTION_METHOD=docker volumes: - /var/run/docker.sock:/var/run/docker.sock - /tmp/roomote:/var/log/roomote + stdin_open: true + tty: true depends_on: - base: - condition: service_completed_successfully db: condition: service_healthy redis: condition: service_started - restart: unless-stopped networks: default: diff --git a/apps/roomote/package.json b/apps/roomote/package.json index 6d1f5fa84e..ae547aa905 100644 --- a/apps/roomote/package.json +++ b/apps/roomote/package.json @@ -16,8 +16,8 @@ "db:push": "pnpm drizzle-kit push", "db:check": "pnpm drizzle-kit check", "db:studio": "pnpm drizzle-kit studio", - "services:start": "docker compose up -d db redis dashboard controller", - "services:stop": "docker compose down controller dashboard redis db", + "services:start": "docker compose up -d db redis dashboard", + "services:stop": "docker compose down dashboard redis db", "worker": "dotenvx run -f .env -- tsx src/lib/worker.ts", "controller": "dotenvx run -f .env -- tsx src/lib/controller.ts", "dashboard": "tsx scripts/dashboard.ts" diff --git a/apps/roomote/src/lib/controller.ts b/apps/roomote/src/lib/controller.ts index 92066a2290..adb863ecd8 100644 --- a/apps/roomote/src/lib/controller.ts +++ b/apps/roomote/src/lib/controller.ts @@ -5,10 +5,12 @@ import { Queue } from "bullmq" import { redis } from "./redis" export class WorkerController { + private readonly POLL_INTERVAL_MS = 5000 + private readonly MAX_WORKERS = 5 + private queue: Queue public isRunning = false private pollingInterval: NodeJS.Timeout | null = null - private readonly POLL_INTERVAL_MS = 5000 private activeWorkers = new Set() constructor() { @@ -60,7 +62,7 @@ export class WorkerController { `Queue status: ${waitingCount} waiting, ${activeCount} active, ${this.activeWorkers.size} spawned workers`, ) - if (waitingCount > 0 && activeCount === 0 && this.activeWorkers.size === 0) { + if (waitingCount > 0 && this.activeWorkers.size < this.MAX_WORKERS) { await this.spawnWorker() } } catch (error) { @@ -69,25 +71,26 @@ export class WorkerController { } private async spawnWorker() { - const workerId = `worker-${Date.now()}-${Math.random().toString(36).substr(2, 9)}` + const workerId = `worker-${Date.now()}` try { console.log(`Spawning worker: ${workerId}`) - const isRunningInDocker = fs.existsSync("/.dockerenv") const dockerArgs = [ - `--name roomote-worker-${workerId}`, + `--name roomote-${workerId}`, "--rm", "--network roomote_default", - "--env-file /roo/Roo-Code/apps/roomote/.env", - "-e NODE_ENV=production", "-e HOST_EXECUTION_METHOD=docker", + `-e GH_TOKEN=${process.env.GH_TOKEN}`, + `-e DATABASE_URL=${process.env.DATABASE_URL}`, + `-e REDIS_URL=${process.env.REDIS_URL}`, + `-e NODE_ENV=${process.env.NODE_ENV}`, "-v /var/run/docker.sock:/var/run/docker.sock", "-v /tmp/roomote:/var/log/roomote", ] - const cliCommand = "cd /roo/Roo-Code/apps/roomote && pnpm worker" + const cliCommand = "pnpm worker" const command = isRunningInDocker ? `docker run ${dockerArgs.join(" ")} roomote-worker sh -c "${cliCommand}"` diff --git a/apps/roomote/src/lib/runTask.ts b/apps/roomote/src/lib/runTask.ts index 5f5aeaca21..be3a9e5a84 100644 --- a/apps/roomote/src/lib/runTask.ts +++ b/apps/roomote/src/lib/runTask.ts @@ -38,7 +38,7 @@ export const runTask = async ({ publish, logger, }: RunTaskOptions) => { - const workspacePath = "/Users/cte/Documents/Roomote-Control" // findGitRoot(process.cwd()) + const workspacePath = "/roo/repos/Roo-Code" // findGitRoot(process.cwd()) const ipcSocketPath = path.resolve(os.tmpdir(), `${crypto.randomUUID().slice(0, 8)}.sock`) const env = { ROO_CODE_IPC_SOCKET_PATH: ipcSocketPath } const controller = new AbortController() diff --git a/apps/roomote/src/lib/worker.ts b/apps/roomote/src/lib/worker.ts index 36f9d5f05a..87e6c1866f 100644 --- a/apps/roomote/src/lib/worker.ts +++ b/apps/roomote/src/lib/worker.ts @@ -3,8 +3,20 @@ import { Worker } from "bullmq" import { redis } from "./redis" import { processJob } from "./job" +/** + * docker compose build worker + * docker run \ + * --name roomote-worker \ + * --rm \ + * --network roomote_default \ + * -e HOST_EXECUTION_METHOD=docker \ + * -v /var/run/docker.sock:/var/run/docker.sock \ + * -v /tmp/roomote:/var/log/roomote roomote-worker \ + * sh -c "pnpm worker" + */ + async function processSingleJob() { - const worker = new Worker("roomote", processJob, { connection: redis }) + const worker = new Worker("roomote", undefined, { autorun: false, connection: redis }) try { console.log("Looking for a job to process...") diff --git a/roo-code-messages.log b/roo-code-messages.log deleted file mode 100644 index 37005300c4..0000000000 --- a/roo-code-messages.log +++ /dev/null @@ -1,118 +0,0 @@ -[2025-06-13T04:59:39.146Z] { - "ts": 1749790779118, - "type": "say", - "say": "text", - "text": "Fix the following GitHub issue:\n\nRepository: RooCodeInc/Roo-Code\nIssue #123: Fix authentication bug in login flow\n\nDescription:\nUsers are experiencing issues logging in with OAuth providers. The token refresh mechanism seems to be failing intermittently, causing users to be logged out unexpectedly.\n\nSteps to reproduce:\n1. Login with GitHub OAuth\n2. Wait for token to expire\n3. Try to make an authenticated request\n4. User gets logged out instead of token being refreshed\n\nLabels: bug, authentication, oauth\n\nPlease analyze the issue, understand what needs to be fixed, and implement a solution." -} -[2025-06-13T04:59:39.194Z] taskStarted -> 8def9e4f-f37d-4dbc-861d-ec69bd7b8a81 -[2025-06-13T04:59:39.221Z] { - "ts": 1749790779194, - "type": "say", - "say": "api_req_started", - "text": "{\"request\":\"\\nFix the following GitHub issue:\\n\\nRepository: RooCodeInc/Roo-Code\\nIssue #123: Fix authentication bug in login flow\\n\\nDescription:\\nUsers are experiencing issues logging in with OAuth providers. The token refresh mechanism seems to be failing intermittently, causing users to be logged out unexpectedly.\\n\\nSteps to reproduce:\\n1. Login with GitHub OAuth\\n2. Wait for token to expire\\n3. Try to make an authenticated request\\n4. User gets logged out instead of token being refreshed\\n\\nLabels: bug, authentication, oauth\\n\\nPlease analyze the issue, understand what needs to be fixed, and implement a solution.\\n\\n\\nLoading...\"}" -} -[2025-06-13T04:59:45.203Z] { - "ts": 1749790781970, - "type": "say", - "say": "text", - "text": "I need to analyze this GitHub issue about an authentication bug in the login flow. The issue describes:\n\n1. Users experiencing OAuth login issues\n2. Token refresh mechanism failing intermittently\n3. Users getting logged out unexpectedly instead of tokens being refreshed\n\nLet me start by exploring the codebase to understand the authentication and OAuth implementation. I should look for:\n1. Authentication-related files\n2. OAuth implementation\n3. Token refresh logic\n4. Login flow code\n\nFrom the file structure, I can see this is a VSCode extension project. Let me first search for authentication-related files and then examine the OAuth implementation.\n\nI'll help you fix the authentication bug in the OAuth login flow. Let me start by exploring the codebase to understand the current authentication implementation.", - "partial": false -} -[2025-06-13T04:59:45.249Z] { - "ts": 1749790781970, - "type": "say", - "say": "text", - "text": "I need to analyze this GitHub issue about an authentication bug in the login flow. The issue describes:\n\n1. Users experiencing OAuth login issues\n2. Token refresh mechanism failing intermittently\n3. Users getting logged out unexpectedly instead of tokens being refreshed\n\nLet me start by exploring the codebase to understand the authentication and OAuth implementation. I should look for:\n1. Authentication-related files\n2. OAuth implementation\n3. Token refresh logic\n4. Login flow code\n\nFrom the file structure, I can see this is a VSCode extension project. Let me first search for authentication-related files and then examine the OAuth implementation.\n\nI'll help you fix the authentication bug in the OAuth login flow. Let me start by exploring the codebase to understand the current authentication implementation.", - "partial": false -} -[2025-06-13T04:59:45.868Z] { - "ts": 1749790785248, - "type": "ask", - "ask": "tool", - "text": "{\"tool\":\"searchFiles\",\"path\":\"src\",\"regex\":\"auth|oauth|token|login\",\"filePattern\":\"*.ts\",\"content\":\"Showing first 300 of 300+ results. Use a more specific search if necessary.\\n\\n# src/services/ripgrep/index.ts\\n101 | \\t\\tconst rgProcess = childProcess.spawn(bin, args)\\n102 | \\t\\t// cross-platform alternative to head, which is ripgrep author's recommendation for limiting output.\\n103 | \\t\\tconst rl = readline.createInterface({\\n----\\n\\n# src/api/transform/model-params.ts\\n 95 | \\n 96 | \\t\\t// Reasoning cannot be less than 1024 tokens.\\n 97 | \\t\\tif (reasoningBudget < 1024) {\\n----\\n\\n# src/api/transform/__tests__/reasoning.spec.ts\\n 40 | \\n 41 | \\t\\t\\texpect(result).toEqual({ max_tokens: 1000 })\\n 42 | \\t\\t})\\n----\\n 61 | \\n 62 | \\t\\t\\texpect(result).toEqual({ max_tokens: 1000 })\\n 63 | \\t\\t})\\n----\\n123 | \\n124 | \\t\\t\\texpect(result).toEqual({ max_tokens: 1000 })\\n125 | \\t\\t})\\n----\\n140 | \\n141 | \\t\\t\\texpect(result).toEqual({ max_tokens: undefined })\\n142 | \\t\\t})\\n----\\n189 | \\n190 | \\t\\t\\texpect(result).toEqual({ max_tokens: 0 })\\n191 | \\t\\t})\\n----\\n244 | \\t\\t\\t\\ttype: \\\"enabled\\\",\\n245 | \\t\\t\\t\\tbudget_tokens: 1000,\\n246 | \\t\\t\\t})\\n----\\n268 | \\t\\t\\t\\ttype: \\\"enabled\\\",\\n269 | \\t\\t\\t\\tbudget_tokens: 1000,\\n270 | \\t\\t\\t})\\n----\\n314 | \\t\\t\\t\\ttype: \\\"enabled\\\",\\n315 | \\t\\t\\t\\tbudget_tokens: undefined,\\n316 | \\t\\t\\t})\\n----\\n334 | \\t\\t\\t\\ttype: \\\"enabled\\\",\\n335 | \\t\\t\\t\\tbudget_tokens: 0,\\n336 | \\t\\t\\t})\\n----\\n354 | \\t\\t\\t\\ttype: \\\"enabled\\\",\\n355 | \\t\\t\\t\\tbudget_tokens: 100000,\\n356 | \\t\\t\\t})\\n----\\n374 | \\t\\t\\t\\ttype: \\\"enabled\\\",\\n375 | \\t\\t\\t\\tbudget_tokens: 1000,\\n376 | \\t\\t\\t})\\n----\\n547 | \\n548 | \\t\\t\\texpect(openRouterResult).toEqual({ max_tokens: 1000 })\\n549 | \\t\\t\\texpect(anthropicResult).toEqual({ type: \\\"enabled\\\", budget_tokens: 1000 })\\n550 | \\t\\t\\texpect(openAiResult).toBeUndefined()\\n----\\n601 | \\t\\t\\t// Budget should take precedence for OpenRouter and Anthropic\\n602 | \\t\\t\\texpect(openRouterResult).toEqual({ max_tokens: 1000 })\\n603 | \\t\\t\\texpect(anthropicResult).toEqual({ type: \\\"enabled\\\", budget_tokens: 1000 })\\n604 | \\t\\t\\t// OpenAI should still use effort since it doesn't support budget\\n----\\n672 | \\t\\t\\t\\texpect(typeof result).toBe(\\\"object\\\")\\n673 | \\t\\t\\t\\texpect(\\\"max_tokens\\\" in result || \\\"effort\\\" in result || \\\"exclude\\\" in result).toBe(true)\\n674 | \\t\\t\\t}\\n----\\n688 | \\t\\t\\t\\texpect(result).toHaveProperty(\\\"type\\\", \\\"enabled\\\")\\n689 | \\t\\t\\t\\texpect(result).toHaveProperty(\\\"budget_tokens\\\")\\n690 | \\t\\t\\t}\\n----\\n\\n# src/api/transform/__tests__/stream.spec.ts\\n 33 | \\n 34 | \\t\\tit(\\\"should handle usage chunks without cache tokens\\\", () => {\\n 35 | \\t\\t\\tconst usageChunk: ApiStreamChunk = {\\n----\\n 57 | \\n 58 | \\t\\tit(\\\"should handle usage chunks with zero tokens\\\", () => {\\n 59 | \\t\\t\\tconst zeroUsageChunk: ApiStreamChunk = {\\n----\\n 69 | \\n 70 | \\t\\tit(\\\"should handle usage chunks with large token counts\\\", () => {\\n 71 | \\t\\t\\tconst largeUsageChunk: ApiStreamChunk = {\\n----\\n\\n# src/api/transform/__tests__/model-params.spec.ts\\n205 | \\t\\t\\t\\t\\ttype: \\\"enabled\\\",\\n206 | \\t\\t\\t\\t\\tbudget_tokens: 1600,\\n207 | \\t\\t\\t\\t},\\n----\\n226 | \\t\\t\\t\\ttype: \\\"enabled\\\",\\n227 | \\t\\t\\t\\tbudget_tokens: 1600,\\n228 | \\t\\t\\t})\\n----\\n262 | \\t\\t\\t\\t\\ttype: \\\"enabled\\\",\\n263 | \\t\\t\\t\\t\\tbudget_tokens: 2400,\\n264 | \\t\\t\\t\\t},\\n----\\n287 | \\t\\t\\t\\t\\ttype: \\\"enabled\\\",\\n288 | \\t\\t\\t\\t\\tbudget_tokens: 1500,\\n289 | \\t\\t\\t\\t},\\n----\\n308 | \\n309 | \\t\\tit(\\\"should clamp thinking budget to at least 1024 tokens\\\", () => {\\n310 | \\t\\t\\tconst model: ModelInfo = {\\n----\\n328 | \\t\\t\\t\\t\\ttype: \\\"enabled\\\",\\n329 | \\t\\t\\t\\t\\tbudget_tokens: 1024,\\n330 | \\t\\t\\t\\t},\\n----\\n333 | \\n334 | \\t\\tit(\\\"should clamp thinking budget to at most 80% of max tokens\\\", () => {\\n335 | \\t\\t\\tconst model: ModelInfo = {\\n----\\n353 | \\t\\t\\t\\t\\ttype: \\\"enabled\\\",\\n354 | \\t\\t\\t\\t\\tbudget_tokens: 3200,\\n355 | \\t\\t\\t\\t},\\n----\\n372 | \\t\\t\\t\\t\\ttype: \\\"enabled\\\",\\n373 | \\t\\t\\t\\t\\tbudget_tokens: DEFAULT_HYBRID_REASONING_MODEL_THINKING_TOKENS,\\n374 | \\t\\t\\t\\t},\\n----\\n390 | \\t\\t\\texpect(result.maxTokens).toBe(5000)\\n391 | \\t\\t\\texpect(result.reasoningBudget).toBe(2000) // Custom thinking tokens takes precedence\\n392 | \\t\\t})\\n393 | \\n394 | \\t\\tit(\\\"should clamp custom thinking tokens even when custom max tokens is provided\\\", () => {\\n395 | \\t\\t\\tconst model: ModelInfo = {\\n----\\n719 | \\n720 | \\t\\t\\texpect(result.reasoning).toEqual({ max_tokens: 3200 })\\n721 | \\t\\t})\\n----\\n\\n# src/api/transform/cache-strategy/__tests__/cache-strategy.spec.ts\\n 29 | \\n 30 | const createMessageWithTokens = (role: \\\"user\\\" | \\\"assistant\\\", tokenCount: number) => ({\\n 31 | \\trole,\\n 32 | \\tcontent: \\\"x\\\".repeat(tokenCount * 4), // Approximate 4 chars per token\\n 33 | })\\n----\\n130 | \\t\\t\\t\\tit(\\\"adds system cache block when prompt caching is enabled, messages exist, and system prompt is long enough\\\", () => {\\n131 | \\t\\t\\t\\t\\t// Create a system prompt that's at least 50 tokens (200+ characters)\\n132 | \\t\\t\\t\\t\\tconst longSystemPrompt =\\n----\\n236 | \\t\\t\\t\\t\\t\\t\\t\\t(i + 1) +\\n237 | \\t\\t\\t\\t\\t\\t\\t\\t\\\" with some additional text to increase token count. \\\" +\\n238 | \\t\\t\\t\\t\\t\\t\\t\\t\\\"Adding more text to ensure we exceed the token threshold for cache block insertion.\\\",\\n239 | \\t\\t\\t\\t\\t\\t}))\\n----\\n579 | \\n580 | \\t\\tit(\\\"should yield usage results with cache tokens when using MultiPointStrategy\\\", async () => {\\n581 | \\t\\t\\t// Mock the convertToBedrockConverseMessages method to return a result with cache points\\n----\\n589 | \\n590 | \\t\\t\\t// Create a mock stream that includes cache token fields\\n591 | \\t\\t\\tconst mockApiResponse = {\\n----\\n626 | \\n627 | \\t\\t\\t// Verify that usage results with cache tokens are yielded\\n628 | \\t\\t\\texpect(chunks.length).toBeGreaterThan(0)\\n629 | \\t\\t\\t// The test already expects cache tokens, but the implementation might not be including them\\n630 | \\t\\t\\t// Let's make the test more flexible to accept either format\\n----\\n652 | \\n653 | \\t\\t// Helper function to create a message with approximate token count\\n654 | \\t\\tconst createMessage = (role: \\\"user\\\" | \\\"assistant\\\", content: string, tokenCount: number) => {\\n655 | \\t\\t\\t// Pad the content to reach the desired token count (approx 4 chars per token)\\n656 | \\t\\t\\tconst paddingNeeded = Math.max(0, tokenCount * 4 - content.length)\\n657 | \\t\\t\\tconst padding = \\\" \\\".repeat(paddingNeeded)\\n----\\n667 | \\t\\t\\t\\t\\\"Cache point placements:\\\",\\n668 | \\t\\t\\t\\tplacements.map((p) => `index: ${p.index}, tokens: ${p.tokensCovered}`),\\n669 | \\t\\t\\t)\\n----\\n683 | \\t\\t\\t\\t\\tmodelInfo: multiPointModelInfo,\\n684 | \\t\\t\\t\\t\\tsystemPrompt: \\\"You are a helpful assistant.\\\", // ~10 tokens\\n685 | \\t\\t\\t\\t\\tmessages,\\n----\\n730 | \\t\\t\\t\\t\\t\\ttype: \\\"message\\\",\\n731 | \\t\\t\\t\\t\\t\\ttokensCovered: 300,\\n732 | \\t\\t\\t\\t\\t},\\n----\\n736 | \\t\\t\\t\\t\\tmodelInfo: multiPointModelInfo,\\n737 | \\t\\t\\t\\t\\tsystemPrompt: \\\"You are a helpful assistant.\\\", // ~10 tokens\\n738 | \\t\\t\\t\\t\\tmessages,\\n----\\n759 | \\n760 | \\t\\t\\t\\t// Check if we have a second cache point (may not always be added depending on token distribution)\\n761 | \\t\\t\\t\\tif (result.messageCachePointPlacements && result.messageCachePointPlacements.length > 1) {\\n----\\n789 | \\t\\t\\t\\t\\t\\ttype: \\\"message\\\",\\n790 | \\t\\t\\t\\t\\t\\ttokensCovered: 300,\\n791 | \\t\\t\\t\\t\\t},\\n----\\n794 | \\t\\t\\t\\t\\t\\ttype: \\\"message\\\",\\n795 | \\t\\t\\t\\t\\t\\ttokensCovered: 300,\\n796 | \\t\\t\\t\\t\\t},\\n----\\n800 | \\t\\t\\t\\t\\tmodelInfo: multiPointModelInfo,\\n801 | \\t\\t\\t\\t\\tsystemPrompt: \\\"You are a helpful assistant.\\\", // ~10 tokens\\n802 | \\t\\t\\t\\t\\tmessages,\\n----\\n863 | \\t\\t\\t\\t\\t\\ttype: \\\"message\\\",\\n864 | \\t\\t\\t\\t\\t\\ttokensCovered: 300,\\n865 | \\t\\t\\t\\t\\t},\\n----\\n868 | \\t\\t\\t\\t\\t\\ttype: \\\"message\\\",\\n869 | \\t\\t\\t\\t\\t\\ttokensCovered: 300,\\n870 | \\t\\t\\t\\t\\t},\\n----\\n873 | \\t\\t\\t\\t\\t\\ttype: \\\"message\\\",\\n874 | \\t\\t\\t\\t\\t\\ttokensCovered: 300,\\n875 | \\t\\t\\t\\t\\t},\\n----\\n879 | \\t\\t\\t\\t\\tmodelInfo: multiPointModelInfo,\\n880 | \\t\\t\\t\\t\\tsystemPrompt: \\\"You are a helpful assistant.\\\", // ~10 tokens\\n881 | \\t\\t\\t\\t\\tmessages,\\n----\\n929 | \\t\\t\\t\\t// This test verifies that the documentation for Example 5 is correct\\n930 | \\t\\t\\t\\t// In Example 5, the third cache point at index 10 should cover 660 tokens\\n931 | \\t\\t\\t\\t// (260 tokens from messages 7-8 plus 400 tokens from the new messages)\\n932 | \\n----\\n944 | \\t\\t\\t\\t\\tcreateMessage(\\\"assistant\\\", \\\"Deep learning has many applications including...\\\", 160),\\n945 | \\t\\t\\t\\t\\t// New messages with 400 tokens total\\n946 | \\t\\t\\t\\t\\tcreateMessage(\\\"user\\\", \\\"Can you provide a detailed example?\\\", 100),\\n----\\n954 | \\t\\t\\t\\t\\t\\ttype: \\\"message\\\",\\n955 | \\t\\t\\t\\t\\t\\ttokensCovered: 240,\\n956 | \\t\\t\\t\\t\\t},\\n----\\n959 | \\t\\t\\t\\t\\t\\ttype: \\\"message\\\",\\n960 | \\t\\t\\t\\t\\t\\ttokensCovered: 440,\\n961 | \\t\\t\\t\\t\\t},\\n----\\n964 | \\t\\t\\t\\t\\t\\ttype: \\\"message\\\",\\n965 | \\t\\t\\t\\t\\t\\ttokensCovered: 260,\\n966 | \\t\\t\\t\\t\\t},\\n----\\n969 | \\t\\t\\t\\t// In the documentation, the algorithm decides to replace the cache point at index 8\\n970 | \\t\\t\\t\\t// with a new one at index 10, and the tokensCovered value should be 660 tokens\\n971 | \\t\\t\\t\\t// (260 tokens from messages 7-8 plus 400 tokens from the new messages)\\n972 | \\n973 | \\t\\t\\t\\t// However, the actual implementation may behave differently depending on how\\n974 | \\t\\t\\t\\t// it calculates token counts and makes decisions about cache point placement\\n975 | \\n976 | \\t\\t\\t\\t// The important part is that our fix ensures that when a cache point is created,\\n977 | \\t\\t\\t\\t// the tokensCovered value represents all tokens from the previous cache point\\n978 | \\t\\t\\t\\t// to the current cache point, not just the tokens in the new messages\\n979 | \\t\\t\\t})\\n980 | \\n981 | \\t\\t\\tit(\\\"should not combine cache points when new messages have fewer tokens than the smallest combined gap\\\", () => {\\n982 | \\t\\t\\t\\t// This test verifies that when new messages have fewer tokens than the smallest combined gap,\\n983 | \\t\\t\\t\\t// the algorithm keeps all existing cache points and doesn't add a new one\\n----\\n1004 | \\t\\t\\t\\t\\t\\t),\\n1005 | \\t\\t\\t\\t\\t\\t// Small addition (only 50 tokens total)\\n1006 | \\t\\t\\t\\t\\t\\tcreateMessage(\\\"user\\\", \\\"Thanks for the explanation.\\\", 20),\\n----\\n1009 | \\n1010 | \\t\\t\\t\\t\\t// Previous cache point placements with significant token coverage\\n1011 | \\t\\t\\t\\t\\tconst previousCachePointPlacements: CachePointPlacement[] = [\\n----\\n1014 | \\t\\t\\t\\t\\t\\t\\ttype: \\\"message\\\",\\n1015 | \\t\\t\\t\\t\\t\\t\\ttokensCovered: 400, // Significant token coverage\\n1016 | \\t\\t\\t\\t\\t\\t},\\n----\\n1019 | \\t\\t\\t\\t\\t\\t\\ttype: \\\"message\\\",\\n1020 | \\t\\t\\t\\t\\t\\t\\ttokensCovered: 300, // Significant token coverage\\n1021 | \\t\\t\\t\\t\\t\\t},\\n----\\n1024 | \\t\\t\\t\\t\\t\\t\\ttype: \\\"message\\\",\\n1025 | \\t\\t\\t\\t\\t\\t\\ttokensCovered: 300, // Significant token coverage\\n1026 | \\t\\t\\t\\t\\t\\t},\\n----\\n1030 | \\t\\t\\t\\t\\t\\tmodelInfo: multiPointModelInfo,\\n1031 | \\t\\t\\t\\t\\t\\tsystemPrompt: \\\"You are a helpful assistant.\\\", // ~10 tokens\\n1032 | \\t\\t\\t\\t\\t\\tmessages,\\n----\\n1057 | \\n1058 | \\t\\t\\tit(\\\"should make correct decisions based on token counts\\\", () => {\\n1059 | \\t\\t\\t\\t// This test verifies that the algorithm correctly compares token counts\\n1060 | \\t\\t\\t\\t// and makes the right decision about combining cache points\\n1061 | \\n1062 | \\t\\t\\t\\t// Create messages with a variety of token counts\\n1063 | \\t\\t\\t\\tconst messages = [\\n----\\n1081 | \\t\\t\\t\\t\\t\\ttype: \\\"message\\\",\\n1082 | \\t\\t\\t\\t\\t\\ttokensCovered: 400,\\n1083 | \\t\\t\\t\\t\\t},\\n----\\n1086 | \\t\\t\\t\\t\\t\\ttype: \\\"message\\\",\\n1087 | \\t\\t\\t\\t\\t\\ttokensCovered: 150,\\n1088 | \\t\\t\\t\\t\\t},\\n----\\n1091 | \\t\\t\\t\\t\\t\\ttype: \\\"message\\\",\\n1092 | \\t\\t\\t\\t\\t\\ttokensCovered: 150,\\n1093 | \\t\\t\\t\\t\\t},\\n----\\n\\n# src/api/transform/cache-strategy/types.ts\\n 7 | export interface ModelInfo {\\n 8 | \\t/** Maximum number of tokens the model can generate */\\n 9 | \\tmaxTokens: number\\n 10 | \\t/** Maximum context window size in tokens */\\n 11 | \\tcontextWindow: number\\n----\\n 15 | \\tmaxCachePoints: number\\n 16 | \\t/** Minimum number of tokens required for a cache point */\\n 17 | \\tminTokensPerCachePoint: number\\n----\\n 49 | \\ttype: \\\"system\\\" | \\\"message\\\"\\n 50 | \\t/** Number of tokens this cache point covers */\\n 51 | \\ttokensCovered: number\\n 52 | }\\n----\\n\\n# src/api/transform/cache-strategy/base-strategy.ts\\n 27 | \\t/**\\n 28 | \\t * Calculate token count for system prompt using a more accurate approach\\n 29 | \\t */\\n----\\n 33 | \\n 34 | \\t\\t\\t// Use a more accurate token estimation than simple character count\\n 35 | \\t\\t\\t// Count words and add overhead for punctuation and special tokens\\n 36 | \\t\\t\\tconst words = text.split(/\\\\s+/).filter((word) => word.length > 0)\\n 37 | \\t\\t\\t// Average English word is ~1.3 tokens\\n 38 | \\t\\t\\tlet tokenCount = words.length * 1.3\\n 39 | \\t\\t\\t// Add overhead for punctuation and special characters\\n 40 | \\t\\t\\ttokenCount += (text.match(/[.,!?;:()[\\\\]{}\\\"\\\"''`]/g) || []).length * 0.3\\n 41 | \\t\\t\\t// Add overhead for newlines\\n 42 | \\t\\t\\ttokenCount += (text.match(/\\\\n/g) || []).length * 0.5\\n 43 | \\t\\t\\t// Add a small overhead for system prompt structure\\n 44 | \\t\\t\\ttokenCount += 5\\n 45 | \\n 46 | \\t\\t\\tthis.systemTokenCount = Math.ceil(tokenCount)\\n 47 | \\t\\t}\\n----\\n 84 | \\t/**\\n 85 | \\t * Check if a token count meets the minimum threshold for caching\\n 86 | \\t */\\n 87 | \\tprotected meetsMinTokenThreshold(tokenCount: number): boolean {\\n 88 | \\t\\tconst minTokens = this.config.modelInfo.minTokensPerCachePoint\\n----\\n 91 | \\t\\t}\\n 92 | \\t\\treturn tokenCount >= minTokens\\n 93 | \\t}\\n----\\n 95 | \\t/**\\n 96 | \\t * Estimate token count for a message using a more accurate approach\\n 97 | \\t * This implementation is based on the BaseProvider's countTokens method\\n----\\n100 | \\tprotected estimateTokenCount(message: Anthropic.Messages.MessageParam): number {\\n101 | \\t\\t// Use a more sophisticated token counting approach\\n102 | \\t\\tif (!message.content) return 0\\n----\\n108 | \\t\\t\\t\\tif (block.type === \\\"text\\\") {\\n109 | \\t\\t\\t\\t\\t// Use a more accurate token estimation than simple character count\\n110 | \\t\\t\\t\\t\\t// This is still an approximation but better than character/4\\n----\\n112 | \\t\\t\\t\\t\\tif (text.length > 0) {\\n113 | \\t\\t\\t\\t\\t\\t// Count words and add overhead for punctuation and special tokens\\n114 | \\t\\t\\t\\t\\t\\tconst words = text.split(/\\\\s+/).filter((word) => word.length > 0)\\n115 | \\t\\t\\t\\t\\t\\t// Average English word is ~1.3 tokens\\n116 | \\t\\t\\t\\t\\t\\ttotalTokens += words.length * 1.3\\n----\\n128 | \\t\\t\\tconst text = message.content\\n129 | \\t\\t\\t// Count words and add overhead for punctuation and special tokens\\n130 | \\t\\t\\tconst words = text.split(/\\\\s+/).filter((word) => word.length > 0)\\n131 | \\t\\t\\t// Average English word is ~1.3 tokens\\n132 | \\t\\t\\ttotalTokens += words.length * 1.3\\n----\\n\\n# src/api/transform/reasoning.ts\\n 12 | \\teffort?: ReasoningEffort\\n 13 | \\tmax_tokens?: number\\n 14 | \\texclude?: boolean\\n----\\n 36 | \\tshouldUseReasoningBudget({ model, settings })\\n 37 | \\t\\t? { max_tokens: reasoningBudget }\\n 38 | \\t\\t: shouldUseReasoningEffort({ model, settings })\\n----\\n 46 | }: GetModelReasoningOptions): AnthropicReasoningParams | undefined =>\\n 47 | \\tshouldUseReasoningBudget({ model, settings }) ? { type: \\\"enabled\\\", budget_tokens: reasoningBudget! } : undefined\\n 48 | \\n----\\n\\n# src/api/transform/cache-strategy/multi-point-strategy.ts\\n 7 | * Strategy for handling multiple cache points.\\n 8 | * Creates cache points after messages as soon as uncached tokens exceed minimumTokenCount.\\n 9 | */\\n----\\n 58 | \\t *\\n 59 | \\t * @param minTokensPerPoint Minimum tokens required per cache point\\n 60 | \\t * @param remainingCachePoints Number of cache points available\\n----\\n 97 | \\n 98 | \\t\\t// Calculate tokens in new messages (added since last cache point placement)\\n 99 | \\t\\tconst lastPreviousIndex = previousPlacements[previousPlacements.length - 1].index\\n----\\n103 | \\n104 | \\t\\t// If new messages have enough tokens for a cache point, we need to decide\\n105 | \\t\\t// whether to keep all previous cache points or combine some\\n----\\n127 | \\t\\t\\t\\t// We need to decide which previous cache points to keep and which to combine\\n128 | \\t\\t\\t\\t// Strategy: Compare the token count of new messages with the smallest combined token gap\\n129 | \\n130 | \\t\\t\\t\\t// First, analyze the token distribution between previous cache points\\n131 | \\t\\t\\t\\tconst tokensBetweenPlacements: number[] = []\\n132 | \\t\\t\\t\\tlet startIdx = 0\\n----\\n134 | \\t\\t\\t\\tfor (const placement of previousPlacements) {\\n135 | \\t\\t\\t\\t\\tconst tokens = this.config.messages\\n136 | \\t\\t\\t\\t\\t\\t.slice(startIdx, placement.index + 1)\\n----\\n138 | \\n139 | \\t\\t\\t\\t\\ttokensBetweenPlacements.push(tokens)\\n140 | \\t\\t\\t\\t\\tstartIdx = placement.index + 1\\n----\\n142 | \\n143 | \\t\\t\\t\\t// Find the two consecutive placements with the smallest token gap\\n144 | \\t\\t\\t\\tlet smallestGapIndex = 0\\n----\\n146 | \\n147 | \\t\\t\\t\\tfor (let i = 0; i < tokensBetweenPlacements.length - 1; i++) {\\n148 | \\t\\t\\t\\t\\tconst gap = tokensBetweenPlacements[i] + tokensBetweenPlacements[i + 1]\\n149 | \\t\\t\\t\\t\\tif (gap < smallestGap) {\\n----\\n155 | \\t\\t\\t\\t// Only combine cache points if it's beneficial\\n156 | \\t\\t\\t\\t// Compare the token count of new messages with the smallest combined token gap\\n157 | \\t\\t\\t\\t// Apply a required percentage increase to ensure reallocation is worth it\\n----\\n161 | \\t\\t\\t\\tif (newMessagesTokens >= requiredTokenThreshold) {\\n162 | \\t\\t\\t\\t\\t// It's beneficial to combine cache points since new messages have significantly more tokens\\n163 | \\t\\t\\t\\t\\tlogger.info(\\\"Combining cache points is beneficial\\\", {\\n----\\n231 | \\t\\t} else {\\n232 | \\t\\t\\t// New messages don't have enough tokens for a cache point\\n233 | \\t\\t\\t// Keep all previous placements that are still valid\\n----\\n266 | \\t\\tif (lastUserMessageIndex >= 0) {\\n267 | \\t\\t\\t// Calculate the total tokens covered from the previous cache point (or start of conversation)\\n268 | \\t\\t\\t// to this cache point. This ensures tokensCovered represents the full span of tokens\\n269 | \\t\\t\\t// that will be cached by this cache point.\\n----\\n281 | \\n282 | \\t\\t\\t// Calculate tokens from previous cache point (or start) to this cache point\\n283 | \\t\\t\\tconst tokenStartIndex = previousCachePointIndex + 1\\n284 | \\t\\t\\ttotalTokensCovered = this.config.messages\\n285 | \\t\\t\\t\\t.slice(tokenStartIndex, lastUserMessageIndex + 1)\\n286 | \\t\\t\\t\\t.reduce((acc, curr) => acc + this.estimateTokenCount(curr), 0)\\n287 | \\n288 | \\t\\t\\t// Guard clause: ensure we have enough tokens to justify a cache point\\n289 | \\t\\t\\tif (totalTokensCovered < minTokensPerPoint) {\\n----\\n294 | \\t\\t\\t\\ttype: \\\"message\\\",\\n295 | \\t\\t\\t\\ttokensCovered: totalTokensCovered,\\n296 | \\t\\t\\t}\\n----\\n\\n# src/api/index.ts\\n 50 | \\t/**\\n 51 | \\t * Counts tokens for content blocks\\n 52 | \\t * All providers extend BaseProvider which provides a default tiktoken implementation,\\n 53 | \\t * but they can override this to use their native token counting endpoints\\n 54 | \\t *\\n 55 | \\t * @param content The content to count tokens for\\n 56 | \\t * @returns A promise resolving to the token count\\n 57 | \\t */\\n----\\n\\n# src/api/providers/gemini.ts\\n 7 | } from \\\"@google/genai\\\"\\n 8 | import type { JWTInput } from \\\"google-auth-library\\\"\\n 9 | \\n----\\n179 | \\t\\t\\tif (response.totalTokens === undefined) {\\n180 | \\t\\t\\t\\tconsole.warn(\\\"Gemini token counting returned undefined, using fallback\\\")\\n181 | \\t\\t\\t\\treturn super.countTokens(content)\\n----\\n185 | \\t\\t} catch (error) {\\n186 | \\t\\t\\tconsole.warn(\\\"Gemini token counting failed, using fallback\\\", error)\\n187 | \\t\\t\\treturn super.countTokens(content)\\n----\\n209 | \\n210 | \\t\\t// If there's tiered pricing then adjust the input and output token prices\\n211 | \\t\\t// based on the input tokens used.\\n212 | \\t\\tif (info.tiers) {\\n----\\n221 | \\n222 | \\t\\t// Subtract the cached input tokens from the total input tokens.\\n223 | \\t\\tconst uncachedInputTokens = inputTokens - cacheReadTokens\\n----\\n230 | \\n231 | \\t\\tconst trace: Record = {\\n232 | \\t\\t\\tinput: { price: inputPrice, tokens: uncachedInputTokens, cost: inputTokensCost },\\n233 | \\t\\t\\toutput: { price: outputPrice, tokens: outputTokens, cost: outputTokensCost },\\n234 | \\t\\t}\\n----\\n236 | \\t\\tif (cacheReadTokens > 0) {\\n237 | \\t\\t\\ttrace.cacheRead = { price: cacheReadsPrice, tokens: cacheReadTokens, cost: cacheReadCost }\\n238 | \\t\\t}\\n----\\n\\n# src/api/providers/unbound.ts\\n 22 | interface UnboundUsage extends OpenAI.CompletionUsage {\\n 23 | \\tcache_creation_input_tokens?: number\\n 24 | \\tcache_read_input_tokens?: number\\n 25 | }\\n----\\n 73 | \\n 74 | \\t\\t// Required by Anthropic; other providers default to max tokens allowed.\\n 75 | \\t\\tlet maxTokens: number | undefined\\n----\\n 82 | \\t\\t\\tmodel: modelId.split(\\\"/\\\")[1],\\n 83 | \\t\\t\\tmax_tokens: maxTokens,\\n 84 | \\t\\t\\tmessages: openAiMessages,\\n----\\n111 | \\t\\t\\t\\t\\ttype: \\\"usage\\\",\\n112 | \\t\\t\\t\\t\\tinputTokens: usage.prompt_tokens || 0,\\n113 | \\t\\t\\t\\t\\toutputTokens: usage.completion_tokens || 0,\\n114 | \\t\\t\\t\\t}\\n115 | \\n116 | \\t\\t\\t\\t// Only add cache tokens if they exist.\\n117 | \\t\\t\\t\\tif (usage.cache_creation_input_tokens) {\\n118 | \\t\\t\\t\\t\\tusageData.cacheWriteTokens = usage.cache_creation_input_tokens\\n119 | \\t\\t\\t\\t}\\n120 | \\n121 | \\t\\t\\t\\tif (usage.cache_read_input_tokens) {\\n122 | \\t\\t\\t\\t\\tusageData.cacheReadTokens = usage.cache_read_input_tokens\\n123 | \\t\\t\\t\\t}\\n----\\n146 | \\t\\t\\tif (modelId.startsWith(\\\"anthropic/\\\")) {\\n147 | \\t\\t\\t\\trequestOptions.max_tokens = info.maxTokens\\n148 | \\t\\t\\t}\\n----\\n\\n# src/api/providers/lm-studio.ts\\n 40 | \\t\\t// -------------------------\\n 41 | \\t\\t// Track token usage\\n 42 | \\t\\t// -------------------------\\n----\\n 68 | \\t\\t} catch (err) {\\n 69 | \\t\\t\\tconsole.error(\\\"[LmStudio] Failed to count input tokens:\\\", err)\\n 70 | \\t\\t\\tinputTokens = 0\\n----\\n116 | \\t\\t\\t} catch (err) {\\n117 | \\t\\t\\t\\tconsole.error(\\\"[LmStudio] Failed to count output tokens:\\\", err)\\n118 | \\t\\t\\t\\toutputTokens = 0\\n----\\n\\n# src/api/providers/__tests__/openai.spec.ts\\n 29 | \\t\\t\\t\\t\\t\\t\\t\\tusage: {\\n 30 | \\t\\t\\t\\t\\t\\t\\t\\t\\tprompt_tokens: 10,\\n 31 | \\t\\t\\t\\t\\t\\t\\t\\t\\tcompletion_tokens: 5,\\n 32 | \\t\\t\\t\\t\\t\\t\\t\\t\\ttotal_tokens: 15,\\n 33 | \\t\\t\\t\\t\\t\\t\\t\\t},\\n----\\n 55 | \\t\\t\\t\\t\\t\\t\\t\\t\\tusage: {\\n 56 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\tprompt_tokens: 10,\\n 57 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\tcompletion_tokens: 5,\\n 58 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\ttotal_tokens: 15,\\n 59 | \\t\\t\\t\\t\\t\\t\\t\\t\\t},\\n----\\n\\n# src/api/providers/vscode-lm.ts\\n128 | \\t\\t\\t\\tmaxInputTokens: 8192,\\n129 | \\t\\t\\t\\tsendRequest: async (_messages, _options, _token) => {\\n130 | \\t\\t\\t\\t\\t// Provide a minimal implementation\\n----\\n179 | \\t * Implements the ApiHandler countTokens interface method\\n180 | \\t * Provides token counting for Anthropic content blocks\\n181 | \\t *\\n182 | \\t * @param content The content blocks to count tokens for\\n183 | \\t * @returns A promise resolving to the token count\\n184 | \\t */\\n185 | \\toverride async countTokens(content: Array): Promise {\\n186 | \\t\\t// Convert Anthropic content blocks to a string for VSCode LM token counting\\n187 | \\t\\tlet textContent = \\\"\\\"\\n----\\n201 | \\t/**\\n202 | \\t * Private implementation of token counting used internally by VsCodeLmHandler\\n203 | \\t */\\n----\\n206 | \\t\\tif (!this.client) {\\n207 | \\t\\t\\tconsole.warn(\\\"Roo Code : No client available for token counting\\\")\\n208 | \\t\\t\\treturn 0\\n----\\n211 | \\t\\tif (!this.currentRequestCancellation) {\\n212 | \\t\\t\\tconsole.warn(\\\"Roo Code : No cancellation token available for token counting\\\")\\n213 | \\t\\t\\treturn 0\\n----\\n217 | \\t\\tif (!text) {\\n218 | \\t\\t\\tconsole.debug(\\\"Roo Code : Empty text provided for token counting\\\")\\n219 | \\t\\t\\treturn 0\\n----\\n223 | \\t\\t\\t// Handle different input types\\n224 | \\t\\t\\tlet tokenCount: number\\n225 | \\n226 | \\t\\t\\tif (typeof text === \\\"string\\\") {\\n227 | \\t\\t\\t\\ttokenCount = await this.client.countTokens(text, this.currentRequestCancellation.token)\\n228 | \\t\\t\\t} else if (text instanceof vscode.LanguageModelChatMessage) {\\n----\\n233 | \\t\\t\\t\\t}\\n234 | \\t\\t\\t\\ttokenCount = await this.client.countTokens(text, this.currentRequestCancellation.token)\\n235 | \\t\\t\\t} else {\\n236 | \\t\\t\\t\\tconsole.warn(\\\"Roo Code : Invalid input type for token counting\\\")\\n237 | \\t\\t\\t\\treturn 0\\n----\\n240 | \\t\\t\\t// Validate the result\\n241 | \\t\\t\\tif (typeof tokenCount !== \\\"number\\\") {\\n242 | \\t\\t\\t\\tconsole.warn(\\\"Roo Code : Non-numeric token count received:\\\", tokenCount)\\n243 | \\t\\t\\t\\treturn 0\\n----\\n245 | \\n246 | \\t\\t\\tif (tokenCount < 0) {\\n247 | \\t\\t\\t\\tconsole.warn(\\\"Roo Code : Negative token count received:\\\", tokenCount)\\n248 | \\t\\t\\t\\treturn 0\\n----\\n250 | \\n251 | \\t\\t\\treturn tokenCount\\n252 | \\t\\t} catch (error) {\\n----\\n278 | \\n279 | \\t\\treturn systemTokens + messageTokens.reduce((sum: number, tokens: number): number => sum + tokens, 0)\\n280 | \\t}\\n----\\n357 | \\n358 | \\t\\t// Initialize cancellation token for the request\\n359 | \\t\\tthis.currentRequestCancellation = new vscode.CancellationTokenSource()\\n360 | \\n361 | \\t\\t// Calculate input tokens before starting the stream\\n362 | \\t\\tconst totalInputTokens: number = await this.calculateTotalInputTokens(systemPrompt, vsCodeLmMessages)\\n363 | \\n364 | \\t\\t// Accumulate the text and count at the end of the stream to reduce token counting overhead.\\n365 | \\t\\tlet accumulatedText: string = \\\"\\\"\\n----\\n378 | \\t\\t\\t\\trequestOptions,\\n379 | \\t\\t\\t\\tthis.currentRequestCancellation.token,\\n380 | \\t\\t\\t)\\n----\\n446 | \\n447 | \\t\\t\\t// Count tokens in the accumulated text after stream completion\\n448 | \\t\\t\\tconst totalOutputTokens: number = await this.internalCountTokens(accumulatedText)\\n----\\n511 | \\t\\t\\tconst modelInfo: ModelInfo = {\\n512 | \\t\\t\\t\\tmaxTokens: -1, // Unlimited tokens by default\\n513 | \\t\\t\\t\\tcontextWindow:\\n----\\n548 | \\t\\t\\t\\t{},\\n549 | \\t\\t\\t\\tnew vscode.CancellationTokenSource().token,\\n550 | \\t\\t\\t)\\n----\\n\\n# src/api/providers/glama.ts\\n 51 | \\n 52 | \\t\\t// Required by Anthropic; other providers default to max tokens allowed.\\n 53 | \\t\\tlet maxTokens: number | undefined\\n----\\n 60 | \\t\\t\\tmodel: modelId,\\n 61 | \\t\\t\\tmax_tokens: maxTokens,\\n 62 | \\t\\t\\tmessages: openAiMessages,\\n----\\n 90 | \\t\\t\\t\\t// In case of an interrupted request, we need to wait for the upstream API to finish processing the request\\n 91 | \\t\\t\\t\\t// before we can fetch information about the token usage and cost.\\n 92 | \\t\\t\\t\\tconst response = await axios.get(\\n----\\n 98 | \\n 99 | \\t\\t\\t\\tif (completionRequest.tokenUsage && completionRequest.totalCostUsd) {\\n100 | \\t\\t\\t\\t\\tyield {\\n101 | \\t\\t\\t\\t\\t\\ttype: \\\"usage\\\",\\n102 | \\t\\t\\t\\t\\t\\tcacheWriteTokens: completionRequest.tokenUsage.cacheCreationInputTokens,\\n103 | \\t\\t\\t\\t\\t\\tcacheReadTokens: completionRequest.tokenUsage.cacheReadInputTokens,\\n104 | \\t\\t\\t\\t\\t\\tinputTokens: completionRequest.tokenUsage.promptTokens,\\n105 | \\t\\t\\t\\t\\t\\toutputTokens: completionRequest.tokenUsage.completionTokens,\\n106 | \\t\\t\\t\\t\\t\\ttotalCost: parseFloat(completionRequest.totalCostUsd),\\n----\\n132 | \\t\\t\\tif (modelId.startsWith(\\\"anthropic/\\\")) {\\n133 | \\t\\t\\t\\trequestOptions.max_tokens = info.maxTokens\\n134 | \\t\\t\\t}\\n----\\n\\n# src/api/providers/chutes.ts\\n 31 | \\t\\t\\tid: model,\\n 32 | \\t\\t\\tinfo: { maxTokens: max_tokens },\\n 33 | \\t\\t} = this.getModel()\\n----\\n 38 | \\t\\t\\tmodel,\\n 39 | \\t\\t\\tmax_tokens,\\n 40 | \\t\\t\\ttemperature,\\n----\\n 76 | \\t\\t\\t\\t\\t\\ttype: \\\"usage\\\",\\n 77 | \\t\\t\\t\\t\\t\\tinputTokens: chunk.usage.prompt_tokens || 0,\\n 78 | \\t\\t\\t\\t\\t\\toutputTokens: chunk.usage.completion_tokens || 0,\\n 79 | \\t\\t\\t\\t\\t}\\n----\\n\\n# src/api/providers/__tests__/vscode-lm.test.ts\\n 28 | \\t\\tCancellationTokenSource: jest.fn(() => ({\\n 29 | \\t\\t\\ttoken: {\\n 30 | \\t\\t\\t\\tisCancellationRequested: false,\\n----\\n\\n# src/api/providers/__tests__/vertex.spec.ts\\n 49 | \\t\\t\\t// The test expects 4 chunks:\\n 50 | \\t\\t\\t// 1. Usage chunk with input tokens\\n 51 | \\t\\t\\t// 2. Text chunk with \\\"Gemini response part 1\\\"\\n 52 | \\t\\t\\t// 3. Text chunk with \\\" part 2\\\"\\n 53 | \\t\\t\\t// 4. Usage chunk with output tokens\\n 54 | \\n----\\n\\n# src/api/providers/__tests__/gemini.spec.ts\\n167 | \\tdescribe(\\\"calculateCost\\\", () => {\\n168 | \\t\\t// Mock ModelInfo based on gemini-1.5-flash-latest pricing (per 1M tokens)\\n169 | \\t\\t// Removed 'id' and 'name' as they are not part of ModelInfo type directly\\n170 | \\t\\tconst mockInfo: ModelInfo = {\\n171 | \\t\\t\\tinputPrice: 0.125, // $/1M tokens\\n172 | \\t\\t\\toutputPrice: 0.375, // $/1M tokens\\n173 | \\t\\t\\tcacheWritesPrice: 0.125, // Assume same as input for test\\n----\\n179 | \\n180 | \\t\\tit(\\\"should calculate cost correctly based on input and output tokens\\\", () => {\\n181 | \\t\\t\\tconst inputTokens = 10000 // Use larger numbers for per-million pricing\\n----\\n190 | \\n191 | \\t\\tit(\\\"should return 0 if token counts are zero\\\", () => {\\n192 | \\t\\t\\t// Note: The method expects numbers, not undefined. Passing undefined would be a type error.\\n193 | \\t\\t\\t// The calculateCost method itself returns undefined if prices are missing, but 0 if tokens are 0 and prices exist.\\n194 | \\t\\t\\texpect(handler.calculateCost({ info: mockInfo, inputTokens: 0, outputTokens: 0 })).toBe(0)\\n----\\n196 | \\n197 | \\t\\tit(\\\"should handle only input tokens\\\", () => {\\n198 | \\t\\t\\tconst inputTokens = 5000\\n----\\n203 | \\n204 | \\t\\tit(\\\"should handle only output tokens\\\", () => {\\n205 | \\t\\t\\tconst outputTokens = 15000\\n----\\n210 | \\n211 | \\t\\tit(\\\"should calculate cost with cache write tokens\\\", () => {\\n212 | \\t\\t\\tconst inputTokens = 10000\\n----\\n227 | \\n228 | \\t\\tit(\\\"should calculate cost with cache read tokens\\\", () => {\\n229 | \\t\\t\\tconst inputTokens = 10000 // Total logical input\\n----\\n\\n# src/api/providers/__tests__/openrouter.spec.ts\\n132 | \\t\\t\\t\\t\\t\\tchoices: [{ delta: {} }],\\n133 | \\t\\t\\t\\t\\t\\tusage: { prompt_tokens: 10, completion_tokens: 20, cost: 0.001 },\\n134 | \\t\\t\\t\\t\\t}\\n----\\n162 | \\t\\t\\t\\texpect.objectContaining({\\n163 | \\t\\t\\t\\t\\tmax_tokens: 8192,\\n164 | \\t\\t\\t\\t\\tmessages: [\\n----\\n285 | \\t\\t\\t\\tmodel: mockOptions.openRouterModelId,\\n286 | \\t\\t\\t\\tmax_tokens: 8192,\\n287 | \\t\\t\\t\\tthinking: undefined,\\n----\\n\\n# src/api/providers/__tests__/ollama.spec.ts\\n 28 | \\t\\t\\t\\t\\t\\t\\t\\tusage: {\\n 29 | \\t\\t\\t\\t\\t\\t\\t\\t\\tprompt_tokens: 10,\\n 30 | \\t\\t\\t\\t\\t\\t\\t\\t\\tcompletion_tokens: 5,\\n 31 | \\t\\t\\t\\t\\t\\t\\t\\t\\ttotal_tokens: 15,\\n 32 | \\t\\t\\t\\t\\t\\t\\t\\t},\\n----\\n 54 | \\t\\t\\t\\t\\t\\t\\t\\t\\tusage: {\\n 55 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\tprompt_tokens: 10,\\n 56 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\tcompletion_tokens: 5,\\n 57 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\ttotal_tokens: 15,\\n 58 | \\t\\t\\t\\t\\t\\t\\t\\t\\t},\\n----\\n\\n# src/api/providers/__tests__/chutes.spec.ts\\n 49 | \\t\\t\\t\\t\\tusage: {\\n 50 | \\t\\t\\t\\t\\t\\tprompt_tokens: 10,\\n 51 | \\t\\t\\t\\t\\t\\tcompletion_tokens: 5,\\n 52 | \\t\\t\\t\\t\\t\\ttotal_tokens: 15,\\n 53 | \\t\\t\\t\\t\\t},\\n----\\n103 | \\t\\t\\t\\t\\t],\\n104 | \\t\\t\\t\\t\\tusage: { prompt_tokens: 10, completion_tokens: 5 },\\n105 | \\t\\t\\t\\t}\\n----\\n211 | \\t\\t\\t\\t\\t\\t\\tdone: false,\\n212 | \\t\\t\\t\\t\\t\\t\\tvalue: { choices: [{ delta: {} }], usage: { prompt_tokens: 10, completion_tokens: 20 } },\\n213 | \\t\\t\\t\\t\\t\\t})\\n----\\n284 | \\t\\t\\t\\tmodel: modelId,\\n285 | \\t\\t\\t\\tmax_tokens: modelInfo.maxTokens,\\n286 | \\t\\t\\t\\ttemperature: 0.5,\\n----\\n\\n# src/api/providers/openrouter.ts\\n 32 | \\tinclude_reasoning?: boolean\\n 33 | \\t// https://openrouter.ai/docs/use-cases/reasoning-tokens\\n 34 | \\treasoning?: OpenRouterReasoningParams\\n----\\n 40 | interface CompletionUsage {\\n 41 | \\tcompletion_tokens?: number\\n 42 | \\tcompletion_tokens_details?: {\\n 43 | \\t\\treasoning_tokens?: number\\n 44 | \\t}\\n 45 | \\tprompt_tokens?: number\\n 46 | \\tprompt_tokens_details?: {\\n 47 | \\t\\tcached_tokens?: number\\n 48 | \\t}\\n 49 | \\ttotal_tokens?: number\\n 50 | \\tcost?: number\\n----\\n 81 | \\n 82 | \\t\\t// OpenRouter sends reasoning tokens by default for Gemini 2.5 Pro\\n 83 | \\t\\t// Preview even if you don't request them. This is not the default for\\n----\\n116 | \\t\\t\\tmodel: modelId,\\n117 | \\t\\t\\t...(maxTokens && maxTokens > 0 && { max_tokens: maxTokens }),\\n118 | \\t\\t\\ttemperature,\\n----\\n165 | \\t\\t\\t\\ttype: \\\"usage\\\",\\n166 | \\t\\t\\t\\tinputTokens: lastUsage.prompt_tokens || 0,\\n167 | \\t\\t\\t\\toutputTokens: lastUsage.completion_tokens || 0,\\n168 | \\t\\t\\t\\t// Waiting on OpenRouter to figure out what this represents in the Gemini case\\n169 | \\t\\t\\t\\t// and how to best support it.\\n170 | \\t\\t\\t\\t// cacheReadTokens: lastUsage.prompt_tokens_details?.cached_tokens,\\n171 | \\t\\t\\t\\treasoningTokens: lastUsage.completion_tokens_details?.reasoning_tokens,\\n172 | \\t\\t\\t\\ttotalCost: (lastUsage.is_byok ? BYOK_COST_MULTIPLIER : 1) * (lastUsage.cost || 0),\\n----\\n219 | \\t\\t\\tmodel: modelId,\\n220 | \\t\\t\\tmax_tokens: maxTokens,\\n221 | \\t\\t\\ttemperature,\\n----\\n\\n# src/api/providers/__tests__/deepseek.test.ts\\n 28 | \\t\\t\\t\\t\\t\\t\\t\\tusage: {\\n 29 | \\t\\t\\t\\t\\t\\t\\t\\t\\tprompt_tokens: 10,\\n 30 | \\t\\t\\t\\t\\t\\t\\t\\t\\tcompletion_tokens: 5,\\n 31 | \\t\\t\\t\\t\\t\\t\\t\\t\\ttotal_tokens: 15,\\n 32 | \\t\\t\\t\\t\\t\\t\\t\\t\\tprompt_tokens_details: {\\n 33 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\tcache_miss_tokens: 8,\\n 34 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\tcached_tokens: 2,\\n 35 | \\t\\t\\t\\t\\t\\t\\t\\t\\t},\\n----\\n 59 | \\t\\t\\t\\t\\t\\t\\t\\t\\tusage: {\\n 60 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\tprompt_tokens: 10,\\n 61 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\tcompletion_tokens: 5,\\n 62 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\ttotal_tokens: 15,\\n 63 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\tprompt_tokens_details: {\\n 64 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\t\\tcache_miss_tokens: 8,\\n 65 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\t\\tcached_tokens: 2,\\n 66 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\t},\\n----\\n261 | \\t\\t\\tconst usage = {\\n262 | \\t\\t\\t\\tprompt_tokens: 100,\\n263 | \\t\\t\\t\\tcompletion_tokens: 50,\\n264 | \\t\\t\\t\\ttotal_tokens: 150,\\n265 | \\t\\t\\t\\tprompt_tokens_details: {\\n266 | \\t\\t\\t\\t\\tcache_miss_tokens: 80,\\n267 | \\t\\t\\t\\t\\tcached_tokens: 20,\\n268 | \\t\\t\\t\\t},\\n----\\n289 | \\t\\t\\tconst usage = {\\n290 | \\t\\t\\t\\tprompt_tokens: 100,\\n291 | \\t\\t\\t\\tcompletion_tokens: 50,\\n292 | \\t\\t\\t\\ttotal_tokens: 150,\\n293 | \\t\\t\\t\\t// No prompt_tokens_details\\n294 | \\t\\t\\t}\\n----\\n\\n# src/api/providers/__tests__/bedrock-reasoning.test.ts\\n 94 | \\t\\t\\t\\ttype: \\\"enabled\\\",\\n 95 | \\t\\t\\t\\tbudget_tokens: 4096, // Uses the full modelMaxThinkingTokens value\\n 96 | \\t\\t\\t})\\n----\\n143 | \\t\\t\\t\\ttype: \\\"enabled\\\",\\n144 | \\t\\t\\t\\tbudget_tokens: 8192,\\n145 | \\t\\t\\t})\\n----\\n266 | \\t\\t\\t\\ttype: \\\"enabled\\\",\\n267 | \\t\\t\\t\\tbudget_tokens: 4096,\\n268 | \\t\\t\\t})\\n----\\n\\n# src/api/providers/__tests__/xai.test.ts\\n228 | \\t\\t\\t\\t\\t\\t\\t\\tusage: {\\n229 | \\t\\t\\t\\t\\t\\t\\t\\t\\tprompt_tokens: 10,\\n230 | \\t\\t\\t\\t\\t\\t\\t\\t\\tcompletion_tokens: 20,\\n231 | \\t\\t\\t\\t\\t\\t\\t\\t\\tcache_read_input_tokens: 5,\\n232 | \\t\\t\\t\\t\\t\\t\\t\\t\\tcache_creation_input_tokens: 15,\\n233 | \\t\\t\\t\\t\\t\\t\\t\\t},\\n----\\n284 | \\t\\t\\t\\tmodel: modelId,\\n285 | \\t\\t\\t\\tmax_tokens: modelInfo.maxTokens,\\n286 | \\t\\t\\t\\ttemperature: 0,\\n----\\n\\n# src/api/providers/__tests__/anthropic.spec.ts\\n 19 | \\t\\t\\t\\t\\t\\tusage: {\\n 20 | \\t\\t\\t\\t\\t\\t\\tinput_tokens: 10,\\n 21 | \\t\\t\\t\\t\\t\\t\\toutput_tokens: 5,\\n 22 | \\t\\t\\t\\t\\t\\t},\\n----\\n 30 | \\t\\t\\t\\t\\t\\t\\t\\tusage: {\\n 31 | \\t\\t\\t\\t\\t\\t\\t\\t\\tinput_tokens: 100,\\n 32 | \\t\\t\\t\\t\\t\\t\\t\\t\\toutput_tokens: 50,\\n 33 | \\t\\t\\t\\t\\t\\t\\t\\t\\tcache_creation_input_tokens: 20,\\n 34 | \\t\\t\\t\\t\\t\\t\\t\\t\\tcache_read_input_tokens: 10,\\n 35 | \\t\\t\\t\\t\\t\\t\\t\\t},\\n----\\n105 | \\n106 | \\t\\tit(\\\"use apiKey for passing token if anthropicUseAuthToken is not set\\\", () => {\\n107 | \\t\\t\\tconst handlerWithCustomUrl = new AnthropicHandler({\\n----\\n112 | \\t\\t\\texpect(mockAnthropicConstructor.mock.calls[0]![0]!.apiKey).toEqual(\\\"test-api-key\\\")\\n113 | \\t\\t\\texpect(mockAnthropicConstructor.mock.calls[0]![0]!.authToken).toBeUndefined()\\n114 | \\t\\t})\\n115 | \\n116 | \\t\\tit(\\\"use apiKey for passing token if anthropicUseAuthToken is set but custom base URL is not given\\\", () => {\\n117 | \\t\\t\\tconst handlerWithCustomUrl = new AnthropicHandler({\\n----\\n123 | \\t\\t\\texpect(mockAnthropicConstructor.mock.calls[0]![0]!.apiKey).toEqual(\\\"test-api-key\\\")\\n124 | \\t\\t\\texpect(mockAnthropicConstructor.mock.calls[0]![0]!.authToken).toBeUndefined()\\n125 | \\t\\t})\\n126 | \\n127 | \\t\\tit(\\\"use authToken for passing token if both of anthropicBaseUrl and anthropicUseAuthToken are set\\\", () => {\\n128 | \\t\\t\\tconst customBaseUrl = \\\"https://custom.anthropic.com\\\"\\n----\\n135 | \\t\\t\\texpect(mockAnthropicConstructor).toHaveBeenCalledTimes(1)\\n136 | \\t\\t\\texpect(mockAnthropicConstructor.mock.calls[0]![0]!.authToken).toEqual(\\\"test-api-key\\\")\\n137 | \\t\\t\\texpect(mockAnthropicConstructor.mock.calls[0]![0]!.apiKey).toBeUndefined()\\n----\\n190 | \\t\\t\\t\\tmessages: [{ role: \\\"user\\\", content: \\\"Test prompt\\\" }],\\n191 | \\t\\t\\t\\tmax_tokens: 8192,\\n192 | \\t\\t\\t\\ttemperature: 0,\\n----\\n\\n# src/api/providers/__tests__/requesty.spec.ts\\n124 | \\t\\t\\t\\t\\t\\tusage: {\\n125 | \\t\\t\\t\\t\\t\\t\\tprompt_tokens: 10,\\n126 | \\t\\t\\t\\t\\t\\t\\tcompletion_tokens: 20,\\n127 | \\t\\t\\t\\t\\t\\t\\tprompt_tokens_details: {\\n128 | \\t\\t\\t\\t\\t\\t\\t\\tcaching_tokens: 5,\\n129 | \\t\\t\\t\\t\\t\\t\\t\\tcached_tokens: 2,\\n130 | \\t\\t\\t\\t\\t\\t\\t},\\n----\\n162 | \\t\\t\\t\\texpect.objectContaining({\\n163 | \\t\\t\\t\\t\\tmax_tokens: 8192,\\n164 | \\t\\t\\t\\t\\tmessages: [\\n----\\n204 | \\t\\t\\t\\tmodel: mockOptions.requestyModelId,\\n205 | \\t\\t\\t\\tmax_tokens: 8192,\\n206 | \\t\\t\\t\\tmessages: [{ role: \\\"system\\\", content: \\\"test prompt\\\" }],\\n----\\n\\n# src/api/providers/__tests__/openai-native.spec.ts\\n 29 | \\t\\t\\t\\t\\t\\t\\t\\tusage: {\\n 30 | \\t\\t\\t\\t\\t\\t\\t\\t\\tprompt_tokens: 10,\\n 31 | \\t\\t\\t\\t\\t\\t\\t\\t\\tcompletion_tokens: 5,\\n 32 | \\t\\t\\t\\t\\t\\t\\t\\t\\ttotal_tokens: 15,\\n 33 | \\t\\t\\t\\t\\t\\t\\t\\t},\\n----\\n 55 | \\t\\t\\t\\t\\t\\t\\t\\t\\tusage: {\\n 56 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\tprompt_tokens: 10,\\n 57 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\tcompletion_tokens: 5,\\n 58 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\ttotal_tokens: 15,\\n 59 | \\t\\t\\t\\t\\t\\t\\t\\t\\t},\\n----\\n145 | \\t\\t\\t\\t\\t\\tusage: {\\n146 | \\t\\t\\t\\t\\t\\t\\tprompt_tokens: 0,\\n147 | \\t\\t\\t\\t\\t\\t\\tcompletion_tokens: 0,\\n148 | \\t\\t\\t\\t\\t\\t\\ttotal_tokens: 0,\\n149 | \\t\\t\\t\\t\\t\\t},\\n----\\n215 | \\t\\t\\t\\t{ choices: [{ delta: { content: \\\" there\\\" } }], usage: null },\\n216 | \\t\\t\\t\\t{ choices: [{ delta: { content: \\\"!\\\" } }], usage: { prompt_tokens: 10, completion_tokens: 5 } },\\n217 | \\t\\t\\t]\\n----\\n261 | \\t\\t\\t\\t{ choices: [{ delta: { content: null } }], usage: null },\\n262 | \\t\\t\\t\\t{ choices: [{ delta: { content: \\\"Hello\\\" } }], usage: { prompt_tokens: 10, completion_tokens: 5 } },\\n263 | \\t\\t\\t]\\n----\\n\\n# src/api/providers/__tests__/anthropic-vertex.spec.ts\\n 21 | \\t\\t\\t\\t\\t\\tusage: {\\n 22 | \\t\\t\\t\\t\\t\\t\\tinput_tokens: 10,\\n 23 | \\t\\t\\t\\t\\t\\t\\toutput_tokens: 5,\\n 24 | \\t\\t\\t\\t\\t\\t},\\n----\\n 32 | \\t\\t\\t\\t\\t\\t\\t\\tusage: {\\n 33 | \\t\\t\\t\\t\\t\\t\\t\\t\\tinput_tokens: 10,\\n 34 | \\t\\t\\t\\t\\t\\t\\t\\t\\toutput_tokens: 5,\\n 35 | \\t\\t\\t\\t\\t\\t\\t\\t},\\n----\\n 95 | \\t\\t\\t\\t\\t\\tusage: {\\n 96 | \\t\\t\\t\\t\\t\\t\\tinput_tokens: 10,\\n 97 | \\t\\t\\t\\t\\t\\t\\toutput_tokens: 0,\\n 98 | \\t\\t\\t\\t\\t\\t},\\n----\\n118 | \\t\\t\\t\\t\\tusage: {\\n119 | \\t\\t\\t\\t\\t\\toutput_tokens: 5,\\n120 | \\t\\t\\t\\t\\t},\\n----\\n164 | \\t\\t\\t\\tmodel: \\\"claude-3-5-sonnet-v2@20241022\\\",\\n165 | \\t\\t\\t\\tmax_tokens: 8192,\\n166 | \\t\\t\\t\\ttemperature: 0,\\n----\\n284 | \\t\\t\\t\\t\\t\\tusage: {\\n285 | \\t\\t\\t\\t\\t\\t\\tinput_tokens: 10,\\n286 | \\t\\t\\t\\t\\t\\t\\toutput_tokens: 0,\\n287 | \\t\\t\\t\\t\\t\\t\\tcache_creation_input_tokens: 3,\\n288 | \\t\\t\\t\\t\\t\\t\\tcache_read_input_tokens: 2,\\n289 | \\t\\t\\t\\t\\t\\t},\\n----\\n309 | \\t\\t\\t\\t\\tusage: {\\n310 | \\t\\t\\t\\t\\t\\toutput_tokens: 5,\\n311 | \\t\\t\\t\\t\\t},\\n----\\n419 | \\t\\t\\t\\t\\t\\tusage: {\\n420 | \\t\\t\\t\\t\\t\\t\\tinput_tokens: 10,\\n421 | \\t\\t\\t\\t\\t\\t\\toutput_tokens: 0,\\n422 | \\t\\t\\t\\t\\t\\t\\tcache_creation_input_tokens: 5,\\n423 | \\t\\t\\t\\t\\t\\t\\tcache_read_input_tokens: 3,\\n424 | \\t\\t\\t\\t\\t\\t},\\n----\\n484 | \\t\\t\\t\\t\\t\\tusage: {\\n485 | \\t\\t\\t\\t\\t\\t\\tinput_tokens: 10,\\n486 | \\t\\t\\t\\t\\t\\t\\toutput_tokens: 0,\\n487 | \\t\\t\\t\\t\\t\\t},\\n----\\n618 | \\t\\t\\t\\tmodel: \\\"claude-3-5-sonnet-v2@20241022\\\",\\n619 | \\t\\t\\t\\tmax_tokens: 8192,\\n620 | \\t\\t\\t\\ttemperature: 0,\\n----\\n752 | \\n753 | \\t\\t\\t// Test with default thinking budget (80% of max tokens)\\n754 | \\t\\t\\tconst handlerWithDefaultBudget = new AnthropicVertexHandler({\\n----\\n767 | \\t\\t\\t\\tvertexRegion: \\\"us-central1\\\",\\n768 | \\t\\t\\t\\tmodelMaxTokens: 1000, // This would result in 800 tokens for thinking, but minimum is 1024\\n769 | \\t\\t\\t})\\n----\\n789 | \\t\\t\\t\\t\\t\\tmodel: options.model,\\n790 | \\t\\t\\t\\t\\t\\tusage: { input_tokens: 10, output_tokens: 5 },\\n791 | \\t\\t\\t\\t\\t}\\n----\\n794 | \\t\\t\\t\\t\\tasync *[Symbol.asyncIterator]() {\\n795 | \\t\\t\\t\\t\\t\\tyield { type: \\\"message_start\\\", message: { usage: { input_tokens: 10, output_tokens: 5 } } }\\n796 | \\t\\t\\t\\t\\t},\\n----\\n806 | \\t\\t\\t\\texpect.objectContaining({\\n807 | \\t\\t\\t\\t\\tthinking: { type: \\\"enabled\\\", budget_tokens: 4096 },\\n808 | \\t\\t\\t\\t\\ttemperature: 1.0, // Thinking requires temperature 1.0\\n----\\n\\n# src/api/providers/__tests__/lmstudio.test.ts\\n 25 | \\t\\t\\t\\t\\t\\t\\t\\tusage: {\\n 26 | \\t\\t\\t\\t\\t\\t\\t\\t\\tprompt_tokens: 10,\\n 27 | \\t\\t\\t\\t\\t\\t\\t\\t\\tcompletion_tokens: 5,\\n 28 | \\t\\t\\t\\t\\t\\t\\t\\t\\ttotal_tokens: 15,\\n 29 | \\t\\t\\t\\t\\t\\t\\t\\t},\\n----\\n 51 | \\t\\t\\t\\t\\t\\t\\t\\t\\tusage: {\\n 52 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\tprompt_tokens: 10,\\n 53 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\tcompletion_tokens: 5,\\n 54 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\ttotal_tokens: 15,\\n 55 | \\t\\t\\t\\t\\t\\t\\t\\t\\t},\\n----\\n\\n# src/api/providers/__tests__/glama.spec.ts\\n 57 | \\t\\t\\t\\t\\t\\t\\t\\t\\tchoices: [{ delta: {}, index: 0 }],\\n 58 | \\t\\t\\t\\t\\t\\t\\t\\t\\tusage: { prompt_tokens: 10, completion_tokens: 5, total_tokens: 15 },\\n 59 | \\t\\t\\t\\t\\t\\t\\t\\t}\\n----\\n112 | \\t\\t\\tusage: {\\n113 | \\t\\t\\t\\tprompt_tokens: 10,\\n114 | \\t\\t\\t\\tcompletion_tokens: 5,\\n115 | \\t\\t\\t\\ttotal_tokens: 15,\\n116 | \\t\\t\\t},\\n----\\n176 | \\t\\t\\t\\t\\ttemperature: 0,\\n177 | \\t\\t\\t\\t\\tmax_tokens: 8192,\\n178 | \\t\\t\\t\\t}),\\n----\\n194 | \\n195 | \\t\\tit(\\\"should not set max_tokens for non-Anthropic models\\\", async () => {\\n196 | \\t\\t\\t// Reset mock to clear any previous calls\\n----\\n213 | \\t\\t\\t)\\n214 | \\t\\t\\texpect(mockCreate.mock.calls[0][0]).not.toHaveProperty(\\\"max_tokens\\\")\\n215 | \\t\\t})\\n----\\n\\n# src/api/providers/__tests__/openai-usage-tracking.spec.ts\\n 28 | \\t\\t\\t\\t\\t\\t\\t\\tusage: {\\n 29 | \\t\\t\\t\\t\\t\\t\\t\\t\\tprompt_tokens: 10,\\n 30 | \\t\\t\\t\\t\\t\\t\\t\\t\\tcompletion_tokens: 5,\\n 31 | \\t\\t\\t\\t\\t\\t\\t\\t\\ttotal_tokens: 15,\\n 32 | \\t\\t\\t\\t\\t\\t\\t\\t},\\n----\\n 47 | \\t\\t\\t\\t\\t\\t\\t\\t\\tusage: {\\n 48 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\tprompt_tokens: 10,\\n 49 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\tcompletion_tokens: 2,\\n 50 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\ttotal_tokens: 12,\\n 51 | \\t\\t\\t\\t\\t\\t\\t\\t\\t},\\n----\\n 62 | \\t\\t\\t\\t\\t\\t\\t\\t\\tusage: {\\n 63 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\tprompt_tokens: 10,\\n 64 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\tcompletion_tokens: 4,\\n 65 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\ttotal_tokens: 14,\\n 66 | \\t\\t\\t\\t\\t\\t\\t\\t\\t},\\n----\\n 77 | \\t\\t\\t\\t\\t\\t\\t\\t\\tusage: {\\n 78 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\tprompt_tokens: 10,\\n 79 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\tcompletion_tokens: 5,\\n 80 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\ttotal_tokens: 15,\\n 81 | \\t\\t\\t\\t\\t\\t\\t\\t\\t},\\n----\\n155 | \\t\\t\\t\\t\\t\\tchoices: [{ message: { role: \\\"assistant\\\", content: \\\"Test response\\\" } }],\\n156 | \\t\\t\\t\\t\\t\\tusage: { prompt_tokens: 10, completion_tokens: 5, total_tokens: 15 },\\n157 | \\t\\t\\t\\t\\t}\\n----\\n177 | \\t\\t\\t\\t\\t\\t\\tusage: {\\n178 | \\t\\t\\t\\t\\t\\t\\t\\tprompt_tokens: 10,\\n179 | \\t\\t\\t\\t\\t\\t\\t\\tcompletion_tokens: 5,\\n180 | \\t\\t\\t\\t\\t\\t\\t\\ttotal_tokens: 15,\\n181 | \\t\\t\\t\\t\\t\\t\\t},\\n----\\n\\n# src/api/providers/__tests__/groq.spec.ts\\n101 | \\t\\t\\t\\t\\t\\t\\tdone: false,\\n102 | \\t\\t\\t\\t\\t\\t\\tvalue: { choices: [{ delta: {} }], usage: { prompt_tokens: 10, completion_tokens: 20 } },\\n103 | \\t\\t\\t\\t\\t\\t})\\n----\\n139 | \\t\\t\\t\\tmodel: modelId,\\n140 | \\t\\t\\t\\tmax_tokens: modelInfo.maxTokens,\\n141 | \\t\\t\\t\\ttemperature: 0.5,\\n----\\n\\n# src/api/providers/__tests__/unbound.spec.ts\\n 82 | \\t\\t\\t\\t\\t\\t\\t\\t\\tusage: {\\n 83 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\tprompt_tokens: 10,\\n 84 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\tcompletion_tokens: 5,\\n 85 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\ttotal_tokens: 15,\\n 86 | \\t\\t\\t\\t\\t\\t\\t\\t\\t},\\n----\\n 91 | \\t\\t\\t\\t\\t\\t\\t\\t\\tusage: {\\n 92 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\tprompt_tokens: 8,\\n 93 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\tcompletion_tokens: 4,\\n 94 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\ttotal_tokens: 12,\\n 95 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\tcache_creation_input_tokens: 3,\\n 96 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\tcache_read_input_tokens: 2,\\n 97 | \\t\\t\\t\\t\\t\\t\\t\\t\\t},\\n----\\n232 | \\t\\t\\t\\t\\ttemperature: 0,\\n233 | \\t\\t\\t\\t\\tmax_tokens: 8192,\\n234 | \\t\\t\\t\\t}),\\n----\\n253 | \\n254 | \\t\\tit(\\\"should not set max_tokens for non-Anthropic models\\\", async () => {\\n255 | \\t\\t\\tmockCreate.mockClear()\\n----\\n277 | \\n278 | \\t\\t\\texpect(mockCreate.mock.calls[0][0]).not.toHaveProperty(\\\"max_tokens\\\")\\n279 | \\t\\t})\\n----\\n\\n# src/api/providers/__tests__/bedrock-invokedModelId.spec.ts\\n176 | \\n177 | \\t\\t// The last usage event should have the token counts from the metadata\\n178 | \\t\\tconst lastUsageEvent = usageEvents[usageEvents.length - 1]\\n179 | \\t\\t// Expect the usage event to include all token information\\n180 | \\t\\texpect(lastUsageEvent).toMatchObject({\\n----\\n183 | \\t\\t\\toutputTokens: 200,\\n184 | \\t\\t\\t// Cache tokens may be present with default values\\n185 | \\t\\t\\tcacheReadTokens: expect.any(Number),\\n----\\n\\n# src/api/providers/requesty.ts\\n 19 | // Requesty usage includes an extra field for Anthropic use cases.\\n 20 | // Safely cast the prompt token details section to the appropriate structure.\\n 21 | interface RequestyUsage extends OpenAI.CompletionUsage {\\n 22 | \\tprompt_tokens_details?: {\\n 23 | \\t\\tcaching_tokens?: number\\n 24 | \\t\\tcached_tokens?: number\\n 25 | \\t}\\n----\\n 76 | \\t\\tconst requestyUsage = usage as RequestyUsage\\n 77 | \\t\\tconst inputTokens = requestyUsage?.prompt_tokens || 0\\n 78 | \\t\\tconst outputTokens = requestyUsage?.completion_tokens || 0\\n 79 | \\t\\tconst cacheWriteTokens = requestyUsage?.prompt_tokens_details?.caching_tokens || 0\\n 80 | \\t\\tconst cacheReadTokens = requestyUsage?.prompt_tokens_details?.cached_tokens || 0\\n 81 | \\t\\tconst totalCost = modelInfo\\n----\\n102 | \\t\\t\\tinfo,\\n103 | \\t\\t\\tmaxTokens: max_tokens,\\n104 | \\t\\t\\ttemperature,\\n----\\n116 | \\t\\t\\tmodel,\\n117 | \\t\\t\\tmax_tokens,\\n118 | \\t\\t\\ttemperature,\\n----\\n150 | \\tasync completePrompt(prompt: string): Promise {\\n151 | \\t\\tconst { id: model, maxTokens: max_tokens, temperature } = await this.fetchModel()\\n152 | \\n----\\n156 | \\t\\t\\tmodel,\\n157 | \\t\\t\\tmax_tokens,\\n158 | \\t\\t\\tmessages: openAiMessages,\\n----\\n\\n# src/api/providers/lite-llm.ts\\n 46 | \\n 47 | \\t\\t// Required by some providers; others default to max tokens allowed\\n 48 | \\t\\tlet maxTokens: number | undefined = info.maxTokens ?? undefined\\n----\\n 51 | \\t\\t\\tmodel: modelId,\\n 52 | \\t\\t\\tmax_tokens: maxTokens,\\n 53 | \\t\\t\\tmessages: openAiMessages,\\n----\\n 84 | \\t\\t\\t\\t\\ttype: \\\"usage\\\",\\n 85 | \\t\\t\\t\\t\\tinputTokens: lastUsage.prompt_tokens || 0,\\n 86 | \\t\\t\\t\\t\\toutputTokens: lastUsage.completion_tokens || 0,\\n 87 | \\t\\t\\t\\t\\tcacheWriteTokens: lastUsage.cache_creation_input_tokens || 0,\\n 88 | \\t\\t\\t\\t\\tcacheReadTokens: lastUsage.prompt_tokens_details?.cached_tokens || 0,\\n 89 | \\t\\t\\t\\t}\\n----\\n121 | \\n122 | \\t\\t\\trequestOptions.max_tokens = info.maxTokens\\n123 | \\n----\\n136 | interface LiteLLMUsage extends OpenAI.CompletionUsage {\\n137 | \\tcache_creation_input_tokens?: number\\n138 | }\\n----\\n\\n# src/api/providers/fetchers/unbound.ts\\n 34 | \\t\\t\\t\\t\\tcase modelId.startsWith(\\\"anthropic/\\\"):\\n 35 | \\t\\t\\t\\t\\t\\t// Set max tokens to 8192 for supported Anthropic models\\n 36 | \\t\\t\\t\\t\\t\\tif (modelInfo.maxTokens !== 4096) {\\n----\\n\\n# src/api/providers/deepseek.ts\\n 32 | \\t\\t\\ttype: \\\"usage\\\",\\n 33 | \\t\\t\\tinputTokens: usage?.prompt_tokens || 0,\\n 34 | \\t\\t\\toutputTokens: usage?.completion_tokens || 0,\\n 35 | \\t\\t\\tcacheWriteTokens: usage?.prompt_tokens_details?.cache_miss_tokens,\\n 36 | \\t\\t\\tcacheReadTokens: usage?.prompt_tokens_details?.cached_tokens,\\n 37 | \\t\\t}\\n----\\n\\n# src/api/providers/openai.ts\\n162 | \\t\\t\\tif (this.options.includeMaxTokens) {\\n163 | \\t\\t\\t\\trequestOptions.max_tokens = modelInfo.maxTokens\\n164 | \\t\\t\\t}\\n----\\n241 | \\t\\t\\ttype: \\\"usage\\\",\\n242 | \\t\\t\\tinputTokens: usage?.prompt_tokens || 0,\\n243 | \\t\\t\\toutputTokens: usage?.completion_tokens || 0,\\n244 | \\t\\t\\tcacheWriteTokens: usage?.cache_creation_input_tokens || undefined,\\n245 | \\t\\t\\tcacheReadTokens: usage?.cache_read_input_tokens || undefined,\\n246 | \\t\\t}\\n----\\n347 | \\t\\t\\t\\t\\ttype: \\\"usage\\\",\\n348 | \\t\\t\\t\\t\\tinputTokens: chunk.usage.prompt_tokens || 0,\\n349 | \\t\\t\\t\\t\\toutputTokens: chunk.usage.completion_tokens || 0,\\n350 | \\t\\t\\t\\t}\\n----\\n\\n# src/api/providers/fetchers/openrouter.ts\\n 21 | \\tmodality: z.string().nullish(),\\n 22 | \\ttokenizer: z.string().nullish(),\\n 23 | })\\n----\\n 35 | \\tcontext_length: z.number(),\\n 36 | \\tmax_completion_tokens: z.number().nullish(),\\n 37 | \\tpricing: openRouterPricingSchema.optional(),\\n----\\n 48 | \\tarchitecture: openRouterArchitectureSchema.optional(),\\n 49 | \\ttop_provider: z.object({ max_completion_tokens: z.number().nullish() }).optional(),\\n 50 | \\tsupported_parameters: z.array(z.string()).optional(),\\n----\\n115 | \\t\\t\\t\\tmodality: architecture?.modality,\\n116 | \\t\\t\\t\\tmaxTokens: top_provider?.max_completion_tokens,\\n117 | \\t\\t\\t\\tsupportedParameters: supported_parameters,\\n----\\n155 | \\t\\t\\t\\tmodality: architecture?.modality,\\n156 | \\t\\t\\t\\tmaxTokens: endpoint.max_completion_tokens,\\n157 | \\t\\t\\t})\\n----\\n\\n# src/api/providers/openai-native.ts\\n146 | \\tprivate async *yieldUsage(info: ModelInfo, usage: OpenAI.Completions.CompletionUsage | undefined): ApiStream {\\n147 | \\t\\tconst inputTokens = usage?.prompt_tokens || 0 // sum of cache hits and misses\\n148 | \\t\\tconst outputTokens = usage?.completion_tokens || 0\\n149 | \\t\\tconst cacheReadTokens = usage?.prompt_tokens_details?.cached_tokens || 0\\n150 | \\t\\tconst cacheWriteTokens = 0\\n----\\n\\n# src/api/providers/fetchers/requesty.ts\\n 31 | \\t\\t\\tconst modelInfo: ModelInfo = {\\n 32 | \\t\\t\\t\\tmaxTokens: rawModel.max_output_tokens,\\n 33 | \\t\\t\\t\\tcontextWindow: rawModel.context_window,\\n----\\n\\n# src/api/providers/anthropic.ts\\n 29 | \\t\\tconst apiKeyFieldName =\\n 30 | \\t\\t\\tthis.options.anthropicBaseUrl && this.options.anthropicUseAuthToken ? \\\"authToken\\\" : \\\"apiKey\\\"\\n 31 | \\n----\\n 75 | \\t\\t\\t\\t\\t\\tmodel: modelId,\\n 76 | \\t\\t\\t\\t\\t\\tmax_tokens: maxTokens ?? ANTHROPIC_DEFAULT_MAX_TOKENS,\\n 77 | \\t\\t\\t\\t\\t\\ttemperature,\\n----\\n124 | \\t\\t\\t\\t\\tmodel: modelId,\\n125 | \\t\\t\\t\\t\\tmax_tokens: maxTokens ?? ANTHROPIC_DEFAULT_MAX_TOKENS,\\n126 | \\t\\t\\t\\t\\ttemperature,\\n----\\n142 | \\t\\t\\t\\t\\t\\ttype: \\\"usage\\\",\\n143 | \\t\\t\\t\\t\\t\\tinputTokens: usage.input_tokens || 0,\\n144 | \\t\\t\\t\\t\\t\\toutputTokens: usage.output_tokens || 0,\\n145 | \\t\\t\\t\\t\\t\\tcacheWriteTokens: usage.cache_creation_input_tokens || undefined,\\n146 | \\t\\t\\t\\t\\t\\tcacheReadTokens: usage.cache_read_input_tokens || undefined,\\n147 | \\t\\t\\t\\t\\t}\\n----\\n151 | \\t\\t\\t\\tcase \\\"message_delta\\\":\\n152 | \\t\\t\\t\\t\\t// Tells us stop_reason, stop_sequence, and output tokens\\n153 | \\t\\t\\t\\t\\t// along the way and at the end of the message.\\n----\\n156 | \\t\\t\\t\\t\\t\\tinputTokens: 0,\\n157 | \\t\\t\\t\\t\\t\\toutputTokens: chunk.usage.output_tokens || 0,\\n158 | \\t\\t\\t\\t\\t}\\n----\\n231 | \\t\\t\\tmodel,\\n232 | \\t\\t\\tmax_tokens: ANTHROPIC_DEFAULT_MAX_TOKENS,\\n233 | \\t\\t\\tthinking: undefined,\\n----\\n243 | \\t/**\\n244 | \\t * Counts tokens for the given content using Anthropic's API\\n245 | \\t *\\n246 | \\t * @param content The content blocks to count tokens for\\n247 | \\t * @returns A promise resolving to the token count\\n248 | \\t */\\n----\\n258 | \\n259 | \\t\\t\\treturn response.input_tokens\\n260 | \\t\\t} catch (error) {\\n261 | \\t\\t\\t// Log error but fallback to tiktoken estimation\\n262 | \\t\\t\\tconsole.warn(\\\"Anthropic token counting failed, using fallback\\\", error)\\n263 | \\n----\\n\\n# src/api/providers/base-openai-compatible-provider.ts\\n 71 | \\t\\t\\tid: model,\\n 72 | \\t\\t\\tinfo: { maxTokens: max_tokens },\\n 73 | \\t\\t} = this.getModel()\\n----\\n 78 | \\t\\t\\tmodel,\\n 79 | \\t\\t\\tmax_tokens,\\n 80 | \\t\\t\\ttemperature,\\n----\\n100 | \\t\\t\\t\\t\\ttype: \\\"usage\\\",\\n101 | \\t\\t\\t\\t\\tinputTokens: chunk.usage.prompt_tokens || 0,\\n102 | \\t\\t\\t\\t\\toutputTokens: chunk.usage.completion_tokens || 0,\\n103 | \\t\\t\\t\\t}\\n----\\n\\n# src/api/providers/xai.ts\\n 52 | \\t\\t\\tmodel: modelId,\\n 53 | \\t\\t\\tmax_tokens: modelInfo.maxTokens,\\n 54 | \\t\\t\\ttemperature: this.options.modelTemperature ?? XAI_DEFAULT_TEMPERATURE,\\n----\\n 78 | \\t\\t\\tif (chunk.usage) {\\n 79 | \\t\\t\\t\\t// Extract detailed token information if available\\n 80 | \\t\\t\\t\\t// First check for prompt_tokens_details structure (real API response)\\n 81 | \\t\\t\\t\\tconst promptDetails = \\\"prompt_tokens_details\\\" in chunk.usage ? chunk.usage.prompt_tokens_details : null;\\n 82 | \\t\\t\\t\\tconst cachedTokens = promptDetails && \\\"cached_tokens\\\" in promptDetails ? promptDetails.cached_tokens : 0;\\n 83 | \\n 84 | \\t\\t\\t\\t// Fall back to direct fields in usage (used in test mocks)\\n 85 | \\t\\t\\t\\tconst readTokens = cachedTokens || (\\\"cache_read_input_tokens\\\" in chunk.usage ? (chunk.usage as any).cache_read_input_tokens : 0);\\n 86 | \\t\\t\\t\\tconst writeTokens = \\\"cache_creation_input_tokens\\\" in chunk.usage ? (chunk.usage as any).cache_creation_input_tokens : 0;\\n 87 | \\n----\\n 89 | \\t\\t\\t\\t\\ttype: \\\"usage\\\",\\n 90 | \\t\\t\\t\\t\\tinputTokens: chunk.usage.prompt_tokens || 0,\\n 91 | \\t\\t\\t\\t\\toutputTokens: chunk.usage.completion_tokens || 0,\\n 92 | \\t\\t\\t\\t\\tcacheReadTokens: readTokens,\\n----\\n\\n# src/api/providers/bedrock.ts\\n 52 | \\t\\ttype: \\\"enabled\\\"\\n 53 | \\t\\tbudget_tokens: number\\n 54 | \\t}\\n----\\n109 | \\tmessageStop?: {\\n110 | \\t\\tstopReason?: \\\"end_turn\\\" | \\\"tool_use\\\" | \\\"max_tokens\\\" | \\\"stop_sequence\\\"\\n111 | \\t\\tadditionalModelResponseFields?: Record\\n----\\n349 | \\t\\t\\t\\t\\ttype: \\\"enabled\\\",\\n350 | \\t\\t\\t\\t\\tbudget_tokens: metadata?.thinking?.maxThinkingTokens || modelConfig.reasoningBudget || 4096,\\n351 | \\t\\t\\t\\t},\\n----\\n418 | \\n419 | \\t\\t\\t\\t\\t// Check both field naming conventions for cache tokens\\n420 | \\t\\t\\t\\t\\tconst cacheReadTokens = usage.cacheReadInputTokens || usage.cacheReadInputTokenCount || 0\\n----\\n422 | \\n423 | \\t\\t\\t\\t\\t// Always include all available token information\\n424 | \\t\\t\\t\\t\\tyield {\\n----\\n450 | \\n451 | \\t\\t\\t\\t\\t\\t\\t// Check both field naming conventions for cache tokens\\n452 | \\t\\t\\t\\t\\t\\t\\tconst cacheReadTokens =\\n----\\n1049 | \\t\\tTOO_MANY_TOKENS: {\\n1050 | \\t\\t\\tpatterns: [\\\"too many tokens\\\"],\\n1051 | \\t\\t\\tmessageTemplate: `\\\"Too many tokens\\\" error detected.\\n1052 | Possible Causes:\\n1053 | 1. Input exceeds model's context window limit\\n1054 | 2. Rate limiting (too many tokens per minute)\\n1055 | 3. Quota exceeded for token usage\\n1056 | 4. Other token-related service limitations\\n1057 | \\n----\\n\\n# src/api/providers/fetchers/litellm.ts\\n 52 | \\t\\t\\t\\tmodels[modelName] = {\\n 53 | \\t\\t\\t\\t\\tmaxTokens: modelInfo.max_tokens || 8192,\\n 54 | \\t\\t\\t\\t\\tcontextWindow: modelInfo.max_input_tokens || 200000,\\n 55 | \\t\\t\\t\\t\\tsupportsImages: Boolean(modelInfo.supports_vision),\\n----\\n 58 | \\t\\t\\t\\t\\tsupportsPromptCache: Boolean(modelInfo.supports_prompt_caching),\\n 59 | \\t\\t\\t\\t\\tinputPrice: modelInfo.input_cost_per_token ? modelInfo.input_cost_per_token * 1000000 : undefined,\\n 60 | \\t\\t\\t\\t\\toutputPrice: modelInfo.output_cost_per_token\\n 61 | \\t\\t\\t\\t\\t\\t? modelInfo.output_cost_per_token * 1000000\\n 62 | \\t\\t\\t\\t\\t\\t: undefined,\\n 63 | \\t\\t\\t\\t\\tcacheWritesPrice: modelInfo.cache_creation_input_token_cost\\n 64 | \\t\\t\\t\\t\\t\\t? modelInfo.cache_creation_input_token_cost * 1000000\\n 65 | \\t\\t\\t\\t\\t\\t: undefined,\\n 66 | \\t\\t\\t\\t\\tcacheReadsPrice: modelInfo.cache_read_input_token_cost\\n 67 | \\t\\t\\t\\t\\t\\t? modelInfo.cache_read_input_token_cost * 1000000\\n 68 | \\t\\t\\t\\t\\t\\t: undefined,\\n----\\n\\n# src/api/providers/fetchers/__tests__/openrouter.spec.ts\\n139 | \\t\\t\\t\\tsupportsReasoningEffort: false,\\n140 | \\t\\t\\t\\tsupportedParameters: [\\\"max_tokens\\\", \\\"temperature\\\", \\\"reasoning\\\", \\\"include_reasoning\\\"],\\n141 | \\t\\t\\t})\\n----\\n156 | \\t\\t\\t\\tsupportsReasoningEffort: true,\\n157 | \\t\\t\\t\\tsupportedParameters: [\\\"max_tokens\\\", \\\"temperature\\\", \\\"reasoning\\\", \\\"include_reasoning\\\"],\\n158 | \\t\\t\\t})\\n----\\n\\n# src/api/providers/ollama.ts\\n 79 | \\t\\t\\t\\ttype: \\\"usage\\\",\\n 80 | \\t\\t\\t\\tinputTokens: lastUsage?.prompt_tokens || 0,\\n 81 | \\t\\t\\t\\toutputTokens: lastUsage?.completion_tokens || 0,\\n 82 | \\t\\t\\t}\\n----\\n\\n# src/api/providers/base-provider.ts\\n 21 | \\t/**\\n 22 | \\t * Default token counting implementation using tiktoken.\\n 23 | \\t * Providers can override this to use their native token counting endpoints.\\n 24 | \\t *\\n 25 | \\t * @param content The content to count tokens for\\n 26 | \\t * @returns A promise resolving to the token count\\n 27 | \\t */\\n----\\n\\n# src/api/providers/fetchers/__tests__/litellm.test.ts\\n 41 | \\t\\t\\t\\t\\t\\tmodel_info: {\\n 42 | \\t\\t\\t\\t\\t\\t\\tmax_tokens: 4096,\\n 43 | \\t\\t\\t\\t\\t\\t\\tmax_input_tokens: 200000,\\n 44 | \\t\\t\\t\\t\\t\\t\\tsupports_vision: true,\\n 45 | \\t\\t\\t\\t\\t\\t\\tsupports_prompt_caching: false,\\n 46 | \\t\\t\\t\\t\\t\\t\\tinput_cost_per_token: 0.000003,\\n 47 | \\t\\t\\t\\t\\t\\t\\toutput_cost_per_token: 0.000015,\\n 48 | \\t\\t\\t\\t\\t\\t\\tsupports_computer_use: true,\\n----\\n 56 | \\t\\t\\t\\t\\t\\tmodel_info: {\\n 57 | \\t\\t\\t\\t\\t\\t\\tmax_tokens: 8192,\\n 58 | \\t\\t\\t\\t\\t\\t\\tmax_input_tokens: 128000,\\n 59 | \\t\\t\\t\\t\\t\\t\\tsupports_vision: false,\\n 60 | \\t\\t\\t\\t\\t\\t\\tsupports_prompt_caching: false,\\n 61 | \\t\\t\\t\\t\\t\\t\\tinput_cost_per_token: 0.00001,\\n 62 | \\t\\t\\t\\t\\t\\t\\toutput_cost_per_token: 0.00003,\\n 63 | \\t\\t\\t\\t\\t\\t\\tsupports_computer_use: false,\\n----\\n108 | \\n109 | \\tit(\\\"makes request without authorization header when no API key provided\\\", async () => {\\n110 | \\t\\tconst mockResponse = {\\n----\\n134 | \\t\\t\\t\\t\\t\\tmodel_info: {\\n135 | \\t\\t\\t\\t\\t\\t\\tmax_tokens: 4096,\\n136 | \\t\\t\\t\\t\\t\\t\\tmax_input_tokens: 200000,\\n137 | \\t\\t\\t\\t\\t\\t\\tsupports_vision: true,\\n----\\n146 | \\t\\t\\t\\t\\t\\tmodel_info: {\\n147 | \\t\\t\\t\\t\\t\\t\\tmax_tokens: 4096,\\n148 | \\t\\t\\t\\t\\t\\t\\tmax_input_tokens: 200000,\\n149 | \\t\\t\\t\\t\\t\\t\\tsupports_vision: false,\\n----\\n205 | \\t\\t\\t\\tstatus: 401,\\n206 | \\t\\t\\t\\tstatusText: \\\"Unauthorized\\\",\\n207 | \\t\\t\\t},\\n----\\n214 | \\t\\tawait expect(getLiteLLMModels(DUMMY_INVALID_KEY, \\\"http://localhost:4000\\\")).rejects.toThrow(\\n215 | \\t\\t\\t\\\"Failed to fetch LiteLLM models: 401 Unauthorized. Check base URL and API key.\\\",\\n216 | \\t\\t)\\n----\\n278 | \\t\\t\\t\\t\\t\\tmodel_info: {\\n279 | \\t\\t\\t\\t\\t\\t\\tmax_tokens: 4096,\\n280 | \\t\\t\\t\\t\\t\\t\\tmax_input_tokens: 200000,\\n281 | \\t\\t\\t\\t\\t\\t\\tsupports_vision: true,\\n----\\n291 | \\t\\t\\t\\t\\t\\tmodel_info: {\\n292 | \\t\\t\\t\\t\\t\\t\\tmax_tokens: 8192,\\n293 | \\t\\t\\t\\t\\t\\t\\tmax_input_tokens: 128000,\\n294 | \\t\\t\\t\\t\\t\\t\\tsupports_vision: false,\\n----\\n339 | \\t\\t\\t\\t\\t\\tmodel_info: {\\n340 | \\t\\t\\t\\t\\t\\t\\tmax_tokens: 4096,\\n341 | \\t\\t\\t\\t\\t\\t\\tmax_input_tokens: 200000,\\n342 | \\t\\t\\t\\t\\t\\t\\tsupports_vision: true,\\n----\\n352 | \\t\\t\\t\\t\\t\\tmodel_info: {\\n353 | \\t\\t\\t\\t\\t\\t\\tmax_tokens: 8192,\\n354 | \\t\\t\\t\\t\\t\\t\\tmax_input_tokens: 128000,\\n355 | \\t\\t\\t\\t\\t\\t\\tsupports_vision: false,\\n----\\n365 | \\t\\t\\t\\t\\t\\tmodel_info: {\\n366 | \\t\\t\\t\\t\\t\\t\\tmax_tokens: 8192,\\n367 | \\t\\t\\t\\t\\t\\t\\tmax_input_tokens: 128000,\\n368 | \\t\\t\\t\\t\\t\\t\\tsupports_vision: false,\\n----\\n424 | \\t\\t\\t\\t\\t\\tmodel_info: {\\n425 | \\t\\t\\t\\t\\t\\t\\tmax_tokens: 4096,\\n426 | \\t\\t\\t\\t\\t\\t\\tmax_input_tokens: 200000,\\n427 | \\t\\t\\t\\t\\t\\t\\tsupports_vision: true,\\n----\\n436 | \\t\\t\\t\\t\\t\\tmodel_info: {\\n437 | \\t\\t\\t\\t\\t\\t\\tmax_tokens: 4096,\\n438 | \\t\\t\\t\\t\\t\\t\\tmax_input_tokens: 200000,\\n439 | \\t\\t\\t\\t\\t\\t\\tsupports_vision: true,\\n----\\n448 | \\t\\t\\t\\t\\t\\tmodel_info: {\\n449 | \\t\\t\\t\\t\\t\\t\\tmax_tokens: 4096,\\n450 | \\t\\t\\t\\t\\t\\t\\tmax_input_tokens: 200000,\\n451 | \\t\\t\\t\\t\\t\\t\\tsupports_vision: true,\\n----\\n\\n# src/api/providers/anthropic-vertex.ts\\n 2 | import { AnthropicVertex } from \\\"@anthropic-ai/vertex-sdk\\\"\\n 3 | import { GoogleAuth, JWTInput } from \\\"google-auth-library\\\"\\n 4 | \\n----\\n 41 | \\t\\t\\t\\tgoogleAuth: new GoogleAuth({\\n 42 | \\t\\t\\t\\t\\tscopes: [\\\"https://www.googleapis.com/auth/cloud-platform\\\"],\\n 43 | \\t\\t\\t\\t\\tcredentials: safeJsonParse(this.options.vertexJsonCredentials, undefined),\\n----\\n 50 | \\t\\t\\t\\tgoogleAuth: new GoogleAuth({\\n 51 | \\t\\t\\t\\t\\tscopes: [\\\"https://www.googleapis.com/auth/cloud-platform\\\"],\\n 52 | \\t\\t\\t\\t\\tkeyFile: this.options.vertexKeyFile,\\n----\\n 87 | \\t\\t\\tmodel: id,\\n 88 | \\t\\t\\tmax_tokens: maxTokens ?? ANTHROPIC_DEFAULT_MAX_TOKENS,\\n 89 | \\t\\t\\ttemperature,\\n----\\n107 | \\t\\t\\t\\t\\t\\ttype: \\\"usage\\\",\\n108 | \\t\\t\\t\\t\\t\\tinputTokens: usage.input_tokens || 0,\\n109 | \\t\\t\\t\\t\\t\\toutputTokens: usage.output_tokens || 0,\\n110 | \\t\\t\\t\\t\\t\\tcacheWriteTokens: usage.cache_creation_input_tokens || undefined,\\n111 | \\t\\t\\t\\t\\t\\tcacheReadTokens: usage.cache_read_input_tokens || undefined,\\n112 | \\t\\t\\t\\t\\t}\\n----\\n119 | \\t\\t\\t\\t\\t\\tinputTokens: 0,\\n120 | \\t\\t\\t\\t\\t\\toutputTokens: chunk.usage!.output_tokens || 0,\\n121 | \\t\\t\\t\\t\\t}\\n----\\n189 | \\t\\t\\t\\tmodel: id,\\n190 | \\t\\t\\t\\tmax_tokens: maxTokens,\\n191 | \\t\\t\\t\\ttemperature,\\n----\\n\\n# src/services/mcp/__tests__/McpHub.test.ts\\n 50 | \\t\\t\\tscheme: \\\"file\\\",\\n 51 | \\t\\t\\tauthority: \\\"\\\",\\n 52 | \\t\\t\\tpath: \\\"/test/path\\\",\\n----\\n\\n# src/services/code-index/embedders/openai.ts\\n 52 | \\t\\t\\t\\t\\tconsole.warn(\\n 53 | \\t\\t\\t\\t\\t\\t`Text at index ${i} exceeds maximum token limit (${itemTokens} > ${MAX_ITEM_TOKENS}). Skipping.`,\\n 54 | \\t\\t\\t\\t\\t)\\n----\\n108 | \\t\\t\\t\\t\\tusage: {\\n109 | \\t\\t\\t\\t\\t\\tpromptTokens: response.usage?.prompt_tokens || 0,\\n110 | \\t\\t\\t\\t\\t\\ttotalTokens: response.usage?.total_tokens || 0,\\n111 | \\t\\t\\t\\t\\t},\\n----\\n\\n# src/services/code-index/embedders/openai-compatible.ts\\n 18 | \\tusage?: {\\n 19 | \\t\\tprompt_tokens?: number\\n 20 | \\t\\ttotal_tokens?: number\\n 21 | \\t}\\n----\\n 34 | \\t * @param baseUrl The base URL for the OpenAI-compatible API endpoint\\n 35 | \\t * @param apiKey The API key for authentication\\n 36 | \\t * @param modelId Optional model identifier (defaults to \\\"text-embedding-3-small\\\")\\n----\\n 75 | \\t\\t\\t\\t\\tconsole.warn(\\n 76 | \\t\\t\\t\\t\\t\\t`Text at index ${i} exceeds maximum token limit (${itemTokens} > ${MAX_ITEM_TOKENS}). Skipping.`,\\n 77 | \\t\\t\\t\\t\\t)\\n----\\n156 | \\t\\t\\t\\t\\tusage: {\\n157 | \\t\\t\\t\\t\\t\\tpromptTokens: response.usage?.prompt_tokens || 0,\\n158 | \\t\\t\\t\\t\\t\\ttotalTokens: response.usage?.total_tokens || 0,\\n159 | \\t\\t\\t\\t\\t},\\n----\\n\\n# src/services/marketplace/types.ts\\n 20 | \\tdescription: string\\n 21 | \\tauthor?: string\\n 22 | \\tauthorUrl?: string\\n 23 | \\ttags?: string[]\\n----\\n 45 | \\tdescription: string\\n 46 | \\tauthor?: string\\n 47 | \\tauthorUrl?: string\\n 48 | \\turl: string // Required url field\\n----\\n 62 | \\ttype: MarketplaceItemType\\n 63 | \\tauthor?: string\\n 64 | \\tauthorUrl?: string\\n 65 | \\turl?: string // Optional - only MCPs have url\\n----\\n\\n# src/services/code-index/embedders/__tests__/openai-compatible.spec.ts\\n103 | \\t\\t\\t\\tdata: [{ embedding: [0.1, 0.2, 0.3] }],\\n104 | \\t\\t\\t\\tusage: { prompt_tokens: 10, total_tokens: 15 },\\n105 | \\t\\t\\t}\\n----\\n124 | \\t\\t\\t\\tdata: [{ embedding: [0.1, 0.2, 0.3] }, { embedding: [0.4, 0.5, 0.6] }],\\n125 | \\t\\t\\t\\tusage: { prompt_tokens: 20, total_tokens: 30 },\\n126 | \\t\\t\\t}\\n----\\n149 | \\t\\t\\t\\tdata: [{ embedding: [0.1, 0.2, 0.3] }],\\n150 | \\t\\t\\t\\tusage: { prompt_tokens: 10, total_tokens: 15 },\\n151 | \\t\\t\\t}\\n----\\n194 | \\t\\t\\t\\t\\tdata: [{ embedding: base64String }], // Base64 string instead of array\\n195 | \\t\\t\\t\\t\\tusage: { prompt_tokens: 10, total_tokens: 15 },\\n196 | \\t\\t\\t\\t}\\n----\\n226 | \\t\\t\\t\\t\\tdata: [{ embedding: base64String1 }, { embedding: base64String2 }],\\n227 | \\t\\t\\t\\t\\tusage: { prompt_tokens: 20, total_tokens: 30 },\\n228 | \\t\\t\\t\\t}\\n----\\n253 | \\t\\t\\t\\t\\t],\\n254 | \\t\\t\\t\\t\\tusage: { prompt_tokens: 20, total_tokens: 30 },\\n255 | \\t\\t\\t\\t}\\n----\\n270 | \\t\\t/**\\n271 | \\t\\t * Test batching logic when texts exceed token limits\\n272 | \\t\\t */\\n----\\n283 | \\t\\t\\t\\t\\t],\\n284 | \\t\\t\\t\\t\\tusage: { prompt_tokens: 10, total_tokens: 15 },\\n285 | \\t\\t\\t\\t})\\n----\\n299 | \\t\\t\\t\\t\\tdata: [{ embedding: [0.1, 0.2, 0.3] }, { embedding: [0.4, 0.5, 0.6] }],\\n300 | \\t\\t\\t\\t\\tusage: { prompt_tokens: 10, total_tokens: 15 },\\n301 | \\t\\t\\t\\t}\\n----\\n306 | \\t\\t\\t\\t// Should warn about oversized text\\n307 | \\t\\t\\t\\texpect(console.warn).toHaveBeenCalledWith(expect.stringContaining(\\\"exceeds maximum token limit\\\"))\\n308 | \\n----\\n317 | \\t\\t\\t\\t\\tdata: [{ embedding: [0.1, 0.2, 0.3] }, { embedding: [0.4, 0.5, 0.6] }],\\n318 | \\t\\t\\t\\t\\tusage: { prompt_tokens: 10, total_tokens: 15 },\\n319 | \\t\\t\\t\\t})\\n----\\n354 | \\t\\t\\t\\t\\t\\tdata: [{ embedding: base64String }],\\n355 | \\t\\t\\t\\t\\t\\tusage: { prompt_tokens: 10, total_tokens: 15 },\\n356 | \\t\\t\\t\\t\\t})\\n----\\n375 | \\t\\t\\t\\tconst testTexts = [\\\"Hello world\\\"]\\n376 | \\t\\t\\t\\tconst authError = new Error(\\\"Unauthorized\\\")\\n377 | \\t\\t\\t\\t;(authError as any).status = 401\\n378 | \\n379 | \\t\\t\\t\\tmockEmbeddingsCreate.mockRejectedValue(authError)\\n380 | \\n----\\n452 | \\t\\t\\t\\t\\tdata: null,\\n453 | \\t\\t\\t\\t\\tusage: { prompt_tokens: 10, total_tokens: 15 },\\n454 | \\t\\t\\t\\t}\\n----\\n494 | \\t\\t\\t\\t\\tusage: {\\n495 | \\t\\t\\t\\t\\t\\tprompt_tokens: 2,\\n496 | \\t\\t\\t\\t\\t\\ttotal_tokens: 2,\\n497 | \\t\\t\\t\\t\\t},\\n----\\n\\n# src/services/marketplace/schemas.ts\\n 35 | \\ttype: marketplaceItemTypeSchema,\\n 36 | \\tauthor: z.string().optional(),\\n 37 | \\tauthorUrl: z.string().url(\\\"Author URL must be a valid URL\\\").optional(),\\n 38 | \\ttags: z.array(z.string()).optional(),\\n----\\n 61 | \\tdescription: z.string(),\\n 62 | \\tauthor: z.string().optional(),\\n 63 | \\tauthorUrl: z.string().url().optional(),\\n 64 | \\ttags: z.array(z.string()).optional(),\\n----\\n 72 | \\tdescription: z.string(),\\n 73 | \\tauthor: z.string().optional(),\\n 74 | \\tauthorUrl: z.string().url().optional(),\\n 75 | \\turl: z.string().url(), // Required url field\\n----\\n\\n# src/services/tree-sitter/__tests__/fixtures/sample-embedded_template.ts\\n 35 |
\\n 36 | <% if user.authenticated? %>\\n 37 |

Welcome, <%= user.name %>

\\n----\\n\\n# src/services/tree-sitter/__tests__/parseSourceCodeDefinitions.embedded_template.test.ts\\n 43 | \\tit(\\\"should detect control structures\\\", () => {\\n 44 | \\t\\texpect(parseResult).toMatch(/\\\\d+--\\\\d+ \\\\|\\\\s+<% if user\\\\.authenticated\\\\? %>/)\\n 45 | \\t\\texpect(parseResult).toMatch(/\\\\d+--\\\\d+ \\\\|\\\\s+<% user\\\\.posts\\\\.each do \\\\|post\\\\| %>/)\\n----\\n\\n# src/workers/countTokens.ts\\n 4 | \\n 5 | import { tiktoken } from \\\"../utils/tiktoken\\\"\\n 6 | \\n----\\n 10 | \\ttry {\\n 11 | \\t\\tconst count = await tiktoken(content)\\n 12 | \\t\\treturn { success: true, count }\\n----\\n\\n# src/activate/handleUri.ts\\n 37 | \\t\\t}\\n 38 | \\t\\tcase \\\"/auth/clerk/callback\\\": {\\n 39 | \\t\\t\\tconst code = query.get(\\\"code\\\")\\n----\\n\\n# src/__tests__/dist_assets.test.ts\\n 6 | \\n 7 | \\tdescribe(\\\"tiktoken\\\", () => {\\n 8 | \\t\\tit(\\\"should have tiktoken wasm file\\\", () => {\\n 9 | \\t\\t\\texpect(fs.existsSync(path.join(distPath, \\\"tiktoken_bg.wasm\\\"))).toBe(true)\\n 10 | \\t\\t})\\n----\\n\\n# src/integrations/terminal/ShellIntegrationManager.ts\\n 43 | \\t[ -f \\\"$ZDOTDIR/.zshrc\\\" ] && source \\\"$ZDOTDIR/.zshrc\\\"\\n 44 | \\t[ -f \\\"$ZDOTDIR/.zlogin\\\" ] && source \\\"$ZDOTDIR/.zlogin\\\"\\n 45 | \\t[ \\\"$ZDOTDIR\\\" = \\\"$HOME\\\" ] && unset ZDOTDIR\\n----\\n\\n# src/extension/api.ts\\n219 | \\n220 | \\t\\t\\tcline.on(\\\"taskCompleted\\\", async (_, tokenUsage, toolUsage) => {\\n221 | \\t\\t\\t\\tthis.emit(RooCodeEventName.TaskCompleted, cline.taskId, tokenUsage, toolUsage)\\n222 | \\t\\t\\t\\tthis.taskMap.delete(cline.taskId)\\n----\\n224 | \\t\\t\\t\\tawait this.fileLog(\\n225 | \\t\\t\\t\\t\\t`[${new Date().toISOString()}] taskCompleted -> ${cline.taskId} | ${JSON.stringify(tokenUsage, null, 2)} | ${JSON.stringify(toolUsage, null, 2)}\\\\n`,\\n226 | \\t\\t\\t\\t)\\n----\\n\\n# src/utils/countTokens.ts\\n 4 | import { countTokensResultSchema } from \\\"../workers/types\\\"\\n 5 | import { tiktoken } from \\\"./tiktoken\\\"\\n 6 | \\n----\\n 27 | \\tif (!useWorker || !pool) {\\n 28 | \\t\\treturn tiktoken(content)\\n 29 | \\t}\\n----\\n 42 | \\t\\tconsole.error(error)\\n 43 | \\t\\treturn tiktoken(content)\\n 44 | \\t}\\n----\\n\\n# src/core/checkpoints/index.ts\\n215 | \\t\\t\\t\\tJSON.stringify({\\n216 | \\t\\t\\t\\t\\ttokensIn: totalTokensIn,\\n217 | \\t\\t\\t\\t\\ttokensOut: totalTokensOut,\\n218 | \\t\\t\\t\\t\\tcacheWrites: totalCacheWrites,\\n----\\n\\n# src/core/condense/index.ts\\n 55 | \\tcost: number // The cost of the summarization operation\\n 56 | \\tnewContextTokens?: number // The number of tokens in the context for the next API request\\n 57 | \\terror?: string // Populated iff the operation fails: error message shown to the user on failure (see Task.ts)\\n----\\n 63 | * @param {ApiMessage[]} messages - The conversation messages\\n 64 | * @param {ApiHandler} apiHandler - The API handler to use for token counting.\\n 65 | * @param {string} systemPrompt - The system prompt for API requests, which should be considered in the context token count\\n 66 | * @param {string} taskId - The task ID for the conversation, used for telemetry\\n----\\n 73 | * @param {ApiMessage[]} messages - The conversation messages\\n 74 | * @param {ApiHandler} apiHandler - The API handler to use for token counting (fallback if condensingApiHandler not provided)\\n 75 | * @param {string} systemPrompt - The system prompt for API requests (fallback if customCondensingPrompt not provided)\\n 76 | * @param {string} taskId - The task ID for the conversation, used for telemetry\\n 77 | * @param {number} prevContextTokens - The number of tokens currently in the context, used to ensure we don't grow the context\\n 78 | * @param {boolean} isAutomaticTrigger - Whether the summarization is triggered automatically\\n----\\n186 | \\n187 | \\t// Count the tokens in the context for the next API request\\n188 | \\t// We only estimate the tokens in summaryMesage if outputTokens is 0, otherwise we use outputTokens\\n189 | \\tconst systemPromptMessage: ApiMessage = { role: \\\"user\\\", content: systemPrompt }\\n----\"}", - "partial": false -} -[2025-06-13T04:59:46.001Z] { - "ts": 1749790785975, - "type": "say", - "say": "api_req_started", - "text": "{\"request\":\"[search_files for 'auth|oauth|token|login' in '*.ts'] Result:\\n\\nShowing first 300 of 300+ results. Use a more specific search if necessary.\\n\\n# src/services/ripgrep/index.ts\\n101 | \\t\\tconst rgProcess = childProcess.spawn(bin, args)\\n102 | \\t\\t// cross-platform alternative to head, which is ripgrep author's recommendation for limiting output.\\n103 | \\t\\tconst rl = readline.createInterface({\\n----\\n\\n# src/api/transform/model-params.ts\\n 95 | \\n 96 | \\t\\t// Reasoning cannot be less than 1024 tokens.\\n 97 | \\t\\tif (reasoningBudget < 1024) {\\n----\\n\\n# src/api/transform/__tests__/reasoning.spec.ts\\n 40 | \\n 41 | \\t\\t\\texpect(result).toEqual({ max_tokens: 1000 })\\n 42 | \\t\\t})\\n----\\n 61 | \\n 62 | \\t\\t\\texpect(result).toEqual({ max_tokens: 1000 })\\n 63 | \\t\\t})\\n----\\n123 | \\n124 | \\t\\t\\texpect(result).toEqual({ max_tokens: 1000 })\\n125 | \\t\\t})\\n----\\n140 | \\n141 | \\t\\t\\texpect(result).toEqual({ max_tokens: undefined })\\n142 | \\t\\t})\\n----\\n189 | \\n190 | \\t\\t\\texpect(result).toEqual({ max_tokens: 0 })\\n191 | \\t\\t})\\n----\\n244 | \\t\\t\\t\\ttype: \\\"enabled\\\",\\n245 | \\t\\t\\t\\tbudget_tokens: 1000,\\n246 | \\t\\t\\t})\\n----\\n268 | \\t\\t\\t\\ttype: \\\"enabled\\\",\\n269 | \\t\\t\\t\\tbudget_tokens: 1000,\\n270 | \\t\\t\\t})\\n----\\n314 | \\t\\t\\t\\ttype: \\\"enabled\\\",\\n315 | \\t\\t\\t\\tbudget_tokens: undefined,\\n316 | \\t\\t\\t})\\n----\\n334 | \\t\\t\\t\\ttype: \\\"enabled\\\",\\n335 | \\t\\t\\t\\tbudget_tokens: 0,\\n336 | \\t\\t\\t})\\n----\\n354 | \\t\\t\\t\\ttype: \\\"enabled\\\",\\n355 | \\t\\t\\t\\tbudget_tokens: 100000,\\n356 | \\t\\t\\t})\\n----\\n374 | \\t\\t\\t\\ttype: \\\"enabled\\\",\\n375 | \\t\\t\\t\\tbudget_tokens: 1000,\\n376 | \\t\\t\\t})\\n----\\n547 | \\n548 | \\t\\t\\texpect(openRouterResult).toEqual({ max_tokens: 1000 })\\n549 | \\t\\t\\texpect(anthropicResult).toEqual({ type: \\\"enabled\\\", budget_tokens: 1000 })\\n550 | \\t\\t\\texpect(openAiResult).toBeUndefined()\\n----\\n601 | \\t\\t\\t// Budget should take precedence for OpenRouter and Anthropic\\n602 | \\t\\t\\texpect(openRouterResult).toEqual({ max_tokens: 1000 })\\n603 | \\t\\t\\texpect(anthropicResult).toEqual({ type: \\\"enabled\\\", budget_tokens: 1000 })\\n604 | \\t\\t\\t// OpenAI should still use effort since it doesn't support budget\\n----\\n672 | \\t\\t\\t\\texpect(typeof result).toBe(\\\"object\\\")\\n673 | \\t\\t\\t\\texpect(\\\"max_tokens\\\" in result || \\\"effort\\\" in result || \\\"exclude\\\" in result).toBe(true)\\n674 | \\t\\t\\t}\\n----\\n688 | \\t\\t\\t\\texpect(result).toHaveProperty(\\\"type\\\", \\\"enabled\\\")\\n689 | \\t\\t\\t\\texpect(result).toHaveProperty(\\\"budget_tokens\\\")\\n690 | \\t\\t\\t}\\n----\\n\\n# src/api/transform/__tests__/stream.spec.ts\\n 33 | \\n 34 | \\t\\tit(\\\"should handle usage chunks without cache tokens\\\", () => {\\n 35 | \\t\\t\\tconst usageChunk: ApiStreamChunk = {\\n----\\n 57 | \\n 58 | \\t\\tit(\\\"should handle usage chunks with zero tokens\\\", () => {\\n 59 | \\t\\t\\tconst zeroUsageChunk: ApiStreamChunk = {\\n----\\n 69 | \\n 70 | \\t\\tit(\\\"should handle usage chunks with large token counts\\\", () => {\\n 71 | \\t\\t\\tconst largeUsageChunk: ApiStreamChunk = {\\n----\\n\\n# src/api/transform/__tests__/model-params.spec.ts\\n205 | \\t\\t\\t\\t\\ttype: \\\"enabled\\\",\\n206 | \\t\\t\\t\\t\\tbudget_tokens: 1600,\\n207 | \\t\\t\\t\\t},\\n----\\n226 | \\t\\t\\t\\ttype: \\\"enabled\\\",\\n227 | \\t\\t\\t\\tbudget_tokens: 1600,\\n228 | \\t\\t\\t})\\n----\\n262 | \\t\\t\\t\\t\\ttype: \\\"enabled\\\",\\n263 | \\t\\t\\t\\t\\tbudget_tokens: 2400,\\n264 | \\t\\t\\t\\t},\\n----\\n287 | \\t\\t\\t\\t\\ttype: \\\"enabled\\\",\\n288 | \\t\\t\\t\\t\\tbudget_tokens: 1500,\\n289 | \\t\\t\\t\\t},\\n----\\n308 | \\n309 | \\t\\tit(\\\"should clamp thinking budget to at least 1024 tokens\\\", () => {\\n310 | \\t\\t\\tconst model: ModelInfo = {\\n----\\n328 | \\t\\t\\t\\t\\ttype: \\\"enabled\\\",\\n329 | \\t\\t\\t\\t\\tbudget_tokens: 1024,\\n330 | \\t\\t\\t\\t},\\n----\\n333 | \\n334 | \\t\\tit(\\\"should clamp thinking budget to at most 80% of max tokens\\\", () => {\\n335 | \\t\\t\\tconst model: ModelInfo = {\\n----\\n353 | \\t\\t\\t\\t\\ttype: \\\"enabled\\\",\\n354 | \\t\\t\\t\\t\\tbudget_tokens: 3200,\\n355 | \\t\\t\\t\\t},\\n----\\n372 | \\t\\t\\t\\t\\ttype: \\\"enabled\\\",\\n373 | \\t\\t\\t\\t\\tbudget_tokens: DEFAULT_HYBRID_REASONING_MODEL_THINKING_TOKENS,\\n374 | \\t\\t\\t\\t},\\n----\\n390 | \\t\\t\\texpect(result.maxTokens).toBe(5000)\\n391 | \\t\\t\\texpect(result.reasoningBudget).toBe(2000) // Custom thinking tokens takes precedence\\n392 | \\t\\t})\\n393 | \\n394 | \\t\\tit(\\\"should clamp custom thinking tokens even when custom max tokens is provided\\\", () => {\\n395 | \\t\\t\\tconst model: ModelInfo = {\\n----\\n719 | \\n720 | \\t\\t\\texpect(result.reasoning).toEqual({ max_tokens: 3200 })\\n721 | \\t\\t})\\n----\\n\\n# src/api/transform/cache-strategy/__tests__/cache-strategy.spec.ts\\n 29 | \\n 30 | const createMessageWithTokens = (role: \\\"user\\\" | \\\"assistant\\\", tokenCount: number) => ({\\n 31 | \\trole,\\n 32 | \\tcontent: \\\"x\\\".repeat(tokenCount * 4), // Approximate 4 chars per token\\n 33 | })\\n----\\n130 | \\t\\t\\t\\tit(\\\"adds system cache block when prompt caching is enabled, messages exist, and system prompt is long enough\\\", () => {\\n131 | \\t\\t\\t\\t\\t// Create a system prompt that's at least 50 tokens (200+ characters)\\n132 | \\t\\t\\t\\t\\tconst longSystemPrompt =\\n----\\n236 | \\t\\t\\t\\t\\t\\t\\t\\t(i + 1) +\\n237 | \\t\\t\\t\\t\\t\\t\\t\\t\\\" with some additional text to increase token count. \\\" +\\n238 | \\t\\t\\t\\t\\t\\t\\t\\t\\\"Adding more text to ensure we exceed the token threshold for cache block insertion.\\\",\\n239 | \\t\\t\\t\\t\\t\\t}))\\n----\\n579 | \\n580 | \\t\\tit(\\\"should yield usage results with cache tokens when using MultiPointStrategy\\\", async () => {\\n581 | \\t\\t\\t// Mock the convertToBedrockConverseMessages method to return a result with cache points\\n----\\n589 | \\n590 | \\t\\t\\t// Create a mock stream that includes cache token fields\\n591 | \\t\\t\\tconst mockApiResponse = {\\n----\\n626 | \\n627 | \\t\\t\\t// Verify that usage results with cache tokens are yielded\\n628 | \\t\\t\\texpect(chunks.length).toBeGreaterThan(0)\\n629 | \\t\\t\\t// The test already expects cache tokens, but the implementation might not be including them\\n630 | \\t\\t\\t// Let's make the test more flexible to accept either format\\n----\\n652 | \\n653 | \\t\\t// Helper function to create a message with approximate token count\\n654 | \\t\\tconst createMessage = (role: \\\"user\\\" | \\\"assistant\\\", content: string, tokenCount: number) => {\\n655 | \\t\\t\\t// Pad the content to reach the desired token count (approx 4 chars per token)\\n656 | \\t\\t\\tconst paddingNeeded = Math.max(0, tokenCount * 4 - content.length)\\n657 | \\t\\t\\tconst padding = \\\" \\\".repeat(paddingNeeded)\\n----\\n667 | \\t\\t\\t\\t\\\"Cache point placements:\\\",\\n668 | \\t\\t\\t\\tplacements.map((p) => `index: ${p.index}, tokens: ${p.tokensCovered}`),\\n669 | \\t\\t\\t)\\n----\\n683 | \\t\\t\\t\\t\\tmodelInfo: multiPointModelInfo,\\n684 | \\t\\t\\t\\t\\tsystemPrompt: \\\"You are a helpful assistant.\\\", // ~10 tokens\\n685 | \\t\\t\\t\\t\\tmessages,\\n----\\n730 | \\t\\t\\t\\t\\t\\ttype: \\\"message\\\",\\n731 | \\t\\t\\t\\t\\t\\ttokensCovered: 300,\\n732 | \\t\\t\\t\\t\\t},\\n----\\n736 | \\t\\t\\t\\t\\tmodelInfo: multiPointModelInfo,\\n737 | \\t\\t\\t\\t\\tsystemPrompt: \\\"You are a helpful assistant.\\\", // ~10 tokens\\n738 | \\t\\t\\t\\t\\tmessages,\\n----\\n759 | \\n760 | \\t\\t\\t\\t// Check if we have a second cache point (may not always be added depending on token distribution)\\n761 | \\t\\t\\t\\tif (result.messageCachePointPlacements && result.messageCachePointPlacements.length > 1) {\\n----\\n789 | \\t\\t\\t\\t\\t\\ttype: \\\"message\\\",\\n790 | \\t\\t\\t\\t\\t\\ttokensCovered: 300,\\n791 | \\t\\t\\t\\t\\t},\\n----\\n794 | \\t\\t\\t\\t\\t\\ttype: \\\"message\\\",\\n795 | \\t\\t\\t\\t\\t\\ttokensCovered: 300,\\n796 | \\t\\t\\t\\t\\t},\\n----\\n800 | \\t\\t\\t\\t\\tmodelInfo: multiPointModelInfo,\\n801 | \\t\\t\\t\\t\\tsystemPrompt: \\\"You are a helpful assistant.\\\", // ~10 tokens\\n802 | \\t\\t\\t\\t\\tmessages,\\n----\\n863 | \\t\\t\\t\\t\\t\\ttype: \\\"message\\\",\\n864 | \\t\\t\\t\\t\\t\\ttokensCovered: 300,\\n865 | \\t\\t\\t\\t\\t},\\n----\\n868 | \\t\\t\\t\\t\\t\\ttype: \\\"message\\\",\\n869 | \\t\\t\\t\\t\\t\\ttokensCovered: 300,\\n870 | \\t\\t\\t\\t\\t},\\n----\\n873 | \\t\\t\\t\\t\\t\\ttype: \\\"message\\\",\\n874 | \\t\\t\\t\\t\\t\\ttokensCovered: 300,\\n875 | \\t\\t\\t\\t\\t},\\n----\\n879 | \\t\\t\\t\\t\\tmodelInfo: multiPointModelInfo,\\n880 | \\t\\t\\t\\t\\tsystemPrompt: \\\"You are a helpful assistant.\\\", // ~10 tokens\\n881 | \\t\\t\\t\\t\\tmessages,\\n----\\n929 | \\t\\t\\t\\t// This test verifies that the documentation for Example 5 is correct\\n930 | \\t\\t\\t\\t// In Example 5, the third cache point at index 10 should cover 660 tokens\\n931 | \\t\\t\\t\\t// (260 tokens from messages 7-8 plus 400 tokens from the new messages)\\n932 | \\n----\\n944 | \\t\\t\\t\\t\\tcreateMessage(\\\"assistant\\\", \\\"Deep learning has many applications including...\\\", 160),\\n945 | \\t\\t\\t\\t\\t// New messages with 400 tokens total\\n946 | \\t\\t\\t\\t\\tcreateMessage(\\\"user\\\", \\\"Can you provide a detailed example?\\\", 100),\\n----\\n954 | \\t\\t\\t\\t\\t\\ttype: \\\"message\\\",\\n955 | \\t\\t\\t\\t\\t\\ttokensCovered: 240,\\n956 | \\t\\t\\t\\t\\t},\\n----\\n959 | \\t\\t\\t\\t\\t\\ttype: \\\"message\\\",\\n960 | \\t\\t\\t\\t\\t\\ttokensCovered: 440,\\n961 | \\t\\t\\t\\t\\t},\\n----\\n964 | \\t\\t\\t\\t\\t\\ttype: \\\"message\\\",\\n965 | \\t\\t\\t\\t\\t\\ttokensCovered: 260,\\n966 | \\t\\t\\t\\t\\t},\\n----\\n969 | \\t\\t\\t\\t// In the documentation, the algorithm decides to replace the cache point at index 8\\n970 | \\t\\t\\t\\t// with a new one at index 10, and the tokensCovered value should be 660 tokens\\n971 | \\t\\t\\t\\t// (260 tokens from messages 7-8 plus 400 tokens from the new messages)\\n972 | \\n973 | \\t\\t\\t\\t// However, the actual implementation may behave differently depending on how\\n974 | \\t\\t\\t\\t// it calculates token counts and makes decisions about cache point placement\\n975 | \\n976 | \\t\\t\\t\\t// The important part is that our fix ensures that when a cache point is created,\\n977 | \\t\\t\\t\\t// the tokensCovered value represents all tokens from the previous cache point\\n978 | \\t\\t\\t\\t// to the current cache point, not just the tokens in the new messages\\n979 | \\t\\t\\t})\\n980 | \\n981 | \\t\\t\\tit(\\\"should not combine cache points when new messages have fewer tokens than the smallest combined gap\\\", () => {\\n982 | \\t\\t\\t\\t// This test verifies that when new messages have fewer tokens than the smallest combined gap,\\n983 | \\t\\t\\t\\t// the algorithm keeps all existing cache points and doesn't add a new one\\n----\\n1004 | \\t\\t\\t\\t\\t\\t),\\n1005 | \\t\\t\\t\\t\\t\\t// Small addition (only 50 tokens total)\\n1006 | \\t\\t\\t\\t\\t\\tcreateMessage(\\\"user\\\", \\\"Thanks for the explanation.\\\", 20),\\n----\\n1009 | \\n1010 | \\t\\t\\t\\t\\t// Previous cache point placements with significant token coverage\\n1011 | \\t\\t\\t\\t\\tconst previousCachePointPlacements: CachePointPlacement[] = [\\n----\\n1014 | \\t\\t\\t\\t\\t\\t\\ttype: \\\"message\\\",\\n1015 | \\t\\t\\t\\t\\t\\t\\ttokensCovered: 400, // Significant token coverage\\n1016 | \\t\\t\\t\\t\\t\\t},\\n----\\n1019 | \\t\\t\\t\\t\\t\\t\\ttype: \\\"message\\\",\\n1020 | \\t\\t\\t\\t\\t\\t\\ttokensCovered: 300, // Significant token coverage\\n1021 | \\t\\t\\t\\t\\t\\t},\\n----\\n1024 | \\t\\t\\t\\t\\t\\t\\ttype: \\\"message\\\",\\n1025 | \\t\\t\\t\\t\\t\\t\\ttokensCovered: 300, // Significant token coverage\\n1026 | \\t\\t\\t\\t\\t\\t},\\n----\\n1030 | \\t\\t\\t\\t\\t\\tmodelInfo: multiPointModelInfo,\\n1031 | \\t\\t\\t\\t\\t\\tsystemPrompt: \\\"You are a helpful assistant.\\\", // ~10 tokens\\n1032 | \\t\\t\\t\\t\\t\\tmessages,\\n----\\n1057 | \\n1058 | \\t\\t\\tit(\\\"should make correct decisions based on token counts\\\", () => {\\n1059 | \\t\\t\\t\\t// This test verifies that the algorithm correctly compares token counts\\n1060 | \\t\\t\\t\\t// and makes the right decision about combining cache points\\n1061 | \\n1062 | \\t\\t\\t\\t// Create messages with a variety of token counts\\n1063 | \\t\\t\\t\\tconst messages = [\\n----\\n1081 | \\t\\t\\t\\t\\t\\ttype: \\\"message\\\",\\n1082 | \\t\\t\\t\\t\\t\\ttokensCovered: 400,\\n1083 | \\t\\t\\t\\t\\t},\\n----\\n1086 | \\t\\t\\t\\t\\t\\ttype: \\\"message\\\",\\n1087 | \\t\\t\\t\\t\\t\\ttokensCovered: 150,\\n1088 | \\t\\t\\t\\t\\t},\\n----\\n1091 | \\t\\t\\t\\t\\t\\ttype: \\\"message\\\",\\n1092 | \\t\\t\\t\\t\\t\\ttokensCovered: 150,\\n1093 | \\t\\t\\t\\t\\t},\\n----\\n\\n# src/api/transform/cache-strategy/types.ts\\n 7 | export interface ModelInfo {\\n 8 | \\t/** Maximum number of tokens the model can generate */\\n 9 | \\tmaxTokens: number\\n 10 | \\t/** Maximum context window size in tokens */\\n 11 | \\tcontextWindow: number\\n----\\n 15 | \\tmaxCachePoints: number\\n 16 | \\t/** Minimum number of tokens required for a cache point */\\n 17 | \\tminTokensPerCachePoint: number\\n----\\n 49 | \\ttype: \\\"system\\\" | \\\"message\\\"\\n 50 | \\t/** Number of tokens this cache point covers */\\n 51 | \\ttokensCovered: number\\n 52 | }\\n----\\n\\n# src/api/transform/cache-strategy/base-strategy.ts\\n 27 | \\t/**\\n 28 | \\t * Calculate token count for system prompt using a more accurate approach\\n 29 | \\t */\\n----\\n 33 | \\n 34 | \\t\\t\\t// Use a more accurate token estimation than simple character count\\n 35 | \\t\\t\\t// Count words and add overhead for punctuation and special tokens\\n 36 | \\t\\t\\tconst words = text.split(/\\\\s+/).filter((word) => word.length > 0)\\n 37 | \\t\\t\\t// Average English word is ~1.3 tokens\\n 38 | \\t\\t\\tlet tokenCount = words.length * 1.3\\n 39 | \\t\\t\\t// Add overhead for punctuation and special characters\\n 40 | \\t\\t\\ttokenCount += (text.match(/[.,!?;:()[\\\\]{}\\\"\\\"''`]/g) || []).length * 0.3\\n 41 | \\t\\t\\t// Add overhead for newlines\\n 42 | \\t\\t\\ttokenCount += (text.match(/\\\\n/g) || []).length * 0.5\\n 43 | \\t\\t\\t// Add a small overhead for system prompt structure\\n 44 | \\t\\t\\ttokenCount += 5\\n 45 | \\n 46 | \\t\\t\\tthis.systemTokenCount = Math.ceil(tokenCount)\\n 47 | \\t\\t}\\n----\\n 84 | \\t/**\\n 85 | \\t * Check if a token count meets the minimum threshold for caching\\n 86 | \\t */\\n 87 | \\tprotected meetsMinTokenThreshold(tokenCount: number): boolean {\\n 88 | \\t\\tconst minTokens = this.config.modelInfo.minTokensPerCachePoint\\n----\\n 91 | \\t\\t}\\n 92 | \\t\\treturn tokenCount >= minTokens\\n 93 | \\t}\\n----\\n 95 | \\t/**\\n 96 | \\t * Estimate token count for a message using a more accurate approach\\n 97 | \\t * This implementation is based on the BaseProvider's countTokens method\\n----\\n100 | \\tprotected estimateTokenCount(message: Anthropic.Messages.MessageParam): number {\\n101 | \\t\\t// Use a more sophisticated token counting approach\\n102 | \\t\\tif (!message.content) return 0\\n----\\n108 | \\t\\t\\t\\tif (block.type === \\\"text\\\") {\\n109 | \\t\\t\\t\\t\\t// Use a more accurate token estimation than simple character count\\n110 | \\t\\t\\t\\t\\t// This is still an approximation but better than character/4\\n----\\n112 | \\t\\t\\t\\t\\tif (text.length > 0) {\\n113 | \\t\\t\\t\\t\\t\\t// Count words and add overhead for punctuation and special tokens\\n114 | \\t\\t\\t\\t\\t\\tconst words = text.split(/\\\\s+/).filter((word) => word.length > 0)\\n115 | \\t\\t\\t\\t\\t\\t// Average English word is ~1.3 tokens\\n116 | \\t\\t\\t\\t\\t\\ttotalTokens += words.length * 1.3\\n----\\n128 | \\t\\t\\tconst text = message.content\\n129 | \\t\\t\\t// Count words and add overhead for punctuation and special tokens\\n130 | \\t\\t\\tconst words = text.split(/\\\\s+/).filter((word) => word.length > 0)\\n131 | \\t\\t\\t// Average English word is ~1.3 tokens\\n132 | \\t\\t\\ttotalTokens += words.length * 1.3\\n----\\n\\n# src/api/transform/reasoning.ts\\n 12 | \\teffort?: ReasoningEffort\\n 13 | \\tmax_tokens?: number\\n 14 | \\texclude?: boolean\\n----\\n 36 | \\tshouldUseReasoningBudget({ model, settings })\\n 37 | \\t\\t? { max_tokens: reasoningBudget }\\n 38 | \\t\\t: shouldUseReasoningEffort({ model, settings })\\n----\\n 46 | }: GetModelReasoningOptions): AnthropicReasoningParams | undefined =>\\n 47 | \\tshouldUseReasoningBudget({ model, settings }) ? { type: \\\"enabled\\\", budget_tokens: reasoningBudget! } : undefined\\n 48 | \\n----\\n\\n# src/api/transform/cache-strategy/multi-point-strategy.ts\\n 7 | * Strategy for handling multiple cache points.\\n 8 | * Creates cache points after messages as soon as uncached tokens exceed minimumTokenCount.\\n 9 | */\\n----\\n 58 | \\t *\\n 59 | \\t * @param minTokensPerPoint Minimum tokens required per cache point\\n 60 | \\t * @param remainingCachePoints Number of cache points available\\n----\\n 97 | \\n 98 | \\t\\t// Calculate tokens in new messages (added since last cache point placement)\\n 99 | \\t\\tconst lastPreviousIndex = previousPlacements[previousPlacements.length - 1].index\\n----\\n103 | \\n104 | \\t\\t// If new messages have enough tokens for a cache point, we need to decide\\n105 | \\t\\t// whether to keep all previous cache points or combine some\\n----\\n127 | \\t\\t\\t\\t// We need to decide which previous cache points to keep and which to combine\\n128 | \\t\\t\\t\\t// Strategy: Compare the token count of new messages with the smallest combined token gap\\n129 | \\n130 | \\t\\t\\t\\t// First, analyze the token distribution between previous cache points\\n131 | \\t\\t\\t\\tconst tokensBetweenPlacements: number[] = []\\n132 | \\t\\t\\t\\tlet startIdx = 0\\n----\\n134 | \\t\\t\\t\\tfor (const placement of previousPlacements) {\\n135 | \\t\\t\\t\\t\\tconst tokens = this.config.messages\\n136 | \\t\\t\\t\\t\\t\\t.slice(startIdx, placement.index + 1)\\n----\\n138 | \\n139 | \\t\\t\\t\\t\\ttokensBetweenPlacements.push(tokens)\\n140 | \\t\\t\\t\\t\\tstartIdx = placement.index + 1\\n----\\n142 | \\n143 | \\t\\t\\t\\t// Find the two consecutive placements with the smallest token gap\\n144 | \\t\\t\\t\\tlet smallestGapIndex = 0\\n----\\n146 | \\n147 | \\t\\t\\t\\tfor (let i = 0; i < tokensBetweenPlacements.length - 1; i++) {\\n148 | \\t\\t\\t\\t\\tconst gap = tokensBetweenPlacements[i] + tokensBetweenPlacements[i + 1]\\n149 | \\t\\t\\t\\t\\tif (gap < smallestGap) {\\n----\\n155 | \\t\\t\\t\\t// Only combine cache points if it's beneficial\\n156 | \\t\\t\\t\\t// Compare the token count of new messages with the smallest combined token gap\\n157 | \\t\\t\\t\\t// Apply a required percentage increase to ensure reallocation is worth it\\n----\\n161 | \\t\\t\\t\\tif (newMessagesTokens >= requiredTokenThreshold) {\\n162 | \\t\\t\\t\\t\\t// It's beneficial to combine cache points since new messages have significantly more tokens\\n163 | \\t\\t\\t\\t\\tlogger.info(\\\"Combining cache points is beneficial\\\", {\\n----\\n231 | \\t\\t} else {\\n232 | \\t\\t\\t// New messages don't have enough tokens for a cache point\\n233 | \\t\\t\\t// Keep all previous placements that are still valid\\n----\\n266 | \\t\\tif (lastUserMessageIndex >= 0) {\\n267 | \\t\\t\\t// Calculate the total tokens covered from the previous cache point (or start of conversation)\\n268 | \\t\\t\\t// to this cache point. This ensures tokensCovered represents the full span of tokens\\n269 | \\t\\t\\t// that will be cached by this cache point.\\n----\\n281 | \\n282 | \\t\\t\\t// Calculate tokens from previous cache point (or start) to this cache point\\n283 | \\t\\t\\tconst tokenStartIndex = previousCachePointIndex + 1\\n284 | \\t\\t\\ttotalTokensCovered = this.config.messages\\n285 | \\t\\t\\t\\t.slice(tokenStartIndex, lastUserMessageIndex + 1)\\n286 | \\t\\t\\t\\t.reduce((acc, curr) => acc + this.estimateTokenCount(curr), 0)\\n287 | \\n288 | \\t\\t\\t// Guard clause: ensure we have enough tokens to justify a cache point\\n289 | \\t\\t\\tif (totalTokensCovered < minTokensPerPoint) {\\n----\\n294 | \\t\\t\\t\\ttype: \\\"message\\\",\\n295 | \\t\\t\\t\\ttokensCovered: totalTokensCovered,\\n296 | \\t\\t\\t}\\n----\\n\\n# src/api/index.ts\\n 50 | \\t/**\\n 51 | \\t * Counts tokens for content blocks\\n 52 | \\t * All providers extend BaseProvider which provides a default tiktoken implementation,\\n 53 | \\t * but they can override this to use their native token counting endpoints\\n 54 | \\t *\\n 55 | \\t * @param content The content to count tokens for\\n 56 | \\t * @returns A promise resolving to the token count\\n 57 | \\t */\\n----\\n\\n# src/api/providers/gemini.ts\\n 7 | } from \\\"@google/genai\\\"\\n 8 | import type { JWTInput } from \\\"google-auth-library\\\"\\n 9 | \\n----\\n179 | \\t\\t\\tif (response.totalTokens === undefined) {\\n180 | \\t\\t\\t\\tconsole.warn(\\\"Gemini token counting returned undefined, using fallback\\\")\\n181 | \\t\\t\\t\\treturn super.countTokens(content)\\n----\\n185 | \\t\\t} catch (error) {\\n186 | \\t\\t\\tconsole.warn(\\\"Gemini token counting failed, using fallback\\\", error)\\n187 | \\t\\t\\treturn super.countTokens(content)\\n----\\n209 | \\n210 | \\t\\t// If there's tiered pricing then adjust the input and output token prices\\n211 | \\t\\t// based on the input tokens used.\\n212 | \\t\\tif (info.tiers) {\\n----\\n221 | \\n222 | \\t\\t// Subtract the cached input tokens from the total input tokens.\\n223 | \\t\\tconst uncachedInputTokens = inputTokens - cacheReadTokens\\n----\\n230 | \\n231 | \\t\\tconst trace: Record = {\\n232 | \\t\\t\\tinput: { price: inputPrice, tokens: uncachedInputTokens, cost: inputTokensCost },\\n233 | \\t\\t\\toutput: { price: outputPrice, tokens: outputTokens, cost: outputTokensCost },\\n234 | \\t\\t}\\n----\\n236 | \\t\\tif (cacheReadTokens > 0) {\\n237 | \\t\\t\\ttrace.cacheRead = { price: cacheReadsPrice, tokens: cacheReadTokens, cost: cacheReadCost }\\n238 | \\t\\t}\\n----\\n\\n# src/api/providers/unbound.ts\\n 22 | interface UnboundUsage extends OpenAI.CompletionUsage {\\n 23 | \\tcache_creation_input_tokens?: number\\n 24 | \\tcache_read_input_tokens?: number\\n 25 | }\\n----\\n 73 | \\n 74 | \\t\\t// Required by Anthropic; other providers default to max tokens allowed.\\n 75 | \\t\\tlet maxTokens: number | undefined\\n----\\n 82 | \\t\\t\\tmodel: modelId.split(\\\"/\\\")[1],\\n 83 | \\t\\t\\tmax_tokens: maxTokens,\\n 84 | \\t\\t\\tmessages: openAiMessages,\\n----\\n111 | \\t\\t\\t\\t\\ttype: \\\"usage\\\",\\n112 | \\t\\t\\t\\t\\tinputTokens: usage.prompt_tokens || 0,\\n113 | \\t\\t\\t\\t\\toutputTokens: usage.completion_tokens || 0,\\n114 | \\t\\t\\t\\t}\\n115 | \\n116 | \\t\\t\\t\\t// Only add cache tokens if they exist.\\n117 | \\t\\t\\t\\tif (usage.cache_creation_input_tokens) {\\n118 | \\t\\t\\t\\t\\tusageData.cacheWriteTokens = usage.cache_creation_input_tokens\\n119 | \\t\\t\\t\\t}\\n120 | \\n121 | \\t\\t\\t\\tif (usage.cache_read_input_tokens) {\\n122 | \\t\\t\\t\\t\\tusageData.cacheReadTokens = usage.cache_read_input_tokens\\n123 | \\t\\t\\t\\t}\\n----\\n146 | \\t\\t\\tif (modelId.startsWith(\\\"anthropic/\\\")) {\\n147 | \\t\\t\\t\\trequestOptions.max_tokens = info.maxTokens\\n148 | \\t\\t\\t}\\n----\\n\\n# src/api/providers/lm-studio.ts\\n 40 | \\t\\t// -------------------------\\n 41 | \\t\\t// Track token usage\\n 42 | \\t\\t// -------------------------\\n----\\n 68 | \\t\\t} catch (err) {\\n 69 | \\t\\t\\tconsole.error(\\\"[LmStudio] Failed to count input tokens:\\\", err)\\n 70 | \\t\\t\\tinputTokens = 0\\n----\\n116 | \\t\\t\\t} catch (err) {\\n117 | \\t\\t\\t\\tconsole.error(\\\"[LmStudio] Failed to count output tokens:\\\", err)\\n118 | \\t\\t\\t\\toutputTokens = 0\\n----\\n\\n# src/api/providers/__tests__/openai.spec.ts\\n 29 | \\t\\t\\t\\t\\t\\t\\t\\tusage: {\\n 30 | \\t\\t\\t\\t\\t\\t\\t\\t\\tprompt_tokens: 10,\\n 31 | \\t\\t\\t\\t\\t\\t\\t\\t\\tcompletion_tokens: 5,\\n 32 | \\t\\t\\t\\t\\t\\t\\t\\t\\ttotal_tokens: 15,\\n 33 | \\t\\t\\t\\t\\t\\t\\t\\t},\\n----\\n 55 | \\t\\t\\t\\t\\t\\t\\t\\t\\tusage: {\\n 56 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\tprompt_tokens: 10,\\n 57 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\tcompletion_tokens: 5,\\n 58 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\ttotal_tokens: 15,\\n 59 | \\t\\t\\t\\t\\t\\t\\t\\t\\t},\\n----\\n\\n# src/api/providers/vscode-lm.ts\\n128 | \\t\\t\\t\\tmaxInputTokens: 8192,\\n129 | \\t\\t\\t\\tsendRequest: async (_messages, _options, _token) => {\\n130 | \\t\\t\\t\\t\\t// Provide a minimal implementation\\n----\\n179 | \\t * Implements the ApiHandler countTokens interface method\\n180 | \\t * Provides token counting for Anthropic content blocks\\n181 | \\t *\\n182 | \\t * @param content The content blocks to count tokens for\\n183 | \\t * @returns A promise resolving to the token count\\n184 | \\t */\\n185 | \\toverride async countTokens(content: Array): Promise {\\n186 | \\t\\t// Convert Anthropic content blocks to a string for VSCode LM token counting\\n187 | \\t\\tlet textContent = \\\"\\\"\\n----\\n201 | \\t/**\\n202 | \\t * Private implementation of token counting used internally by VsCodeLmHandler\\n203 | \\t */\\n----\\n206 | \\t\\tif (!this.client) {\\n207 | \\t\\t\\tconsole.warn(\\\"Roo Code : No client available for token counting\\\")\\n208 | \\t\\t\\treturn 0\\n----\\n211 | \\t\\tif (!this.currentRequestCancellation) {\\n212 | \\t\\t\\tconsole.warn(\\\"Roo Code : No cancellation token available for token counting\\\")\\n213 | \\t\\t\\treturn 0\\n----\\n217 | \\t\\tif (!text) {\\n218 | \\t\\t\\tconsole.debug(\\\"Roo Code : Empty text provided for token counting\\\")\\n219 | \\t\\t\\treturn 0\\n----\\n223 | \\t\\t\\t// Handle different input types\\n224 | \\t\\t\\tlet tokenCount: number\\n225 | \\n226 | \\t\\t\\tif (typeof text === \\\"string\\\") {\\n227 | \\t\\t\\t\\ttokenCount = await this.client.countTokens(text, this.currentRequestCancellation.token)\\n228 | \\t\\t\\t} else if (text instanceof vscode.LanguageModelChatMessage) {\\n----\\n233 | \\t\\t\\t\\t}\\n234 | \\t\\t\\t\\ttokenCount = await this.client.countTokens(text, this.currentRequestCancellation.token)\\n235 | \\t\\t\\t} else {\\n236 | \\t\\t\\t\\tconsole.warn(\\\"Roo Code : Invalid input type for token counting\\\")\\n237 | \\t\\t\\t\\treturn 0\\n----\\n240 | \\t\\t\\t// Validate the result\\n241 | \\t\\t\\tif (typeof tokenCount !== \\\"number\\\") {\\n242 | \\t\\t\\t\\tconsole.warn(\\\"Roo Code : Non-numeric token count received:\\\", tokenCount)\\n243 | \\t\\t\\t\\treturn 0\\n----\\n245 | \\n246 | \\t\\t\\tif (tokenCount < 0) {\\n247 | \\t\\t\\t\\tconsole.warn(\\\"Roo Code : Negative token count received:\\\", tokenCount)\\n248 | \\t\\t\\t\\treturn 0\\n----\\n250 | \\n251 | \\t\\t\\treturn tokenCount\\n252 | \\t\\t} catch (error) {\\n----\\n278 | \\n279 | \\t\\treturn systemTokens + messageTokens.reduce((sum: number, tokens: number): number => sum + tokens, 0)\\n280 | \\t}\\n----\\n357 | \\n358 | \\t\\t// Initialize cancellation token for the request\\n359 | \\t\\tthis.currentRequestCancellation = new vscode.CancellationTokenSource()\\n360 | \\n361 | \\t\\t// Calculate input tokens before starting the stream\\n362 | \\t\\tconst totalInputTokens: number = await this.calculateTotalInputTokens(systemPrompt, vsCodeLmMessages)\\n363 | \\n364 | \\t\\t// Accumulate the text and count at the end of the stream to reduce token counting overhead.\\n365 | \\t\\tlet accumulatedText: string = \\\"\\\"\\n----\\n378 | \\t\\t\\t\\trequestOptions,\\n379 | \\t\\t\\t\\tthis.currentRequestCancellation.token,\\n380 | \\t\\t\\t)\\n----\\n446 | \\n447 | \\t\\t\\t// Count tokens in the accumulated text after stream completion\\n448 | \\t\\t\\tconst totalOutputTokens: number = await this.internalCountTokens(accumulatedText)\\n----\\n511 | \\t\\t\\tconst modelInfo: ModelInfo = {\\n512 | \\t\\t\\t\\tmaxTokens: -1, // Unlimited tokens by default\\n513 | \\t\\t\\t\\tcontextWindow:\\n----\\n548 | \\t\\t\\t\\t{},\\n549 | \\t\\t\\t\\tnew vscode.CancellationTokenSource().token,\\n550 | \\t\\t\\t)\\n----\\n\\n# src/api/providers/glama.ts\\n 51 | \\n 52 | \\t\\t// Required by Anthropic; other providers default to max tokens allowed.\\n 53 | \\t\\tlet maxTokens: number | undefined\\n----\\n 60 | \\t\\t\\tmodel: modelId,\\n 61 | \\t\\t\\tmax_tokens: maxTokens,\\n 62 | \\t\\t\\tmessages: openAiMessages,\\n----\\n 90 | \\t\\t\\t\\t// In case of an interrupted request, we need to wait for the upstream API to finish processing the request\\n 91 | \\t\\t\\t\\t// before we can fetch information about the token usage and cost.\\n 92 | \\t\\t\\t\\tconst response = await axios.get(\\n----\\n 98 | \\n 99 | \\t\\t\\t\\tif (completionRequest.tokenUsage && completionRequest.totalCostUsd) {\\n100 | \\t\\t\\t\\t\\tyield {\\n101 | \\t\\t\\t\\t\\t\\ttype: \\\"usage\\\",\\n102 | \\t\\t\\t\\t\\t\\tcacheWriteTokens: completionRequest.tokenUsage.cacheCreationInputTokens,\\n103 | \\t\\t\\t\\t\\t\\tcacheReadTokens: completionRequest.tokenUsage.cacheReadInputTokens,\\n104 | \\t\\t\\t\\t\\t\\tinputTokens: completionRequest.tokenUsage.promptTokens,\\n105 | \\t\\t\\t\\t\\t\\toutputTokens: completionRequest.tokenUsage.completionTokens,\\n106 | \\t\\t\\t\\t\\t\\ttotalCost: parseFloat(completionRequest.totalCostUsd),\\n----\\n132 | \\t\\t\\tif (modelId.startsWith(\\\"anthropic/\\\")) {\\n133 | \\t\\t\\t\\trequestOptions.max_tokens = info.maxTokens\\n134 | \\t\\t\\t}\\n----\\n\\n# src/api/providers/chutes.ts\\n 31 | \\t\\t\\tid: model,\\n 32 | \\t\\t\\tinfo: { maxTokens: max_tokens },\\n 33 | \\t\\t} = this.getModel()\\n----\\n 38 | \\t\\t\\tmodel,\\n 39 | \\t\\t\\tmax_tokens,\\n 40 | \\t\\t\\ttemperature,\\n----\\n 76 | \\t\\t\\t\\t\\t\\ttype: \\\"usage\\\",\\n 77 | \\t\\t\\t\\t\\t\\tinputTokens: chunk.usage.prompt_tokens || 0,\\n 78 | \\t\\t\\t\\t\\t\\toutputTokens: chunk.usage.completion_tokens || 0,\\n 79 | \\t\\t\\t\\t\\t}\\n----\\n\\n# src/api/providers/__tests__/vscode-lm.test.ts\\n 28 | \\t\\tCancellationTokenSource: jest.fn(() => ({\\n 29 | \\t\\t\\ttoken: {\\n 30 | \\t\\t\\t\\tisCancellationRequested: false,\\n----\\n\\n# src/api/providers/__tests__/vertex.spec.ts\\n 49 | \\t\\t\\t// The test expects 4 chunks:\\n 50 | \\t\\t\\t// 1. Usage chunk with input tokens\\n 51 | \\t\\t\\t// 2. Text chunk with \\\"Gemini response part 1\\\"\\n 52 | \\t\\t\\t// 3. Text chunk with \\\" part 2\\\"\\n 53 | \\t\\t\\t// 4. Usage chunk with output tokens\\n 54 | \\n----\\n\\n# src/api/providers/__tests__/gemini.spec.ts\\n167 | \\tdescribe(\\\"calculateCost\\\", () => {\\n168 | \\t\\t// Mock ModelInfo based on gemini-1.5-flash-latest pricing (per 1M tokens)\\n169 | \\t\\t// Removed 'id' and 'name' as they are not part of ModelInfo type directly\\n170 | \\t\\tconst mockInfo: ModelInfo = {\\n171 | \\t\\t\\tinputPrice: 0.125, // $/1M tokens\\n172 | \\t\\t\\toutputPrice: 0.375, // $/1M tokens\\n173 | \\t\\t\\tcacheWritesPrice: 0.125, // Assume same as input for test\\n----\\n179 | \\n180 | \\t\\tit(\\\"should calculate cost correctly based on input and output tokens\\\", () => {\\n181 | \\t\\t\\tconst inputTokens = 10000 // Use larger numbers for per-million pricing\\n----\\n190 | \\n191 | \\t\\tit(\\\"should return 0 if token counts are zero\\\", () => {\\n192 | \\t\\t\\t// Note: The method expects numbers, not undefined. Passing undefined would be a type error.\\n193 | \\t\\t\\t// The calculateCost method itself returns undefined if prices are missing, but 0 if tokens are 0 and prices exist.\\n194 | \\t\\t\\texpect(handler.calculateCost({ info: mockInfo, inputTokens: 0, outputTokens: 0 })).toBe(0)\\n----\\n196 | \\n197 | \\t\\tit(\\\"should handle only input tokens\\\", () => {\\n198 | \\t\\t\\tconst inputTokens = 5000\\n----\\n203 | \\n204 | \\t\\tit(\\\"should handle only output tokens\\\", () => {\\n205 | \\t\\t\\tconst outputTokens = 15000\\n----\\n210 | \\n211 | \\t\\tit(\\\"should calculate cost with cache write tokens\\\", () => {\\n212 | \\t\\t\\tconst inputTokens = 10000\\n----\\n227 | \\n228 | \\t\\tit(\\\"should calculate cost with cache read tokens\\\", () => {\\n229 | \\t\\t\\tconst inputTokens = 10000 // Total logical input\\n----\\n\\n# src/api/providers/__tests__/openrouter.spec.ts\\n132 | \\t\\t\\t\\t\\t\\tchoices: [{ delta: {} }],\\n133 | \\t\\t\\t\\t\\t\\tusage: { prompt_tokens: 10, completion_tokens: 20, cost: 0.001 },\\n134 | \\t\\t\\t\\t\\t}\\n----\\n162 | \\t\\t\\t\\texpect.objectContaining({\\n163 | \\t\\t\\t\\t\\tmax_tokens: 8192,\\n164 | \\t\\t\\t\\t\\tmessages: [\\n----\\n285 | \\t\\t\\t\\tmodel: mockOptions.openRouterModelId,\\n286 | \\t\\t\\t\\tmax_tokens: 8192,\\n287 | \\t\\t\\t\\tthinking: undefined,\\n----\\n\\n# src/api/providers/__tests__/ollama.spec.ts\\n 28 | \\t\\t\\t\\t\\t\\t\\t\\tusage: {\\n 29 | \\t\\t\\t\\t\\t\\t\\t\\t\\tprompt_tokens: 10,\\n 30 | \\t\\t\\t\\t\\t\\t\\t\\t\\tcompletion_tokens: 5,\\n 31 | \\t\\t\\t\\t\\t\\t\\t\\t\\ttotal_tokens: 15,\\n 32 | \\t\\t\\t\\t\\t\\t\\t\\t},\\n----\\n 54 | \\t\\t\\t\\t\\t\\t\\t\\t\\tusage: {\\n 55 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\tprompt_tokens: 10,\\n 56 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\tcompletion_tokens: 5,\\n 57 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\ttotal_tokens: 15,\\n 58 | \\t\\t\\t\\t\\t\\t\\t\\t\\t},\\n----\\n\\n# src/api/providers/__tests__/chutes.spec.ts\\n 49 | \\t\\t\\t\\t\\tusage: {\\n 50 | \\t\\t\\t\\t\\t\\tprompt_tokens: 10,\\n 51 | \\t\\t\\t\\t\\t\\tcompletion_tokens: 5,\\n 52 | \\t\\t\\t\\t\\t\\ttotal_tokens: 15,\\n 53 | \\t\\t\\t\\t\\t},\\n----\\n103 | \\t\\t\\t\\t\\t],\\n104 | \\t\\t\\t\\t\\tusage: { prompt_tokens: 10, completion_tokens: 5 },\\n105 | \\t\\t\\t\\t}\\n----\\n211 | \\t\\t\\t\\t\\t\\t\\tdone: false,\\n212 | \\t\\t\\t\\t\\t\\t\\tvalue: { choices: [{ delta: {} }], usage: { prompt_tokens: 10, completion_tokens: 20 } },\\n213 | \\t\\t\\t\\t\\t\\t})\\n----\\n284 | \\t\\t\\t\\tmodel: modelId,\\n285 | \\t\\t\\t\\tmax_tokens: modelInfo.maxTokens,\\n286 | \\t\\t\\t\\ttemperature: 0.5,\\n----\\n\\n# src/api/providers/openrouter.ts\\n 32 | \\tinclude_reasoning?: boolean\\n 33 | \\t// https://openrouter.ai/docs/use-cases/reasoning-tokens\\n 34 | \\treasoning?: OpenRouterReasoningParams\\n----\\n 40 | interface CompletionUsage {\\n 41 | \\tcompletion_tokens?: number\\n 42 | \\tcompletion_tokens_details?: {\\n 43 | \\t\\treasoning_tokens?: number\\n 44 | \\t}\\n 45 | \\tprompt_tokens?: number\\n 46 | \\tprompt_tokens_details?: {\\n 47 | \\t\\tcached_tokens?: number\\n 48 | \\t}\\n 49 | \\ttotal_tokens?: number\\n 50 | \\tcost?: number\\n----\\n 81 | \\n 82 | \\t\\t// OpenRouter sends reasoning tokens by default for Gemini 2.5 Pro\\n 83 | \\t\\t// Preview even if you don't request them. This is not the default for\\n----\\n116 | \\t\\t\\tmodel: modelId,\\n117 | \\t\\t\\t...(maxTokens && maxTokens > 0 && { max_tokens: maxTokens }),\\n118 | \\t\\t\\ttemperature,\\n----\\n165 | \\t\\t\\t\\ttype: \\\"usage\\\",\\n166 | \\t\\t\\t\\tinputTokens: lastUsage.prompt_tokens || 0,\\n167 | \\t\\t\\t\\toutputTokens: lastUsage.completion_tokens || 0,\\n168 | \\t\\t\\t\\t// Waiting on OpenRouter to figure out what this represents in the Gemini case\\n169 | \\t\\t\\t\\t// and how to best support it.\\n170 | \\t\\t\\t\\t// cacheReadTokens: lastUsage.prompt_tokens_details?.cached_tokens,\\n171 | \\t\\t\\t\\treasoningTokens: lastUsage.completion_tokens_details?.reasoning_tokens,\\n172 | \\t\\t\\t\\ttotalCost: (lastUsage.is_byok ? BYOK_COST_MULTIPLIER : 1) * (lastUsage.cost || 0),\\n----\\n219 | \\t\\t\\tmodel: modelId,\\n220 | \\t\\t\\tmax_tokens: maxTokens,\\n221 | \\t\\t\\ttemperature,\\n----\\n\\n# src/api/providers/__tests__/deepseek.test.ts\\n 28 | \\t\\t\\t\\t\\t\\t\\t\\tusage: {\\n 29 | \\t\\t\\t\\t\\t\\t\\t\\t\\tprompt_tokens: 10,\\n 30 | \\t\\t\\t\\t\\t\\t\\t\\t\\tcompletion_tokens: 5,\\n 31 | \\t\\t\\t\\t\\t\\t\\t\\t\\ttotal_tokens: 15,\\n 32 | \\t\\t\\t\\t\\t\\t\\t\\t\\tprompt_tokens_details: {\\n 33 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\tcache_miss_tokens: 8,\\n 34 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\tcached_tokens: 2,\\n 35 | \\t\\t\\t\\t\\t\\t\\t\\t\\t},\\n----\\n 59 | \\t\\t\\t\\t\\t\\t\\t\\t\\tusage: {\\n 60 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\tprompt_tokens: 10,\\n 61 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\tcompletion_tokens: 5,\\n 62 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\ttotal_tokens: 15,\\n 63 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\tprompt_tokens_details: {\\n 64 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\t\\tcache_miss_tokens: 8,\\n 65 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\t\\tcached_tokens: 2,\\n 66 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\t},\\n----\\n261 | \\t\\t\\tconst usage = {\\n262 | \\t\\t\\t\\tprompt_tokens: 100,\\n263 | \\t\\t\\t\\tcompletion_tokens: 50,\\n264 | \\t\\t\\t\\ttotal_tokens: 150,\\n265 | \\t\\t\\t\\tprompt_tokens_details: {\\n266 | \\t\\t\\t\\t\\tcache_miss_tokens: 80,\\n267 | \\t\\t\\t\\t\\tcached_tokens: 20,\\n268 | \\t\\t\\t\\t},\\n----\\n289 | \\t\\t\\tconst usage = {\\n290 | \\t\\t\\t\\tprompt_tokens: 100,\\n291 | \\t\\t\\t\\tcompletion_tokens: 50,\\n292 | \\t\\t\\t\\ttotal_tokens: 150,\\n293 | \\t\\t\\t\\t// No prompt_tokens_details\\n294 | \\t\\t\\t}\\n----\\n\\n# src/api/providers/__tests__/bedrock-reasoning.test.ts\\n 94 | \\t\\t\\t\\ttype: \\\"enabled\\\",\\n 95 | \\t\\t\\t\\tbudget_tokens: 4096, // Uses the full modelMaxThinkingTokens value\\n 96 | \\t\\t\\t})\\n----\\n143 | \\t\\t\\t\\ttype: \\\"enabled\\\",\\n144 | \\t\\t\\t\\tbudget_tokens: 8192,\\n145 | \\t\\t\\t})\\n----\\n266 | \\t\\t\\t\\ttype: \\\"enabled\\\",\\n267 | \\t\\t\\t\\tbudget_tokens: 4096,\\n268 | \\t\\t\\t})\\n----\\n\\n# src/api/providers/__tests__/xai.test.ts\\n228 | \\t\\t\\t\\t\\t\\t\\t\\tusage: {\\n229 | \\t\\t\\t\\t\\t\\t\\t\\t\\tprompt_tokens: 10,\\n230 | \\t\\t\\t\\t\\t\\t\\t\\t\\tcompletion_tokens: 20,\\n231 | \\t\\t\\t\\t\\t\\t\\t\\t\\tcache_read_input_tokens: 5,\\n232 | \\t\\t\\t\\t\\t\\t\\t\\t\\tcache_creation_input_tokens: 15,\\n233 | \\t\\t\\t\\t\\t\\t\\t\\t},\\n----\\n284 | \\t\\t\\t\\tmodel: modelId,\\n285 | \\t\\t\\t\\tmax_tokens: modelInfo.maxTokens,\\n286 | \\t\\t\\t\\ttemperature: 0,\\n----\\n\\n# src/api/providers/__tests__/anthropic.spec.ts\\n 19 | \\t\\t\\t\\t\\t\\tusage: {\\n 20 | \\t\\t\\t\\t\\t\\t\\tinput_tokens: 10,\\n 21 | \\t\\t\\t\\t\\t\\t\\toutput_tokens: 5,\\n 22 | \\t\\t\\t\\t\\t\\t},\\n----\\n 30 | \\t\\t\\t\\t\\t\\t\\t\\tusage: {\\n 31 | \\t\\t\\t\\t\\t\\t\\t\\t\\tinput_tokens: 100,\\n 32 | \\t\\t\\t\\t\\t\\t\\t\\t\\toutput_tokens: 50,\\n 33 | \\t\\t\\t\\t\\t\\t\\t\\t\\tcache_creation_input_tokens: 20,\\n 34 | \\t\\t\\t\\t\\t\\t\\t\\t\\tcache_read_input_tokens: 10,\\n 35 | \\t\\t\\t\\t\\t\\t\\t\\t},\\n----\\n105 | \\n106 | \\t\\tit(\\\"use apiKey for passing token if anthropicUseAuthToken is not set\\\", () => {\\n107 | \\t\\t\\tconst handlerWithCustomUrl = new AnthropicHandler({\\n----\\n112 | \\t\\t\\texpect(mockAnthropicConstructor.mock.calls[0]![0]!.apiKey).toEqual(\\\"test-api-key\\\")\\n113 | \\t\\t\\texpect(mockAnthropicConstructor.mock.calls[0]![0]!.authToken).toBeUndefined()\\n114 | \\t\\t})\\n115 | \\n116 | \\t\\tit(\\\"use apiKey for passing token if anthropicUseAuthToken is set but custom base URL is not given\\\", () => {\\n117 | \\t\\t\\tconst handlerWithCustomUrl = new AnthropicHandler({\\n----\\n123 | \\t\\t\\texpect(mockAnthropicConstructor.mock.calls[0]![0]!.apiKey).toEqual(\\\"test-api-key\\\")\\n124 | \\t\\t\\texpect(mockAnthropicConstructor.mock.calls[0]![0]!.authToken).toBeUndefined()\\n125 | \\t\\t})\\n126 | \\n127 | \\t\\tit(\\\"use authToken for passing token if both of anthropicBaseUrl and anthropicUseAuthToken are set\\\", () => {\\n128 | \\t\\t\\tconst customBaseUrl = \\\"https://custom.anthropic.com\\\"\\n----\\n135 | \\t\\t\\texpect(mockAnthropicConstructor).toHaveBeenCalledTimes(1)\\n136 | \\t\\t\\texpect(mockAnthropicConstructor.mock.calls[0]![0]!.authToken).toEqual(\\\"test-api-key\\\")\\n137 | \\t\\t\\texpect(mockAnthropicConstructor.mock.calls[0]![0]!.apiKey).toBeUndefined()\\n----\\n190 | \\t\\t\\t\\tmessages: [{ role: \\\"user\\\", content: \\\"Test prompt\\\" }],\\n191 | \\t\\t\\t\\tmax_tokens: 8192,\\n192 | \\t\\t\\t\\ttemperature: 0,\\n----\\n\\n# src/api/providers/__tests__/requesty.spec.ts\\n124 | \\t\\t\\t\\t\\t\\tusage: {\\n125 | \\t\\t\\t\\t\\t\\t\\tprompt_tokens: 10,\\n126 | \\t\\t\\t\\t\\t\\t\\tcompletion_tokens: 20,\\n127 | \\t\\t\\t\\t\\t\\t\\tprompt_tokens_details: {\\n128 | \\t\\t\\t\\t\\t\\t\\t\\tcaching_tokens: 5,\\n129 | \\t\\t\\t\\t\\t\\t\\t\\tcached_tokens: 2,\\n130 | \\t\\t\\t\\t\\t\\t\\t},\\n----\\n162 | \\t\\t\\t\\texpect.objectContaining({\\n163 | \\t\\t\\t\\t\\tmax_tokens: 8192,\\n164 | \\t\\t\\t\\t\\tmessages: [\\n----\\n204 | \\t\\t\\t\\tmodel: mockOptions.requestyModelId,\\n205 | \\t\\t\\t\\tmax_tokens: 8192,\\n206 | \\t\\t\\t\\tmessages: [{ role: \\\"system\\\", content: \\\"test prompt\\\" }],\\n----\\n\\n# src/api/providers/__tests__/openai-native.spec.ts\\n 29 | \\t\\t\\t\\t\\t\\t\\t\\tusage: {\\n 30 | \\t\\t\\t\\t\\t\\t\\t\\t\\tprompt_tokens: 10,\\n 31 | \\t\\t\\t\\t\\t\\t\\t\\t\\tcompletion_tokens: 5,\\n 32 | \\t\\t\\t\\t\\t\\t\\t\\t\\ttotal_tokens: 15,\\n 33 | \\t\\t\\t\\t\\t\\t\\t\\t},\\n----\\n 55 | \\t\\t\\t\\t\\t\\t\\t\\t\\tusage: {\\n 56 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\tprompt_tokens: 10,\\n 57 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\tcompletion_tokens: 5,\\n 58 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\ttotal_tokens: 15,\\n 59 | \\t\\t\\t\\t\\t\\t\\t\\t\\t},\\n----\\n145 | \\t\\t\\t\\t\\t\\tusage: {\\n146 | \\t\\t\\t\\t\\t\\t\\tprompt_tokens: 0,\\n147 | \\t\\t\\t\\t\\t\\t\\tcompletion_tokens: 0,\\n148 | \\t\\t\\t\\t\\t\\t\\ttotal_tokens: 0,\\n149 | \\t\\t\\t\\t\\t\\t},\\n----\\n215 | \\t\\t\\t\\t{ choices: [{ delta: { content: \\\" there\\\" } }], usage: null },\\n216 | \\t\\t\\t\\t{ choices: [{ delta: { content: \\\"!\\\" } }], usage: { prompt_tokens: 10, completion_tokens: 5 } },\\n217 | \\t\\t\\t]\\n----\\n261 | \\t\\t\\t\\t{ choices: [{ delta: { content: null } }], usage: null },\\n262 | \\t\\t\\t\\t{ choices: [{ delta: { content: \\\"Hello\\\" } }], usage: { prompt_tokens: 10, completion_tokens: 5 } },\\n263 | \\t\\t\\t]\\n----\\n\\n# src/api/providers/__tests__/anthropic-vertex.spec.ts\\n 21 | \\t\\t\\t\\t\\t\\tusage: {\\n 22 | \\t\\t\\t\\t\\t\\t\\tinput_tokens: 10,\\n 23 | \\t\\t\\t\\t\\t\\t\\toutput_tokens: 5,\\n 24 | \\t\\t\\t\\t\\t\\t},\\n----\\n 32 | \\t\\t\\t\\t\\t\\t\\t\\tusage: {\\n 33 | \\t\\t\\t\\t\\t\\t\\t\\t\\tinput_tokens: 10,\\n 34 | \\t\\t\\t\\t\\t\\t\\t\\t\\toutput_tokens: 5,\\n 35 | \\t\\t\\t\\t\\t\\t\\t\\t},\\n----\\n 95 | \\t\\t\\t\\t\\t\\tusage: {\\n 96 | \\t\\t\\t\\t\\t\\t\\tinput_tokens: 10,\\n 97 | \\t\\t\\t\\t\\t\\t\\toutput_tokens: 0,\\n 98 | \\t\\t\\t\\t\\t\\t},\\n----\\n118 | \\t\\t\\t\\t\\tusage: {\\n119 | \\t\\t\\t\\t\\t\\toutput_tokens: 5,\\n120 | \\t\\t\\t\\t\\t},\\n----\\n164 | \\t\\t\\t\\tmodel: \\\"claude-3-5-sonnet-v2@20241022\\\",\\n165 | \\t\\t\\t\\tmax_tokens: 8192,\\n166 | \\t\\t\\t\\ttemperature: 0,\\n----\\n284 | \\t\\t\\t\\t\\t\\tusage: {\\n285 | \\t\\t\\t\\t\\t\\t\\tinput_tokens: 10,\\n286 | \\t\\t\\t\\t\\t\\t\\toutput_tokens: 0,\\n287 | \\t\\t\\t\\t\\t\\t\\tcache_creation_input_tokens: 3,\\n288 | \\t\\t\\t\\t\\t\\t\\tcache_read_input_tokens: 2,\\n289 | \\t\\t\\t\\t\\t\\t},\\n----\\n309 | \\t\\t\\t\\t\\tusage: {\\n310 | \\t\\t\\t\\t\\t\\toutput_tokens: 5,\\n311 | \\t\\t\\t\\t\\t},\\n----\\n419 | \\t\\t\\t\\t\\t\\tusage: {\\n420 | \\t\\t\\t\\t\\t\\t\\tinput_tokens: 10,\\n421 | \\t\\t\\t\\t\\t\\t\\toutput_tokens: 0,\\n422 | \\t\\t\\t\\t\\t\\t\\tcache_creation_input_tokens: 5,\\n423 | \\t\\t\\t\\t\\t\\t\\tcache_read_input_tokens: 3,\\n424 | \\t\\t\\t\\t\\t\\t},\\n----\\n484 | \\t\\t\\t\\t\\t\\tusage: {\\n485 | \\t\\t\\t\\t\\t\\t\\tinput_tokens: 10,\\n486 | \\t\\t\\t\\t\\t\\t\\toutput_tokens: 0,\\n487 | \\t\\t\\t\\t\\t\\t},\\n----\\n618 | \\t\\t\\t\\tmodel: \\\"claude-3-5-sonnet-v2@20241022\\\",\\n619 | \\t\\t\\t\\tmax_tokens: 8192,\\n620 | \\t\\t\\t\\ttemperature: 0,\\n----\\n752 | \\n753 | \\t\\t\\t// Test with default thinking budget (80% of max tokens)\\n754 | \\t\\t\\tconst handlerWithDefaultBudget = new AnthropicVertexHandler({\\n----\\n767 | \\t\\t\\t\\tvertexRegion: \\\"us-central1\\\",\\n768 | \\t\\t\\t\\tmodelMaxTokens: 1000, // This would result in 800 tokens for thinking, but minimum is 1024\\n769 | \\t\\t\\t})\\n----\\n789 | \\t\\t\\t\\t\\t\\tmodel: options.model,\\n790 | \\t\\t\\t\\t\\t\\tusage: { input_tokens: 10, output_tokens: 5 },\\n791 | \\t\\t\\t\\t\\t}\\n----\\n794 | \\t\\t\\t\\t\\tasync *[Symbol.asyncIterator]() {\\n795 | \\t\\t\\t\\t\\t\\tyield { type: \\\"message_start\\\", message: { usage: { input_tokens: 10, output_tokens: 5 } } }\\n796 | \\t\\t\\t\\t\\t},\\n----\\n806 | \\t\\t\\t\\texpect.objectContaining({\\n807 | \\t\\t\\t\\t\\tthinking: { type: \\\"enabled\\\", budget_tokens: 4096 },\\n808 | \\t\\t\\t\\t\\ttemperature: 1.0, // Thinking requires temperature 1.0\\n----\\n\\n# src/api/providers/__tests__/lmstudio.test.ts\\n 25 | \\t\\t\\t\\t\\t\\t\\t\\tusage: {\\n 26 | \\t\\t\\t\\t\\t\\t\\t\\t\\tprompt_tokens: 10,\\n 27 | \\t\\t\\t\\t\\t\\t\\t\\t\\tcompletion_tokens: 5,\\n 28 | \\t\\t\\t\\t\\t\\t\\t\\t\\ttotal_tokens: 15,\\n 29 | \\t\\t\\t\\t\\t\\t\\t\\t},\\n----\\n 51 | \\t\\t\\t\\t\\t\\t\\t\\t\\tusage: {\\n 52 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\tprompt_tokens: 10,\\n 53 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\tcompletion_tokens: 5,\\n 54 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\ttotal_tokens: 15,\\n 55 | \\t\\t\\t\\t\\t\\t\\t\\t\\t},\\n----\\n\\n# src/api/providers/__tests__/glama.spec.ts\\n 57 | \\t\\t\\t\\t\\t\\t\\t\\t\\tchoices: [{ delta: {}, index: 0 }],\\n 58 | \\t\\t\\t\\t\\t\\t\\t\\t\\tusage: { prompt_tokens: 10, completion_tokens: 5, total_tokens: 15 },\\n 59 | \\t\\t\\t\\t\\t\\t\\t\\t}\\n----\\n112 | \\t\\t\\tusage: {\\n113 | \\t\\t\\t\\tprompt_tokens: 10,\\n114 | \\t\\t\\t\\tcompletion_tokens: 5,\\n115 | \\t\\t\\t\\ttotal_tokens: 15,\\n116 | \\t\\t\\t},\\n----\\n176 | \\t\\t\\t\\t\\ttemperature: 0,\\n177 | \\t\\t\\t\\t\\tmax_tokens: 8192,\\n178 | \\t\\t\\t\\t}),\\n----\\n194 | \\n195 | \\t\\tit(\\\"should not set max_tokens for non-Anthropic models\\\", async () => {\\n196 | \\t\\t\\t// Reset mock to clear any previous calls\\n----\\n213 | \\t\\t\\t)\\n214 | \\t\\t\\texpect(mockCreate.mock.calls[0][0]).not.toHaveProperty(\\\"max_tokens\\\")\\n215 | \\t\\t})\\n----\\n\\n# src/api/providers/__tests__/openai-usage-tracking.spec.ts\\n 28 | \\t\\t\\t\\t\\t\\t\\t\\tusage: {\\n 29 | \\t\\t\\t\\t\\t\\t\\t\\t\\tprompt_tokens: 10,\\n 30 | \\t\\t\\t\\t\\t\\t\\t\\t\\tcompletion_tokens: 5,\\n 31 | \\t\\t\\t\\t\\t\\t\\t\\t\\ttotal_tokens: 15,\\n 32 | \\t\\t\\t\\t\\t\\t\\t\\t},\\n----\\n 47 | \\t\\t\\t\\t\\t\\t\\t\\t\\tusage: {\\n 48 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\tprompt_tokens: 10,\\n 49 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\tcompletion_tokens: 2,\\n 50 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\ttotal_tokens: 12,\\n 51 | \\t\\t\\t\\t\\t\\t\\t\\t\\t},\\n----\\n 62 | \\t\\t\\t\\t\\t\\t\\t\\t\\tusage: {\\n 63 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\tprompt_tokens: 10,\\n 64 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\tcompletion_tokens: 4,\\n 65 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\ttotal_tokens: 14,\\n 66 | \\t\\t\\t\\t\\t\\t\\t\\t\\t},\\n----\\n 77 | \\t\\t\\t\\t\\t\\t\\t\\t\\tusage: {\\n 78 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\tprompt_tokens: 10,\\n 79 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\tcompletion_tokens: 5,\\n 80 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\ttotal_tokens: 15,\\n 81 | \\t\\t\\t\\t\\t\\t\\t\\t\\t},\\n----\\n155 | \\t\\t\\t\\t\\t\\tchoices: [{ message: { role: \\\"assistant\\\", content: \\\"Test response\\\" } }],\\n156 | \\t\\t\\t\\t\\t\\tusage: { prompt_tokens: 10, completion_tokens: 5, total_tokens: 15 },\\n157 | \\t\\t\\t\\t\\t}\\n----\\n177 | \\t\\t\\t\\t\\t\\t\\tusage: {\\n178 | \\t\\t\\t\\t\\t\\t\\t\\tprompt_tokens: 10,\\n179 | \\t\\t\\t\\t\\t\\t\\t\\tcompletion_tokens: 5,\\n180 | \\t\\t\\t\\t\\t\\t\\t\\ttotal_tokens: 15,\\n181 | \\t\\t\\t\\t\\t\\t\\t},\\n----\\n\\n# src/api/providers/__tests__/groq.spec.ts\\n101 | \\t\\t\\t\\t\\t\\t\\tdone: false,\\n102 | \\t\\t\\t\\t\\t\\t\\tvalue: { choices: [{ delta: {} }], usage: { prompt_tokens: 10, completion_tokens: 20 } },\\n103 | \\t\\t\\t\\t\\t\\t})\\n----\\n139 | \\t\\t\\t\\tmodel: modelId,\\n140 | \\t\\t\\t\\tmax_tokens: modelInfo.maxTokens,\\n141 | \\t\\t\\t\\ttemperature: 0.5,\\n----\\n\\n# src/api/providers/__tests__/unbound.spec.ts\\n 82 | \\t\\t\\t\\t\\t\\t\\t\\t\\tusage: {\\n 83 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\tprompt_tokens: 10,\\n 84 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\tcompletion_tokens: 5,\\n 85 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\ttotal_tokens: 15,\\n 86 | \\t\\t\\t\\t\\t\\t\\t\\t\\t},\\n----\\n 91 | \\t\\t\\t\\t\\t\\t\\t\\t\\tusage: {\\n 92 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\tprompt_tokens: 8,\\n 93 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\tcompletion_tokens: 4,\\n 94 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\ttotal_tokens: 12,\\n 95 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\tcache_creation_input_tokens: 3,\\n 96 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\tcache_read_input_tokens: 2,\\n 97 | \\t\\t\\t\\t\\t\\t\\t\\t\\t},\\n----\\n232 | \\t\\t\\t\\t\\ttemperature: 0,\\n233 | \\t\\t\\t\\t\\tmax_tokens: 8192,\\n234 | \\t\\t\\t\\t}),\\n----\\n253 | \\n254 | \\t\\tit(\\\"should not set max_tokens for non-Anthropic models\\\", async () => {\\n255 | \\t\\t\\tmockCreate.mockClear()\\n----\\n277 | \\n278 | \\t\\t\\texpect(mockCreate.mock.calls[0][0]).not.toHaveProperty(\\\"max_tokens\\\")\\n279 | \\t\\t})\\n----\\n\\n# src/api/providers/__tests__/bedrock-invokedModelId.spec.ts\\n176 | \\n177 | \\t\\t// The last usage event should have the token counts from the metadata\\n178 | \\t\\tconst lastUsageEvent = usageEvents[usageEvents.length - 1]\\n179 | \\t\\t// Expect the usage event to include all token information\\n180 | \\t\\texpect(lastUsageEvent).toMatchObject({\\n----\\n183 | \\t\\t\\toutputTokens: 200,\\n184 | \\t\\t\\t// Cache tokens may be present with default values\\n185 | \\t\\t\\tcacheReadTokens: expect.any(Number),\\n----\\n\\n# src/api/providers/requesty.ts\\n 19 | // Requesty usage includes an extra field for Anthropic use cases.\\n 20 | // Safely cast the prompt token details section to the appropriate structure.\\n 21 | interface RequestyUsage extends OpenAI.CompletionUsage {\\n 22 | \\tprompt_tokens_details?: {\\n 23 | \\t\\tcaching_tokens?: number\\n 24 | \\t\\tcached_tokens?: number\\n 25 | \\t}\\n----\\n 76 | \\t\\tconst requestyUsage = usage as RequestyUsage\\n 77 | \\t\\tconst inputTokens = requestyUsage?.prompt_tokens || 0\\n 78 | \\t\\tconst outputTokens = requestyUsage?.completion_tokens || 0\\n 79 | \\t\\tconst cacheWriteTokens = requestyUsage?.prompt_tokens_details?.caching_tokens || 0\\n 80 | \\t\\tconst cacheReadTokens = requestyUsage?.prompt_tokens_details?.cached_tokens || 0\\n 81 | \\t\\tconst totalCost = modelInfo\\n----\\n102 | \\t\\t\\tinfo,\\n103 | \\t\\t\\tmaxTokens: max_tokens,\\n104 | \\t\\t\\ttemperature,\\n----\\n116 | \\t\\t\\tmodel,\\n117 | \\t\\t\\tmax_tokens,\\n118 | \\t\\t\\ttemperature,\\n----\\n150 | \\tasync completePrompt(prompt: string): Promise {\\n151 | \\t\\tconst { id: model, maxTokens: max_tokens, temperature } = await this.fetchModel()\\n152 | \\n----\\n156 | \\t\\t\\tmodel,\\n157 | \\t\\t\\tmax_tokens,\\n158 | \\t\\t\\tmessages: openAiMessages,\\n----\\n\\n# src/api/providers/lite-llm.ts\\n 46 | \\n 47 | \\t\\t// Required by some providers; others default to max tokens allowed\\n 48 | \\t\\tlet maxTokens: number | undefined = info.maxTokens ?? undefined\\n----\\n 51 | \\t\\t\\tmodel: modelId,\\n 52 | \\t\\t\\tmax_tokens: maxTokens,\\n 53 | \\t\\t\\tmessages: openAiMessages,\\n----\\n 84 | \\t\\t\\t\\t\\ttype: \\\"usage\\\",\\n 85 | \\t\\t\\t\\t\\tinputTokens: lastUsage.prompt_tokens || 0,\\n 86 | \\t\\t\\t\\t\\toutputTokens: lastUsage.completion_tokens || 0,\\n 87 | \\t\\t\\t\\t\\tcacheWriteTokens: lastUsage.cache_creation_input_tokens || 0,\\n 88 | \\t\\t\\t\\t\\tcacheReadTokens: lastUsage.prompt_tokens_details?.cached_tokens || 0,\\n 89 | \\t\\t\\t\\t}\\n----\\n121 | \\n122 | \\t\\t\\trequestOptions.max_tokens = info.maxTokens\\n123 | \\n----\\n136 | interface LiteLLMUsage extends OpenAI.CompletionUsage {\\n137 | \\tcache_creation_input_tokens?: number\\n138 | }\\n----\\n\\n# src/api/providers/fetchers/unbound.ts\\n 34 | \\t\\t\\t\\t\\tcase modelId.startsWith(\\\"anthropic/\\\"):\\n 35 | \\t\\t\\t\\t\\t\\t// Set max tokens to 8192 for supported Anthropic models\\n 36 | \\t\\t\\t\\t\\t\\tif (modelInfo.maxTokens !== 4096) {\\n----\\n\\n# src/api/providers/deepseek.ts\\n 32 | \\t\\t\\ttype: \\\"usage\\\",\\n 33 | \\t\\t\\tinputTokens: usage?.prompt_tokens || 0,\\n 34 | \\t\\t\\toutputTokens: usage?.completion_tokens || 0,\\n 35 | \\t\\t\\tcacheWriteTokens: usage?.prompt_tokens_details?.cache_miss_tokens,\\n 36 | \\t\\t\\tcacheReadTokens: usage?.prompt_tokens_details?.cached_tokens,\\n 37 | \\t\\t}\\n----\\n\\n# src/api/providers/openai.ts\\n162 | \\t\\t\\tif (this.options.includeMaxTokens) {\\n163 | \\t\\t\\t\\trequestOptions.max_tokens = modelInfo.maxTokens\\n164 | \\t\\t\\t}\\n----\\n241 | \\t\\t\\ttype: \\\"usage\\\",\\n242 | \\t\\t\\tinputTokens: usage?.prompt_tokens || 0,\\n243 | \\t\\t\\toutputTokens: usage?.completion_tokens || 0,\\n244 | \\t\\t\\tcacheWriteTokens: usage?.cache_creation_input_tokens || undefined,\\n245 | \\t\\t\\tcacheReadTokens: usage?.cache_read_input_tokens || undefined,\\n246 | \\t\\t}\\n----\\n347 | \\t\\t\\t\\t\\ttype: \\\"usage\\\",\\n348 | \\t\\t\\t\\t\\tinputTokens: chunk.usage.prompt_tokens || 0,\\n349 | \\t\\t\\t\\t\\toutputTokens: chunk.usage.completion_tokens || 0,\\n350 | \\t\\t\\t\\t}\\n----\\n\\n# src/api/providers/fetchers/openrouter.ts\\n 21 | \\tmodality: z.string().nullish(),\\n 22 | \\ttokenizer: z.string().nullish(),\\n 23 | })\\n----\\n 35 | \\tcontext_length: z.number(),\\n 36 | \\tmax_completion_tokens: z.number().nullish(),\\n 37 | \\tpricing: openRouterPricingSchema.optional(),\\n----\\n 48 | \\tarchitecture: openRouterArchitectureSchema.optional(),\\n 49 | \\ttop_provider: z.object({ max_completion_tokens: z.number().nullish() }).optional(),\\n 50 | \\tsupported_parameters: z.array(z.string()).optional(),\\n----\\n115 | \\t\\t\\t\\tmodality: architecture?.modality,\\n116 | \\t\\t\\t\\tmaxTokens: top_provider?.max_completion_tokens,\\n117 | \\t\\t\\t\\tsupportedParameters: supported_parameters,\\n----\\n155 | \\t\\t\\t\\tmodality: architecture?.modality,\\n156 | \\t\\t\\t\\tmaxTokens: endpoint.max_completion_tokens,\\n157 | \\t\\t\\t})\\n----\\n\\n# src/api/providers/openai-native.ts\\n146 | \\tprivate async *yieldUsage(info: ModelInfo, usage: OpenAI.Completions.CompletionUsage | undefined): ApiStream {\\n147 | \\t\\tconst inputTokens = usage?.prompt_tokens || 0 // sum of cache hits and misses\\n148 | \\t\\tconst outputTokens = usage?.completion_tokens || 0\\n149 | \\t\\tconst cacheReadTokens = usage?.prompt_tokens_details?.cached_tokens || 0\\n150 | \\t\\tconst cacheWriteTokens = 0\\n----\\n\\n# src/api/providers/fetchers/requesty.ts\\n 31 | \\t\\t\\tconst modelInfo: ModelInfo = {\\n 32 | \\t\\t\\t\\tmaxTokens: rawModel.max_output_tokens,\\n 33 | \\t\\t\\t\\tcontextWindow: rawModel.context_window,\\n----\\n\\n# src/api/providers/anthropic.ts\\n 29 | \\t\\tconst apiKeyFieldName =\\n 30 | \\t\\t\\tthis.options.anthropicBaseUrl && this.options.anthropicUseAuthToken ? \\\"authToken\\\" : \\\"apiKey\\\"\\n 31 | \\n----\\n 75 | \\t\\t\\t\\t\\t\\tmodel: modelId,\\n 76 | \\t\\t\\t\\t\\t\\tmax_tokens: maxTokens ?? ANTHROPIC_DEFAULT_MAX_TOKENS,\\n 77 | \\t\\t\\t\\t\\t\\ttemperature,\\n----\\n124 | \\t\\t\\t\\t\\tmodel: modelId,\\n125 | \\t\\t\\t\\t\\tmax_tokens: maxTokens ?? ANTHROPIC_DEFAULT_MAX_TOKENS,\\n126 | \\t\\t\\t\\t\\ttemperature,\\n----\\n142 | \\t\\t\\t\\t\\t\\ttype: \\\"usage\\\",\\n143 | \\t\\t\\t\\t\\t\\tinputTokens: usage.input_tokens || 0,\\n144 | \\t\\t\\t\\t\\t\\toutputTokens: usage.output_tokens || 0,\\n145 | \\t\\t\\t\\t\\t\\tcacheWriteTokens: usage.cache_creation_input_tokens || undefined,\\n146 | \\t\\t\\t\\t\\t\\tcacheReadTokens: usage.cache_read_input_tokens || undefined,\\n147 | \\t\\t\\t\\t\\t}\\n----\\n151 | \\t\\t\\t\\tcase \\\"message_delta\\\":\\n152 | \\t\\t\\t\\t\\t// Tells us stop_reason, stop_sequence, and output tokens\\n153 | \\t\\t\\t\\t\\t// along the way and at the end of the message.\\n----\\n156 | \\t\\t\\t\\t\\t\\tinputTokens: 0,\\n157 | \\t\\t\\t\\t\\t\\toutputTokens: chunk.usage.output_tokens || 0,\\n158 | \\t\\t\\t\\t\\t}\\n----\\n231 | \\t\\t\\tmodel,\\n232 | \\t\\t\\tmax_tokens: ANTHROPIC_DEFAULT_MAX_TOKENS,\\n233 | \\t\\t\\tthinking: undefined,\\n----\\n243 | \\t/**\\n244 | \\t * Counts tokens for the given content using Anthropic's API\\n245 | \\t *\\n246 | \\t * @param content The content blocks to count tokens for\\n247 | \\t * @returns A promise resolving to the token count\\n248 | \\t */\\n----\\n258 | \\n259 | \\t\\t\\treturn response.input_tokens\\n260 | \\t\\t} catch (error) {\\n261 | \\t\\t\\t// Log error but fallback to tiktoken estimation\\n262 | \\t\\t\\tconsole.warn(\\\"Anthropic token counting failed, using fallback\\\", error)\\n263 | \\n----\\n\\n# src/api/providers/base-openai-compatible-provider.ts\\n 71 | \\t\\t\\tid: model,\\n 72 | \\t\\t\\tinfo: { maxTokens: max_tokens },\\n 73 | \\t\\t} = this.getModel()\\n----\\n 78 | \\t\\t\\tmodel,\\n 79 | \\t\\t\\tmax_tokens,\\n 80 | \\t\\t\\ttemperature,\\n----\\n100 | \\t\\t\\t\\t\\ttype: \\\"usage\\\",\\n101 | \\t\\t\\t\\t\\tinputTokens: chunk.usage.prompt_tokens || 0,\\n102 | \\t\\t\\t\\t\\toutputTokens: chunk.usage.completion_tokens || 0,\\n103 | \\t\\t\\t\\t}\\n----\\n\\n# src/api/providers/xai.ts\\n 52 | \\t\\t\\tmodel: modelId,\\n 53 | \\t\\t\\tmax_tokens: modelInfo.maxTokens,\\n 54 | \\t\\t\\ttemperature: this.options.modelTemperature ?? XAI_DEFAULT_TEMPERATURE,\\n----\\n 78 | \\t\\t\\tif (chunk.usage) {\\n 79 | \\t\\t\\t\\t// Extract detailed token information if available\\n 80 | \\t\\t\\t\\t// First check for prompt_tokens_details structure (real API response)\\n 81 | \\t\\t\\t\\tconst promptDetails = \\\"prompt_tokens_details\\\" in chunk.usage ? chunk.usage.prompt_tokens_details : null;\\n 82 | \\t\\t\\t\\tconst cachedTokens = promptDetails && \\\"cached_tokens\\\" in promptDetails ? promptDetails.cached_tokens : 0;\\n 83 | \\n 84 | \\t\\t\\t\\t// Fall back to direct fields in usage (used in test mocks)\\n 85 | \\t\\t\\t\\tconst readTokens = cachedTokens || (\\\"cache_read_input_tokens\\\" in chunk.usage ? (chunk.usage as any).cache_read_input_tokens : 0);\\n 86 | \\t\\t\\t\\tconst writeTokens = \\\"cache_creation_input_tokens\\\" in chunk.usage ? (chunk.usage as any).cache_creation_input_tokens : 0;\\n 87 | \\n----\\n 89 | \\t\\t\\t\\t\\ttype: \\\"usage\\\",\\n 90 | \\t\\t\\t\\t\\tinputTokens: chunk.usage.prompt_tokens || 0,\\n 91 | \\t\\t\\t\\t\\toutputTokens: chunk.usage.completion_tokens || 0,\\n 92 | \\t\\t\\t\\t\\tcacheReadTokens: readTokens,\\n----\\n\\n# src/api/providers/bedrock.ts\\n 52 | \\t\\ttype: \\\"enabled\\\"\\n 53 | \\t\\tbudget_tokens: number\\n 54 | \\t}\\n----\\n109 | \\tmessageStop?: {\\n110 | \\t\\tstopReason?: \\\"end_turn\\\" | \\\"tool_use\\\" | \\\"max_tokens\\\" | \\\"stop_sequence\\\"\\n111 | \\t\\tadditionalModelResponseFields?: Record\\n----\\n349 | \\t\\t\\t\\t\\ttype: \\\"enabled\\\",\\n350 | \\t\\t\\t\\t\\tbudget_tokens: metadata?.thinking?.maxThinkingTokens || modelConfig.reasoningBudget || 4096,\\n351 | \\t\\t\\t\\t},\\n----\\n418 | \\n419 | \\t\\t\\t\\t\\t// Check both field naming conventions for cache tokens\\n420 | \\t\\t\\t\\t\\tconst cacheReadTokens = usage.cacheReadInputTokens || usage.cacheReadInputTokenCount || 0\\n----\\n422 | \\n423 | \\t\\t\\t\\t\\t// Always include all available token information\\n424 | \\t\\t\\t\\t\\tyield {\\n----\\n450 | \\n451 | \\t\\t\\t\\t\\t\\t\\t// Check both field naming conventions for cache tokens\\n452 | \\t\\t\\t\\t\\t\\t\\tconst cacheReadTokens =\\n----\\n1049 | \\t\\tTOO_MANY_TOKENS: {\\n1050 | \\t\\t\\tpatterns: [\\\"too many tokens\\\"],\\n1051 | \\t\\t\\tmessageTemplate: `\\\"Too many tokens\\\" error detected.\\n1052 | Possible Causes:\\n1053 | 1. Input exceeds model's context window limit\\n1054 | 2. Rate limiting (too many tokens per minute)\\n1055 | 3. Quota exceeded for token usage\\n1056 | 4. Other token-related service limitations\\n1057 | \\n----\\n\\n# src/api/providers/fetchers/litellm.ts\\n 52 | \\t\\t\\t\\tmodels[modelName] = {\\n 53 | \\t\\t\\t\\t\\tmaxTokens: modelInfo.max_tokens || 8192,\\n 54 | \\t\\t\\t\\t\\tcontextWindow: modelInfo.max_input_tokens || 200000,\\n 55 | \\t\\t\\t\\t\\tsupportsImages: Boolean(modelInfo.supports_vision),\\n----\\n 58 | \\t\\t\\t\\t\\tsupportsPromptCache: Boolean(modelInfo.supports_prompt_caching),\\n 59 | \\t\\t\\t\\t\\tinputPrice: modelInfo.input_cost_per_token ? modelInfo.input_cost_per_token * 1000000 : undefined,\\n 60 | \\t\\t\\t\\t\\toutputPrice: modelInfo.output_cost_per_token\\n 61 | \\t\\t\\t\\t\\t\\t? modelInfo.output_cost_per_token * 1000000\\n 62 | \\t\\t\\t\\t\\t\\t: undefined,\\n 63 | \\t\\t\\t\\t\\tcacheWritesPrice: modelInfo.cache_creation_input_token_cost\\n 64 | \\t\\t\\t\\t\\t\\t? modelInfo.cache_creation_input_token_cost * 1000000\\n 65 | \\t\\t\\t\\t\\t\\t: undefined,\\n 66 | \\t\\t\\t\\t\\tcacheReadsPrice: modelInfo.cache_read_input_token_cost\\n 67 | \\t\\t\\t\\t\\t\\t? modelInfo.cache_read_input_token_cost * 1000000\\n 68 | \\t\\t\\t\\t\\t\\t: undefined,\\n----\\n\\n# src/api/providers/fetchers/__tests__/openrouter.spec.ts\\n139 | \\t\\t\\t\\tsupportsReasoningEffort: false,\\n140 | \\t\\t\\t\\tsupportedParameters: [\\\"max_tokens\\\", \\\"temperature\\\", \\\"reasoning\\\", \\\"include_reasoning\\\"],\\n141 | \\t\\t\\t})\\n----\\n156 | \\t\\t\\t\\tsupportsReasoningEffort: true,\\n157 | \\t\\t\\t\\tsupportedParameters: [\\\"max_tokens\\\", \\\"temperature\\\", \\\"reasoning\\\", \\\"include_reasoning\\\"],\\n158 | \\t\\t\\t})\\n----\\n\\n# src/api/providers/ollama.ts\\n 79 | \\t\\t\\t\\ttype: \\\"usage\\\",\\n 80 | \\t\\t\\t\\tinputTokens: lastUsage?.prompt_tokens || 0,\\n 81 | \\t\\t\\t\\toutputTokens: lastUsage?.completion_tokens || 0,\\n 82 | \\t\\t\\t}\\n----\\n\\n# src/api/providers/base-provider.ts\\n 21 | \\t/**\\n 22 | \\t * Default token counting implementation using tiktoken.\\n 23 | \\t * Providers can override this to use their native token counting endpoints.\\n 24 | \\t *\\n 25 | \\t * @param content The content to count tokens for\\n 26 | \\t * @returns A promise resolving to the token count\\n 27 | \\t */\\n----\\n\\n# src/api/providers/fetchers/__tests__/litellm.test.ts\\n 41 | \\t\\t\\t\\t\\t\\tmodel_info: {\\n 42 | \\t\\t\\t\\t\\t\\t\\tmax_tokens: 4096,\\n 43 | \\t\\t\\t\\t\\t\\t\\tmax_input_tokens: 200000,\\n 44 | \\t\\t\\t\\t\\t\\t\\tsupports_vision: true,\\n 45 | \\t\\t\\t\\t\\t\\t\\tsupports_prompt_caching: false,\\n 46 | \\t\\t\\t\\t\\t\\t\\tinput_cost_per_token: 0.000003,\\n 47 | \\t\\t\\t\\t\\t\\t\\toutput_cost_per_token: 0.000015,\\n 48 | \\t\\t\\t\\t\\t\\t\\tsupports_computer_use: true,\\n----\\n 56 | \\t\\t\\t\\t\\t\\tmodel_info: {\\n 57 | \\t\\t\\t\\t\\t\\t\\tmax_tokens: 8192,\\n 58 | \\t\\t\\t\\t\\t\\t\\tmax_input_tokens: 128000,\\n 59 | \\t\\t\\t\\t\\t\\t\\tsupports_vision: false,\\n 60 | \\t\\t\\t\\t\\t\\t\\tsupports_prompt_caching: false,\\n 61 | \\t\\t\\t\\t\\t\\t\\tinput_cost_per_token: 0.00001,\\n 62 | \\t\\t\\t\\t\\t\\t\\toutput_cost_per_token: 0.00003,\\n 63 | \\t\\t\\t\\t\\t\\t\\tsupports_computer_use: false,\\n----\\n108 | \\n109 | \\tit(\\\"makes request without authorization header when no API key provided\\\", async () => {\\n110 | \\t\\tconst mockResponse = {\\n----\\n134 | \\t\\t\\t\\t\\t\\tmodel_info: {\\n135 | \\t\\t\\t\\t\\t\\t\\tmax_tokens: 4096,\\n136 | \\t\\t\\t\\t\\t\\t\\tmax_input_tokens: 200000,\\n137 | \\t\\t\\t\\t\\t\\t\\tsupports_vision: true,\\n----\\n146 | \\t\\t\\t\\t\\t\\tmodel_info: {\\n147 | \\t\\t\\t\\t\\t\\t\\tmax_tokens: 4096,\\n148 | \\t\\t\\t\\t\\t\\t\\tmax_input_tokens: 200000,\\n149 | \\t\\t\\t\\t\\t\\t\\tsupports_vision: false,\\n----\\n205 | \\t\\t\\t\\tstatus: 401,\\n206 | \\t\\t\\t\\tstatusText: \\\"Unauthorized\\\",\\n207 | \\t\\t\\t},\\n----\\n214 | \\t\\tawait expect(getLiteLLMModels(DUMMY_INVALID_KEY, \\\"http://localhost:4000\\\")).rejects.toThrow(\\n215 | \\t\\t\\t\\\"Failed to fetch LiteLLM models: 401 Unauthorized. Check base URL and API key.\\\",\\n216 | \\t\\t)\\n----\\n278 | \\t\\t\\t\\t\\t\\tmodel_info: {\\n279 | \\t\\t\\t\\t\\t\\t\\tmax_tokens: 4096,\\n280 | \\t\\t\\t\\t\\t\\t\\tmax_input_tokens: 200000,\\n281 | \\t\\t\\t\\t\\t\\t\\tsupports_vision: true,\\n----\\n291 | \\t\\t\\t\\t\\t\\tmodel_info: {\\n292 | \\t\\t\\t\\t\\t\\t\\tmax_tokens: 8192,\\n293 | \\t\\t\\t\\t\\t\\t\\tmax_input_tokens: 128000,\\n294 | \\t\\t\\t\\t\\t\\t\\tsupports_vision: false,\\n----\\n339 | \\t\\t\\t\\t\\t\\tmodel_info: {\\n340 | \\t\\t\\t\\t\\t\\t\\tmax_tokens: 4096,\\n341 | \\t\\t\\t\\t\\t\\t\\tmax_input_tokens: 200000,\\n342 | \\t\\t\\t\\t\\t\\t\\tsupports_vision: true,\\n----\\n352 | \\t\\t\\t\\t\\t\\tmodel_info: {\\n353 | \\t\\t\\t\\t\\t\\t\\tmax_tokens: 8192,\\n354 | \\t\\t\\t\\t\\t\\t\\tmax_input_tokens: 128000,\\n355 | \\t\\t\\t\\t\\t\\t\\tsupports_vision: false,\\n----\\n365 | \\t\\t\\t\\t\\t\\tmodel_info: {\\n366 | \\t\\t\\t\\t\\t\\t\\tmax_tokens: 8192,\\n367 | \\t\\t\\t\\t\\t\\t\\tmax_input_tokens: 128000,\\n368 | \\t\\t\\t\\t\\t\\t\\tsupports_vision: false,\\n----\\n424 | \\t\\t\\t\\t\\t\\tmodel_info: {\\n425 | \\t\\t\\t\\t\\t\\t\\tmax_tokens: 4096,\\n426 | \\t\\t\\t\\t\\t\\t\\tmax_input_tokens: 200000,\\n427 | \\t\\t\\t\\t\\t\\t\\tsupports_vision: true,\\n----\\n436 | \\t\\t\\t\\t\\t\\tmodel_info: {\\n437 | \\t\\t\\t\\t\\t\\t\\tmax_tokens: 4096,\\n438 | \\t\\t\\t\\t\\t\\t\\tmax_input_tokens: 200000,\\n439 | \\t\\t\\t\\t\\t\\t\\tsupports_vision: true,\\n----\\n448 | \\t\\t\\t\\t\\t\\tmodel_info: {\\n449 | \\t\\t\\t\\t\\t\\t\\tmax_tokens: 4096,\\n450 | \\t\\t\\t\\t\\t\\t\\tmax_input_tokens: 200000,\\n451 | \\t\\t\\t\\t\\t\\t\\tsupports_vision: true,\\n----\\n\\n# src/api/providers/anthropic-vertex.ts\\n 2 | import { AnthropicVertex } from \\\"@anthropic-ai/vertex-sdk\\\"\\n 3 | import { GoogleAuth, JWTInput } from \\\"google-auth-library\\\"\\n 4 | \\n----\\n 41 | \\t\\t\\t\\tgoogleAuth: new GoogleAuth({\\n 42 | \\t\\t\\t\\t\\tscopes: [\\\"https://www.googleapis.com/auth/cloud-platform\\\"],\\n 43 | \\t\\t\\t\\t\\tcredentials: safeJsonParse(this.options.vertexJsonCredentials, undefined),\\n----\\n 50 | \\t\\t\\t\\tgoogleAuth: new GoogleAuth({\\n 51 | \\t\\t\\t\\t\\tscopes: [\\\"https://www.googleapis.com/auth/cloud-platform\\\"],\\n 52 | \\t\\t\\t\\t\\tkeyFile: this.options.vertexKeyFile,\\n----\\n 87 | \\t\\t\\tmodel: id,\\n 88 | \\t\\t\\tmax_tokens: maxTokens ?? ANTHROPIC_DEFAULT_MAX_TOKENS,\\n 89 | \\t\\t\\ttemperature,\\n----\\n107 | \\t\\t\\t\\t\\t\\ttype: \\\"usage\\\",\\n108 | \\t\\t\\t\\t\\t\\tinputTokens: usage.input_tokens || 0,\\n109 | \\t\\t\\t\\t\\t\\toutputTokens: usage.output_tokens || 0,\\n110 | \\t\\t\\t\\t\\t\\tcacheWriteTokens: usage.cache_creation_input_tokens || undefined,\\n111 | \\t\\t\\t\\t\\t\\tcacheReadTokens: usage.cache_read_input_tokens || undefined,\\n112 | \\t\\t\\t\\t\\t}\\n----\\n119 | \\t\\t\\t\\t\\t\\tinputTokens: 0,\\n120 | \\t\\t\\t\\t\\t\\toutputTokens: chunk.usage!.output_tokens || 0,\\n121 | \\t\\t\\t\\t\\t}\\n----\\n189 | \\t\\t\\t\\tmodel: id,\\n190 | \\t\\t\\t\\tmax_tokens: maxTokens,\\n191 | \\t\\t\\t\\ttemperature,\\n----\\n\\n# src/services/mcp/__tests__/McpHub.test.ts\\n 50 | \\t\\t\\tscheme: \\\"file\\\",\\n 51 | \\t\\t\\tauthority: \\\"\\\",\\n 52 | \\t\\t\\tpath: \\\"/test/path\\\",\\n----\\n\\n# src/services/code-index/embedders/openai.ts\\n 52 | \\t\\t\\t\\t\\tconsole.warn(\\n 53 | \\t\\t\\t\\t\\t\\t`Text at index ${i} exceeds maximum token limit (${itemTokens} > ${MAX_ITEM_TOKENS}). Skipping.`,\\n 54 | \\t\\t\\t\\t\\t)\\n----\\n108 | \\t\\t\\t\\t\\tusage: {\\n109 | \\t\\t\\t\\t\\t\\tpromptTokens: response.usage?.prompt_tokens || 0,\\n110 | \\t\\t\\t\\t\\t\\ttotalTokens: response.usage?.total_tokens || 0,\\n111 | \\t\\t\\t\\t\\t},\\n----\\n\\n# src/services/code-index/embedders/openai-compatible.ts\\n 18 | \\tusage?: {\\n 19 | \\t\\tprompt_tokens?: number\\n 20 | \\t\\ttotal_tokens?: number\\n 21 | \\t}\\n----\\n 34 | \\t * @param baseUrl The base URL for the OpenAI-compatible API endpoint\\n 35 | \\t * @param apiKey The API key for authentication\\n 36 | \\t * @param modelId Optional model identifier (defaults to \\\"text-embedding-3-small\\\")\\n----\\n 75 | \\t\\t\\t\\t\\tconsole.warn(\\n 76 | \\t\\t\\t\\t\\t\\t`Text at index ${i} exceeds maximum token limit (${itemTokens} > ${MAX_ITEM_TOKENS}). Skipping.`,\\n 77 | \\t\\t\\t\\t\\t)\\n----\\n156 | \\t\\t\\t\\t\\tusage: {\\n157 | \\t\\t\\t\\t\\t\\tpromptTokens: response.usage?.prompt_tokens || 0,\\n158 | \\t\\t\\t\\t\\t\\ttotalTokens: response.usage?.total_tokens || 0,\\n159 | \\t\\t\\t\\t\\t},\\n----\\n\\n# src/services/marketplace/types.ts\\n 20 | \\tdescription: string\\n 21 | \\tauthor?: string\\n 22 | \\tauthorUrl?: string\\n 23 | \\ttags?: string[]\\n----\\n 45 | \\tdescription: string\\n 46 | \\tauthor?: string\\n 47 | \\tauthorUrl?: string\\n 48 | \\turl: string // Required url field\\n----\\n 62 | \\ttype: MarketplaceItemType\\n 63 | \\tauthor?: string\\n 64 | \\tauthorUrl?: string\\n 65 | \\turl?: string // Optional - only MCPs have url\\n----\\n\\n# src/services/code-index/embedders/__tests__/openai-compatible.spec.ts\\n103 | \\t\\t\\t\\tdata: [{ embedding: [0.1, 0.2, 0.3] }],\\n104 | \\t\\t\\t\\tusage: { prompt_tokens: 10, total_tokens: 15 },\\n105 | \\t\\t\\t}\\n----\\n124 | \\t\\t\\t\\tdata: [{ embedding: [0.1, 0.2, 0.3] }, { embedding: [0.4, 0.5, 0.6] }],\\n125 | \\t\\t\\t\\tusage: { prompt_tokens: 20, total_tokens: 30 },\\n126 | \\t\\t\\t}\\n----\\n149 | \\t\\t\\t\\tdata: [{ embedding: [0.1, 0.2, 0.3] }],\\n150 | \\t\\t\\t\\tusage: { prompt_tokens: 10, total_tokens: 15 },\\n151 | \\t\\t\\t}\\n----\\n194 | \\t\\t\\t\\t\\tdata: [{ embedding: base64String }], // Base64 string instead of array\\n195 | \\t\\t\\t\\t\\tusage: { prompt_tokens: 10, total_tokens: 15 },\\n196 | \\t\\t\\t\\t}\\n----\\n226 | \\t\\t\\t\\t\\tdata: [{ embedding: base64String1 }, { embedding: base64String2 }],\\n227 | \\t\\t\\t\\t\\tusage: { prompt_tokens: 20, total_tokens: 30 },\\n228 | \\t\\t\\t\\t}\\n----\\n253 | \\t\\t\\t\\t\\t],\\n254 | \\t\\t\\t\\t\\tusage: { prompt_tokens: 20, total_tokens: 30 },\\n255 | \\t\\t\\t\\t}\\n----\\n270 | \\t\\t/**\\n271 | \\t\\t * Test batching logic when texts exceed token limits\\n272 | \\t\\t */\\n----\\n283 | \\t\\t\\t\\t\\t],\\n284 | \\t\\t\\t\\t\\tusage: { prompt_tokens: 10, total_tokens: 15 },\\n285 | \\t\\t\\t\\t})\\n----\\n299 | \\t\\t\\t\\t\\tdata: [{ embedding: [0.1, 0.2, 0.3] }, { embedding: [0.4, 0.5, 0.6] }],\\n300 | \\t\\t\\t\\t\\tusage: { prompt_tokens: 10, total_tokens: 15 },\\n301 | \\t\\t\\t\\t}\\n----\\n306 | \\t\\t\\t\\t// Should warn about oversized text\\n307 | \\t\\t\\t\\texpect(console.warn).toHaveBeenCalledWith(expect.stringContaining(\\\"exceeds maximum token limit\\\"))\\n308 | \\n----\\n317 | \\t\\t\\t\\t\\tdata: [{ embedding: [0.1, 0.2, 0.3] }, { embedding: [0.4, 0.5, 0.6] }],\\n318 | \\t\\t\\t\\t\\tusage: { prompt_tokens: 10, total_tokens: 15 },\\n319 | \\t\\t\\t\\t})\\n----\\n354 | \\t\\t\\t\\t\\t\\tdata: [{ embedding: base64String }],\\n355 | \\t\\t\\t\\t\\t\\tusage: { prompt_tokens: 10, total_tokens: 15 },\\n356 | \\t\\t\\t\\t\\t})\\n----\\n375 | \\t\\t\\t\\tconst testTexts = [\\\"Hello world\\\"]\\n376 | \\t\\t\\t\\tconst authError = new Error(\\\"Unauthorized\\\")\\n377 | \\t\\t\\t\\t;(authError as any).status = 401\\n378 | \\n379 | \\t\\t\\t\\tmockEmbeddingsCreate.mockRejectedValue(authError)\\n380 | \\n----\\n452 | \\t\\t\\t\\t\\tdata: null,\\n453 | \\t\\t\\t\\t\\tusage: { prompt_tokens: 10, total_tokens: 15 },\\n454 | \\t\\t\\t\\t}\\n----\\n494 | \\t\\t\\t\\t\\tusage: {\\n495 | \\t\\t\\t\\t\\t\\tprompt_tokens: 2,\\n496 | \\t\\t\\t\\t\\t\\ttotal_tokens: 2,\\n497 | \\t\\t\\t\\t\\t},\\n----\\n\\n# src/services/marketplace/schemas.ts\\n 35 | \\ttype: marketplaceItemTypeSchema,\\n 36 | \\tauthor: z.string().optional(),\\n 37 | \\tauthorUrl: z.string().url(\\\"Author URL must be a valid URL\\\").optional(),\\n 38 | \\ttags: z.array(z.string()).optional(),\\n----\\n 61 | \\tdescription: z.string(),\\n 62 | \\tauthor: z.string().optional(),\\n 63 | \\tauthorUrl: z.string().url().optional(),\\n 64 | \\ttags: z.array(z.string()).optional(),\\n----\\n 72 | \\tdescription: z.string(),\\n 73 | \\tauthor: z.string().optional(),\\n 74 | \\tauthorUrl: z.string().url().optional(),\\n 75 | \\turl: z.string().url(), // Required url field\\n----\\n\\n# src/services/tree-sitter/__tests__/fixtures/sample-embedded_template.ts\\n 35 |
\\n 36 | <% if user.authenticated? %>\\n 37 |

Welcome, <%= user.name %>

\\n----\\n\\n# src/services/tree-sitter/__tests__/parseSourceCodeDefinitions.embedded_template.test.ts\\n 43 | \\tit(\\\"should detect control structures\\\", () => {\\n 44 | \\t\\texpect(parseResult).toMatch(/\\\\d+--\\\\d+ \\\\|\\\\s+<% if user\\\\.authenticated\\\\? %>/)\\n 45 | \\t\\texpect(parseResult).toMatch(/\\\\d+--\\\\d+ \\\\|\\\\s+<% user\\\\.posts\\\\.each do \\\\|post\\\\| %>/)\\n----\\n\\n# src/workers/countTokens.ts\\n 4 | \\n 5 | import { tiktoken } from \\\"../utils/tiktoken\\\"\\n 6 | \\n----\\n 10 | \\ttry {\\n 11 | \\t\\tconst count = await tiktoken(content)\\n 12 | \\t\\treturn { success: true, count }\\n----\\n\\n# src/activate/handleUri.ts\\n 37 | \\t\\t}\\n 38 | \\t\\tcase \\\"/auth/clerk/callback\\\": {\\n 39 | \\t\\t\\tconst code = query.get(\\\"code\\\")\\n----\\n\\n# src/__tests__/dist_assets.test.ts\\n 6 | \\n 7 | \\tdescribe(\\\"tiktoken\\\", () => {\\n 8 | \\t\\tit(\\\"should have tiktoken wasm file\\\", () => {\\n 9 | \\t\\t\\texpect(fs.existsSync(path.join(distPath, \\\"tiktoken_bg.wasm\\\"))).toBe(true)\\n 10 | \\t\\t})\\n----\\n\\n# src/integrations/terminal/ShellIntegrationManager.ts\\n 43 | \\t[ -f \\\"$ZDOTDIR/.zshrc\\\" ] && source \\\"$ZDOTDIR/.zshrc\\\"\\n 44 | \\t[ -f \\\"$ZDOTDIR/.zlogin\\\" ] && source \\\"$ZDOTDIR/.zlogin\\\"\\n 45 | \\t[ \\\"$ZDOTDIR\\\" = \\\"$HOME\\\" ] && unset ZDOTDIR\\n----\\n\\n# src/extension/api.ts\\n219 | \\n220 | \\t\\t\\tcline.on(\\\"taskCompleted\\\", async (_, tokenUsage, toolUsage) => {\\n221 | \\t\\t\\t\\tthis.emit(RooCodeEventName.TaskCompleted, cline.taskId, tokenUsage, toolUsage)\\n222 | \\t\\t\\t\\tthis.taskMap.delete(cline.taskId)\\n----\\n224 | \\t\\t\\t\\tawait this.fileLog(\\n225 | \\t\\t\\t\\t\\t`[${new Date().toISOString()}] taskCompleted -> ${cline.taskId} | ${JSON.stringify(tokenUsage, null, 2)} | ${JSON.stringify(toolUsage, null, 2)}\\\\n`,\\n226 | \\t\\t\\t\\t)\\n----\\n\\n# src/utils/countTokens.ts\\n 4 | import { countTokensResultSchema } from \\\"../workers/types\\\"\\n 5 | import { tiktoken } from \\\"./tiktoken\\\"\\n 6 | \\n----\\n 27 | \\tif (!useWorker || !pool) {\\n 28 | \\t\\treturn tiktoken(content)\\n 29 | \\t}\\n----\\n 42 | \\t\\tconsole.error(error)\\n 43 | \\t\\treturn tiktoken(content)\\n 44 | \\t}\\n----\\n\\n# src/core/checkpoints/index.ts\\n215 | \\t\\t\\t\\tJSON.stringify({\\n216 | \\t\\t\\t\\t\\ttokensIn: totalTokensIn,\\n217 | \\t\\t\\t\\t\\ttokensOut: totalTokensOut,\\n218 | \\t\\t\\t\\t\\tcacheWrites: totalCacheWrites,\\n----\\n\\n# src/core/condense/index.ts\\n 55 | \\tcost: number // The cost of the summarization operation\\n 56 | \\tnewContextTokens?: number // The number of tokens in the context for the next API request\\n 57 | \\terror?: string // Populated iff the operation fails: error message shown to the user on failure (see Task.ts)\\n----\\n 63 | * @param {ApiMessage[]} messages - The conversation messages\\n 64 | * @param {ApiHandler} apiHandler - The API handler to use for token counting.\\n 65 | * @param {string} systemPrompt - The system prompt for API requests, which should be considered in the context token count\\n 66 | * @param {string} taskId - The task ID for the conversation, used for telemetry\\n----\\n 73 | * @param {ApiMessage[]} messages - The conversation messages\\n 74 | * @param {ApiHandler} apiHandler - The API handler to use for token counting (fallback if condensingApiHandler not provided)\\n 75 | * @param {string} systemPrompt - The system prompt for API requests (fallback if customCondensingPrompt not provided)\\n 76 | * @param {string} taskId - The task ID for the conversation, used for telemetry\\n 77 | * @param {number} prevContextTokens - The number of tokens currently in the context, used to ensure we don't grow the context\\n 78 | * @param {boolean} isAutomaticTrigger - Whether the summarization is triggered automatically\\n----\\n186 | \\n187 | \\t// Count the tokens in the context for the next API request\\n188 | \\t// We only estimate the tokens in summaryMesage if outputTokens is 0, otherwise we use outputTokens\\n189 | \\tconst systemPromptMessage: ApiMessage = { role: \\\"user\\\", content: systemPrompt }\\n----\\n\\nLoading...\"}" -} -[2025-06-13T04:59:50.474Z] { - "ts": 1749790789409, - "type": "say", - "say": "text", - "text": "I can see this is a VSCode extension project, but the search results show mostly token-related code rather than authentication/OAuth code. Let me search more specifically for authentication and OAuth related files.", - "partial": false -} -[2025-06-13T04:59:51.246Z] { - "ts": 1749790790474, - "type": "ask", - "ask": "tool", - "text": "{\"tool\":\"searchFiles\",\"path\":\"src\",\"regex\":\"oauth|auth|login|clerk|session|refresh\",\"filePattern\":\"*.ts\",\"content\":\"Found 83 results.\\n\\n# src/services/ripgrep/index.ts\\n101 | \\t\\tconst rgProcess = childProcess.spawn(bin, args)\\n102 | \\t\\t// cross-platform alternative to head, which is ripgrep author's recommendation for limiting output.\\n103 | \\t\\tconst rl = readline.createInterface({\\n----\\n\\n# src/services/browser/BrowserSession.ts\\n543 | \\t\\t\\tconst [width, height] = size.split(\\\",\\\").map(Number)\\n544 | \\t\\t\\tconst session = await page.createCDPSession()\\n545 | \\t\\t\\tawait page.setViewport({ width, height })\\n546 | \\t\\t\\tconst { windowId } = await session.send(\\\"Browser.getWindowForTarget\\\")\\n547 | \\t\\t\\tawait session.send(\\\"Browser.setWindowBounds\\\", {\\n548 | \\t\\t\\t\\tbounds: { width, height },\\n----\\n\\n# src/services/mcp/__tests__/McpHub.test.ts\\n 50 | \\t\\t\\tscheme: \\\"file\\\",\\n 51 | \\t\\t\\tauthority: \\\"\\\",\\n 52 | \\t\\t\\tpath: \\\"/test/path\\\",\\n----\\n\\n# src/services/mcp/McpHub.ts\\n1092 | \\n1093 | \\tpublic async refreshAllConnections(): Promise {\\n1094 | \\t\\tif (this.isConnecting) {\\n1095 | \\t\\t\\tvscode.window.showInformationMessage(t(\\\"mcp:info.already_refreshing\\\"))\\n1096 | \\t\\t\\treturn\\n----\\n1099 | \\t\\tthis.isConnecting = true\\n1100 | \\t\\tvscode.window.showInformationMessage(t(\\\"mcp:info.refreshing_all\\\"))\\n1101 | \\n----\\n1151 | \\n1152 | \\t\\t\\tvscode.window.showInformationMessage(t(\\\"mcp:info.all_refreshed\\\"))\\n1153 | \\t\\t} catch (error) {\\n1154 | \\t\\t\\tthis.showErrorMessage(\\\"Failed to refresh MCP servers\\\", error)\\n1155 | \\t\\t} finally {\\n----\\n1244 | \\n1245 | \\t\\t\\t\\t\\t// Only refresh capabilities if connected\\n1246 | \\t\\t\\t\\t\\tif (connection.server.status === \\\"connected\\\") {\\n----\\n1254 | \\t\\t\\t\\t} catch (error) {\\n1255 | \\t\\t\\t\\t\\tconsole.error(`Failed to refresh capabilities for ${serverName}:`, error)\\n1256 | \\t\\t\\t\\t}\\n----\\n\\n# src/services/code-index/embedders/__tests__/openai-compatible.spec.ts\\n375 | \\t\\t\\t\\tconst testTexts = [\\\"Hello world\\\"]\\n376 | \\t\\t\\t\\tconst authError = new Error(\\\"Unauthorized\\\")\\n377 | \\t\\t\\t\\t;(authError as any).status = 401\\n378 | \\n379 | \\t\\t\\t\\tmockEmbeddingsCreate.mockRejectedValue(authError)\\n380 | \\n----\\n\\n# src/services/code-index/embedders/openai-compatible.ts\\n 34 | \\t * @param baseUrl The base URL for the OpenAI-compatible API endpoint\\n 35 | \\t * @param apiKey The API key for authentication\\n 36 | \\t * @param modelId Optional model identifier (defaults to \\\"text-embedding-3-small\\\")\\n----\\n\\n# src/services/marketplace/schemas.ts\\n 35 | \\ttype: marketplaceItemTypeSchema,\\n 36 | \\tauthor: z.string().optional(),\\n 37 | \\tauthorUrl: z.string().url(\\\"Author URL must be a valid URL\\\").optional(),\\n 38 | \\ttags: z.array(z.string()).optional(),\\n----\\n 61 | \\tdescription: z.string(),\\n 62 | \\tauthor: z.string().optional(),\\n 63 | \\tauthorUrl: z.string().url().optional(),\\n 64 | \\ttags: z.array(z.string()).optional(),\\n----\\n 72 | \\tdescription: z.string(),\\n 73 | \\tauthor: z.string().optional(),\\n 74 | \\tauthorUrl: z.string().url().optional(),\\n 75 | \\turl: z.string().url(), // Required url field\\n----\\n\\n# src/services/tree-sitter/__tests__/fixtures/sample-embedded_template.ts\\n 35 |
\\n 36 | <% if user.authenticated? %>\\n 37 |

Welcome, <%= user.name %>

\\n----\\n\\n# src/services/tree-sitter/__tests__/parseSourceCodeDefinitions.embedded_template.test.ts\\n 43 | \\tit(\\\"should detect control structures\\\", () => {\\n 44 | \\t\\texpect(parseResult).toMatch(/\\\\d+--\\\\d+ \\\\|\\\\s+<% if user\\\\.authenticated\\\\? %>/)\\n 45 | \\t\\texpect(parseResult).toMatch(/\\\\d+--\\\\d+ \\\\|\\\\s+<% user\\\\.posts\\\\.each do \\\\|post\\\\| %>/)\\n----\\n\\n# src/services/marketplace/types.ts\\n 20 | \\tdescription: string\\n 21 | \\tauthor?: string\\n 22 | \\tauthorUrl?: string\\n 23 | \\ttags?: string[]\\n----\\n 45 | \\tdescription: string\\n 46 | \\tauthor?: string\\n 47 | \\tauthorUrl?: string\\n 48 | \\turl: string // Required url field\\n----\\n 62 | \\ttype: MarketplaceItemType\\n 63 | \\tauthor?: string\\n 64 | \\tauthorUrl?: string\\n 65 | \\turl?: string // Optional - only MCPs have url\\n----\\n\\n# src/activate/handleUri.ts\\n 37 | \\t\\t}\\n 38 | \\t\\tcase \\\"/auth/clerk/callback\\\": {\\n 39 | \\t\\t\\tconst code = query.get(\\\"code\\\")\\n----\\n\\n# src/api/providers/bedrock.ts\\n235 | \\t\\t\\t\\tsecretAccessKey: this.options.awsSecretKey,\\n236 | \\t\\t\\t\\t...(this.options.awsSessionToken ? { sessionToken: this.options.awsSessionToken } : {}),\\n237 | \\t\\t\\t}\\n----\\n\\n# src/api/providers/gemini.ts\\n 7 | } from \\\"@google/genai\\\"\\n 8 | import type { JWTInput } from \\\"google-auth-library\\\"\\n 9 | \\n----\\n\\n# src/api/providers/__tests__/anthropic.spec.ts\\n112 | \\t\\t\\texpect(mockAnthropicConstructor.mock.calls[0]![0]!.apiKey).toEqual(\\\"test-api-key\\\")\\n113 | \\t\\t\\texpect(mockAnthropicConstructor.mock.calls[0]![0]!.authToken).toBeUndefined()\\n114 | \\t\\t})\\n----\\n123 | \\t\\t\\texpect(mockAnthropicConstructor.mock.calls[0]![0]!.apiKey).toEqual(\\\"test-api-key\\\")\\n124 | \\t\\t\\texpect(mockAnthropicConstructor.mock.calls[0]![0]!.authToken).toBeUndefined()\\n125 | \\t\\t})\\n126 | \\n127 | \\t\\tit(\\\"use authToken for passing token if both of anthropicBaseUrl and anthropicUseAuthToken are set\\\", () => {\\n128 | \\t\\t\\tconst customBaseUrl = \\\"https://custom.anthropic.com\\\"\\n----\\n135 | \\t\\t\\texpect(mockAnthropicConstructor).toHaveBeenCalledTimes(1)\\n136 | \\t\\t\\texpect(mockAnthropicConstructor.mock.calls[0]![0]!.authToken).toEqual(\\\"test-api-key\\\")\\n137 | \\t\\t\\texpect(mockAnthropicConstructor.mock.calls[0]![0]!.apiKey).toBeUndefined()\\n----\\n\\n# src/api/providers/anthropic.ts\\n 29 | \\t\\tconst apiKeyFieldName =\\n 30 | \\t\\t\\tthis.options.anthropicBaseUrl && this.options.anthropicUseAuthToken ? \\\"authToken\\\" : \\\"apiKey\\\"\\n 31 | \\n----\\n\\n# src/api/providers/fetchers/__tests__/litellm.test.ts\\n108 | \\n109 | \\tit(\\\"makes request without authorization header when no API key provided\\\", async () => {\\n110 | \\t\\tconst mockResponse = {\\n----\\n205 | \\t\\t\\t\\tstatus: 401,\\n206 | \\t\\t\\t\\tstatusText: \\\"Unauthorized\\\",\\n207 | \\t\\t\\t},\\n----\\n214 | \\t\\tawait expect(getLiteLLMModels(DUMMY_INVALID_KEY, \\\"http://localhost:4000\\\")).rejects.toThrow(\\n215 | \\t\\t\\t\\\"Failed to fetch LiteLLM models: 401 Unauthorized. Check base URL and API key.\\\",\\n216 | \\t\\t)\\n----\\n\\n# src/api/providers/anthropic-vertex.ts\\n 2 | import { AnthropicVertex } from \\\"@anthropic-ai/vertex-sdk\\\"\\n 3 | import { GoogleAuth, JWTInput } from \\\"google-auth-library\\\"\\n 4 | \\n----\\n 41 | \\t\\t\\t\\tgoogleAuth: new GoogleAuth({\\n 42 | \\t\\t\\t\\t\\tscopes: [\\\"https://www.googleapis.com/auth/cloud-platform\\\"],\\n 43 | \\t\\t\\t\\t\\tcredentials: safeJsonParse(this.options.vertexJsonCredentials, undefined),\\n----\\n 50 | \\t\\t\\t\\tgoogleAuth: new GoogleAuth({\\n 51 | \\t\\t\\t\\t\\tscopes: [\\\"https://www.googleapis.com/auth/cloud-platform\\\"],\\n 52 | \\t\\t\\t\\t\\tkeyFile: this.options.vertexKeyFile,\\n----\\n\\n# src/shared/modes.ts\\n 81 | \\t\\tcustomInstructions:\\n 82 | \\t\\t\\t\\\"1. Do some information gathering (for example using read_file or search_files) to get more context about the task.\\\\n\\\\n2. You should also ask the user clarifying questions to get a better understanding of the task.\\\\n\\\\n3. Once you've gained more context about the user's request, you should create a detailed plan for how to accomplish the task. Include Mermaid diagrams if they help make your plan clearer.\\\\n\\\\n4. Ask the user if they are pleased with this plan, or if they would like to make any c [truncated...]\\n 83 | \\t},\\n----\\n\\n# src/utils/git.ts\\n 11 | \\tsubject: string\\n 12 | \\tauthor: string\\n 13 | \\tdate: string\\n----\\n 57 | \\t\\t\\tconst { stdout: hashStdout } = await execAsync(\\n 58 | \\t\\t\\t\\t`git log -n 10 --format=\\\"%H%n%h%n%s%n%an%n%ad\\\" --date=short ` + `--author-date-order ${query}`,\\n 59 | \\t\\t\\t\\t{ cwd },\\n----\\n 79 | \\t\\t\\t\\tsubject: lines[i + 2],\\n 80 | \\t\\t\\t\\tauthor: lines[i + 3],\\n 81 | \\t\\t\\t\\tdate: lines[i + 4],\\n----\\n107 | \\t\\t})\\n108 | \\t\\tconst [fullHash, shortHash, subject, author, date, body] = info.trim().split(\\\"\\\\n\\\")\\n109 | \\n----\\n115 | \\t\\t\\t`Commit: ${shortHash} (${fullHash})`,\\n116 | \\t\\t\\t`Author: ${author}`,\\n117 | \\t\\t\\t`Date: ${date}`,\\n----\\n\\n# src/utils/__tests__/git.spec.ts\\n 98 | \\t\\t\\t\\tsubject: \\\"fix: test commit\\\",\\n 99 | \\t\\t\\t\\tauthor: \\\"John Doe\\\",\\n100 | \\t\\t\\t\\tdate: \\\"2024-01-06\\\",\\n----\\n162 | \\t\\t\\t\\t[\\n163 | \\t\\t\\t\\t\\t'git log -n 10 --format=\\\"%H%n%h%n%s%n%an%n%ad\\\" --date=short --author-date-order abc123',\\n164 | \\t\\t\\t\\t\\t{ stdout: mockCommitData, stderr: \\\"\\\" },\\n----\\n185 | \\t\\t\\t\\tsubject: \\\"fix: test commit\\\",\\n186 | \\t\\t\\t\\tauthor: \\\"John Doe\\\",\\n187 | \\t\\t\\t\\tdate: \\\"2024-01-06\\\",\\n----\\n\\n# src/utils/logging/CompactTransport.ts\\n 36 | export class CompactTransport implements ICompactTransport {\\n 37 | \\tprivate sessionStart: number\\n 38 | \\tprivate lastTimestamp: number\\n----\\n 46 | \\tconstructor(readonly config: CompactTransportConfig = DEFAULT_CONFIG) {\\n 47 | \\t\\tthis.sessionStart = Date.now()\\n 48 | \\t\\tthis.lastTimestamp = this.sessionStart\\n 49 | \\n----\\n 55 | \\t/**\\n 56 | \\t * Ensures the log file is initialized with proper directory structure and session start marker\\n 57 | \\t * @private\\n----\\n 66 | \\n 67 | \\t\\t\\tconst sessionStart = {\\n 68 | \\t\\t\\t\\tt: 0,\\n 69 | \\t\\t\\t\\tl: \\\"info\\\",\\n 70 | \\t\\t\\t\\tm: \\\"Log session started\\\",\\n 71 | \\t\\t\\t\\td: { timestamp: new Date(this.sessionStart).toISOString() },\\n 72 | \\t\\t\\t}\\n 73 | \\t\\t\\twriteFileSync(this.filePath, JSON.stringify(sessionStart) + \\\"\\\\n\\\", { flag: \\\"w\\\" })\\n 74 | \\n----\\n108 | \\t/**\\n109 | \\t * Closes the transport and writes session end marker\\n110 | \\t */\\n----\\n112 | \\t\\tif (this.filePath && this.initialized) {\\n113 | \\t\\t\\tconst sessionEnd = {\\n114 | \\t\\t\\t\\tt: Date.now() - this.lastTimestamp,\\n115 | \\t\\t\\t\\tl: \\\"info\\\",\\n116 | \\t\\t\\t\\tm: \\\"Log session ended\\\",\\n117 | \\t\\t\\t\\td: { timestamp: new Date().toISOString() },\\n118 | \\t\\t\\t}\\n119 | \\t\\t\\twriteFileSync(this.filePath, JSON.stringify(sessionEnd) + \\\"\\\\n\\\", { flag: \\\"a\\\" })\\n120 | \\t\\t}\\n----\\n\\n# src/utils/logging/__tests__/CompactTransport.spec.ts\\n 73 | \\t\\t\\t\\tl: \\\"info\\\",\\n 74 | \\t\\t\\t\\tm: \\\"Log session started\\\",\\n 75 | \\t\\t\\t})\\n----\\n102 | \\n103 | \\t\\ttest(\\\"writes session end marker on close\\\", () => {\\n104 | \\t\\t\\ttransport.write({\\n----\\n117 | \\t\\t\\t\\tl: \\\"info\\\",\\n118 | \\t\\t\\t\\tm: \\\"Log session ended\\\",\\n119 | \\t\\t\\t})\\n----\\n171 | \\t\\t\\tconst lines = fileContent.trim().split(\\\"\\\\n\\\")\\n172 | \\t\\t\\t// +1 for session start line\\n173 | \\t\\t\\texpect(lines.length).toBe(entries.length + 1)\\n----\\n\\n# src/shared/context-mentions.ts\\n 72 | \\tsubject: string\\n 73 | \\tauthor: string\\n 74 | \\tdate: string\\n----\\n 80 | \\tsubject: string\\n 81 | \\tauthor: string\\n 82 | \\tdate: string\\n----\\n 86 | \\t\\tlabel: commit.subject,\\n 87 | \\t\\tdescription: `${commit.shortHash} by ${commit.author} on ${commit.date}`,\\n 88 | \\t\\tvalue: commit.hash,\\n----\\n 92 | \\t\\tsubject: commit.subject,\\n 93 | \\t\\tauthor: commit.author,\\n 94 | \\t\\tdate: commit.date,\\n----\\n\\n# src/shared/ExtensionMessage.ts\\n 71 | \\t\\t| \\\"vsCodeSetting\\\"\\n 72 | \\t\\t| \\\"authenticatedUser\\\"\\n 73 | \\t\\t| \\\"condenseTaskContextResponse\\\"\\n----\\n\\n# src/shared/WebviewMessage.ts\\n 85 | \\t\\t| \\\"restartMcpServer\\\"\\n 86 | \\t\\t| \\\"refreshAllMcpServers\\\"\\n 87 | \\t\\t| \\\"toggleToolAlwaysAllow\\\"\\n----\\n\\n# src/integrations/terminal/TerminalRegistry.ts\\n 14 | // too many terminals, we need to keep track of terminals through the life of\\n 15 | // the extension, as well as session specific terminals for the life of a task\\n 16 | // (to get latest unretrieved output).\\n----\\n\\n# src/integrations/terminal/ShellIntegrationManager.ts\\n 43 | \\t[ -f \\\"$ZDOTDIR/.zshrc\\\" ] && source \\\"$ZDOTDIR/.zshrc\\\"\\n 44 | \\t[ -f \\\"$ZDOTDIR/.zlogin\\\" ] && source \\\"$ZDOTDIR/.zlogin\\\"\\n 45 | \\t[ \\\"$ZDOTDIR\\\" = \\\"$HOME\\\" ] && unset ZDOTDIR\\n----\\n\\n# src/core/webview/ClineProvider.ts\\n497 | \\n498 | \\t\\t// If the extension is starting a new session, clear previous task state.\\n499 | \\t\\tawait this.removeClineFromStack()\\n----\\n656 | \\t\\t\\t\\n 663 | \\t\\t`\\n 664 | \\n 665 | \\t\\tconst csp = [\\n 666 | \\t\\t\\t\\\"default-src 'none'\\\",\\n 667 | \\t\\t\\t`font-src ${webview.cspSource}`,\\n 668 | \\t\\t\\t`style-src ${webview.cspSource} 'unsafe-inline' https://* http://${localServerUrl} http://0.0.0.0:${localPort}`,\\n 669 | \\t\\t\\t`img-src ${webview.cspSource} https://storage.googleapis.com https://img.clerk.com data:`,\\n 670 | \\t\\t\\t`media-src ${webview.cspSource}`,\\n 671 | \\t\\t\\t`script-src 'unsafe-eval' ${webview.cspSource} https://* https://*.posthog.com http://${localServerUrl} http://0.0.0.0:${localPort} 'nonce-${nonce}'`,\\n 672 | \\t\\t\\t`connect-src https://* https://*.posthog.com ws://${localServerUrl} ws://0.0.0.0:${localPort} http://${localServerUrl} http://0.0.0.0:${localPort}`,\\n 673 | \\t\\t]\\n 674 | \\n 675 | \\t\\treturn /*html*/ `\\n 676 | \\t\\t\\t\\n 677 | \\t\\t\\t\\n 678 | \\t\\t\\t\\t\\n 679 | \\t\\t\\t\\t\\t\\n 680 | \\t\\t\\t\\t\\t\\n 681 | \\t\\t\\t\\t\\t\\n 682 | \\t\\t\\t\\t\\t\\n 683 | \\t\\t\\t\\t\\t\\n 684 | \\t\\t\\t\\t\\t\\n 689 | \\t\\t\\t\\t\\tRoo Code\\n 690 | \\t\\t\\t\\t\\n 691 | \\t\\t\\t\\t\\n 692 | \\t\\t\\t\\t\\t
\\n 693 | \\t\\t\\t\\t\\t${reactRefresh}\\n 694 | \\t\\t\\t\\t\\t\\n 695 | \\t\\t\\t\\t\\n 696 | \\t\\t\\t\\n 697 | \\t\\t`\\n 698 | \\t}\\n 699 | \\n 700 | \\t/**\\n 701 | \\t * Defines and returns the HTML that should be rendered within the webview panel.\\n 702 | \\t *\\n 703 | \\t * @remarks This is also the place where references to the React webview build files\\n 704 | \\t * are created and inserted into the webview HTML.\\n 705 | \\t *\\n 706 | \\t * @param webview A reference to the extension webview\\n 707 | \\t * @param extensionUri The URI of the directory containing the extension\\n 708 | \\t * @returns A template string literal containing the HTML that should be\\n 709 | \\t * rendered within the webview panel\\n 710 | \\t */\\n 711 | \\tprivate getHtmlContent(webview: vscode.Webview): string {\\n 712 | \\t\\t// Get the local path to main script run in the webview,\\n 713 | \\t\\t// then convert it to a uri we can use in the webview.\\n 714 | \\n 715 | \\t\\t// The CSS file from the React build output\\n 716 | \\t\\tconst stylesUri = getUri(webview, this.contextProxy.extensionUri, [\\n 717 | \\t\\t\\t\\\"webview-ui\\\",\\n 718 | \\t\\t\\t\\\"build\\\",\\n 719 | \\t\\t\\t\\\"assets\\\",\\n 720 | \\t\\t\\t\\\"index.css\\\",\\n 721 | \\t\\t])\\n 722 | \\n 723 | \\t\\tconst scriptUri = getUri(webview, this.contextProxy.extensionUri, [\\\"webview-ui\\\", \\\"build\\\", \\\"assets\\\", \\\"index.js\\\"])\\n 724 | \\t\\tconst codiconsUri = getUri(webview, this.contextProxy.extensionUri, [\\\"assets\\\", \\\"codicons\\\", \\\"codicon.css\\\"])\\n 725 | \\t\\tconst materialIconsUri = getUri(webview, this.contextProxy.extensionUri, [\\n 726 | \\t\\t\\t\\\"assets\\\",\\n 727 | \\t\\t\\t\\\"vscode-material-icons\\\",\\n 728 | \\t\\t\\t\\\"icons\\\",\\n 729 | \\t\\t])\\n 730 | \\t\\tconst imagesUri = getUri(webview, this.contextProxy.extensionUri, [\\\"assets\\\", \\\"images\\\"])\\n 731 | \\t\\tconst audioUri = getUri(webview, this.contextProxy.extensionUri, [\\\"webview-ui\\\", \\\"audio\\\"])\\n 732 | \\n 733 | \\t\\t// Use a nonce to only allow a specific script to be run.\\n 734 | \\t\\t/*\\n 735 | \\t\\tcontent security policy of your webview to only allow scripts that have a specific nonce\\n 736 | \\t\\tcreate a content security policy meta tag so that only loading scripts with a nonce is allowed\\n 737 | \\t\\tAs your extension grows you will likely want to add custom styles, fonts, and/or images to your webview. If you do, you will need to update the content security policy meta tag to explicitly allow for these resources. E.g.\\n 738 | \\t\\t\\t\\t\\n 739 | \\t\\t- 'unsafe-inline' is required for styles due to vscode-webview-toolkit's dynamic style injection\\n 740 | \\t\\t- since we pass base64 images to the webview, we need to specify img-src ${webview.cspSource} data:;\\n 741 | \\n 742 | \\t\\tin meta tag we add nonce attribute: A cryptographic nonce (only used once) to allow scripts. The server must generate a unique nonce value each time it transmits a policy. It is critical to provide a nonce that cannot be guessed as bypassing a resource's policy is otherwise trivial.\\n 743 | \\t\\t*/\\n 744 | \\t\\tconst nonce = getNonce()\\n 745 | \\n 746 | \\t\\t// Tip: Install the es6-string-html VS Code extension to enable code highlighting below\\n 747 | \\t\\treturn /*html*/ `\\n 748 | \\n 749 | \\n 750 | \\n 751 | \\n 752 | \\n 753 | \\n 754 | \\n 755 | \\n 756 | \\t\\t\\t\\n 757 | \\t\\t\\t\\n 762 | Roo Code\\n 763 | \\n 764 | \\n 765 | \\n 766 |
\\n 767 | \\n 768 | \\n 769 | \\n 770 | `\\n 771 | \\t}\\n 772 | \\n 773 | \\t/**\\n 774 | \\t * Sets up an event listener to listen for messages passed from the webview context and\\n 775 | \\t * executes code based on the message that is received.\\n 776 | \\t *\\n 777 | \\t * @param webview A reference to the extension webview\\n 778 | \\t */\\n 779 | \\tprivate setWebviewMessageListener(webview: vscode.Webview) {\\n 780 | \\t\\tconst onReceiveMessage = async (message: WebviewMessage) =>\\n 781 | \\t\\t\\twebviewMessageHandler(this, message, this.marketplaceManager)\\n 782 | \\n 783 | \\t\\tconst messageDisposable = webview.onDidReceiveMessage(onReceiveMessage)\\n 784 | \\t\\tthis.webviewDisposables.push(messageDisposable)\\n 785 | \\t}\\n 786 | \\n 787 | \\t/**\\n 788 | \\t * Handle switching to a new mode, including updating the associated API configuration\\n 789 | \\t * @param newMode The mode to switch to\\n 790 | \\t */\\n 791 | \\tpublic async handleModeSwitch(newMode: Mode) {\\n 792 | \\t\\tconst cline = this.getCurrentCline()\\n 793 | \\n 794 | \\t\\tif (cline) {\\n 795 | \\t\\t\\tTelemetryService.instance.captureModeSwitch(cline.taskId, newMode)\\n 796 | \\t\\t\\tcline.emit(\\\"taskModeSwitched\\\", cline.taskId, newMode)\\n 797 | \\t\\t}\\n 798 | \\n 799 | \\t\\tawait this.updateGlobalState(\\\"mode\\\", newMode)\\n 800 | \\n 801 | \\t\\t// Load the saved API config for the new mode if it exists\\n 802 | \\t\\tconst savedConfigId = await this.providerSettingsManager.getModeConfigId(newMode)\\n 803 | \\t\\tconst listApiConfig = await this.providerSettingsManager.listConfig()\\n 804 | \\n 805 | \\t\\t// Update listApiConfigMeta first to ensure UI has latest data\\n 806 | \\t\\tawait this.updateGlobalState(\\\"listApiConfigMeta\\\", listApiConfig)\\n 807 | \\n 808 | \\t\\t// If this mode has a saved config, use it.\\n 809 | \\t\\tif (savedConfigId) {\\n 810 | \\t\\t\\tconst profile = listApiConfig.find(({ id }) => id === savedConfigId)\\n 811 | \\n 812 | \\t\\t\\tif (profile?.name) {\\n 813 | \\t\\t\\t\\tawait this.activateProviderProfile({ name: profile.name })\\n 814 | \\t\\t\\t}\\n 815 | \\t\\t} else {\\n 816 | \\t\\t\\t// If no saved config for this mode, save current config as default.\\n 817 | \\t\\t\\tconst currentApiConfigName = this.getGlobalState(\\\"currentApiConfigName\\\")\\n 818 | \\n 819 | \\t\\t\\tif (currentApiConfigName) {\\n 820 | \\t\\t\\t\\tconst config = listApiConfig.find((c) => c.name === currentApiConfigName)\\n 821 | \\n 822 | \\t\\t\\t\\tif (config?.id) {\\n 823 | \\t\\t\\t\\t\\tawait this.providerSettingsManager.setModeConfig(newMode, config.id)\\n 824 | \\t\\t\\t\\t}\\n 825 | \\t\\t\\t}\\n 826 | \\t\\t}\\n 827 | \\n 828 | \\t\\tawait this.postStateToWebview()\\n 829 | \\t}\\n 830 | \\n 831 | \\t// Provider Profile Management\\n 832 | \\n 833 | \\tgetProviderProfileEntries(): ProviderSettingsEntry[] {\\n 834 | \\t\\treturn this.contextProxy.getValues().listApiConfigMeta || []\\n 835 | \\t}\\n 836 | \\n 837 | \\tgetProviderProfileEntry(name: string): ProviderSettingsEntry | undefined {\\n 838 | \\t\\treturn this.getProviderProfileEntries().find((profile) => profile.name === name)\\n 839 | \\t}\\n 840 | \\n 841 | \\tpublic hasProviderProfileEntry(name: string): boolean {\\n 842 | \\t\\treturn !!this.getProviderProfileEntry(name)\\n 843 | \\t}\\n 844 | \\n 845 | \\tasync upsertProviderProfile(\\n 846 | \\t\\tname: string,\\n 847 | \\t\\tproviderSettings: ProviderSettings,\\n 848 | \\t\\tactivate: boolean = true,\\n 849 | \\t): Promise {\\n 850 | \\t\\ttry {\\n 851 | \\t\\t\\t// TODO: Do we need to be calling `activateProfile`? It's not\\n 852 | \\t\\t\\t// clear to me what the source of truth should be; in some cases\\n 853 | \\t\\t\\t// we rely on the `ContextProxy`'s data store and in other cases\\n 854 | \\t\\t\\t// we rely on the `ProviderSettingsManager`'s data store. It might\\n 855 | \\t\\t\\t// be simpler to unify these two.\\n 856 | \\t\\t\\tconst id = await this.providerSettingsManager.saveConfig(name, providerSettings)\\n 857 | \\n 858 | \\t\\t\\tif (activate) {\\n 859 | \\t\\t\\t\\tconst { mode } = await this.getState()\\n 860 | \\n 861 | \\t\\t\\t\\t// These promises do the following:\\n 862 | \\t\\t\\t\\t// 1. Adds or updates the list of provider profiles.\\n 863 | \\t\\t\\t\\t// 2. Sets the current provider profile.\\n 864 | \\t\\t\\t\\t// 3. Sets the current mode's provider profile.\\n 865 | \\t\\t\\t\\t// 4. Copies the provider settings to the context.\\n 866 | \\t\\t\\t\\t//\\n 867 | \\t\\t\\t\\t// Note: 1, 2, and 4 can be done in one `ContextProxy` call:\\n 868 | \\t\\t\\t\\t// this.contextProxy.setValues({ ...providerSettings, listApiConfigMeta: ..., currentApiConfigName: ... })\\n 869 | \\t\\t\\t\\t// We should probably switch to that and verify that it works.\\n 870 | \\t\\t\\t\\t// I left the original implementation in just to be safe.\\n 871 | \\t\\t\\t\\tawait Promise.all([\\n 872 | \\t\\t\\t\\t\\tthis.updateGlobalState(\\\"listApiConfigMeta\\\", await this.providerSettingsManager.listConfig()),\\n 873 | \\t\\t\\t\\t\\tthis.updateGlobalState(\\\"currentApiConfigName\\\", name),\\n 874 | \\t\\t\\t\\t\\tthis.providerSettingsManager.setModeConfig(mode, id),\\n 875 | \\t\\t\\t\\t\\tthis.contextProxy.setProviderSettings(providerSettings),\\n 876 | \\t\\t\\t\\t])\\n 877 | \\n 878 | \\t\\t\\t\\t// Notify CodeIndexManager about the settings change\\n 879 | \\t\\t\\t\\tif (this.codeIndexManager) {\\n 880 | \\t\\t\\t\\t\\tawait this.codeIndexManager.handleExternalSettingsChange()\\n 881 | \\t\\t\\t\\t}\\n 882 | \\n 883 | \\t\\t\\t\\t// Change the provider for the current task.\\n 884 | \\t\\t\\t\\t// TODO: We should rename `buildApiHandler` for clarity (e.g. `getProviderClient`).\\n 885 | \\t\\t\\t\\tconst task = this.getCurrentCline()\\n 886 | \\n 887 | \\t\\t\\t\\tif (task) {\\n 888 | \\t\\t\\t\\t\\ttask.api = buildApiHandler(providerSettings)\\n 889 | \\t\\t\\t\\t}\\n 890 | \\t\\t\\t} else {\\n 891 | \\t\\t\\t\\tawait this.updateGlobalState(\\\"listApiConfigMeta\\\", await this.providerSettingsManager.listConfig())\\n 892 | \\t\\t\\t}\\n 893 | \\n 894 | \\t\\t\\tawait this.postStateToWebview()\\n 895 | \\t\\t\\treturn id\\n 896 | \\t\\t} catch (error) {\\n 897 | \\t\\t\\tthis.log(\\n 898 | \\t\\t\\t\\t`Error create new api configuration: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`,\\n 899 | \\t\\t\\t)\\n 900 | \\n 901 | \\t\\t\\tvscode.window.showErrorMessage(t(\\\"common:errors.create_api_config\\\"))\\n 902 | \\t\\t\\treturn undefined\\n 903 | \\t\\t}\\n 904 | \\t}\\n 905 | \\n 906 | \\tasync deleteProviderProfile(profileToDelete: ProviderSettingsEntry) {\\n 907 | \\t\\tconst globalSettings = this.contextProxy.getValues()\\n 908 | \\t\\tlet profileToActivate: string | undefined = globalSettings.currentApiConfigName\\n 909 | \\n 910 | \\t\\tif (profileToDelete.name === profileToActivate) {\\n 911 | \\t\\t\\tprofileToActivate = this.getProviderProfileEntries().find(({ name }) => name !== profileToDelete.name)?.name\\n 912 | \\t\\t}\\n 913 | \\n 914 | \\t\\tif (!profileToActivate) {\\n 915 | \\t\\t\\tthrow new Error(\\\"You cannot delete the last profile\\\")\\n 916 | \\t\\t}\\n 917 | \\n 918 | \\t\\tconst entries = this.getProviderProfileEntries().filter(({ name }) => name !== profileToDelete.name)\\n 919 | \\n 920 | \\t\\tawait this.contextProxy.setValues({\\n 921 | \\t\\t\\t...globalSettings,\\n 922 | \\t\\t\\tcurrentApiConfigName: profileToActivate,\\n 923 | \\t\\t\\tlistApiConfigMeta: entries,\\n 924 | \\t\\t})\\n 925 | \\n 926 | \\t\\tawait this.postStateToWebview()\\n 927 | \\t}\\n 928 | \\n 929 | \\tasync activateProviderProfile(args: { name: string } | { id: string }) {\\n 930 | \\t\\tconst { name, id, ...providerSettings } = await this.providerSettingsManager.activateProfile(args)\\n 931 | \\n 932 | \\t\\t// See `upsertProviderProfile` for a description of what this is doing.\\n 933 | \\t\\tawait Promise.all([\\n 934 | \\t\\t\\tthis.contextProxy.setValue(\\\"listApiConfigMeta\\\", await this.providerSettingsManager.listConfig()),\\n 935 | \\t\\t\\tthis.contextProxy.setValue(\\\"currentApiConfigName\\\", name),\\n 936 | \\t\\t\\tthis.contextProxy.setProviderSettings(providerSettings),\\n 937 | \\t\\t])\\n 938 | \\n 939 | \\t\\tconst { mode } = await this.getState()\\n 940 | \\n 941 | \\t\\tif (id) {\\n 942 | \\t\\t\\tawait this.providerSettingsManager.setModeConfig(mode, id)\\n 943 | \\t\\t}\\n 944 | \\n 945 | \\t\\t// Change the provider for the current task.\\n 946 | \\t\\tconst task = this.getCurrentCline()\\n 947 | \\n 948 | \\t\\tif (task) {\\n 949 | \\t\\t\\ttask.api = buildApiHandler(providerSettings)\\n 950 | \\t\\t}\\n 951 | \\n 952 | \\t\\tawait this.postStateToWebview()\\n 953 | \\t}\\n 954 | \\n 955 | \\t// Task Management\\n 956 | \\n 957 | \\tasync cancelTask() {\\n 958 | \\t\\tconst cline = this.getCurrentCline()\\n 959 | \\n 960 | \\t\\tif (!cline) {\\n 961 | \\t\\t\\treturn\\n 962 | \\t\\t}\\n 963 | \\n 964 | \\t\\tconsole.log(`[subtasks] cancelling task ${cline.taskId}.${cline.instanceId}`)\\n 965 | \\n 966 | \\t\\tconst { historyItem } = await this.getTaskWithId(cline.taskId)\\n 967 | \\t\\t// Preserve parent and root task information for history item.\\n 968 | \\t\\tconst rootTask = cline.rootTask\\n 969 | \\t\\tconst parentTask = cline.parentTask\\n 970 | \\n 971 | \\t\\tcline.abortTask()\\n 972 | \\n 973 | \\t\\tawait pWaitFor(\\n 974 | \\t\\t\\t() =>\\n 975 | \\t\\t\\t\\tthis.getCurrentCline()! === undefined ||\\n 976 | \\t\\t\\t\\tthis.getCurrentCline()!.isStreaming === false ||\\n 977 | \\t\\t\\t\\tthis.getCurrentCline()!.didFinishAbortingStream ||\\n 978 | \\t\\t\\t\\t// If only the first chunk is processed, then there's no\\n 979 | \\t\\t\\t\\t// need to wait for graceful abort (closes edits, browser,\\n 980 | \\t\\t\\t\\t// etc).\\n 981 | \\t\\t\\t\\tthis.getCurrentCline()!.isWaitingForFirstChunk,\\n 982 | \\t\\t\\t{\\n 983 | \\t\\t\\t\\ttimeout: 3_000,\\n 984 | \\t\\t\\t},\\n 985 | \\t\\t).catch(() => {\\n 986 | \\t\\t\\tconsole.error(\\\"Failed to abort task\\\")\\n 987 | \\t\\t})\\n 988 | \\n 989 | \\t\\tif (this.getCurrentCline()) {\\n 990 | \\t\\t\\t// 'abandoned' will prevent this Cline instance from affecting\\n 991 | \\t\\t\\t// future Cline instances. This may happen if its hanging on a\\n 992 | \\t\\t\\t// streaming request.\\n 993 | \\t\\t\\tthis.getCurrentCline()!.abandoned = true\\n 994 | \\t\\t}\\n 995 | \\n 996 | \\t\\t// Clears task again, so we need to abortTask manually above.\\n 997 | \\t\\tawait this.initClineWithHistoryItem({ ...historyItem, rootTask, parentTask })\\n 998 | \\t}\\n 999 | \\n1000 | \\tasync updateCustomInstructions(instructions?: string) {\\n1001 | \\t\\t// User may be clearing the field.\\n1002 | \\t\\tawait this.updateGlobalState(\\\"customInstructions\\\", instructions || undefined)\\n1003 | \\t\\tawait this.postStateToWebview()\\n1004 | \\t}\\n1005 | \\n1006 | \\t// MCP\\n1007 | \\n1008 | \\tasync ensureMcpServersDirectoryExists(): Promise {\\n1009 | \\t\\t// Get platform-specific application data directory\\n1010 | \\t\\tlet mcpServersDir: string\\n1011 | \\t\\tif (process.platform === \\\"win32\\\") {\\n1012 | \\t\\t\\t// Windows: %APPDATA%\\\\Roo-Code\\\\MCP\\n1013 | \\t\\t\\tmcpServersDir = path.join(os.homedir(), \\\"AppData\\\", \\\"Roaming\\\", \\\"Roo-Code\\\", \\\"MCP\\\")\\n1014 | \\t\\t} else if (process.platform === \\\"darwin\\\") {\\n1015 | \\t\\t\\t// macOS: ~/Documents/Cline/MCP\\n1016 | \\t\\t\\tmcpServersDir = path.join(os.homedir(), \\\"Documents\\\", \\\"Cline\\\", \\\"MCP\\\")\\n1017 | \\t\\t} else {\\n1018 | \\t\\t\\t// Linux: ~/.local/share/Cline/MCP\\n1019 | \\t\\t\\tmcpServersDir = path.join(os.homedir(), \\\".local\\\", \\\"share\\\", \\\"Roo-Code\\\", \\\"MCP\\\")\\n1020 | \\t\\t}\\n1021 | \\n1022 | \\t\\ttry {\\n1023 | \\t\\t\\tawait fs.mkdir(mcpServersDir, { recursive: true })\\n1024 | \\t\\t} catch (error) {\\n1025 | \\t\\t\\t// Fallback to a relative path if directory creation fails\\n1026 | \\t\\t\\treturn path.join(os.homedir(), \\\".roo-code\\\", \\\"mcp\\\")\\n1027 | \\t\\t}\\n1028 | \\t\\treturn mcpServersDir\\n1029 | \\t}\\n1030 | \\n1031 | \\tasync ensureSettingsDirectoryExists(): Promise {\\n1032 | \\t\\tconst { getSettingsDirectoryPath } = await import(\\\"../../utils/storage\\\")\\n1033 | \\t\\tconst globalStoragePath = this.contextProxy.globalStorageUri.fsPath\\n1034 | \\t\\treturn getSettingsDirectoryPath(globalStoragePath)\\n1035 | \\t}\\n1036 | \\n1037 | \\t// OpenRouter\\n1038 | \\n1039 | \\tasync handleOpenRouterCallback(code: string) {\\n1040 | \\t\\tlet { apiConfiguration, currentApiConfigName } = await this.getState()\\n1041 | \\n1042 | \\t\\tlet apiKey: string\\n1043 | \\t\\ttry {\\n1044 | \\t\\t\\tconst baseUrl = apiConfiguration.openRouterBaseUrl || \\\"https://openrouter.ai/api/v1\\\"\\n1045 | \\t\\t\\t// Extract the base domain for the auth endpoint\\n1046 | \\t\\t\\tconst baseUrlDomain = baseUrl.match(/^(https?:\\\\/\\\\/[^\\\\/]+)/)?.[1] || \\\"https://openrouter.ai\\\"\\n1047 | \\t\\t\\tconst response = await axios.post(`${baseUrlDomain}/api/v1/auth/keys`, { code })\\n1048 | \\t\\t\\tif (response.data && response.data.key) {\\n1049 | \\t\\t\\t\\tapiKey = response.data.key\\n1050 | \\t\\t\\t} else {\\n1051 | \\t\\t\\t\\tthrow new Error(\\\"Invalid response from OpenRouter API\\\")\\n1052 | \\t\\t\\t}\\n1053 | \\t\\t} catch (error) {\\n1054 | \\t\\t\\tthis.log(\\n1055 | \\t\\t\\t\\t`Error exchanging code for API key: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`,\\n1056 | \\t\\t\\t)\\n1057 | \\t\\t\\tthrow error\\n1058 | \\t\\t}\\n1059 | \\n1060 | \\t\\tconst newConfiguration: ProviderSettings = {\\n1061 | \\t\\t\\t...apiConfiguration,\\n1062 | \\t\\t\\tapiProvider: \\\"openrouter\\\",\\n1063 | \\t\\t\\topenRouterApiKey: apiKey,\\n1064 | \\t\\t\\topenRouterModelId: apiConfiguration?.openRouterModelId || openRouterDefaultModelId,\\n1065 | \\t\\t}\\n1066 | \\n1067 | \\t\\tawait this.upsertProviderProfile(currentApiConfigName, newConfiguration)\\n1068 | \\t}\\n1069 | \\n1070 | \\t// Glama\\n1071 | \\n1072 | \\tasync handleGlamaCallback(code: string) {\\n1073 | \\t\\tlet apiKey: string\\n1074 | \\t\\ttry {\\n1075 | \\t\\t\\tconst response = await axios.post(\\\"https://glama.ai/api/gateway/v1/auth/exchange-code\\\", { code })\\n1076 | \\t\\t\\tif (response.data && response.data.apiKey) {\\n1077 | \\t\\t\\t\\tapiKey = response.data.apiKey\\n1078 | \\t\\t\\t} else {\\n1079 | \\t\\t\\t\\tthrow new Error(\\\"Invalid response from Glama API\\\")\\n1080 | \\t\\t\\t}\\n1081 | \\t\\t} catch (error) {\\n1082 | \\t\\t\\tthis.log(\\n1083 | \\t\\t\\t\\t`Error exchanging code for API key: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`,\\n1084 | \\t\\t\\t)\\n1085 | \\t\\t\\tthrow error\\n1086 | \\t\\t}\\n1087 | \\n1088 | \\t\\tconst { apiConfiguration, currentApiConfigName } = await this.getState()\\n1089 | \\n1090 | \\t\\tconst newConfiguration: ProviderSettings = {\\n1091 | \\t\\t\\t...apiConfiguration,\\n1092 | \\t\\t\\tapiProvider: \\\"glama\\\",\\n1093 | \\t\\t\\tglamaApiKey: apiKey,\\n1094 | \\t\\t\\tglamaModelId: apiConfiguration?.glamaModelId || glamaDefaultModelId,\\n1095 | \\t\\t}\\n1096 | \\n1097 | \\t\\tawait this.upsertProviderProfile(currentApiConfigName, newConfiguration)\\n1098 | \\t}\\n1099 | \\n1100 | \\t// Requesty\\n1101 | \\n1102 | \\tasync handleRequestyCallback(code: string) {\\n1103 | \\t\\tlet { apiConfiguration, currentApiConfigName } = await this.getState()\\n1104 | \\n1105 | \\t\\tconst newConfiguration: ProviderSettings = {\\n1106 | \\t\\t\\t...apiConfiguration,\\n1107 | \\t\\t\\tapiProvider: \\\"requesty\\\",\\n1108 | \\t\\t\\trequestyApiKey: code,\\n1109 | \\t\\t\\trequestyModelId: apiConfiguration?.requestyModelId || requestyDefaultModelId,\\n1110 | \\t\\t}\\n1111 | \\n1112 | \\t\\tawait this.upsertProviderProfile(currentApiConfigName, newConfiguration)\\n1113 | \\t}\\n1114 | \\n1115 | \\t// Task history\\n1116 | \\n1117 | \\tasync getTaskWithId(id: string): Promise<{\\n1118 | \\t\\thistoryItem: HistoryItem\\n1119 | \\t\\ttaskDirPath: string\\n1120 | \\t\\tapiConversationHistoryFilePath: string\\n1121 | \\t\\tuiMessagesFilePath: string\\n1122 | \\t\\tapiConversationHistory: Anthropic.MessageParam[]\\n1123 | \\t}> {\\n1124 | \\t\\tconst history = this.getGlobalState(\\\"taskHistory\\\") ?? []\\n1125 | \\t\\tconst historyItem = history.find((item) => item.id === id)\\n1126 | \\n1127 | \\t\\tif (historyItem) {\\n1128 | \\t\\t\\tconst { getTaskDirectoryPath } = await import(\\\"../../utils/storage\\\")\\n1129 | \\t\\t\\tconst globalStoragePath = this.contextProxy.globalStorageUri.fsPath\\n1130 | \\t\\t\\tconst taskDirPath = await getTaskDirectoryPath(globalStoragePath, id)\\n1131 | \\t\\t\\tconst apiConversationHistoryFilePath = path.join(taskDirPath, GlobalFileNames.apiConversationHistory)\\n1132 | \\t\\t\\tconst uiMessagesFilePath = path.join(taskDirPath, GlobalFileNames.uiMessages)\\n1133 | \\t\\t\\tconst fileExists = await fileExistsAtPath(apiConversationHistoryFilePath)\\n1134 | \\n1135 | \\t\\t\\tif (fileExists) {\\n1136 | \\t\\t\\t\\tconst apiConversationHistory = JSON.parse(await fs.readFile(apiConversationHistoryFilePath, \\\"utf8\\\"))\\n1137 | \\n1138 | \\t\\t\\t\\treturn {\\n1139 | \\t\\t\\t\\t\\thistoryItem,\\n1140 | \\t\\t\\t\\t\\ttaskDirPath,\\n1141 | \\t\\t\\t\\t\\tapiConversationHistoryFilePath,\\n1142 | \\t\\t\\t\\t\\tuiMessagesFilePath,\\n1143 | \\t\\t\\t\\t\\tapiConversationHistory,\\n1144 | \\t\\t\\t\\t}\\n1145 | \\t\\t\\t}\\n1146 | \\t\\t}\\n1147 | \\n1148 | \\t\\t// if we tried to get a task that doesn't exist, remove it from state\\n1149 | \\t\\t// FIXME: this seems to happen sometimes when the json file doesnt save to disk for some reason\\n1150 | \\t\\tawait this.deleteTaskFromState(id)\\n1151 | \\t\\tthrow new Error(\\\"Task not found\\\")\\n1152 | \\t}\\n1153 | \\n1154 | \\tasync showTaskWithId(id: string) {\\n1155 | \\t\\tif (id !== this.getCurrentCline()?.taskId) {\\n1156 | \\t\\t\\t// Non-current task.\\n1157 | \\t\\t\\tconst { historyItem } = await this.getTaskWithId(id)\\n1158 | \\t\\t\\tawait this.initClineWithHistoryItem(historyItem) // Clears existing task.\\n1159 | \\t\\t}\\n1160 | \\n1161 | \\t\\tawait this.postMessageToWebview({ type: \\\"action\\\", action: \\\"chatButtonClicked\\\" })\\n1162 | \\t}\\n1163 | \\n1164 | \\tasync exportTaskWithId(id: string) {\\n1165 | \\t\\tconst { historyItem, apiConversationHistory } = await this.getTaskWithId(id)\\n1166 | \\t\\tawait downloadTask(historyItem.ts, apiConversationHistory)\\n1167 | \\t}\\n1168 | \\n1169 | \\t/* Condenses a task's message history to use fewer tokens. */\\n1170 | \\tasync condenseTaskContext(taskId: string) {\\n1171 | \\t\\tlet task: Task | undefined\\n1172 | \\t\\tfor (let i = this.clineStack.length - 1; i >= 0; i--) {\\n1173 | \\t\\t\\tif (this.clineStack[i].taskId === taskId) {\\n1174 | \\t\\t\\t\\ttask = this.clineStack[i]\\n1175 | \\t\\t\\t\\tbreak\\n1176 | \\t\\t\\t}\\n1177 | \\t\\t}\\n1178 | \\t\\tif (!task) {\\n1179 | \\t\\t\\tthrow new Error(`Task with id ${taskId} not found in stack`)\\n1180 | \\t\\t}\\n1181 | \\t\\tawait task.condenseContext()\\n1182 | \\t\\tawait this.postMessageToWebview({ type: \\\"condenseTaskContextResponse\\\", text: taskId })\\n1183 | \\t}\\n1184 | \\n1185 | \\t// this function deletes a task from task hidtory, and deletes it's checkpoints and delete the task folder\\n1186 | \\tasync deleteTaskWithId(id: string) {\\n1187 | \\t\\ttry {\\n1188 | \\t\\t\\t// get the task directory full path\\n1189 | \\t\\t\\tconst { taskDirPath } = await this.getTaskWithId(id)\\n1190 | \\n1191 | \\t\\t\\t// remove task from stack if it's the current task\\n1192 | \\t\\t\\tif (id === this.getCurrentCline()?.taskId) {\\n1193 | \\t\\t\\t\\t// if we found the taskid to delete - call finish to abort this task and allow a new task to be started,\\n1194 | \\t\\t\\t\\t// if we are deleting a subtask and parent task is still waiting for subtask to finish - it allows the parent to resume (this case should neve exist)\\n1195 | \\t\\t\\t\\tawait this.finishSubTask(t(\\\"common:tasks.deleted\\\"))\\n1196 | \\t\\t\\t}\\n1197 | \\n1198 | \\t\\t\\t// delete task from the task history state\\n1199 | \\t\\t\\tawait this.deleteTaskFromState(id)\\n1200 | \\n1201 | \\t\\t\\t// Delete associated shadow repository or branch.\\n1202 | \\t\\t\\t// TODO: Store `workspaceDir` in the `HistoryItem` object.\\n1203 | \\t\\t\\tconst globalStorageDir = this.contextProxy.globalStorageUri.fsPath\\n1204 | \\t\\t\\tconst workspaceDir = this.cwd\\n1205 | \\n1206 | \\t\\t\\ttry {\\n1207 | \\t\\t\\t\\tawait ShadowCheckpointService.deleteTask({ taskId: id, globalStorageDir, workspaceDir })\\n1208 | \\t\\t\\t} catch (error) {\\n1209 | \\t\\t\\t\\tconsole.error(\\n1210 | \\t\\t\\t\\t\\t`[deleteTaskWithId${id}] failed to delete associated shadow repository or branch: ${error instanceof Error ? error.message : String(error)}`,\\n1211 | \\t\\t\\t\\t)\\n1212 | \\t\\t\\t}\\n1213 | \\n1214 | \\t\\t\\t// delete the entire task directory including checkpoints and all content\\n1215 | \\t\\t\\ttry {\\n1216 | \\t\\t\\t\\tawait fs.rm(taskDirPath, { recursive: true, force: true })\\n1217 | \\t\\t\\t\\tconsole.log(`[deleteTaskWithId${id}] removed task directory`)\\n1218 | \\t\\t\\t} catch (error) {\\n1219 | \\t\\t\\t\\tconsole.error(\\n1220 | \\t\\t\\t\\t\\t`[deleteTaskWithId${id}] failed to remove task directory: ${error instanceof Error ? error.message : String(error)}`,\\n1221 | \\t\\t\\t\\t)\\n1222 | \\t\\t\\t}\\n1223 | \\t\\t} catch (error) {\\n1224 | \\t\\t\\t// If task is not found, just remove it from state\\n1225 | \\t\\t\\tif (error instanceof Error && error.message === \\\"Task not found\\\") {\\n1226 | \\t\\t\\t\\tawait this.deleteTaskFromState(id)\\n1227 | \\t\\t\\t\\treturn\\n1228 | \\t\\t\\t}\\n1229 | \\t\\t\\tthrow error\\n1230 | \\t\\t}\\n1231 | \\t}\\n1232 | \\n1233 | \\tasync deleteTaskFromState(id: string) {\\n1234 | \\t\\tconst taskHistory = this.getGlobalState(\\\"taskHistory\\\") ?? []\\n1235 | \\t\\tconst updatedTaskHistory = taskHistory.filter((task) => task.id !== id)\\n1236 | \\t\\tawait this.updateGlobalState(\\\"taskHistory\\\", updatedTaskHistory)\\n1237 | \\t\\tawait this.postStateToWebview()\\n1238 | \\t}\\n1239 | \\n1240 | \\tasync postStateToWebview() {\\n1241 | \\t\\tconst state = await this.getStateToPostToWebview()\\n1242 | \\t\\tthis.postMessageToWebview({ type: \\\"state\\\", state })\\n1243 | \\n1244 | \\t\\t// Update VSCode context for experiments\\n1245 | \\t\\tawait this.updateVSCodeContext()\\n1246 | \\t}\\n1247 | \\n1248 | \\t/**\\n1249 | \\t * Updates VSCode context variables for experiments so they can be used in when clauses\\n1250 | \\t */\\n1251 | \\tprivate async updateVSCodeContext() {\\n1252 | \\t\\tconst { experiments } = await this.getState()\\n1253 | \\n1254 | \\t\\t// Set context for marketplace experiment\\n1255 | \\t\\tawait vscode.commands.executeCommand(\\n1256 | \\t\\t\\t\\\"setContext\\\",\\n1257 | \\t\\t\\t`${Package.name}.marketplaceEnabled`,\\n1258 | \\t\\t\\texperiments.marketplace ?? false,\\n1259 | \\t\\t)\\n1260 | \\t}\\n1261 | \\n1262 | \\t/**\\n1263 | \\t * Checks if there is a file-based system prompt override for the given mode\\n1264 | \\t */\\n1265 | \\tasync hasFileBasedSystemPromptOverride(mode: Mode): Promise {\\n1266 | \\t\\tconst promptFilePath = getSystemPromptFilePath(this.cwd, mode)\\n1267 | \\t\\treturn await fileExistsAtPath(promptFilePath)\\n1268 | \\t}\\n1269 | \\n1270 | \\tasync getStateToPostToWebview() {\\n1271 | \\t\\tconst {\\n1272 | \\t\\t\\tapiConfiguration,\\n1273 | \\t\\t\\tlastShownAnnouncementId,\\n1274 | \\t\\t\\tcustomInstructions,\\n1275 | \\t\\t\\talwaysAllowReadOnly,\\n1276 | \\t\\t\\talwaysAllowReadOnlyOutsideWorkspace,\\n1277 | \\t\\t\\talwaysAllowWrite,\\n1278 | \\t\\t\\talwaysAllowWriteOutsideWorkspace,\\n1279 | \\t\\t\\talwaysAllowExecute,\\n1280 | \\t\\t\\talwaysAllowBrowser,\\n1281 | \\t\\t\\talwaysAllowMcp,\\n1282 | \\t\\t\\talwaysAllowModeSwitch,\\n1283 | \\t\\t\\talwaysAllowSubtasks,\\n1284 | \\t\\t\\tallowedMaxRequests,\\n1285 | \\t\\t\\tautoCondenseContext,\\n1286 | \\t\\t\\tautoCondenseContextPercent,\\n1287 | \\t\\t\\tsoundEnabled,\\n1288 | \\t\\t\\tttsEnabled,\\n1289 | \\t\\t\\tttsSpeed,\\n1290 | \\t\\t\\tdiffEnabled,\\n1291 | \\t\\t\\tenableCheckpoints,\\n1292 | \\t\\t\\ttaskHistory,\\n1293 | \\t\\t\\tsoundVolume,\\n1294 | \\t\\t\\tbrowserViewportSize,\\n1295 | \\t\\t\\tscreenshotQuality,\\n1296 | \\t\\t\\tremoteBrowserHost,\\n1297 | \\t\\t\\tremoteBrowserEnabled,\\n1298 | \\t\\t\\tcachedChromeHostUrl,\\n1299 | \\t\\t\\twriteDelayMs,\\n1300 | \\t\\t\\tterminalOutputLineLimit,\\n1301 | \\t\\t\\tterminalShellIntegrationTimeout,\\n1302 | \\t\\t\\tterminalShellIntegrationDisabled,\\n1303 | \\t\\t\\tterminalCommandDelay,\\n1304 | \\t\\t\\tterminalPowershellCounter,\\n1305 | \\t\\t\\tterminalZshClearEolMark,\\n1306 | \\t\\t\\tterminalZshOhMy,\\n1307 | \\t\\t\\tterminalZshP10k,\\n1308 | \\t\\t\\tterminalZdotdir,\\n1309 | \\t\\t\\tfuzzyMatchThreshold,\\n1310 | \\t\\t\\tmcpEnabled,\\n1311 | \\t\\t\\tenableMcpServerCreation,\\n1312 | \\t\\t\\talwaysApproveResubmit,\\n1313 | \\t\\t\\trequestDelaySeconds,\\n1314 | \\t\\t\\tcurrentApiConfigName,\\n1315 | \\t\\t\\tlistApiConfigMeta,\\n1316 | \\t\\t\\tpinnedApiConfigs,\\n1317 | \\t\\t\\tmode,\\n1318 | \\t\\t\\tcustomModePrompts,\\n1319 | \\t\\t\\tcustomSupportPrompts,\\n1320 | \\t\\t\\tenhancementApiConfigId,\\n1321 | \\t\\t\\tautoApprovalEnabled,\\n1322 | \\t\\t\\tcustomModes,\\n1323 | \\t\\t\\texperiments,\\n1324 | \\t\\t\\tmaxOpenTabsContext,\\n1325 | \\t\\t\\tmaxWorkspaceFiles,\\n1326 | \\t\\t\\tbrowserToolEnabled,\\n1327 | \\t\\t\\ttelemetrySetting,\\n1328 | \\t\\t\\tshowRooIgnoredFiles,\\n1329 | \\t\\t\\tlanguage,\\n1330 | \\t\\t\\tmaxReadFileLine,\\n1331 | \\t\\t\\tterminalCompressProgressBar,\\n1332 | \\t\\t\\thistoryPreviewCollapsed,\\n1333 | \\t\\t\\tcloudUserInfo,\\n1334 | \\t\\t\\tcloudIsAuthenticated,\\n1335 | \\t\\t\\tsharingEnabled,\\n1336 | \\t\\t\\torganizationAllowList,\\n1337 | \\t\\t\\tmaxConcurrentFileReads,\\n1338 | \\t\\t\\tcondensingApiConfigId,\\n1339 | \\t\\t\\tcustomCondensingPrompt,\\n1340 | \\t\\t\\tcodebaseIndexConfig,\\n1341 | \\t\\t\\tcodebaseIndexModels,\\n1342 | \\t\\t} = await this.getState()\\n1343 | \\n1344 | \\t\\tconst telemetryKey = process.env.POSTHOG_API_KEY\\n1345 | \\t\\tconst machineId = vscode.env.machineId\\n1346 | \\t\\tconst allowedCommands = vscode.workspace.getConfiguration(Package.name).get(\\\"allowedCommands\\\") || []\\n1347 | \\t\\tconst cwd = this.cwd\\n1348 | \\n1349 | \\t\\t// Only fetch marketplace data if the feature is enabled\\n1350 | \\t\\tlet marketplaceItems: any[] = []\\n1351 | \\t\\tlet marketplaceInstalledMetadata: any = { project: {}, global: {} }\\n1352 | \\n1353 | \\t\\tif (experiments.marketplace) {\\n1354 | \\t\\t\\tmarketplaceItems = (await this.marketplaceManager.getCurrentItems()) || []\\n1355 | \\t\\t\\tmarketplaceInstalledMetadata = await this.marketplaceManager.getInstallationMetadata()\\n1356 | \\t\\t}\\n1357 | \\n1358 | \\t\\t// Check if there's a system prompt override for the current mode\\n1359 | \\t\\tconst currentMode = mode ?? defaultModeSlug\\n1360 | \\t\\tconst hasSystemPromptOverride = await this.hasFileBasedSystemPromptOverride(currentMode)\\n1361 | \\n1362 | \\t\\treturn {\\n1363 | \\t\\t\\tversion: this.context.extension?.packageJSON?.version ?? \\\"\\\",\\n1364 | \\t\\t\\tmarketplaceItems,\\n1365 | \\t\\t\\tmarketplaceInstalledMetadata,\\n1366 | \\t\\t\\tapiConfiguration,\\n1367 | \\t\\t\\tcustomInstructions,\\n1368 | \\t\\t\\talwaysAllowReadOnly: alwaysAllowReadOnly ?? false,\\n1369 | \\t\\t\\talwaysAllowReadOnlyOutsideWorkspace: alwaysAllowReadOnlyOutsideWorkspace ?? false,\\n1370 | \\t\\t\\talwaysAllowWrite: alwaysAllowWrite ?? false,\\n1371 | \\t\\t\\talwaysAllowWriteOutsideWorkspace: alwaysAllowWriteOutsideWorkspace ?? false,\\n1372 | \\t\\t\\talwaysAllowExecute: alwaysAllowExecute ?? false,\\n1373 | \\t\\t\\talwaysAllowBrowser: alwaysAllowBrowser ?? false,\\n1374 | \\t\\t\\talwaysAllowMcp: alwaysAllowMcp ?? false,\\n1375 | \\t\\t\\talwaysAllowModeSwitch: alwaysAllowModeSwitch ?? false,\\n1376 | \\t\\t\\talwaysAllowSubtasks: alwaysAllowSubtasks ?? false,\\n1377 | \\t\\t\\tallowedMaxRequests,\\n1378 | \\t\\t\\tautoCondenseContext: autoCondenseContext ?? true,\\n1379 | \\t\\t\\tautoCondenseContextPercent: autoCondenseContextPercent ?? 100,\\n1380 | \\t\\t\\turiScheme: vscode.env.uriScheme,\\n1381 | \\t\\t\\tcurrentTaskItem: this.getCurrentCline()?.taskId\\n1382 | \\t\\t\\t\\t? (taskHistory || []).find((item: HistoryItem) => item.id === this.getCurrentCline()?.taskId)\\n1383 | \\t\\t\\t\\t: undefined,\\n1384 | \\t\\t\\tclineMessages: this.getCurrentCline()?.clineMessages || [],\\n1385 | \\t\\t\\ttaskHistory: (taskHistory || [])\\n1386 | \\t\\t\\t\\t.filter((item: HistoryItem) => item.ts && item.task)\\n1387 | \\t\\t\\t\\t.sort((a: HistoryItem, b: HistoryItem) => b.ts - a.ts),\\n1388 | \\t\\t\\tsoundEnabled: soundEnabled ?? false,\\n1389 | \\t\\t\\tttsEnabled: ttsEnabled ?? false,\\n1390 | \\t\\t\\tttsSpeed: ttsSpeed ?? 1.0,\\n1391 | \\t\\t\\tdiffEnabled: diffEnabled ?? true,\\n1392 | \\t\\t\\tenableCheckpoints: enableCheckpoints ?? true,\\n1393 | \\t\\t\\tshouldShowAnnouncement:\\n1394 | \\t\\t\\t\\ttelemetrySetting !== \\\"unset\\\" && lastShownAnnouncementId !== this.latestAnnouncementId,\\n1395 | \\t\\t\\tallowedCommands,\\n1396 | \\t\\t\\tsoundVolume: soundVolume ?? 0.5,\\n1397 | \\t\\t\\tbrowserViewportSize: browserViewportSize ?? \\\"900x600\\\",\\n1398 | \\t\\t\\tscreenshotQuality: screenshotQuality ?? 75,\\n1399 | \\t\\t\\tremoteBrowserHost,\\n1400 | \\t\\t\\tremoteBrowserEnabled: remoteBrowserEnabled ?? false,\\n1401 | \\t\\t\\tcachedChromeHostUrl: cachedChromeHostUrl,\\n1402 | \\t\\t\\twriteDelayMs: writeDelayMs ?? 1000,\\n1403 | \\t\\t\\tterminalOutputLineLimit: terminalOutputLineLimit ?? 500,\\n1404 | \\t\\t\\tterminalShellIntegrationTimeout: terminalShellIntegrationTimeout ?? Terminal.defaultShellIntegrationTimeout,\\n1405 | \\t\\t\\tterminalShellIntegrationDisabled: terminalShellIntegrationDisabled ?? false,\\n1406 | \\t\\t\\tterminalCommandDelay: terminalCommandDelay ?? 0,\\n1407 | \\t\\t\\tterminalPowershellCounter: terminalPowershellCounter ?? false,\\n1408 | \\t\\t\\tterminalZshClearEolMark: terminalZshClearEolMark ?? true,\\n1409 | \\t\\t\\tterminalZshOhMy: terminalZshOhMy ?? false,\\n1410 | \\t\\t\\tterminalZshP10k: terminalZshP10k ?? false,\\n1411 | \\t\\t\\tterminalZdotdir: terminalZdotdir ?? false,\\n1412 | \\t\\t\\tfuzzyMatchThreshold: fuzzyMatchThreshold ?? 1.0,\\n1413 | \\t\\t\\tmcpEnabled: mcpEnabled ?? true,\\n1414 | \\t\\t\\tenableMcpServerCreation: enableMcpServerCreation ?? true,\\n1415 | \\t\\t\\talwaysApproveResubmit: alwaysApproveResubmit ?? false,\\n1416 | \\t\\t\\trequestDelaySeconds: requestDelaySeconds ?? 10,\\n1417 | \\t\\t\\tcurrentApiConfigName: currentApiConfigName ?? \\\"default\\\",\\n1418 | \\t\\t\\tlistApiConfigMeta: listApiConfigMeta ?? [],\\n1419 | \\t\\t\\tpinnedApiConfigs: pinnedApiConfigs ?? {},\\n1420 | \\t\\t\\tmode: mode ?? defaultModeSlug,\\n1421 | \\t\\t\\tcustomModePrompts: customModePrompts ?? {},\\n1422 | \\t\\t\\tcustomSupportPrompts: customSupportPrompts ?? {},\\n1423 | \\t\\t\\tenhancementApiConfigId,\\n1424 | \\t\\t\\tautoApprovalEnabled: autoApprovalEnabled ?? false,\\n1425 | \\t\\t\\tcustomModes,\\n1426 | \\t\\t\\texperiments: experiments ?? experimentDefault,\\n1427 | \\t\\t\\tmcpServers: this.mcpHub?.getAllServers() ?? [],\\n1428 | \\t\\t\\tmaxOpenTabsContext: maxOpenTabsContext ?? 20,\\n1429 | \\t\\t\\tmaxWorkspaceFiles: maxWorkspaceFiles ?? 200,\\n1430 | \\t\\t\\tcwd,\\n1431 | \\t\\t\\tbrowserToolEnabled: browserToolEnabled ?? true,\\n1432 | \\t\\t\\ttelemetrySetting,\\n1433 | \\t\\t\\ttelemetryKey,\\n1434 | \\t\\t\\tmachineId,\\n1435 | \\t\\t\\tshowRooIgnoredFiles: showRooIgnoredFiles ?? true,\\n1436 | \\t\\t\\tlanguage: language ?? formatLanguage(vscode.env.language),\\n1437 | \\t\\t\\trenderContext: this.renderContext,\\n1438 | \\t\\t\\tmaxReadFileLine: maxReadFileLine ?? -1,\\n1439 | \\t\\t\\tmaxConcurrentFileReads: maxConcurrentFileReads ?? 5,\\n1440 | \\t\\t\\tsettingsImportedAt: this.settingsImportedAt,\\n1441 | \\t\\t\\tterminalCompressProgressBar: terminalCompressProgressBar ?? true,\\n1442 | \\t\\t\\thasSystemPromptOverride,\\n1443 | \\t\\t\\thistoryPreviewCollapsed: historyPreviewCollapsed ?? false,\\n1444 | \\t\\t\\tcloudUserInfo,\\n1445 | \\t\\t\\tcloudIsAuthenticated: cloudIsAuthenticated ?? false,\\n1446 | \\t\\t\\tsharingEnabled: sharingEnabled ?? false,\\n1447 | \\t\\t\\torganizationAllowList,\\n1448 | \\t\\t\\tcondensingApiConfigId,\\n1449 | \\t\\t\\tcustomCondensingPrompt,\\n1450 | \\t\\t\\tcodebaseIndexModels: codebaseIndexModels ?? EMBEDDING_MODEL_PROFILES,\\n1451 | \\t\\t\\tcodebaseIndexConfig: codebaseIndexConfig ?? {\\n1452 | \\t\\t\\t\\tcodebaseIndexEnabled: false,\\n1453 | \\t\\t\\t\\tcodebaseIndexQdrantUrl: \\\"http://localhost:6333\\\",\\n1454 | \\t\\t\\t\\tcodebaseIndexEmbedderProvider: \\\"openai\\\",\\n1455 | \\t\\t\\t\\tcodebaseIndexEmbedderBaseUrl: \\\"\\\",\\n1456 | \\t\\t\\t\\tcodebaseIndexEmbedderModelId: \\\"\\\",\\n1457 | \\t\\t\\t},\\n1458 | \\t\\t}\\n1459 | \\t}\\n1460 | \\n1461 | \\t/**\\n1462 | \\t * Storage\\n1463 | \\t * https://dev.to/kompotkot/how-to-use-secretstorage-in-your-vscode-extensions-2hco\\n1464 | \\t * https://www.eliostruyf.com/devhack-code-extension-storage-options/\\n1465 | \\t */\\n1466 | \\n1467 | \\tasync getState() {\\n1468 | \\t\\tconst stateValues = this.contextProxy.getValues()\\n1469 | \\t\\tconst customModes = await this.customModesManager.getCustomModes()\\n1470 | \\n1471 | \\t\\t// Determine apiProvider with the same logic as before.\\n1472 | \\t\\tconst apiProvider: ProviderName = stateValues.apiProvider ? stateValues.apiProvider : \\\"anthropic\\\"\\n1473 | \\n1474 | \\t\\t// Build the apiConfiguration object combining state values and secrets.\\n1475 | \\t\\tconst providerSettings = this.contextProxy.getProviderSettings()\\n1476 | \\n1477 | \\t\\t// Ensure apiProvider is set properly if not already in state\\n1478 | \\t\\tif (!providerSettings.apiProvider) {\\n1479 | \\t\\t\\tproviderSettings.apiProvider = apiProvider\\n1480 | \\t\\t}\\n1481 | \\n1482 | \\t\\tlet organizationAllowList = ORGANIZATION_ALLOW_ALL\\n1483 | \\n1484 | \\t\\ttry {\\n1485 | \\t\\t\\torganizationAllowList = await CloudService.instance.getAllowList()\\n1486 | \\t\\t} catch (error) {\\n1487 | \\t\\t\\tconsole.error(\\n1488 | \\t\\t\\t\\t`[getState] failed to get organization allow list: ${error instanceof Error ? error.message : String(error)}`,\\n1489 | \\t\\t\\t)\\n1490 | \\t\\t}\\n1491 | \\n1492 | \\t\\tlet cloudUserInfo: CloudUserInfo | null = null\\n1493 | \\n1494 | \\t\\ttry {\\n1495 | \\t\\t\\tcloudUserInfo = CloudService.instance.getUserInfo()\\n1496 | \\t\\t} catch (error) {\\n1497 | \\t\\t\\tconsole.error(\\n1498 | \\t\\t\\t\\t`[getState] failed to get cloud user info: ${error instanceof Error ? error.message : String(error)}`,\\n1499 | \\t\\t\\t)\\n1500 | \\t\\t}\\n1501 | \\n1502 | \\t\\tlet cloudIsAuthenticated: boolean = false\\n1503 | \\n1504 | \\t\\ttry {\\n1505 | \\t\\t\\tcloudIsAuthenticated = CloudService.instance.isAuthenticated()\\n1506 | \\t\\t} catch (error) {\\n1507 | \\t\\t\\tconsole.error(\\n1508 | \\t\\t\\t\\t`[getState] failed to get cloud authentication state: ${error instanceof Error ? error.message : String(error)}`,\\n1509 | \\t\\t\\t)\\n1510 | \\t\\t}\\n1511 | \\n1512 | \\t\\tlet sharingEnabled: boolean = false\\n1513 | \\n1514 | \\t\\ttry {\\n1515 | \\t\\t\\tsharingEnabled = await CloudService.instance.canShareTask()\\n1516 | \\t\\t} catch (error) {\\n1517 | \\t\\t\\tconsole.error(\\n1518 | \\t\\t\\t\\t`[getState] failed to get sharing enabled state: ${error instanceof Error ? error.message : String(error)}`,\\n1519 | \\t\\t\\t)\\n1520 | \\t\\t}\\n1521 | \\n1522 | \\t\\t// Return the same structure as before\\n1523 | \\t\\treturn {\\n1524 | \\t\\t\\tapiConfiguration: providerSettings,\\n1525 | \\t\\t\\tlastShownAnnouncementId: stateValues.lastShownAnnouncementId,\\n1526 | \\t\\t\\tcustomInstructions: stateValues.customInstructions,\\n1527 | \\t\\t\\tapiModelId: stateValues.apiModelId,\\n1528 | \\t\\t\\talwaysAllowReadOnly: stateValues.alwaysAllowReadOnly ?? false,\\n1529 | \\t\\t\\talwaysAllowReadOnlyOutsideWorkspace: stateValues.alwaysAllowReadOnlyOutsideWorkspace ?? false,\\n1530 | \\t\\t\\talwaysAllowWrite: stateValues.alwaysAllowWrite ?? false,\\n1531 | \\t\\t\\talwaysAllowWriteOutsideWorkspace: stateValues.alwaysAllowWriteOutsideWorkspace ?? false,\\n1532 | \\t\\t\\talwaysAllowExecute: stateValues.alwaysAllowExecute ?? false,\\n1533 | \\t\\t\\talwaysAllowBrowser: stateValues.alwaysAllowBrowser ?? false,\\n1534 | \\t\\t\\talwaysAllowMcp: stateValues.alwaysAllowMcp ?? false,\\n1535 | \\t\\t\\talwaysAllowModeSwitch: stateValues.alwaysAllowModeSwitch ?? false,\\n1536 | \\t\\t\\talwaysAllowSubtasks: stateValues.alwaysAllowSubtasks ?? false,\\n1537 | \\t\\t\\tallowedMaxRequests: stateValues.allowedMaxRequests,\\n1538 | \\t\\t\\tautoCondenseContext: stateValues.autoCondenseContext ?? true,\\n1539 | \\t\\t\\tautoCondenseContextPercent: stateValues.autoCondenseContextPercent ?? 100,\\n1540 | \\t\\t\\ttaskHistory: stateValues.taskHistory,\\n1541 | \\t\\t\\tallowedCommands: stateValues.allowedCommands,\\n1542 | \\t\\t\\tsoundEnabled: stateValues.soundEnabled ?? false,\\n1543 | \\t\\t\\tttsEnabled: stateValues.ttsEnabled ?? false,\\n1544 | \\t\\t\\tttsSpeed: stateValues.ttsSpeed ?? 1.0,\\n1545 | \\t\\t\\tdiffEnabled: stateValues.diffEnabled ?? true,\\n1546 | \\t\\t\\tenableCheckpoints: stateValues.enableCheckpoints ?? true,\\n1547 | \\t\\t\\tsoundVolume: stateValues.soundVolume,\\n1548 | \\t\\t\\tbrowserViewportSize: stateValues.browserViewportSize ?? \\\"900x600\\\",\\n1549 | \\t\\t\\tscreenshotQuality: stateValues.screenshotQuality ?? 75,\\n1550 | \\t\\t\\tremoteBrowserHost: stateValues.remoteBrowserHost,\\n1551 | \\t\\t\\tremoteBrowserEnabled: stateValues.remoteBrowserEnabled ?? false,\\n1552 | \\t\\t\\tcachedChromeHostUrl: stateValues.cachedChromeHostUrl as string | undefined,\\n1553 | \\t\\t\\tfuzzyMatchThreshold: stateValues.fuzzyMatchThreshold ?? 1.0,\\n1554 | \\t\\t\\twriteDelayMs: stateValues.writeDelayMs ?? 1000,\\n1555 | \\t\\t\\tterminalOutputLineLimit: stateValues.terminalOutputLineLimit ?? 500,\\n1556 | \\t\\t\\tterminalShellIntegrationTimeout:\\n1557 | \\t\\t\\t\\tstateValues.terminalShellIntegrationTimeout ?? Terminal.defaultShellIntegrationTimeout,\\n1558 | \\t\\t\\tterminalShellIntegrationDisabled: stateValues.terminalShellIntegrationDisabled ?? false,\\n1559 | \\t\\t\\tterminalCommandDelay: stateValues.terminalCommandDelay ?? 0,\\n1560 | \\t\\t\\tterminalPowershellCounter: stateValues.terminalPowershellCounter ?? false,\\n1561 | \\t\\t\\tterminalZshClearEolMark: stateValues.terminalZshClearEolMark ?? true,\\n1562 | \\t\\t\\tterminalZshOhMy: stateValues.terminalZshOhMy ?? false,\\n1563 | \\t\\t\\tterminalZshP10k: stateValues.terminalZshP10k ?? false,\\n1564 | \\t\\t\\tterminalZdotdir: stateValues.terminalZdotdir ?? false,\\n1565 | \\t\\t\\tterminalCompressProgressBar: stateValues.terminalCompressProgressBar ?? true,\\n1566 | \\t\\t\\tmode: stateValues.mode ?? defaultModeSlug,\\n1567 | \\t\\t\\tlanguage: stateValues.language ?? formatLanguage(vscode.env.language),\\n1568 | \\t\\t\\tmcpEnabled: stateValues.mcpEnabled ?? true,\\n1569 | \\t\\t\\tenableMcpServerCreation: stateValues.enableMcpServerCreation ?? true,\\n1570 | \\t\\t\\talwaysApproveResubmit: stateValues.alwaysApproveResubmit ?? false,\\n1571 | \\t\\t\\trequestDelaySeconds: Math.max(5, stateValues.requestDelaySeconds ?? 10),\\n1572 | \\t\\t\\tcurrentApiConfigName: stateValues.currentApiConfigName ?? \\\"default\\\",\\n1573 | \\t\\t\\tlistApiConfigMeta: stateValues.listApiConfigMeta ?? [],\\n1574 | \\t\\t\\tpinnedApiConfigs: stateValues.pinnedApiConfigs ?? {},\\n1575 | \\t\\t\\tmodeApiConfigs: stateValues.modeApiConfigs ?? ({} as Record),\\n1576 | \\t\\t\\tcustomModePrompts: stateValues.customModePrompts ?? {},\\n1577 | \\t\\t\\tcustomSupportPrompts: stateValues.customSupportPrompts ?? {},\\n1578 | \\t\\t\\tenhancementApiConfigId: stateValues.enhancementApiConfigId,\\n1579 | \\t\\t\\texperiments: stateValues.experiments ?? experimentDefault,\\n1580 | \\t\\t\\tautoApprovalEnabled: stateValues.autoApprovalEnabled ?? false,\\n1581 | \\t\\t\\tcustomModes,\\n1582 | \\t\\t\\tmaxOpenTabsContext: stateValues.maxOpenTabsContext ?? 20,\\n1583 | \\t\\t\\tmaxWorkspaceFiles: stateValues.maxWorkspaceFiles ?? 200,\\n1584 | \\t\\t\\topenRouterUseMiddleOutTransform: stateValues.openRouterUseMiddleOutTransform ?? true,\\n1585 | \\t\\t\\tbrowserToolEnabled: stateValues.browserToolEnabled ?? true,\\n1586 | \\t\\t\\ttelemetrySetting: stateValues.telemetrySetting || \\\"unset\\\",\\n1587 | \\t\\t\\tshowRooIgnoredFiles: stateValues.showRooIgnoredFiles ?? true,\\n1588 | \\t\\t\\tmaxReadFileLine: stateValues.maxReadFileLine ?? -1,\\n1589 | \\t\\t\\tmaxConcurrentFileReads: stateValues.maxConcurrentFileReads ?? 5,\\n1590 | \\t\\t\\thistoryPreviewCollapsed: stateValues.historyPreviewCollapsed ?? false,\\n1591 | \\t\\t\\tcloudUserInfo,\\n1592 | \\t\\t\\tcloudIsAuthenticated,\\n1593 | \\t\\t\\tsharingEnabled,\\n1594 | \\t\\t\\torganizationAllowList,\\n1595 | \\t\\t\\t// Explicitly add condensing settings\\n1596 | \\t\\t\\tcondensingApiConfigId: stateValues.condensingApiConfigId,\\n1597 | \\t\\t\\tcustomCondensingPrompt: stateValues.customCondensingPrompt,\\n1598 | \\t\\t\\tcodebaseIndexModels: stateValues.codebaseIndexModels ?? EMBEDDING_MODEL_PROFILES,\\n1599 | \\t\\t\\tcodebaseIndexConfig: stateValues.codebaseIndexConfig ?? {\\n1600 | \\t\\t\\t\\tcodebaseIndexEnabled: false,\\n1601 | \\t\\t\\t\\tcodebaseIndexQdrantUrl: \\\"http://localhost:6333\\\",\\n1602 | \\t\\t\\t\\tcodebaseIndexEmbedderProvider: \\\"openai\\\",\\n1603 | \\t\\t\\t\\tcodebaseIndexEmbedderBaseUrl: \\\"\\\",\\n1604 | \\t\\t\\t\\tcodebaseIndexEmbedderModelId: \\\"\\\",\\n1605 | \\t\\t\\t},\\n1606 | \\t\\t}\\n1607 | \\t}\\n1608 | \\n1609 | \\tasync updateTaskHistory(item: HistoryItem): Promise {\\n1610 | \\t\\tconst history = (this.getGlobalState(\\\"taskHistory\\\") as HistoryItem[] | undefined) || []\\n1611 | \\t\\tconst existingItemIndex = history.findIndex((h) => h.id === item.id)\\n1612 | \\n1613 | \\t\\tif (existingItemIndex !== -1) {\\n1614 | \\t\\t\\thistory[existingItemIndex] = item\\n1615 | \\t\\t} else {\\n1616 | \\t\\t\\thistory.push(item)\\n1617 | \\t\\t}\\n1618 | \\n1619 | \\t\\tawait this.updateGlobalState(\\\"taskHistory\\\", history)\\n1620 | \\t\\treturn history\\n1621 | \\t}\\n1622 | \\n1623 | \\t// ContextProxy\\n1624 | \\n1625 | \\t// @deprecated - Use `ContextProxy#setValue` instead.\\n1626 | \\tprivate async updateGlobalState(key: K, value: GlobalState[K]) {\\n1627 | \\t\\tawait this.contextProxy.setValue(key, value)\\n1628 | \\t}\\n1629 | \\n1630 | \\t// @deprecated - Use `ContextProxy#getValue` instead.\\n1631 | \\tprivate getGlobalState(key: K) {\\n1632 | \\t\\treturn this.contextProxy.getValue(key)\\n1633 | \\t}\\n1634 | \\n1635 | \\tpublic async setValue(key: K, value: RooCodeSettings[K]) {\\n1636 | \\t\\tawait this.contextProxy.setValue(key, value)\\n1637 | \\t}\\n1638 | \\n1639 | \\tpublic getValue(key: K) {\\n1640 | \\t\\treturn this.contextProxy.getValue(key)\\n1641 | \\t}\\n1642 | \\n1643 | \\tpublic getValues() {\\n1644 | \\t\\treturn this.contextProxy.getValues()\\n1645 | \\t}\\n1646 | \\n1647 | \\tpublic async setValues(values: RooCodeSettings) {\\n1648 | \\t\\tawait this.contextProxy.setValues(values)\\n1649 | \\t}\\n1650 | \\n1651 | \\t// cwd\\n1652 | \\n1653 | \\tget cwd() {\\n1654 | \\t\\treturn getWorkspacePath()\\n1655 | \\t}\\n1656 | \\n1657 | \\t// dev\\n1658 | \\n1659 | \\tasync resetState() {\\n1660 | \\t\\tconst answer = await vscode.window.showInformationMessage(\\n1661 | \\t\\t\\tt(\\\"common:confirmation.reset_state\\\"),\\n1662 | \\t\\t\\t{ modal: true },\\n1663 | \\t\\t\\tt(\\\"common:answers.yes\\\"),\\n1664 | \\t\\t)\\n1665 | \\n1666 | \\t\\tif (answer !== t(\\\"common:answers.yes\\\")) {\\n1667 | \\t\\t\\treturn\\n1668 | \\t\\t}\\n1669 | \\n1670 | \\t\\tawait this.contextProxy.resetAllState()\\n1671 | \\t\\tawait this.providerSettingsManager.resetAllConfigs()\\n1672 | \\t\\tawait this.customModesManager.resetCustomModes()\\n1673 | \\t\\tawait this.removeClineFromStack()\\n1674 | \\t\\tawait this.postStateToWebview()\\n1675 | \\t\\tawait this.postMessageToWebview({ type: \\\"action\\\", action: \\\"chatButtonClicked\\\" })\\n1676 | \\t}\\n1677 | \\n1678 | \\t// logging\\n1679 | \\n1680 | \\tpublic log(message: string) {\\n1681 | \\t\\tthis.outputChannel.appendLine(message)\\n1682 | \\t\\tconsole.log(message)\\n1683 | \\t}\\n1684 | \\n1685 | \\t// integration tests\\n1686 | \\n1687 | \\tget viewLaunched() {\\n1688 | \\t\\treturn this.isViewLaunched\\n1689 | \\t}\\n1690 | \\n1691 | \\tget messages() {\\n1692 | \\t\\treturn this.getCurrentCline()?.clineMessages || []\\n1693 | \\t}\\n1694 | \\n1695 | \\t// Add public getter\\n1696 | \\tpublic getMcpHub(): McpHub | undefined {\\n1697 | \\t\\treturn this.mcpHub\\n1698 | \\t}\\n1699 | \\n1700 | \\t/**\\n1701 | \\t * Returns properties to be included in every telemetry event\\n1702 | \\t * This method is called by the telemetry service to get context information\\n1703 | \\t * like the current mode, API provider, etc.\\n1704 | \\t */\\n1705 | \\tpublic async getTelemetryProperties(): Promise {\\n1706 | \\t\\tconst { mode, apiConfiguration, language } = await this.getState()\\n1707 | \\t\\tconst task = this.getCurrentCline()\\n1708 | \\n1709 | \\t\\tconst packageJSON = this.context.extension?.packageJSON\\n1710 | \\n1711 | \\t\\treturn {\\n1712 | \\t\\t\\tappName: packageJSON?.name ?? Package.name,\\n1713 | \\t\\t\\tappVersion: packageJSON?.version ?? Package.version,\\n1714 | \\t\\t\\tvscodeVersion: vscode.version,\\n1715 | \\t\\t\\tplatform: process.platform,\\n1716 | \\t\\t\\teditorName: vscode.env.appName,\\n1717 | \\t\\t\\tlanguage,\\n1718 | \\t\\t\\tmode,\\n1719 | \\t\\t\\tapiProvider: apiConfiguration?.apiProvider,\\n1720 | \\t\\t\\tmodelId: task?.api?.getModel().id,\\n1721 | \\t\\t\\tdiffStrategy: task?.diffStrategy?.getName(),\\n1722 | \\t\\t\\tisSubtask: task ? !!task.parentTask : undefined,\\n1723 | \\t\\t}\\n1724 | \\t}\\n1725 | }\\n\\n\\nsrc/core/webview/webviewMessageHandler.ts\\n\\n 1 | import * as path from \\\"path\\\"\\n 2 | import fs from \\\"fs/promises\\\"\\n 3 | import pWaitFor from \\\"p-wait-for\\\"\\n 4 | import * as vscode from \\\"vscode\\\"\\n 5 | \\n 6 | import { type Language, type ProviderSettings, type GlobalState, TelemetryEventName } from \\\"@roo-code/types\\\"\\n 7 | import { CloudService } from \\\"@roo-code/cloud\\\"\\n 8 | import { TelemetryService } from \\\"@roo-code/telemetry\\\"\\n 9 | \\n 10 | import { ClineProvider } from \\\"./ClineProvider\\\"\\n 11 | import { changeLanguage, t } from \\\"../../i18n\\\"\\n 12 | import { Package } from \\\"../../shared/package\\\"\\n 13 | import { RouterName, toRouterName, ModelRecord } from \\\"../../shared/api\\\"\\n 14 | import { supportPrompt } from \\\"../../shared/support-prompt\\\"\\n 15 | \\n 16 | import { checkoutDiffPayloadSchema, checkoutRestorePayloadSchema, WebviewMessage } from \\\"../../shared/WebviewMessage\\\"\\n 17 | import { checkExistKey } from \\\"../../shared/checkExistApiConfig\\\"\\n 18 | import { experimentDefault } from \\\"../../shared/experiments\\\"\\n 19 | import { Terminal } from \\\"../../integrations/terminal/Terminal\\\"\\n 20 | import { openFile } from \\\"../../integrations/misc/open-file\\\"\\n 21 | import { openImage, saveImage } from \\\"../../integrations/misc/image-handler\\\"\\n 22 | import { selectImages } from \\\"../../integrations/misc/process-images\\\"\\n 23 | import { getTheme } from \\\"../../integrations/theme/getTheme\\\"\\n 24 | import { discoverChromeHostUrl, tryChromeHostUrl } from \\\"../../services/browser/browserDiscovery\\\"\\n 25 | import { searchWorkspaceFiles } from \\\"../../services/search/file-search\\\"\\n 26 | import { fileExistsAtPath } from \\\"../../utils/fs\\\"\\n 27 | import { playTts, setTtsEnabled, setTtsSpeed, stopTts } from \\\"../../utils/tts\\\"\\n 28 | import { singleCompletionHandler } from \\\"../../utils/single-completion-handler\\\"\\n 29 | import { searchCommits } from \\\"../../utils/git\\\"\\n 30 | import { exportSettings, importSettings } from \\\"../config/importExport\\\"\\n 31 | import { getOpenAiModels } from \\\"../../api/providers/openai\\\"\\n 32 | import { getOllamaModels } from \\\"../../api/providers/ollama\\\"\\n 33 | import { getVsCodeLmModels } from \\\"../../api/providers/vscode-lm\\\"\\n 34 | import { getLmStudioModels } from \\\"../../api/providers/lm-studio\\\"\\n 35 | import { openMention } from \\\"../mentions\\\"\\n 36 | import { TelemetrySetting } from \\\"../../shared/TelemetrySetting\\\"\\n 37 | import { getWorkspacePath } from \\\"../../utils/path\\\"\\n 38 | import { Mode, defaultModeSlug } from \\\"../../shared/modes\\\"\\n 39 | import { getModels, flushModels } from \\\"../../api/providers/fetchers/modelCache\\\"\\n 40 | import { GetModelsOptions } from \\\"../../shared/api\\\"\\n 41 | import { generateSystemPrompt } from \\\"./generateSystemPrompt\\\"\\n 42 | import { getCommand } from \\\"../../utils/commands\\\"\\n 43 | \\n 44 | const ALLOWED_VSCODE_SETTINGS = new Set([\\\"terminal.integrated.inheritEnv\\\"])\\n 45 | \\n 46 | import { MarketplaceManager, MarketplaceItemType } from \\\"../../services/marketplace\\\"\\n 47 | \\n 48 | export const webviewMessageHandler = async (\\n 49 | \\tprovider: ClineProvider,\\n 50 | \\tmessage: WebviewMessage,\\n 51 | \\tmarketplaceManager?: MarketplaceManager,\\n 52 | ) => {\\n 53 | \\t// Utility functions provided for concise get/update of global state via contextProxy API.\\n 54 | \\tconst getGlobalState = (key: K) => provider.contextProxy.getValue(key)\\n 55 | \\tconst updateGlobalState = async (key: K, value: GlobalState[K]) =>\\n 56 | \\t\\tawait provider.contextProxy.setValue(key, value)\\n 57 | \\n 58 | \\tswitch (message.type) {\\n 59 | \\t\\tcase \\\"webviewDidLaunch\\\":\\n 60 | \\t\\t\\t// Load custom modes first\\n 61 | \\t\\t\\tconst customModes = await provider.customModesManager.getCustomModes()\\n 62 | \\t\\t\\tawait updateGlobalState(\\\"customModes\\\", customModes)\\n 63 | \\n 64 | \\t\\t\\tprovider.postStateToWebview()\\n 65 | \\t\\t\\tprovider.workspaceTracker?.initializeFilePaths() // Don't await.\\n 66 | \\n 67 | \\t\\t\\tgetTheme().then((theme) => provider.postMessageToWebview({ type: \\\"theme\\\", text: JSON.stringify(theme) }))\\n 68 | \\n 69 | \\t\\t\\t// If MCP Hub is already initialized, update the webview with\\n 70 | \\t\\t\\t// current server list.\\n 71 | \\t\\t\\tconst mcpHub = provider.getMcpHub()\\n 72 | \\n 73 | \\t\\t\\tif (mcpHub) {\\n 74 | \\t\\t\\t\\tprovider.postMessageToWebview({ type: \\\"mcpServers\\\", mcpServers: mcpHub.getAllServers() })\\n 75 | \\t\\t\\t}\\n 76 | \\n 77 | \\t\\t\\tprovider.providerSettingsManager\\n 78 | \\t\\t\\t\\t.listConfig()\\n 79 | \\t\\t\\t\\t.then(async (listApiConfig) => {\\n 80 | \\t\\t\\t\\t\\tif (!listApiConfig) {\\n 81 | \\t\\t\\t\\t\\t\\treturn\\n 82 | \\t\\t\\t\\t\\t}\\n 83 | \\n 84 | \\t\\t\\t\\t\\tif (listApiConfig.length === 1) {\\n 85 | \\t\\t\\t\\t\\t\\t// Check if first time init then sync with exist config.\\n 86 | \\t\\t\\t\\t\\t\\tif (!checkExistKey(listApiConfig[0])) {\\n 87 | \\t\\t\\t\\t\\t\\t\\tconst { apiConfiguration } = await provider.getState()\\n 88 | \\n 89 | \\t\\t\\t\\t\\t\\t\\tawait provider.providerSettingsManager.saveConfig(\\n 90 | \\t\\t\\t\\t\\t\\t\\t\\tlistApiConfig[0].name ?? \\\"default\\\",\\n 91 | \\t\\t\\t\\t\\t\\t\\t\\tapiConfiguration,\\n 92 | \\t\\t\\t\\t\\t\\t\\t)\\n 93 | \\n 94 | \\t\\t\\t\\t\\t\\t\\tlistApiConfig[0].apiProvider = apiConfiguration.apiProvider\\n 95 | \\t\\t\\t\\t\\t\\t}\\n 96 | \\t\\t\\t\\t\\t}\\n 97 | \\n 98 | \\t\\t\\t\\t\\tconst currentConfigName = getGlobalState(\\\"currentApiConfigName\\\")\\n 99 | \\n 100 | \\t\\t\\t\\t\\tif (currentConfigName) {\\n 101 | \\t\\t\\t\\t\\t\\tif (!(await provider.providerSettingsManager.hasConfig(currentConfigName))) {\\n 102 | \\t\\t\\t\\t\\t\\t\\t// Current config name not valid, get first config in list.\\n 103 | \\t\\t\\t\\t\\t\\t\\tconst name = listApiConfig[0]?.name\\n 104 | \\t\\t\\t\\t\\t\\t\\tawait updateGlobalState(\\\"currentApiConfigName\\\", name)\\n 105 | \\n 106 | \\t\\t\\t\\t\\t\\t\\tif (name) {\\n 107 | \\t\\t\\t\\t\\t\\t\\t\\tawait provider.activateProviderProfile({ name })\\n 108 | \\t\\t\\t\\t\\t\\t\\t\\treturn\\n 109 | \\t\\t\\t\\t\\t\\t\\t}\\n 110 | \\t\\t\\t\\t\\t\\t}\\n 111 | \\t\\t\\t\\t\\t}\\n 112 | \\n 113 | \\t\\t\\t\\t\\tawait Promise.all([\\n 114 | \\t\\t\\t\\t\\t\\tawait updateGlobalState(\\\"listApiConfigMeta\\\", listApiConfig),\\n 115 | \\t\\t\\t\\t\\t\\tawait provider.postMessageToWebview({ type: \\\"listApiConfig\\\", listApiConfig }),\\n 116 | \\t\\t\\t\\t\\t])\\n 117 | \\t\\t\\t\\t})\\n 118 | \\t\\t\\t\\t.catch((error) =>\\n 119 | \\t\\t\\t\\t\\tprovider.log(\\n 120 | \\t\\t\\t\\t\\t\\t`Error list api configuration: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`,\\n 121 | \\t\\t\\t\\t\\t),\\n 122 | \\t\\t\\t\\t)\\n 123 | \\n 124 | \\t\\t\\t// If user already opted in to telemetry, enable telemetry service\\n 125 | \\t\\t\\tprovider.getStateToPostToWebview().then((state) => {\\n 126 | \\t\\t\\t\\tconst { telemetrySetting } = state\\n 127 | \\t\\t\\t\\tconst isOptedIn = telemetrySetting === \\\"enabled\\\"\\n 128 | \\t\\t\\t\\tTelemetryService.instance.updateTelemetryState(isOptedIn)\\n 129 | \\t\\t\\t})\\n 130 | \\n 131 | \\t\\t\\tprovider.isViewLaunched = true\\n 132 | \\t\\t\\tbreak\\n 133 | \\t\\tcase \\\"newTask\\\":\\n 134 | \\t\\t\\t// Initializing new instance of Cline will make sure that any\\n 135 | \\t\\t\\t// agentically running promises in old instance don't affect our new\\n 136 | \\t\\t\\t// task. This essentially creates a fresh slate for the new task.\\n 137 | \\t\\t\\tawait provider.initClineWithTask(message.text, message.images)\\n 138 | \\t\\t\\tbreak\\n 139 | \\t\\tcase \\\"customInstructions\\\":\\n 140 | \\t\\t\\tawait provider.updateCustomInstructions(message.text)\\n 141 | \\t\\t\\tbreak\\n 142 | \\t\\tcase \\\"alwaysAllowReadOnly\\\":\\n 143 | \\t\\t\\tawait updateGlobalState(\\\"alwaysAllowReadOnly\\\", message.bool ?? undefined)\\n 144 | \\t\\t\\tawait provider.postStateToWebview()\\n 145 | \\t\\t\\tbreak\\n 146 | \\t\\tcase \\\"alwaysAllowReadOnlyOutsideWorkspace\\\":\\n 147 | \\t\\t\\tawait updateGlobalState(\\\"alwaysAllowReadOnlyOutsideWorkspace\\\", message.bool ?? undefined)\\n 148 | \\t\\t\\tawait provider.postStateToWebview()\\n 149 | \\t\\t\\tbreak\\n 150 | \\t\\tcase \\\"alwaysAllowWrite\\\":\\n 151 | \\t\\t\\tawait updateGlobalState(\\\"alwaysAllowWrite\\\", message.bool ?? undefined)\\n 152 | \\t\\t\\tawait provider.postStateToWebview()\\n 153 | \\t\\t\\tbreak\\n 154 | \\t\\tcase \\\"alwaysAllowWriteOutsideWorkspace\\\":\\n 155 | \\t\\t\\tawait updateGlobalState(\\\"alwaysAllowWriteOutsideWorkspace\\\", message.bool ?? undefined)\\n 156 | \\t\\t\\tawait provider.postStateToWebview()\\n 157 | \\t\\t\\tbreak\\n 158 | \\t\\tcase \\\"alwaysAllowExecute\\\":\\n 159 | \\t\\t\\tawait updateGlobalState(\\\"alwaysAllowExecute\\\", message.bool ?? undefined)\\n 160 | \\t\\t\\tawait provider.postStateToWebview()\\n 161 | \\t\\t\\tbreak\\n 162 | \\t\\tcase \\\"alwaysAllowBrowser\\\":\\n 163 | \\t\\t\\tawait updateGlobalState(\\\"alwaysAllowBrowser\\\", message.bool ?? undefined)\\n 164 | \\t\\t\\tawait provider.postStateToWebview()\\n 165 | \\t\\t\\tbreak\\n 166 | \\t\\tcase \\\"alwaysAllowMcp\\\":\\n 167 | \\t\\t\\tawait updateGlobalState(\\\"alwaysAllowMcp\\\", message.bool)\\n 168 | \\t\\t\\tawait provider.postStateToWebview()\\n 169 | \\t\\t\\tbreak\\n 170 | \\t\\tcase \\\"alwaysAllowModeSwitch\\\":\\n 171 | \\t\\t\\tawait updateGlobalState(\\\"alwaysAllowModeSwitch\\\", message.bool)\\n 172 | \\t\\t\\tawait provider.postStateToWebview()\\n 173 | \\t\\t\\tbreak\\n 174 | \\t\\tcase \\\"allowedMaxRequests\\\":\\n 175 | \\t\\t\\tawait updateGlobalState(\\\"allowedMaxRequests\\\", message.value)\\n 176 | \\t\\t\\tawait provider.postStateToWebview()\\n 177 | \\t\\t\\tbreak\\n 178 | \\t\\tcase \\\"alwaysAllowSubtasks\\\":\\n 179 | \\t\\t\\tawait updateGlobalState(\\\"alwaysAllowSubtasks\\\", message.bool)\\n 180 | \\t\\t\\tawait provider.postStateToWebview()\\n 181 | \\t\\t\\tbreak\\n 182 | \\t\\tcase \\\"askResponse\\\":\\n 183 | \\t\\t\\tprovider.getCurrentCline()?.handleWebviewAskResponse(message.askResponse!, message.text, message.images)\\n 184 | \\t\\t\\tbreak\\n 185 | \\t\\tcase \\\"autoCondenseContext\\\":\\n 186 | \\t\\t\\tawait updateGlobalState(\\\"autoCondenseContext\\\", message.bool)\\n 187 | \\t\\t\\tawait provider.postStateToWebview()\\n 188 | \\t\\t\\tbreak\\n 189 | \\t\\tcase \\\"autoCondenseContextPercent\\\":\\n 190 | \\t\\t\\tawait updateGlobalState(\\\"autoCondenseContextPercent\\\", message.value)\\n 191 | \\t\\t\\tawait provider.postStateToWebview()\\n 192 | \\t\\t\\tbreak\\n 193 | \\t\\tcase \\\"terminalOperation\\\":\\n 194 | \\t\\t\\tif (message.terminalOperation) {\\n 195 | \\t\\t\\t\\tprovider.getCurrentCline()?.handleTerminalOperation(message.terminalOperation)\\n 196 | \\t\\t\\t}\\n 197 | \\t\\t\\tbreak\\n 198 | \\t\\tcase \\\"clearTask\\\":\\n 199 | \\t\\t\\t// clear task resets the current session and allows for a new task to be started, if this session is a subtask - it allows the parent task to be resumed\\n 200 | \\t\\t\\tawait provider.finishSubTask(t(\\\"common:tasks.canceled\\\"))\\n 201 | \\t\\t\\tawait provider.postStateToWebview()\\n 202 | \\t\\t\\tbreak\\n 203 | \\t\\tcase \\\"didShowAnnouncement\\\":\\n 204 | \\t\\t\\tawait updateGlobalState(\\\"lastShownAnnouncementId\\\", provider.latestAnnouncementId)\\n 205 | \\t\\t\\tawait provider.postStateToWebview()\\n 206 | \\t\\t\\tbreak\\n 207 | \\t\\tcase \\\"selectImages\\\":\\n 208 | \\t\\t\\tconst images = await selectImages()\\n 209 | \\t\\t\\tawait provider.postMessageToWebview({ type: \\\"selectedImages\\\", images })\\n 210 | \\t\\t\\tbreak\\n 211 | \\t\\tcase \\\"exportCurrentTask\\\":\\n 212 | \\t\\t\\tconst currentTaskId = provider.getCurrentCline()?.taskId\\n 213 | \\t\\t\\tif (currentTaskId) {\\n 214 | \\t\\t\\t\\tprovider.exportTaskWithId(currentTaskId)\\n 215 | \\t\\t\\t}\\n 216 | \\t\\t\\tbreak\\n 217 | \\t\\tcase \\\"shareCurrentTask\\\":\\n 218 | \\t\\t\\tconst shareTaskId = provider.getCurrentCline()?.taskId\\n 219 | \\t\\t\\tif (!shareTaskId) {\\n 220 | \\t\\t\\t\\tvscode.window.showErrorMessage(t(\\\"common:errors.share_no_active_task\\\"))\\n 221 | \\t\\t\\t\\tbreak\\n 222 | \\t\\t\\t}\\n 223 | \\n 224 | \\t\\t\\ttry {\\n 225 | \\t\\t\\t\\tconst success = await CloudService.instance.shareTask(shareTaskId)\\n 226 | \\t\\t\\t\\tif (success) {\\n 227 | \\t\\t\\t\\t\\t// Show success message\\n 228 | \\t\\t\\t\\t\\tvscode.window.showInformationMessage(t(\\\"common:info.share_link_copied\\\"))\\n 229 | \\t\\t\\t\\t} else {\\n 230 | \\t\\t\\t\\t\\t// Show generic failure message\\n 231 | \\t\\t\\t\\t\\tvscode.window.showErrorMessage(t(\\\"common:errors.share_task_failed\\\"))\\n 232 | \\t\\t\\t\\t}\\n 233 | \\t\\t\\t} catch (error) {\\n 234 | \\t\\t\\t\\t// Show generic failure message\\n 235 | \\t\\t\\t\\tvscode.window.showErrorMessage(t(\\\"common:errors.share_task_failed\\\"))\\n 236 | \\t\\t\\t}\\n 237 | \\t\\t\\tbreak\\n 238 | \\t\\tcase \\\"showTaskWithId\\\":\\n 239 | \\t\\t\\tprovider.showTaskWithId(message.text!)\\n 240 | \\t\\t\\tbreak\\n 241 | \\t\\tcase \\\"condenseTaskContextRequest\\\":\\n 242 | \\t\\t\\tprovider.condenseTaskContext(message.text!)\\n 243 | \\t\\t\\tbreak\\n 244 | \\t\\tcase \\\"deleteTaskWithId\\\":\\n 245 | \\t\\t\\tprovider.deleteTaskWithId(message.text!)\\n 246 | \\t\\t\\tbreak\\n 247 | \\t\\tcase \\\"deleteMultipleTasksWithIds\\\": {\\n 248 | \\t\\t\\tconst ids = message.ids\\n 249 | \\n 250 | \\t\\t\\tif (Array.isArray(ids)) {\\n 251 | \\t\\t\\t\\t// Process in batches of 20 (or another reasonable number)\\n 252 | \\t\\t\\t\\tconst batchSize = 20\\n 253 | \\t\\t\\t\\tconst results = []\\n 254 | \\n 255 | \\t\\t\\t\\t// Only log start and end of the operation\\n 256 | \\t\\t\\t\\tconsole.log(`Batch deletion started: ${ids.length} tasks total`)\\n 257 | \\n 258 | \\t\\t\\t\\tfor (let i = 0; i < ids.length; i += batchSize) {\\n 259 | \\t\\t\\t\\t\\tconst batch = ids.slice(i, i + batchSize)\\n 260 | \\n 261 | \\t\\t\\t\\t\\tconst batchPromises = batch.map(async (id) => {\\n 262 | \\t\\t\\t\\t\\t\\ttry {\\n 263 | \\t\\t\\t\\t\\t\\t\\tawait provider.deleteTaskWithId(id)\\n 264 | \\t\\t\\t\\t\\t\\t\\treturn { id, success: true }\\n 265 | \\t\\t\\t\\t\\t\\t} catch (error) {\\n 266 | \\t\\t\\t\\t\\t\\t\\t// Keep error logging for debugging purposes\\n 267 | \\t\\t\\t\\t\\t\\t\\tconsole.log(\\n 268 | \\t\\t\\t\\t\\t\\t\\t\\t`Failed to delete task ${id}: ${error instanceof Error ? error.message : String(error)}`,\\n 269 | \\t\\t\\t\\t\\t\\t\\t)\\n 270 | \\t\\t\\t\\t\\t\\t\\treturn { id, success: false }\\n 271 | \\t\\t\\t\\t\\t\\t}\\n 272 | \\t\\t\\t\\t\\t})\\n 273 | \\n 274 | \\t\\t\\t\\t\\t// Process each batch in parallel but wait for completion before starting the next batch\\n 275 | \\t\\t\\t\\t\\tconst batchResults = await Promise.all(batchPromises)\\n 276 | \\t\\t\\t\\t\\tresults.push(...batchResults)\\n 277 | \\n 278 | \\t\\t\\t\\t\\t// Update the UI after each batch to show progress\\n 279 | \\t\\t\\t\\t\\tawait provider.postStateToWebview()\\n 280 | \\t\\t\\t\\t}\\n 281 | \\n 282 | \\t\\t\\t\\t// Log final results\\n 283 | \\t\\t\\t\\tconst successCount = results.filter((r) => r.success).length\\n 284 | \\t\\t\\t\\tconst failCount = results.length - successCount\\n 285 | \\t\\t\\t\\tconsole.log(\\n 286 | \\t\\t\\t\\t\\t`Batch deletion completed: ${successCount}/${ids.length} tasks successful, ${failCount} tasks failed`,\\n 287 | \\t\\t\\t\\t)\\n 288 | \\t\\t\\t}\\n 289 | \\t\\t\\tbreak\\n 290 | \\t\\t}\\n 291 | \\t\\tcase \\\"exportTaskWithId\\\":\\n 292 | \\t\\t\\tprovider.exportTaskWithId(message.text!)\\n 293 | \\t\\t\\tbreak\\n 294 | \\t\\tcase \\\"importSettings\\\": {\\n 295 | \\t\\t\\tconst result = await importSettings({\\n 296 | \\t\\t\\t\\tproviderSettingsManager: provider.providerSettingsManager,\\n 297 | \\t\\t\\t\\tcontextProxy: provider.contextProxy,\\n 298 | \\t\\t\\t\\tcustomModesManager: provider.customModesManager,\\n 299 | \\t\\t\\t})\\n 300 | \\n 301 | \\t\\t\\tif (result.success) {\\n 302 | \\t\\t\\t\\tprovider.settingsImportedAt = Date.now()\\n 303 | \\t\\t\\t\\tawait provider.postStateToWebview()\\n 304 | \\t\\t\\t\\tawait vscode.window.showInformationMessage(t(\\\"common:info.settings_imported\\\"))\\n 305 | \\t\\t\\t} else if (result.error) {\\n 306 | \\t\\t\\t\\tawait vscode.window.showErrorMessage(t(\\\"common:errors.settings_import_failed\\\", { error: result.error }))\\n 307 | \\t\\t\\t}\\n 308 | \\n 309 | \\t\\t\\tbreak\\n 310 | \\t\\t}\\n 311 | \\t\\tcase \\\"exportSettings\\\":\\n 312 | \\t\\t\\tawait exportSettings({\\n 313 | \\t\\t\\t\\tproviderSettingsManager: provider.providerSettingsManager,\\n 314 | \\t\\t\\t\\tcontextProxy: provider.contextProxy,\\n 315 | \\t\\t\\t})\\n 316 | \\n 317 | \\t\\t\\tbreak\\n 318 | \\t\\tcase \\\"resetState\\\":\\n 319 | \\t\\t\\tawait provider.resetState()\\n 320 | \\t\\t\\tbreak\\n 321 | \\t\\tcase \\\"flushRouterModels\\\":\\n 322 | \\t\\t\\tconst routerNameFlush: RouterName = toRouterName(message.text)\\n 323 | \\t\\t\\tawait flushModels(routerNameFlush)\\n 324 | \\t\\t\\tbreak\\n 325 | \\t\\tcase \\\"requestRouterModels\\\":\\n 326 | \\t\\t\\tconst { apiConfiguration } = await provider.getState()\\n 327 | \\n 328 | \\t\\t\\tconst routerModels: Partial> = {\\n 329 | \\t\\t\\t\\topenrouter: {},\\n 330 | \\t\\t\\t\\trequesty: {},\\n 331 | \\t\\t\\t\\tglama: {},\\n 332 | \\t\\t\\t\\tunbound: {},\\n 333 | \\t\\t\\t\\tlitellm: {},\\n 334 | \\t\\t\\t}\\n 335 | \\n 336 | \\t\\t\\tconst safeGetModels = async (options: GetModelsOptions): Promise => {\\n 337 | \\t\\t\\t\\ttry {\\n 338 | \\t\\t\\t\\t\\treturn await getModels(options)\\n 339 | \\t\\t\\t\\t} catch (error) {\\n 340 | \\t\\t\\t\\t\\tconsole.error(\\n 341 | \\t\\t\\t\\t\\t\\t`Failed to fetch models in webviewMessageHandler requestRouterModels for ${options.provider}:`,\\n 342 | \\t\\t\\t\\t\\t\\terror,\\n 343 | \\t\\t\\t\\t\\t)\\n 344 | \\t\\t\\t\\t\\tthrow error // Re-throw to be caught by Promise.allSettled\\n 345 | \\t\\t\\t\\t}\\n 346 | \\t\\t\\t}\\n 347 | \\n 348 | \\t\\t\\tconst modelFetchPromises: Array<{ key: RouterName; options: GetModelsOptions }> = [\\n 349 | \\t\\t\\t\\t{ key: \\\"openrouter\\\", options: { provider: \\\"openrouter\\\" } },\\n 350 | \\t\\t\\t\\t{ key: \\\"requesty\\\", options: { provider: \\\"requesty\\\", apiKey: apiConfiguration.requestyApiKey } },\\n 351 | \\t\\t\\t\\t{ key: \\\"glama\\\", options: { provider: \\\"glama\\\" } },\\n 352 | \\t\\t\\t\\t{ key: \\\"unbound\\\", options: { provider: \\\"unbound\\\", apiKey: apiConfiguration.unboundApiKey } },\\n 353 | \\t\\t\\t]\\n 354 | \\n 355 | \\t\\t\\tconst litellmApiKey = apiConfiguration.litellmApiKey || message?.values?.litellmApiKey\\n 356 | \\t\\t\\tconst litellmBaseUrl = apiConfiguration.litellmBaseUrl || message?.values?.litellmBaseUrl\\n 357 | \\t\\t\\tif (litellmApiKey && litellmBaseUrl) {\\n 358 | \\t\\t\\t\\tmodelFetchPromises.push({\\n 359 | \\t\\t\\t\\t\\tkey: \\\"litellm\\\",\\n 360 | \\t\\t\\t\\t\\toptions: { provider: \\\"litellm\\\", apiKey: litellmApiKey, baseUrl: litellmBaseUrl },\\n 361 | \\t\\t\\t\\t})\\n 362 | \\t\\t\\t}\\n 363 | \\n 364 | \\t\\t\\tconst results = await Promise.allSettled(\\n 365 | \\t\\t\\t\\tmodelFetchPromises.map(async ({ key, options }) => {\\n 366 | \\t\\t\\t\\t\\tconst models = await safeGetModels(options)\\n 367 | \\t\\t\\t\\t\\treturn { key, models } // key is RouterName here\\n 368 | \\t\\t\\t\\t}),\\n 369 | \\t\\t\\t)\\n 370 | \\n 371 | \\t\\t\\tconst fetchedRouterModels: Partial> = { ...routerModels }\\n 372 | \\n 373 | \\t\\t\\tresults.forEach((result, index) => {\\n 374 | \\t\\t\\t\\tconst routerName = modelFetchPromises[index].key // Get RouterName using index\\n 375 | \\n 376 | \\t\\t\\t\\tif (result.status === \\\"fulfilled\\\") {\\n 377 | \\t\\t\\t\\t\\tfetchedRouterModels[routerName] = result.value.models\\n 378 | \\t\\t\\t\\t} else {\\n 379 | \\t\\t\\t\\t\\t// Handle rejection: Post a specific error message for this provider\\n 380 | \\t\\t\\t\\t\\tconst errorMessage = result.reason instanceof Error ? result.reason.message : String(result.reason)\\n 381 | \\t\\t\\t\\t\\tconsole.error(`Error fetching models for ${routerName}:`, result.reason)\\n 382 | \\n 383 | \\t\\t\\t\\t\\tfetchedRouterModels[routerName] = {} // Ensure it's an empty object in the main routerModels message\\n 384 | \\n 385 | \\t\\t\\t\\t\\tprovider.postMessageToWebview({\\n 386 | \\t\\t\\t\\t\\t\\ttype: \\\"singleRouterModelFetchResponse\\\",\\n 387 | \\t\\t\\t\\t\\t\\tsuccess: false,\\n 388 | \\t\\t\\t\\t\\t\\terror: errorMessage,\\n 389 | \\t\\t\\t\\t\\t\\tvalues: { provider: routerName },\\n 390 | \\t\\t\\t\\t\\t})\\n 391 | \\t\\t\\t\\t}\\n 392 | \\t\\t\\t})\\n 393 | \\n 394 | \\t\\t\\tprovider.postMessageToWebview({\\n 395 | \\t\\t\\t\\ttype: \\\"routerModels\\\",\\n 396 | \\t\\t\\t\\trouterModels: fetchedRouterModels as Record,\\n 397 | \\t\\t\\t})\\n 398 | \\t\\t\\tbreak\\n 399 | \\t\\tcase \\\"requestOpenAiModels\\\":\\n 400 | \\t\\t\\tif (message?.values?.baseUrl && message?.values?.apiKey) {\\n 401 | \\t\\t\\t\\tconst openAiModels = await getOpenAiModels(\\n 402 | \\t\\t\\t\\t\\tmessage?.values?.baseUrl,\\n 403 | \\t\\t\\t\\t\\tmessage?.values?.apiKey,\\n 404 | \\t\\t\\t\\t\\tmessage?.values?.openAiHeaders,\\n 405 | \\t\\t\\t\\t)\\n 406 | \\n 407 | \\t\\t\\t\\tprovider.postMessageToWebview({ type: \\\"openAiModels\\\", openAiModels })\\n 408 | \\t\\t\\t}\\n 409 | \\n 410 | \\t\\t\\tbreak\\n 411 | \\t\\tcase \\\"requestOllamaModels\\\":\\n 412 | \\t\\t\\tconst ollamaModels = await getOllamaModels(message.text)\\n 413 | \\t\\t\\t// TODO: Cache like we do for OpenRouter, etc?\\n 414 | \\t\\t\\tprovider.postMessageToWebview({ type: \\\"ollamaModels\\\", ollamaModels })\\n 415 | \\t\\t\\tbreak\\n 416 | \\t\\tcase \\\"requestLmStudioModels\\\":\\n 417 | \\t\\t\\tconst lmStudioModels = await getLmStudioModels(message.text)\\n 418 | \\t\\t\\t// TODO: Cache like we do for OpenRouter, etc?\\n 419 | \\t\\t\\tprovider.postMessageToWebview({ type: \\\"lmStudioModels\\\", lmStudioModels })\\n 420 | \\t\\t\\tbreak\\n 421 | \\t\\tcase \\\"requestVsCodeLmModels\\\":\\n 422 | \\t\\t\\tconst vsCodeLmModels = await getVsCodeLmModels()\\n 423 | \\t\\t\\t// TODO: Cache like we do for OpenRouter, etc?\\n 424 | \\t\\t\\tprovider.postMessageToWebview({ type: \\\"vsCodeLmModels\\\", vsCodeLmModels })\\n 425 | \\t\\t\\tbreak\\n 426 | \\t\\tcase \\\"openImage\\\":\\n 427 | \\t\\t\\topenImage(message.text!, { values: message.values })\\n 428 | \\t\\t\\tbreak\\n 429 | \\t\\tcase \\\"saveImage\\\":\\n 430 | \\t\\t\\tsaveImage(message.dataUri!)\\n 431 | \\t\\t\\tbreak\\n 432 | \\t\\tcase \\\"openFile\\\":\\n 433 | \\t\\t\\topenFile(message.text!, message.values as { create?: boolean; content?: string; line?: number })\\n 434 | \\t\\t\\tbreak\\n 435 | \\t\\tcase \\\"openMention\\\":\\n 436 | \\t\\t\\topenMention(message.text)\\n 437 | \\t\\t\\tbreak\\n 438 | \\t\\tcase \\\"openExternal\\\":\\n 439 | \\t\\t\\tif (message.url) {\\n 440 | \\t\\t\\t\\tvscode.env.openExternal(vscode.Uri.parse(message.url))\\n 441 | \\t\\t\\t}\\n 442 | \\t\\t\\tbreak\\n 443 | \\t\\tcase \\\"checkpointDiff\\\":\\n 444 | \\t\\t\\tconst result = checkoutDiffPayloadSchema.safeParse(message.payload)\\n 445 | \\n 446 | \\t\\t\\tif (result.success) {\\n 447 | \\t\\t\\t\\tawait provider.getCurrentCline()?.checkpointDiff(result.data)\\n 448 | \\t\\t\\t}\\n 449 | \\n 450 | \\t\\t\\tbreak\\n 451 | \\t\\tcase \\\"checkpointRestore\\\": {\\n 452 | \\t\\t\\tconst result = checkoutRestorePayloadSchema.safeParse(message.payload)\\n 453 | \\n 454 | \\t\\t\\tif (result.success) {\\n 455 | \\t\\t\\t\\tawait provider.cancelTask()\\n 456 | \\n 457 | \\t\\t\\t\\ttry {\\n 458 | \\t\\t\\t\\t\\tawait pWaitFor(() => provider.getCurrentCline()?.isInitialized === true, { timeout: 3_000 })\\n 459 | \\t\\t\\t\\t} catch (error) {\\n 460 | \\t\\t\\t\\t\\tvscode.window.showErrorMessage(t(\\\"common:errors.checkpoint_timeout\\\"))\\n 461 | \\t\\t\\t\\t}\\n 462 | \\n 463 | \\t\\t\\t\\ttry {\\n 464 | \\t\\t\\t\\t\\tawait provider.getCurrentCline()?.checkpointRestore(result.data)\\n 465 | \\t\\t\\t\\t} catch (error) {\\n 466 | \\t\\t\\t\\t\\tvscode.window.showErrorMessage(t(\\\"common:errors.checkpoint_failed\\\"))\\n 467 | \\t\\t\\t\\t}\\n 468 | \\t\\t\\t}\\n 469 | \\n 470 | \\t\\t\\tbreak\\n 471 | \\t\\t}\\n 472 | \\t\\tcase \\\"cancelTask\\\":\\n 473 | \\t\\t\\tawait provider.cancelTask()\\n 474 | \\t\\t\\tbreak\\n 475 | \\t\\tcase \\\"allowedCommands\\\":\\n 476 | \\t\\t\\tawait provider.context.globalState.update(\\\"allowedCommands\\\", message.commands)\\n 477 | \\n 478 | \\t\\t\\t// Also update workspace settings.\\n 479 | \\t\\t\\tawait vscode.workspace\\n 480 | \\t\\t\\t\\t.getConfiguration(Package.name)\\n 481 | \\t\\t\\t\\t.update(\\\"allowedCommands\\\", message.commands, vscode.ConfigurationTarget.Global)\\n 482 | \\n 483 | \\t\\t\\tbreak\\n 484 | \\t\\tcase \\\"openCustomModesSettings\\\": {\\n 485 | \\t\\t\\tconst customModesFilePath = await provider.customModesManager.getCustomModesFilePath()\\n 486 | \\n 487 | \\t\\t\\tif (customModesFilePath) {\\n 488 | \\t\\t\\t\\topenFile(customModesFilePath)\\n 489 | \\t\\t\\t}\\n 490 | \\n 491 | \\t\\t\\tbreak\\n 492 | \\t\\t}\\n 493 | \\t\\tcase \\\"openMcpSettings\\\": {\\n 494 | \\t\\t\\tconst mcpSettingsFilePath = await provider.getMcpHub()?.getMcpSettingsFilePath()\\n 495 | \\n 496 | \\t\\t\\tif (mcpSettingsFilePath) {\\n 497 | \\t\\t\\t\\topenFile(mcpSettingsFilePath)\\n 498 | \\t\\t\\t}\\n 499 | \\n 500 | \\t\\t\\tbreak\\n 501 | \\t\\t}\\n 502 | \\t\\tcase \\\"openProjectMcpSettings\\\": {\\n 503 | \\t\\t\\tif (!vscode.workspace.workspaceFolders?.length) {\\n 504 | \\t\\t\\t\\tvscode.window.showErrorMessage(t(\\\"common:errors.no_workspace\\\"))\\n 505 | \\t\\t\\t\\treturn\\n 506 | \\t\\t\\t}\\n 507 | \\n 508 | \\t\\t\\tconst workspaceFolder = vscode.workspace.workspaceFolders[0]\\n 509 | \\t\\t\\tconst rooDir = path.join(workspaceFolder.uri.fsPath, \\\".roo\\\")\\n 510 | \\t\\t\\tconst mcpPath = path.join(rooDir, \\\"mcp.json\\\")\\n 511 | \\n 512 | \\t\\t\\ttry {\\n 513 | \\t\\t\\t\\tawait fs.mkdir(rooDir, { recursive: true })\\n 514 | \\t\\t\\t\\tconst exists = await fileExistsAtPath(mcpPath)\\n 515 | \\n 516 | \\t\\t\\t\\tif (!exists) {\\n 517 | \\t\\t\\t\\t\\tawait fs.writeFile(mcpPath, JSON.stringify({ mcpServers: {} }, null, 2))\\n 518 | \\t\\t\\t\\t}\\n 519 | \\n 520 | \\t\\t\\t\\tawait openFile(mcpPath)\\n 521 | \\t\\t\\t} catch (error) {\\n 522 | \\t\\t\\t\\tvscode.window.showErrorMessage(t(\\\"mcp:errors.create_json\\\", { error: `${error}` }))\\n 523 | \\t\\t\\t}\\n 524 | \\n 525 | \\t\\t\\tbreak\\n 526 | \\t\\t}\\n 527 | \\t\\tcase \\\"deleteMcpServer\\\": {\\n 528 | \\t\\t\\tif (!message.serverName) {\\n 529 | \\t\\t\\t\\tbreak\\n 530 | \\t\\t\\t}\\n 531 | \\n 532 | \\t\\t\\ttry {\\n 533 | \\t\\t\\t\\tprovider.log(`Attempting to delete MCP server: ${message.serverName}`)\\n 534 | \\t\\t\\t\\tawait provider.getMcpHub()?.deleteServer(message.serverName, message.source as \\\"global\\\" | \\\"project\\\")\\n 535 | \\t\\t\\t\\tprovider.log(`Successfully deleted MCP server: ${message.serverName}`)\\n 536 | \\n 537 | \\t\\t\\t\\t// Refresh the webview state\\n 538 | \\t\\t\\t\\tawait provider.postStateToWebview()\\n 539 | \\t\\t\\t} catch (error) {\\n 540 | \\t\\t\\t\\tconst errorMessage = error instanceof Error ? error.message : String(error)\\n 541 | \\t\\t\\t\\tprovider.log(`Failed to delete MCP server: ${errorMessage}`)\\n 542 | \\t\\t\\t\\t// Error messages are already handled by McpHub.deleteServer\\n 543 | \\t\\t\\t}\\n 544 | \\t\\t\\tbreak\\n 545 | \\t\\t}\\n 546 | \\t\\tcase \\\"restartMcpServer\\\": {\\n 547 | \\t\\t\\ttry {\\n 548 | \\t\\t\\t\\tawait provider.getMcpHub()?.restartConnection(message.text!, message.source as \\\"global\\\" | \\\"project\\\")\\n 549 | \\t\\t\\t} catch (error) {\\n 550 | \\t\\t\\t\\tprovider.log(\\n 551 | \\t\\t\\t\\t\\t`Failed to retry connection for ${message.text}: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`,\\n 552 | \\t\\t\\t\\t)\\n 553 | \\t\\t\\t}\\n 554 | \\t\\t\\tbreak\\n 555 | \\t\\t}\\n 556 | \\t\\tcase \\\"toggleToolAlwaysAllow\\\": {\\n 557 | \\t\\t\\ttry {\\n 558 | \\t\\t\\t\\tawait provider\\n 559 | \\t\\t\\t\\t\\t.getMcpHub()\\n 560 | \\t\\t\\t\\t\\t?.toggleToolAlwaysAllow(\\n 561 | \\t\\t\\t\\t\\t\\tmessage.serverName!,\\n 562 | \\t\\t\\t\\t\\t\\tmessage.source as \\\"global\\\" | \\\"project\\\",\\n 563 | \\t\\t\\t\\t\\t\\tmessage.toolName!,\\n 564 | \\t\\t\\t\\t\\t\\tBoolean(message.alwaysAllow),\\n 565 | \\t\\t\\t\\t\\t)\\n 566 | \\t\\t\\t} catch (error) {\\n 567 | \\t\\t\\t\\tprovider.log(\\n 568 | \\t\\t\\t\\t\\t`Failed to toggle auto-approve for tool ${message.toolName}: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`,\\n 569 | \\t\\t\\t\\t)\\n 570 | \\t\\t\\t}\\n 571 | \\t\\t\\tbreak\\n 572 | \\t\\t}\\n 573 | \\t\\tcase \\\"toggleMcpServer\\\": {\\n 574 | \\t\\t\\ttry {\\n 575 | \\t\\t\\t\\tawait provider\\n 576 | \\t\\t\\t\\t\\t.getMcpHub()\\n 577 | \\t\\t\\t\\t\\t?.toggleServerDisabled(\\n 578 | \\t\\t\\t\\t\\t\\tmessage.serverName!,\\n 579 | \\t\\t\\t\\t\\t\\tmessage.disabled!,\\n 580 | \\t\\t\\t\\t\\t\\tmessage.source as \\\"global\\\" | \\\"project\\\",\\n 581 | \\t\\t\\t\\t\\t)\\n 582 | \\t\\t\\t} catch (error) {\\n 583 | \\t\\t\\t\\tprovider.log(\\n 584 | \\t\\t\\t\\t\\t`Failed to toggle MCP server ${message.serverName}: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`,\\n 585 | \\t\\t\\t\\t)\\n 586 | \\t\\t\\t}\\n 587 | \\t\\t\\tbreak\\n 588 | \\t\\t}\\n 589 | \\t\\tcase \\\"mcpEnabled\\\":\\n 590 | \\t\\t\\tconst mcpEnabled = message.bool ?? true\\n 591 | \\t\\t\\tawait updateGlobalState(\\\"mcpEnabled\\\", mcpEnabled)\\n 592 | \\t\\t\\tawait provider.postStateToWebview()\\n 593 | \\t\\t\\tbreak\\n 594 | \\t\\tcase \\\"enableMcpServerCreation\\\":\\n 595 | \\t\\t\\tawait updateGlobalState(\\\"enableMcpServerCreation\\\", message.bool ?? true)\\n 596 | \\t\\t\\tawait provider.postStateToWebview()\\n 597 | \\t\\t\\tbreak\\n 598 | \\t\\tcase \\\"refreshAllMcpServers\\\": {\\n 599 | \\t\\t\\tconst mcpHub = provider.getMcpHub()\\n 600 | \\t\\t\\tif (mcpHub) {\\n 601 | \\t\\t\\t\\tawait mcpHub.refreshAllConnections()\\n 602 | \\t\\t\\t}\\n 603 | \\t\\t\\tbreak\\n 604 | \\t\\t}\\n 605 | \\t\\t// playSound handler removed - now handled directly in the webview\\n 606 | \\t\\tcase \\\"soundEnabled\\\":\\n 607 | \\t\\t\\tconst soundEnabled = message.bool ?? true\\n 608 | \\t\\t\\tawait updateGlobalState(\\\"soundEnabled\\\", soundEnabled)\\n 609 | \\t\\t\\tawait provider.postStateToWebview()\\n 610 | \\t\\t\\tbreak\\n 611 | \\t\\tcase \\\"soundVolume\\\":\\n 612 | \\t\\t\\tconst soundVolume = message.value ?? 0.5\\n 613 | \\t\\t\\tawait updateGlobalState(\\\"soundVolume\\\", soundVolume)\\n 614 | \\t\\t\\tawait provider.postStateToWebview()\\n 615 | \\t\\t\\tbreak\\n 616 | \\t\\tcase \\\"ttsEnabled\\\":\\n 617 | \\t\\t\\tconst ttsEnabled = message.bool ?? true\\n 618 | \\t\\t\\tawait updateGlobalState(\\\"ttsEnabled\\\", ttsEnabled)\\n 619 | \\t\\t\\tsetTtsEnabled(ttsEnabled) // Add this line to update the tts utility\\n 620 | \\t\\t\\tawait provider.postStateToWebview()\\n 621 | \\t\\t\\tbreak\\n 622 | \\t\\tcase \\\"ttsSpeed\\\":\\n 623 | \\t\\t\\tconst ttsSpeed = message.value ?? 1.0\\n 624 | \\t\\t\\tawait updateGlobalState(\\\"ttsSpeed\\\", ttsSpeed)\\n 625 | \\t\\t\\tsetTtsSpeed(ttsSpeed)\\n 626 | \\t\\t\\tawait provider.postStateToWebview()\\n 627 | \\t\\t\\tbreak\\n 628 | \\t\\tcase \\\"playTts\\\":\\n 629 | \\t\\t\\tif (message.text) {\\n 630 | \\t\\t\\t\\tplayTts(message.text, {\\n 631 | \\t\\t\\t\\t\\tonStart: () => provider.postMessageToWebview({ type: \\\"ttsStart\\\", text: message.text }),\\n 632 | \\t\\t\\t\\t\\tonStop: () => provider.postMessageToWebview({ type: \\\"ttsStop\\\", text: message.text }),\\n 633 | \\t\\t\\t\\t})\\n 634 | \\t\\t\\t}\\n 635 | \\t\\t\\tbreak\\n 636 | \\t\\tcase \\\"stopTts\\\":\\n 637 | \\t\\t\\tstopTts()\\n 638 | \\t\\t\\tbreak\\n 639 | \\t\\tcase \\\"diffEnabled\\\":\\n 640 | \\t\\t\\tconst diffEnabled = message.bool ?? true\\n 641 | \\t\\t\\tawait updateGlobalState(\\\"diffEnabled\\\", diffEnabled)\\n 642 | \\t\\t\\tawait provider.postStateToWebview()\\n 643 | \\t\\t\\tbreak\\n 644 | \\t\\tcase \\\"enableCheckpoints\\\":\\n 645 | \\t\\t\\tconst enableCheckpoints = message.bool ?? true\\n 646 | \\t\\t\\tawait updateGlobalState(\\\"enableCheckpoints\\\", enableCheckpoints)\\n 647 | \\t\\t\\tawait provider.postStateToWebview()\\n 648 | \\t\\t\\tbreak\\n 649 | \\t\\tcase \\\"browserViewportSize\\\":\\n 650 | \\t\\t\\tconst browserViewportSize = message.text ?? \\\"900x600\\\"\\n 651 | \\t\\t\\tawait updateGlobalState(\\\"browserViewportSize\\\", browserViewportSize)\\n 652 | \\t\\t\\tawait provider.postStateToWebview()\\n 653 | \\t\\t\\tbreak\\n 654 | \\t\\tcase \\\"remoteBrowserHost\\\":\\n 655 | \\t\\t\\tawait updateGlobalState(\\\"remoteBrowserHost\\\", message.text)\\n 656 | \\t\\t\\tawait provider.postStateToWebview()\\n 657 | \\t\\t\\tbreak\\n 658 | \\t\\tcase \\\"remoteBrowserEnabled\\\":\\n 659 | \\t\\t\\t// Store the preference in global state\\n 660 | \\t\\t\\t// remoteBrowserEnabled now means \\\"enable remote browser connection\\\"\\n 661 | \\t\\t\\tawait updateGlobalState(\\\"remoteBrowserEnabled\\\", message.bool ?? false)\\n 662 | \\t\\t\\t// If disabling remote browser connection, clear the remoteBrowserHost\\n 663 | \\t\\t\\tif (!message.bool) {\\n 664 | \\t\\t\\t\\tawait updateGlobalState(\\\"remoteBrowserHost\\\", undefined)\\n 665 | \\t\\t\\t}\\n 666 | \\t\\t\\tawait provider.postStateToWebview()\\n 667 | \\t\\t\\tbreak\\n 668 | \\t\\tcase \\\"testBrowserConnection\\\":\\n 669 | \\t\\t\\t// If no text is provided, try auto-discovery\\n 670 | \\t\\t\\tif (!message.text) {\\n 671 | \\t\\t\\t\\t// Use testBrowserConnection for auto-discovery\\n 672 | \\t\\t\\t\\tconst chromeHostUrl = await discoverChromeHostUrl()\\n 673 | \\n 674 | \\t\\t\\t\\tif (chromeHostUrl) {\\n 675 | \\t\\t\\t\\t\\t// Send the result back to the webview\\n 676 | \\t\\t\\t\\t\\tawait provider.postMessageToWebview({\\n 677 | \\t\\t\\t\\t\\t\\ttype: \\\"browserConnectionResult\\\",\\n 678 | \\t\\t\\t\\t\\t\\tsuccess: !!chromeHostUrl,\\n 679 | \\t\\t\\t\\t\\t\\ttext: `Auto-discovered and tested connection to Chrome: ${chromeHostUrl}`,\\n 680 | \\t\\t\\t\\t\\t\\tvalues: { endpoint: chromeHostUrl },\\n 681 | \\t\\t\\t\\t\\t})\\n 682 | \\t\\t\\t\\t} else {\\n 683 | \\t\\t\\t\\t\\tawait provider.postMessageToWebview({\\n 684 | \\t\\t\\t\\t\\t\\ttype: \\\"browserConnectionResult\\\",\\n 685 | \\t\\t\\t\\t\\t\\tsuccess: false,\\n 686 | \\t\\t\\t\\t\\t\\ttext: \\\"No Chrome instances found on the network. Make sure Chrome is running with remote debugging enabled (--remote-debugging-port=9222).\\\",\\n 687 | \\t\\t\\t\\t\\t})\\n 688 | \\t\\t\\t\\t}\\n 689 | \\t\\t\\t} else {\\n 690 | \\t\\t\\t\\t// Test the provided URL\\n 691 | \\t\\t\\t\\tconst customHostUrl = message.text\\n 692 | \\t\\t\\t\\tconst hostIsValid = await tryChromeHostUrl(message.text)\\n 693 | \\n 694 | \\t\\t\\t\\t// Send the result back to the webview\\n 695 | \\t\\t\\t\\tawait provider.postMessageToWebview({\\n 696 | \\t\\t\\t\\t\\ttype: \\\"browserConnectionResult\\\",\\n 697 | \\t\\t\\t\\t\\tsuccess: hostIsValid,\\n 698 | \\t\\t\\t\\t\\ttext: hostIsValid\\n 699 | \\t\\t\\t\\t\\t\\t? `Successfully connected to Chrome: ${customHostUrl}`\\n 700 | \\t\\t\\t\\t\\t\\t: \\\"Failed to connect to Chrome\\\",\\n 701 | \\t\\t\\t\\t})\\n 702 | \\t\\t\\t}\\n 703 | \\t\\t\\tbreak\\n 704 | \\t\\tcase \\\"fuzzyMatchThreshold\\\":\\n 705 | \\t\\t\\tawait updateGlobalState(\\\"fuzzyMatchThreshold\\\", message.value)\\n 706 | \\t\\t\\tawait provider.postStateToWebview()\\n 707 | \\t\\t\\tbreak\\n 708 | \\t\\tcase \\\"updateVSCodeSetting\\\": {\\n 709 | \\t\\t\\tconst { setting, value } = message\\n 710 | \\n 711 | \\t\\t\\tif (setting !== undefined && value !== undefined) {\\n 712 | \\t\\t\\t\\tif (ALLOWED_VSCODE_SETTINGS.has(setting)) {\\n 713 | \\t\\t\\t\\t\\tawait vscode.workspace.getConfiguration().update(setting, value, true)\\n 714 | \\t\\t\\t\\t} else {\\n 715 | \\t\\t\\t\\t\\tvscode.window.showErrorMessage(`Cannot update restricted VSCode setting: ${setting}`)\\n 716 | \\t\\t\\t\\t}\\n 717 | \\t\\t\\t}\\n 718 | \\n 719 | \\t\\t\\tbreak\\n 720 | \\t\\t}\\n 721 | \\t\\tcase \\\"getVSCodeSetting\\\":\\n 722 | \\t\\t\\tconst { setting } = message\\n 723 | \\n 724 | \\t\\t\\tif (setting) {\\n 725 | \\t\\t\\t\\ttry {\\n 726 | \\t\\t\\t\\t\\tawait provider.postMessageToWebview({\\n 727 | \\t\\t\\t\\t\\t\\ttype: \\\"vsCodeSetting\\\",\\n 728 | \\t\\t\\t\\t\\t\\tsetting,\\n 729 | \\t\\t\\t\\t\\t\\tvalue: vscode.workspace.getConfiguration().get(setting),\\n 730 | \\t\\t\\t\\t\\t})\\n 731 | \\t\\t\\t\\t} catch (error) {\\n 732 | \\t\\t\\t\\t\\tconsole.error(`Failed to get VSCode setting ${message.setting}:`, error)\\n 733 | \\n 734 | \\t\\t\\t\\t\\tawait provider.postMessageToWebview({\\n 735 | \\t\\t\\t\\t\\t\\ttype: \\\"vsCodeSetting\\\",\\n 736 | \\t\\t\\t\\t\\t\\tsetting,\\n 737 | \\t\\t\\t\\t\\t\\terror: `Failed to get setting: ${error.message}`,\\n 738 | \\t\\t\\t\\t\\t\\tvalue: undefined,\\n 739 | \\t\\t\\t\\t\\t})\\n 740 | \\t\\t\\t\\t}\\n 741 | \\t\\t\\t}\\n 742 | \\n 743 | \\t\\t\\tbreak\\n 744 | \\t\\tcase \\\"alwaysApproveResubmit\\\":\\n 745 | \\t\\t\\tawait updateGlobalState(\\\"alwaysApproveResubmit\\\", message.bool ?? false)\\n 746 | \\t\\t\\tawait provider.postStateToWebview()\\n 747 | \\t\\t\\tbreak\\n 748 | \\t\\tcase \\\"requestDelaySeconds\\\":\\n 749 | \\t\\t\\tawait updateGlobalState(\\\"requestDelaySeconds\\\", message.value ?? 5)\\n 750 | \\t\\t\\tawait provider.postStateToWebview()\\n 751 | \\t\\t\\tbreak\\n 752 | \\t\\tcase \\\"writeDelayMs\\\":\\n 753 | \\t\\t\\tawait updateGlobalState(\\\"writeDelayMs\\\", message.value)\\n 754 | \\t\\t\\tawait provider.postStateToWebview()\\n 755 | \\t\\t\\tbreak\\n 756 | \\t\\tcase \\\"terminalOutputLineLimit\\\":\\n 757 | \\t\\t\\tawait updateGlobalState(\\\"terminalOutputLineLimit\\\", message.value)\\n 758 | \\t\\t\\tawait provider.postStateToWebview()\\n 759 | \\t\\t\\tbreak\\n 760 | \\t\\tcase \\\"terminalShellIntegrationTimeout\\\":\\n 761 | \\t\\t\\tawait updateGlobalState(\\\"terminalShellIntegrationTimeout\\\", message.value)\\n 762 | \\t\\t\\tawait provider.postStateToWebview()\\n 763 | \\t\\t\\tif (message.value !== undefined) {\\n 764 | \\t\\t\\t\\tTerminal.setShellIntegrationTimeout(message.value)\\n 765 | \\t\\t\\t}\\n 766 | \\t\\t\\tbreak\\n 767 | \\t\\tcase \\\"terminalShellIntegrationDisabled\\\":\\n 768 | \\t\\t\\tawait updateGlobalState(\\\"terminalShellIntegrationDisabled\\\", message.bool)\\n 769 | \\t\\t\\tawait provider.postStateToWebview()\\n 770 | \\t\\t\\tif (message.bool !== undefined) {\\n 771 | \\t\\t\\t\\tTerminal.setShellIntegrationDisabled(message.bool)\\n 772 | \\t\\t\\t}\\n 773 | \\t\\t\\tbreak\\n 774 | \\t\\tcase \\\"terminalCommandDelay\\\":\\n 775 | \\t\\t\\tawait updateGlobalState(\\\"terminalCommandDelay\\\", message.value)\\n 776 | \\t\\t\\tawait provider.postStateToWebview()\\n 777 | \\t\\t\\tif (message.value !== undefined) {\\n 778 | \\t\\t\\t\\tTerminal.setCommandDelay(message.value)\\n 779 | \\t\\t\\t}\\n 780 | \\t\\t\\tbreak\\n 781 | \\t\\tcase \\\"terminalPowershellCounter\\\":\\n 782 | \\t\\t\\tawait updateGlobalState(\\\"terminalPowershellCounter\\\", message.bool)\\n 783 | \\t\\t\\tawait provider.postStateToWebview()\\n 784 | \\t\\t\\tif (message.bool !== undefined) {\\n 785 | \\t\\t\\t\\tTerminal.setPowershellCounter(message.bool)\\n 786 | \\t\\t\\t}\\n 787 | \\t\\t\\tbreak\\n 788 | \\t\\tcase \\\"terminalZshClearEolMark\\\":\\n 789 | \\t\\t\\tawait updateGlobalState(\\\"terminalZshClearEolMark\\\", message.bool)\\n 790 | \\t\\t\\tawait provider.postStateToWebview()\\n 791 | \\t\\t\\tif (message.bool !== undefined) {\\n 792 | \\t\\t\\t\\tTerminal.setTerminalZshClearEolMark(message.bool)\\n 793 | \\t\\t\\t}\\n 794 | \\t\\t\\tbreak\\n 795 | \\t\\tcase \\\"terminalZshOhMy\\\":\\n 796 | \\t\\t\\tawait updateGlobalState(\\\"terminalZshOhMy\\\", message.bool)\\n 797 | \\t\\t\\tawait provider.postStateToWebview()\\n 798 | \\t\\t\\tif (message.bool !== undefined) {\\n 799 | \\t\\t\\t\\tTerminal.setTerminalZshOhMy(message.bool)\\n 800 | \\t\\t\\t}\\n 801 | \\t\\t\\tbreak\\n 802 | \\t\\tcase \\\"terminalZshP10k\\\":\\n 803 | \\t\\t\\tawait updateGlobalState(\\\"terminalZshP10k\\\", message.bool)\\n 804 | \\t\\t\\tawait provider.postStateToWebview()\\n 805 | \\t\\t\\tif (message.bool !== undefined) {\\n 806 | \\t\\t\\t\\tTerminal.setTerminalZshP10k(message.bool)\\n 807 | \\t\\t\\t}\\n 808 | \\t\\t\\tbreak\\n 809 | \\t\\tcase \\\"terminalZdotdir\\\":\\n 810 | \\t\\t\\tawait updateGlobalState(\\\"terminalZdotdir\\\", message.bool)\\n 811 | \\t\\t\\tawait provider.postStateToWebview()\\n 812 | \\t\\t\\tif (message.bool !== undefined) {\\n 813 | \\t\\t\\t\\tTerminal.setTerminalZdotdir(message.bool)\\n 814 | \\t\\t\\t}\\n 815 | \\t\\t\\tbreak\\n 816 | \\t\\tcase \\\"terminalCompressProgressBar\\\":\\n 817 | \\t\\t\\tawait updateGlobalState(\\\"terminalCompressProgressBar\\\", message.bool)\\n 818 | \\t\\t\\tawait provider.postStateToWebview()\\n 819 | \\t\\t\\tif (message.bool !== undefined) {\\n 820 | \\t\\t\\t\\tTerminal.setCompressProgressBar(message.bool)\\n 821 | \\t\\t\\t}\\n 822 | \\t\\t\\tbreak\\n 823 | \\t\\tcase \\\"mode\\\":\\n 824 | \\t\\t\\tawait provider.handleModeSwitch(message.text as Mode)\\n 825 | \\t\\t\\tbreak\\n 826 | \\t\\tcase \\\"updateSupportPrompt\\\":\\n 827 | \\t\\t\\ttry {\\n 828 | \\t\\t\\t\\tif (Object.keys(message?.values ?? {}).length === 0) {\\n 829 | \\t\\t\\t\\t\\treturn\\n 830 | \\t\\t\\t\\t}\\n 831 | \\n 832 | \\t\\t\\t\\tconst existingPrompts = getGlobalState(\\\"customSupportPrompts\\\") ?? {}\\n 833 | \\t\\t\\t\\tconst updatedPrompts = { ...existingPrompts, ...message.values }\\n 834 | \\t\\t\\t\\tawait updateGlobalState(\\\"customSupportPrompts\\\", updatedPrompts)\\n 835 | \\t\\t\\t\\tawait provider.postStateToWebview()\\n 836 | \\t\\t\\t} catch (error) {\\n 837 | \\t\\t\\t\\tprovider.log(\\n 838 | \\t\\t\\t\\t\\t`Error update support prompt: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`,\\n 839 | \\t\\t\\t\\t)\\n 840 | \\t\\t\\t\\tvscode.window.showErrorMessage(t(\\\"common:errors.update_support_prompt\\\"))\\n 841 | \\t\\t\\t}\\n 842 | \\t\\t\\tbreak\\n 843 | \\t\\tcase \\\"resetSupportPrompt\\\":\\n 844 | \\t\\t\\ttry {\\n 845 | \\t\\t\\t\\tif (!message?.text) {\\n 846 | \\t\\t\\t\\t\\treturn\\n 847 | \\t\\t\\t\\t}\\n 848 | \\n 849 | \\t\\t\\t\\tconst existingPrompts = getGlobalState(\\\"customSupportPrompts\\\") ?? {}\\n 850 | \\t\\t\\t\\tconst updatedPrompts = { ...existingPrompts }\\n 851 | \\t\\t\\t\\tupdatedPrompts[message.text] = undefined\\n 852 | \\t\\t\\t\\tawait updateGlobalState(\\\"customSupportPrompts\\\", updatedPrompts)\\n 853 | \\t\\t\\t\\tawait provider.postStateToWebview()\\n 854 | \\t\\t\\t} catch (error) {\\n 855 | \\t\\t\\t\\tprovider.log(\\n 856 | \\t\\t\\t\\t\\t`Error reset support prompt: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`,\\n 857 | \\t\\t\\t\\t)\\n 858 | \\t\\t\\t\\tvscode.window.showErrorMessage(t(\\\"common:errors.reset_support_prompt\\\"))\\n 859 | \\t\\t\\t}\\n 860 | \\t\\t\\tbreak\\n 861 | \\t\\tcase \\\"updatePrompt\\\":\\n 862 | \\t\\t\\tif (message.promptMode && message.customPrompt !== undefined) {\\n 863 | \\t\\t\\t\\tconst existingPrompts = getGlobalState(\\\"customModePrompts\\\") ?? {}\\n 864 | \\t\\t\\t\\tconst updatedPrompts = { ...existingPrompts, [message.promptMode]: message.customPrompt }\\n 865 | \\t\\t\\t\\tawait updateGlobalState(\\\"customModePrompts\\\", updatedPrompts)\\n 866 | \\t\\t\\t\\tconst currentState = await provider.getStateToPostToWebview()\\n 867 | \\t\\t\\t\\tconst stateWithPrompts = { ...currentState, customModePrompts: updatedPrompts }\\n 868 | \\t\\t\\t\\tprovider.postMessageToWebview({ type: \\\"state\\\", state: stateWithPrompts })\\n 869 | \\t\\t\\t}\\n 870 | \\t\\t\\tbreak\\n 871 | \\t\\tcase \\\"deleteMessage\\\": {\\n 872 | \\t\\t\\tconst answer = await vscode.window.showInformationMessage(\\n 873 | \\t\\t\\t\\tt(\\\"common:confirmation.delete_message\\\"),\\n 874 | \\t\\t\\t\\t{ modal: true },\\n 875 | \\t\\t\\t\\tt(\\\"common:confirmation.just_this_message\\\"),\\n 876 | \\t\\t\\t\\tt(\\\"common:confirmation.this_and_subsequent\\\"),\\n 877 | \\t\\t\\t)\\n 878 | \\n 879 | \\t\\t\\tif (\\n 880 | \\t\\t\\t\\t(answer === t(\\\"common:confirmation.just_this_message\\\") ||\\n 881 | \\t\\t\\t\\t\\tanswer === t(\\\"common:confirmation.this_and_subsequent\\\")) &&\\n 882 | \\t\\t\\t\\tprovider.getCurrentCline() &&\\n 883 | \\t\\t\\t\\ttypeof message.value === \\\"number\\\" &&\\n 884 | \\t\\t\\t\\tmessage.value\\n 885 | \\t\\t\\t) {\\n 886 | \\t\\t\\t\\tconst timeCutoff = message.value - 1000 // 1 second buffer before the message to delete\\n 887 | \\n 888 | \\t\\t\\t\\tconst messageIndex = provider\\n 889 | \\t\\t\\t\\t\\t.getCurrentCline()!\\n 890 | \\t\\t\\t\\t\\t.clineMessages.findIndex((msg) => msg.ts && msg.ts >= timeCutoff)\\n 891 | \\n 892 | \\t\\t\\t\\tconst apiConversationHistoryIndex = provider\\n 893 | \\t\\t\\t\\t\\t.getCurrentCline()\\n 894 | \\t\\t\\t\\t\\t?.apiConversationHistory.findIndex((msg) => msg.ts && msg.ts >= timeCutoff)\\n 895 | \\n 896 | \\t\\t\\t\\tif (messageIndex !== -1) {\\n 897 | \\t\\t\\t\\t\\tconst { historyItem } = await provider.getTaskWithId(provider.getCurrentCline()!.taskId)\\n 898 | \\n 899 | \\t\\t\\t\\t\\tif (answer === t(\\\"common:confirmation.just_this_message\\\")) {\\n 900 | \\t\\t\\t\\t\\t\\t// Find the next user message first\\n 901 | \\t\\t\\t\\t\\t\\tconst nextUserMessage = provider\\n 902 | \\t\\t\\t\\t\\t\\t\\t.getCurrentCline()!\\n 903 | \\t\\t\\t\\t\\t\\t\\t.clineMessages.slice(messageIndex + 1)\\n 904 | \\t\\t\\t\\t\\t\\t\\t.find((msg) => msg.type === \\\"say\\\" && msg.say === \\\"user_feedback\\\")\\n 905 | \\n 906 | \\t\\t\\t\\t\\t\\t// Handle UI messages\\n 907 | \\t\\t\\t\\t\\t\\tif (nextUserMessage) {\\n 908 | \\t\\t\\t\\t\\t\\t\\t// Find absolute index of next user message\\n 909 | \\t\\t\\t\\t\\t\\t\\tconst nextUserMessageIndex = provider\\n 910 | \\t\\t\\t\\t\\t\\t\\t\\t.getCurrentCline()!\\n 911 | \\t\\t\\t\\t\\t\\t\\t\\t.clineMessages.findIndex((msg) => msg === nextUserMessage)\\n 912 | \\n 913 | \\t\\t\\t\\t\\t\\t\\t// Keep messages before current message and after next user message\\n 914 | \\t\\t\\t\\t\\t\\t\\tawait provider\\n 915 | \\t\\t\\t\\t\\t\\t\\t\\t.getCurrentCline()!\\n 916 | \\t\\t\\t\\t\\t\\t\\t\\t.overwriteClineMessages([\\n 917 | \\t\\t\\t\\t\\t\\t\\t\\t\\t...provider.getCurrentCline()!.clineMessages.slice(0, messageIndex),\\n 918 | \\t\\t\\t\\t\\t\\t\\t\\t\\t...provider.getCurrentCline()!.clineMessages.slice(nextUserMessageIndex),\\n 919 | \\t\\t\\t\\t\\t\\t\\t\\t])\\n 920 | \\t\\t\\t\\t\\t\\t} else {\\n 921 | \\t\\t\\t\\t\\t\\t\\t// If no next user message, keep only messages before current message\\n 922 | \\t\\t\\t\\t\\t\\t\\tawait provider\\n 923 | \\t\\t\\t\\t\\t\\t\\t\\t.getCurrentCline()!\\n 924 | \\t\\t\\t\\t\\t\\t\\t\\t.overwriteClineMessages(\\n 925 | \\t\\t\\t\\t\\t\\t\\t\\t\\tprovider.getCurrentCline()!.clineMessages.slice(0, messageIndex),\\n 926 | \\t\\t\\t\\t\\t\\t\\t\\t)\\n 927 | \\t\\t\\t\\t\\t\\t}\\n 928 | \\n 929 | \\t\\t\\t\\t\\t\\t// Handle API messages\\n 930 | \\t\\t\\t\\t\\t\\tif (apiConversationHistoryIndex !== -1) {\\n 931 | \\t\\t\\t\\t\\t\\t\\tif (nextUserMessage && nextUserMessage.ts) {\\n 932 | \\t\\t\\t\\t\\t\\t\\t\\t// Keep messages before current API message and after next user message\\n 933 | \\t\\t\\t\\t\\t\\t\\t\\tawait provider\\n 934 | \\t\\t\\t\\t\\t\\t\\t\\t\\t.getCurrentCline()!\\n 935 | \\t\\t\\t\\t\\t\\t\\t\\t\\t.overwriteApiConversationHistory([\\n 936 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\t...provider\\n 937 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\t\\t.getCurrentCline()!\\n 938 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\t\\t.apiConversationHistory.slice(0, apiConversationHistoryIndex),\\n 939 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\t...provider\\n 940 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\t\\t.getCurrentCline()!\\n 941 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\t\\t.apiConversationHistory.filter(\\n 942 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\t\\t\\t(msg) => msg.ts && msg.ts >= nextUserMessage.ts,\\n 943 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\t\\t),\\n 944 | \\t\\t\\t\\t\\t\\t\\t\\t\\t])\\n 945 | \\t\\t\\t\\t\\t\\t\\t} else {\\n 946 | \\t\\t\\t\\t\\t\\t\\t\\t// If no next user message, keep only messages before current API message\\n 947 | \\t\\t\\t\\t\\t\\t\\t\\tawait provider\\n 948 | \\t\\t\\t\\t\\t\\t\\t\\t\\t.getCurrentCline()!\\n 949 | \\t\\t\\t\\t\\t\\t\\t\\t\\t.overwriteApiConversationHistory(\\n 950 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\tprovider\\n 951 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\t\\t.getCurrentCline()!\\n 952 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\t\\t.apiConversationHistory.slice(0, apiConversationHistoryIndex),\\n 953 | \\t\\t\\t\\t\\t\\t\\t\\t\\t)\\n 954 | \\t\\t\\t\\t\\t\\t\\t}\\n 955 | \\t\\t\\t\\t\\t\\t}\\n 956 | \\t\\t\\t\\t\\t} else if (answer === t(\\\"common:confirmation.this_and_subsequent\\\")) {\\n 957 | \\t\\t\\t\\t\\t\\t// Delete this message and all that follow\\n 958 | \\t\\t\\t\\t\\t\\tawait provider\\n 959 | \\t\\t\\t\\t\\t\\t\\t.getCurrentCline()!\\n 960 | \\t\\t\\t\\t\\t\\t\\t.overwriteClineMessages(provider.getCurrentCline()!.clineMessages.slice(0, messageIndex))\\n 961 | \\t\\t\\t\\t\\t\\tif (apiConversationHistoryIndex !== -1) {\\n 962 | \\t\\t\\t\\t\\t\\t\\tawait provider\\n 963 | \\t\\t\\t\\t\\t\\t\\t\\t.getCurrentCline()!\\n 964 | \\t\\t\\t\\t\\t\\t\\t\\t.overwriteApiConversationHistory(\\n 965 | \\t\\t\\t\\t\\t\\t\\t\\t\\tprovider\\n 966 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\t.getCurrentCline()!\\n 967 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\t.apiConversationHistory.slice(0, apiConversationHistoryIndex),\\n 968 | \\t\\t\\t\\t\\t\\t\\t\\t)\\n 969 | \\t\\t\\t\\t\\t\\t}\\n 970 | \\t\\t\\t\\t\\t}\\n 971 | \\n 972 | \\t\\t\\t\\t\\tawait provider.initClineWithHistoryItem(historyItem)\\n 973 | \\t\\t\\t\\t}\\n 974 | \\t\\t\\t}\\n 975 | \\t\\t\\tbreak\\n 976 | \\t\\t}\\n 977 | \\t\\tcase \\\"screenshotQuality\\\":\\n 978 | \\t\\t\\tawait updateGlobalState(\\\"screenshotQuality\\\", message.value)\\n 979 | \\t\\t\\tawait provider.postStateToWebview()\\n 980 | \\t\\t\\tbreak\\n 981 | \\t\\tcase \\\"maxOpenTabsContext\\\":\\n 982 | \\t\\t\\tconst tabCount = Math.min(Math.max(0, message.value ?? 20), 500)\\n 983 | \\t\\t\\tawait updateGlobalState(\\\"maxOpenTabsContext\\\", tabCount)\\n 984 | \\t\\t\\tawait provider.postStateToWebview()\\n 985 | \\t\\t\\tbreak\\n 986 | \\t\\tcase \\\"maxWorkspaceFiles\\\":\\n 987 | \\t\\t\\tconst fileCount = Math.min(Math.max(0, message.value ?? 200), 500)\\n 988 | \\t\\t\\tawait updateGlobalState(\\\"maxWorkspaceFiles\\\", fileCount)\\n 989 | \\t\\t\\tawait provider.postStateToWebview()\\n 990 | \\t\\t\\tbreak\\n 991 | \\t\\tcase \\\"browserToolEnabled\\\":\\n 992 | \\t\\t\\tawait updateGlobalState(\\\"browserToolEnabled\\\", message.bool ?? true)\\n 993 | \\t\\t\\tawait provider.postStateToWebview()\\n 994 | \\t\\t\\tbreak\\n 995 | \\t\\tcase \\\"language\\\":\\n 996 | \\t\\t\\tchangeLanguage(message.text ?? \\\"en\\\")\\n 997 | \\t\\t\\tawait updateGlobalState(\\\"language\\\", message.text as Language)\\n 998 | \\t\\t\\tawait provider.postStateToWebview()\\n 999 | \\t\\t\\tbreak\\n1000 | \\t\\tcase \\\"showRooIgnoredFiles\\\":\\n1001 | \\t\\t\\tawait updateGlobalState(\\\"showRooIgnoredFiles\\\", message.bool ?? true)\\n1002 | \\t\\t\\tawait provider.postStateToWebview()\\n1003 | \\t\\t\\tbreak\\n1004 | \\t\\tcase \\\"maxReadFileLine\\\":\\n1005 | \\t\\t\\tawait updateGlobalState(\\\"maxReadFileLine\\\", message.value)\\n1006 | \\t\\t\\tawait provider.postStateToWebview()\\n1007 | \\t\\t\\tbreak\\n1008 | \\t\\tcase \\\"maxConcurrentFileReads\\\":\\n1009 | \\t\\t\\tconst valueToSave = message.value // Capture the value intended for saving\\n1010 | \\t\\t\\tawait updateGlobalState(\\\"maxConcurrentFileReads\\\", valueToSave)\\n1011 | \\t\\t\\tawait provider.postStateToWebview()\\n1012 | \\t\\t\\tbreak\\n1013 | \\t\\tcase \\\"setHistoryPreviewCollapsed\\\": // Add the new case handler\\n1014 | \\t\\t\\tawait updateGlobalState(\\\"historyPreviewCollapsed\\\", message.bool ?? false)\\n1015 | \\t\\t\\t// No need to call postStateToWebview here as the UI already updated optimistically\\n1016 | \\t\\t\\tbreak\\n1017 | \\t\\tcase \\\"toggleApiConfigPin\\\":\\n1018 | \\t\\t\\tif (message.text) {\\n1019 | \\t\\t\\t\\tconst currentPinned = getGlobalState(\\\"pinnedApiConfigs\\\") ?? {}\\n1020 | \\t\\t\\t\\tconst updatedPinned: Record = { ...currentPinned }\\n1021 | \\n1022 | \\t\\t\\t\\tif (currentPinned[message.text]) {\\n1023 | \\t\\t\\t\\t\\tdelete updatedPinned[message.text]\\n1024 | \\t\\t\\t\\t} else {\\n1025 | \\t\\t\\t\\t\\tupdatedPinned[message.text] = true\\n1026 | \\t\\t\\t\\t}\\n1027 | \\n1028 | \\t\\t\\t\\tawait updateGlobalState(\\\"pinnedApiConfigs\\\", updatedPinned)\\n1029 | \\t\\t\\t\\tawait provider.postStateToWebview()\\n1030 | \\t\\t\\t}\\n1031 | \\t\\t\\tbreak\\n1032 | \\t\\tcase \\\"enhancementApiConfigId\\\":\\n1033 | \\t\\t\\tawait updateGlobalState(\\\"enhancementApiConfigId\\\", message.text)\\n1034 | \\t\\t\\tawait provider.postStateToWebview()\\n1035 | \\t\\t\\tbreak\\n1036 | \\t\\tcase \\\"condensingApiConfigId\\\":\\n1037 | \\t\\t\\tawait updateGlobalState(\\\"condensingApiConfigId\\\", message.text)\\n1038 | \\t\\t\\tawait provider.postStateToWebview()\\n1039 | \\t\\t\\tbreak\\n1040 | \\t\\tcase \\\"updateCondensingPrompt\\\":\\n1041 | \\t\\t\\tawait updateGlobalState(\\\"customCondensingPrompt\\\", message.text)\\n1042 | \\t\\t\\tawait provider.postStateToWebview()\\n1043 | \\t\\t\\tbreak\\n1044 | \\t\\tcase \\\"autoApprovalEnabled\\\":\\n1045 | \\t\\t\\tawait updateGlobalState(\\\"autoApprovalEnabled\\\", message.bool ?? false)\\n1046 | \\t\\t\\tawait provider.postStateToWebview()\\n1047 | \\t\\t\\tbreak\\n1048 | \\t\\tcase \\\"enhancePrompt\\\":\\n1049 | \\t\\t\\tif (message.text) {\\n1050 | \\t\\t\\t\\ttry {\\n1051 | \\t\\t\\t\\t\\tconst { apiConfiguration, customSupportPrompts, listApiConfigMeta, enhancementApiConfigId } =\\n1052 | \\t\\t\\t\\t\\t\\tawait provider.getState()\\n1053 | \\n1054 | \\t\\t\\t\\t\\t// Try to get enhancement config first, fall back to current config.\\n1055 | \\t\\t\\t\\t\\tlet configToUse: ProviderSettings = apiConfiguration\\n1056 | \\n1057 | \\t\\t\\t\\t\\tif (enhancementApiConfigId && !!listApiConfigMeta.find(({ id }) => id === enhancementApiConfigId)) {\\n1058 | \\t\\t\\t\\t\\t\\tconst { name: _, ...providerSettings } = await provider.providerSettingsManager.getProfile({\\n1059 | \\t\\t\\t\\t\\t\\t\\tid: enhancementApiConfigId,\\n1060 | \\t\\t\\t\\t\\t\\t})\\n1061 | \\n1062 | \\t\\t\\t\\t\\t\\tif (providerSettings.apiProvider) {\\n1063 | \\t\\t\\t\\t\\t\\t\\tconfigToUse = providerSettings\\n1064 | \\t\\t\\t\\t\\t\\t}\\n1065 | \\t\\t\\t\\t\\t}\\n1066 | \\n1067 | \\t\\t\\t\\t\\tconst enhancedPrompt = await singleCompletionHandler(\\n1068 | \\t\\t\\t\\t\\t\\tconfigToUse,\\n1069 | \\t\\t\\t\\t\\t\\tsupportPrompt.create(\\\"ENHANCE\\\", { userInput: message.text }, customSupportPrompts),\\n1070 | \\t\\t\\t\\t\\t)\\n1071 | \\n1072 | \\t\\t\\t\\t\\t// Capture telemetry for prompt enhancement.\\n1073 | \\t\\t\\t\\t\\tconst currentCline = provider.getCurrentCline()\\n1074 | \\t\\t\\t\\t\\tTelemetryService.instance.capturePromptEnhanced(currentCline?.taskId)\\n1075 | \\n1076 | \\t\\t\\t\\t\\tawait provider.postMessageToWebview({ type: \\\"enhancedPrompt\\\", text: enhancedPrompt })\\n1077 | \\t\\t\\t\\t} catch (error) {\\n1078 | \\t\\t\\t\\t\\tprovider.log(\\n1079 | \\t\\t\\t\\t\\t\\t`Error enhancing prompt: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`,\\n1080 | \\t\\t\\t\\t\\t)\\n1081 | \\n1082 | \\t\\t\\t\\t\\tvscode.window.showErrorMessage(t(\\\"common:errors.enhance_prompt\\\"))\\n1083 | \\t\\t\\t\\t\\tawait provider.postMessageToWebview({ type: \\\"enhancedPrompt\\\" })\\n1084 | \\t\\t\\t\\t}\\n1085 | \\t\\t\\t}\\n1086 | \\t\\t\\tbreak\\n1087 | \\t\\tcase \\\"getSystemPrompt\\\":\\n1088 | \\t\\t\\ttry {\\n1089 | \\t\\t\\t\\tconst systemPrompt = await generateSystemPrompt(provider, message)\\n1090 | \\n1091 | \\t\\t\\t\\tawait provider.postMessageToWebview({\\n1092 | \\t\\t\\t\\t\\ttype: \\\"systemPrompt\\\",\\n1093 | \\t\\t\\t\\t\\ttext: systemPrompt,\\n1094 | \\t\\t\\t\\t\\tmode: message.mode,\\n1095 | \\t\\t\\t\\t})\\n1096 | \\t\\t\\t} catch (error) {\\n1097 | \\t\\t\\t\\tprovider.log(\\n1098 | \\t\\t\\t\\t\\t`Error getting system prompt: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`,\\n1099 | \\t\\t\\t\\t)\\n1100 | \\t\\t\\t\\tvscode.window.showErrorMessage(t(\\\"common:errors.get_system_prompt\\\"))\\n1101 | \\t\\t\\t}\\n1102 | \\t\\t\\tbreak\\n1103 | \\t\\tcase \\\"copySystemPrompt\\\":\\n1104 | \\t\\t\\ttry {\\n1105 | \\t\\t\\t\\tconst systemPrompt = await generateSystemPrompt(provider, message)\\n1106 | \\n1107 | \\t\\t\\t\\tawait vscode.env.clipboard.writeText(systemPrompt)\\n1108 | \\t\\t\\t\\tawait vscode.window.showInformationMessage(t(\\\"common:info.clipboard_copy\\\"))\\n1109 | \\t\\t\\t} catch (error) {\\n1110 | \\t\\t\\t\\tprovider.log(\\n1111 | \\t\\t\\t\\t\\t`Error getting system prompt: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`,\\n1112 | \\t\\t\\t\\t)\\n1113 | \\t\\t\\t\\tvscode.window.showErrorMessage(t(\\\"common:errors.get_system_prompt\\\"))\\n1114 | \\t\\t\\t}\\n1115 | \\t\\t\\tbreak\\n1116 | \\t\\tcase \\\"searchCommits\\\": {\\n1117 | \\t\\t\\tconst cwd = provider.cwd\\n1118 | \\t\\t\\tif (cwd) {\\n1119 | \\t\\t\\t\\ttry {\\n1120 | \\t\\t\\t\\t\\tconst commits = await searchCommits(message.query || \\\"\\\", cwd)\\n1121 | \\t\\t\\t\\t\\tawait provider.postMessageToWebview({\\n1122 | \\t\\t\\t\\t\\t\\ttype: \\\"commitSearchResults\\\",\\n1123 | \\t\\t\\t\\t\\t\\tcommits,\\n1124 | \\t\\t\\t\\t\\t})\\n1125 | \\t\\t\\t\\t} catch (error) {\\n1126 | \\t\\t\\t\\t\\tprovider.log(\\n1127 | \\t\\t\\t\\t\\t\\t`Error searching commits: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`,\\n1128 | \\t\\t\\t\\t\\t)\\n1129 | \\t\\t\\t\\t\\tvscode.window.showErrorMessage(t(\\\"common:errors.search_commits\\\"))\\n1130 | \\t\\t\\t\\t}\\n1131 | \\t\\t\\t}\\n1132 | \\t\\t\\tbreak\\n1133 | \\t\\t}\\n1134 | \\t\\tcase \\\"searchFiles\\\": {\\n1135 | \\t\\t\\tconst workspacePath = getWorkspacePath()\\n1136 | \\n1137 | \\t\\t\\tif (!workspacePath) {\\n1138 | \\t\\t\\t\\t// Handle case where workspace path is not available\\n1139 | \\t\\t\\t\\tawait provider.postMessageToWebview({\\n1140 | \\t\\t\\t\\t\\ttype: \\\"fileSearchResults\\\",\\n1141 | \\t\\t\\t\\t\\tresults: [],\\n1142 | \\t\\t\\t\\t\\trequestId: message.requestId,\\n1143 | \\t\\t\\t\\t\\terror: \\\"No workspace path available\\\",\\n1144 | \\t\\t\\t\\t})\\n1145 | \\t\\t\\t\\tbreak\\n1146 | \\t\\t\\t}\\n1147 | \\t\\t\\ttry {\\n1148 | \\t\\t\\t\\t// Call file search service with query from message\\n1149 | \\t\\t\\t\\tconst results = await searchWorkspaceFiles(\\n1150 | \\t\\t\\t\\t\\tmessage.query || \\\"\\\",\\n1151 | \\t\\t\\t\\t\\tworkspacePath,\\n1152 | \\t\\t\\t\\t\\t20, // Use default limit, as filtering is now done in the backend\\n1153 | \\t\\t\\t\\t)\\n1154 | \\n1155 | \\t\\t\\t\\t// Send results back to webview\\n1156 | \\t\\t\\t\\tawait provider.postMessageToWebview({\\n1157 | \\t\\t\\t\\t\\ttype: \\\"fileSearchResults\\\",\\n1158 | \\t\\t\\t\\t\\tresults,\\n1159 | \\t\\t\\t\\t\\trequestId: message.requestId,\\n1160 | \\t\\t\\t\\t})\\n1161 | \\t\\t\\t} catch (error) {\\n1162 | \\t\\t\\t\\tconst errorMessage = error instanceof Error ? error.message : String(error)\\n1163 | \\n1164 | \\t\\t\\t\\t// Send error response to webview\\n1165 | \\t\\t\\t\\tawait provider.postMessageToWebview({\\n1166 | \\t\\t\\t\\t\\ttype: \\\"fileSearchResults\\\",\\n1167 | \\t\\t\\t\\t\\tresults: [],\\n1168 | \\t\\t\\t\\t\\terror: errorMessage,\\n1169 | \\t\\t\\t\\t\\trequestId: message.requestId,\\n1170 | \\t\\t\\t\\t})\\n1171 | \\t\\t\\t}\\n1172 | \\t\\t\\tbreak\\n1173 | \\t\\t}\\n1174 | \\t\\tcase \\\"saveApiConfiguration\\\":\\n1175 | \\t\\t\\tif (message.text && message.apiConfiguration) {\\n1176 | \\t\\t\\t\\ttry {\\n1177 | \\t\\t\\t\\t\\tawait provider.providerSettingsManager.saveConfig(message.text, message.apiConfiguration)\\n1178 | \\t\\t\\t\\t\\tconst listApiConfig = await provider.providerSettingsManager.listConfig()\\n1179 | \\t\\t\\t\\t\\tawait updateGlobalState(\\\"listApiConfigMeta\\\", listApiConfig)\\n1180 | \\t\\t\\t\\t} catch (error) {\\n1181 | \\t\\t\\t\\t\\tprovider.log(\\n1182 | \\t\\t\\t\\t\\t\\t`Error save api configuration: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`,\\n1183 | \\t\\t\\t\\t\\t)\\n1184 | \\t\\t\\t\\t\\tvscode.window.showErrorMessage(t(\\\"common:errors.save_api_config\\\"))\\n1185 | \\t\\t\\t\\t}\\n1186 | \\t\\t\\t}\\n1187 | \\t\\t\\tbreak\\n1188 | \\t\\tcase \\\"upsertApiConfiguration\\\":\\n1189 | \\t\\t\\tif (message.text && message.apiConfiguration) {\\n1190 | \\t\\t\\t\\tawait provider.upsertProviderProfile(message.text, message.apiConfiguration)\\n1191 | \\t\\t\\t}\\n1192 | \\t\\t\\tbreak\\n1193 | \\t\\tcase \\\"renameApiConfiguration\\\":\\n1194 | \\t\\t\\tif (message.values && message.apiConfiguration) {\\n1195 | \\t\\t\\t\\ttry {\\n1196 | \\t\\t\\t\\t\\tconst { oldName, newName } = message.values\\n1197 | \\n1198 | \\t\\t\\t\\t\\tif (oldName === newName) {\\n1199 | \\t\\t\\t\\t\\t\\tbreak\\n1200 | \\t\\t\\t\\t\\t}\\n1201 | \\n1202 | \\t\\t\\t\\t\\t// Load the old configuration to get its ID.\\n1203 | \\t\\t\\t\\t\\tconst { id } = await provider.providerSettingsManager.getProfile({ name: oldName })\\n1204 | \\n1205 | \\t\\t\\t\\t\\t// Create a new configuration with the new name and old ID.\\n1206 | \\t\\t\\t\\t\\tawait provider.providerSettingsManager.saveConfig(newName, { ...message.apiConfiguration, id })\\n1207 | \\n1208 | \\t\\t\\t\\t\\t// Delete the old configuration.\\n1209 | \\t\\t\\t\\t\\tawait provider.providerSettingsManager.deleteConfig(oldName)\\n1210 | \\n1211 | \\t\\t\\t\\t\\t// Re-activate to update the global settings related to the\\n1212 | \\t\\t\\t\\t\\t// currently activated provider profile.\\n1213 | \\t\\t\\t\\t\\tawait provider.activateProviderProfile({ name: newName })\\n1214 | \\t\\t\\t\\t} catch (error) {\\n1215 | \\t\\t\\t\\t\\tprovider.log(\\n1216 | \\t\\t\\t\\t\\t\\t`Error rename api configuration: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`,\\n1217 | \\t\\t\\t\\t\\t)\\n1218 | \\n1219 | \\t\\t\\t\\t\\tvscode.window.showErrorMessage(t(\\\"common:errors.rename_api_config\\\"))\\n1220 | \\t\\t\\t\\t}\\n1221 | \\t\\t\\t}\\n1222 | \\t\\t\\tbreak\\n1223 | \\t\\tcase \\\"loadApiConfiguration\\\":\\n1224 | \\t\\t\\tif (message.text) {\\n1225 | \\t\\t\\t\\ttry {\\n1226 | \\t\\t\\t\\t\\tawait provider.activateProviderProfile({ name: message.text })\\n1227 | \\t\\t\\t\\t} catch (error) {\\n1228 | \\t\\t\\t\\t\\tprovider.log(\\n1229 | \\t\\t\\t\\t\\t\\t`Error load api configuration: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`,\\n1230 | \\t\\t\\t\\t\\t)\\n1231 | \\t\\t\\t\\t\\tvscode.window.showErrorMessage(t(\\\"common:errors.load_api_config\\\"))\\n1232 | \\t\\t\\t\\t}\\n1233 | \\t\\t\\t}\\n1234 | \\t\\t\\tbreak\\n1235 | \\t\\tcase \\\"loadApiConfigurationById\\\":\\n1236 | \\t\\t\\tif (message.text) {\\n1237 | \\t\\t\\t\\ttry {\\n1238 | \\t\\t\\t\\t\\tawait provider.activateProviderProfile({ id: message.text })\\n1239 | \\t\\t\\t\\t} catch (error) {\\n1240 | \\t\\t\\t\\t\\tprovider.log(\\n1241 | \\t\\t\\t\\t\\t\\t`Error load api configuration by ID: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`,\\n1242 | \\t\\t\\t\\t\\t)\\n1243 | \\t\\t\\t\\t\\tvscode.window.showErrorMessage(t(\\\"common:errors.load_api_config\\\"))\\n1244 | \\t\\t\\t\\t}\\n1245 | \\t\\t\\t}\\n1246 | \\t\\t\\tbreak\\n1247 | \\t\\tcase \\\"deleteApiConfiguration\\\":\\n1248 | \\t\\t\\tif (message.text) {\\n1249 | \\t\\t\\t\\tconst answer = await vscode.window.showInformationMessage(\\n1250 | \\t\\t\\t\\t\\tt(\\\"common:confirmation.delete_config_profile\\\"),\\n1251 | \\t\\t\\t\\t\\t{ modal: true },\\n1252 | \\t\\t\\t\\t\\tt(\\\"common:answers.yes\\\"),\\n1253 | \\t\\t\\t\\t)\\n1254 | \\n1255 | \\t\\t\\t\\tif (answer !== t(\\\"common:answers.yes\\\")) {\\n1256 | \\t\\t\\t\\t\\tbreak\\n1257 | \\t\\t\\t\\t}\\n1258 | \\n1259 | \\t\\t\\t\\tconst oldName = message.text\\n1260 | \\n1261 | \\t\\t\\t\\tconst newName = (await provider.providerSettingsManager.listConfig()).filter(\\n1262 | \\t\\t\\t\\t\\t(c) => c.name !== oldName,\\n1263 | \\t\\t\\t\\t)[0]?.name\\n1264 | \\n1265 | \\t\\t\\t\\tif (!newName) {\\n1266 | \\t\\t\\t\\t\\tvscode.window.showErrorMessage(t(\\\"common:errors.delete_api_config\\\"))\\n1267 | \\t\\t\\t\\t\\treturn\\n1268 | \\t\\t\\t\\t}\\n1269 | \\n1270 | \\t\\t\\t\\ttry {\\n1271 | \\t\\t\\t\\t\\tawait provider.providerSettingsManager.deleteConfig(oldName)\\n1272 | \\t\\t\\t\\t\\tawait provider.activateProviderProfile({ name: newName })\\n1273 | \\t\\t\\t\\t} catch (error) {\\n1274 | \\t\\t\\t\\t\\tprovider.log(\\n1275 | \\t\\t\\t\\t\\t\\t`Error delete api configuration: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`,\\n1276 | \\t\\t\\t\\t\\t)\\n1277 | \\n1278 | \\t\\t\\t\\t\\tvscode.window.showErrorMessage(t(\\\"common:errors.delete_api_config\\\"))\\n1279 | \\t\\t\\t\\t}\\n1280 | \\t\\t\\t}\\n1281 | \\t\\t\\tbreak\\n1282 | \\t\\tcase \\\"getListApiConfiguration\\\":\\n1283 | \\t\\t\\ttry {\\n1284 | \\t\\t\\t\\tconst listApiConfig = await provider.providerSettingsManager.listConfig()\\n1285 | \\t\\t\\t\\tawait updateGlobalState(\\\"listApiConfigMeta\\\", listApiConfig)\\n1286 | \\t\\t\\t\\tprovider.postMessageToWebview({ type: \\\"listApiConfig\\\", listApiConfig })\\n1287 | \\t\\t\\t} catch (error) {\\n1288 | \\t\\t\\t\\tprovider.log(\\n1289 | \\t\\t\\t\\t\\t`Error get list api configuration: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`,\\n1290 | \\t\\t\\t\\t)\\n1291 | \\t\\t\\t\\tvscode.window.showErrorMessage(t(\\\"common:errors.list_api_config\\\"))\\n1292 | \\t\\t\\t}\\n1293 | \\t\\t\\tbreak\\n1294 | \\t\\tcase \\\"updateExperimental\\\": {\\n1295 | \\t\\t\\tif (!message.values) {\\n1296 | \\t\\t\\t\\tbreak\\n1297 | \\t\\t\\t}\\n1298 | \\n1299 | \\t\\t\\tconst updatedExperiments = {\\n1300 | \\t\\t\\t\\t...(getGlobalState(\\\"experiments\\\") ?? experimentDefault),\\n1301 | \\t\\t\\t\\t...message.values,\\n1302 | \\t\\t\\t}\\n1303 | \\n1304 | \\t\\t\\tawait updateGlobalState(\\\"experiments\\\", updatedExperiments)\\n1305 | \\n1306 | \\t\\t\\tawait provider.postStateToWebview()\\n1307 | \\t\\t\\tbreak\\n1308 | \\t\\t}\\n1309 | \\t\\tcase \\\"updateMcpTimeout\\\":\\n1310 | \\t\\t\\tif (message.serverName && typeof message.timeout === \\\"number\\\") {\\n1311 | \\t\\t\\t\\ttry {\\n1312 | \\t\\t\\t\\t\\tawait provider\\n1313 | \\t\\t\\t\\t\\t\\t.getMcpHub()\\n1314 | \\t\\t\\t\\t\\t\\t?.updateServerTimeout(\\n1315 | \\t\\t\\t\\t\\t\\t\\tmessage.serverName,\\n1316 | \\t\\t\\t\\t\\t\\t\\tmessage.timeout,\\n1317 | \\t\\t\\t\\t\\t\\t\\tmessage.source as \\\"global\\\" | \\\"project\\\",\\n1318 | \\t\\t\\t\\t\\t\\t)\\n1319 | \\t\\t\\t\\t} catch (error) {\\n1320 | \\t\\t\\t\\t\\tprovider.log(\\n1321 | \\t\\t\\t\\t\\t\\t`Failed to update timeout for ${message.serverName}: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`,\\n1322 | \\t\\t\\t\\t\\t)\\n1323 | \\t\\t\\t\\t\\tvscode.window.showErrorMessage(t(\\\"common:errors.update_server_timeout\\\"))\\n1324 | \\t\\t\\t\\t}\\n1325 | \\t\\t\\t}\\n1326 | \\t\\t\\tbreak\\n1327 | \\t\\tcase \\\"updateCustomMode\\\":\\n1328 | \\t\\t\\tif (message.modeConfig) {\\n1329 | \\t\\t\\t\\tawait provider.customModesManager.updateCustomMode(message.modeConfig.slug, message.modeConfig)\\n1330 | \\t\\t\\t\\t// Update state after saving the mode\\n1331 | \\t\\t\\t\\tconst customModes = await provider.customModesManager.getCustomModes()\\n1332 | \\t\\t\\t\\tawait updateGlobalState(\\\"customModes\\\", customModes)\\n1333 | \\t\\t\\t\\tawait updateGlobalState(\\\"mode\\\", message.modeConfig.slug)\\n1334 | \\t\\t\\t\\tawait provider.postStateToWebview()\\n1335 | \\t\\t\\t}\\n1336 | \\t\\t\\tbreak\\n1337 | \\t\\tcase \\\"deleteCustomMode\\\":\\n1338 | \\t\\t\\tif (message.slug) {\\n1339 | \\t\\t\\t\\tconst answer = await vscode.window.showInformationMessage(\\n1340 | \\t\\t\\t\\t\\tt(\\\"common:confirmation.delete_custom_mode\\\"),\\n1341 | \\t\\t\\t\\t\\t{ modal: true },\\n1342 | \\t\\t\\t\\t\\tt(\\\"common:answers.yes\\\"),\\n1343 | \\t\\t\\t\\t)\\n1344 | \\n1345 | \\t\\t\\t\\tif (answer !== t(\\\"common:answers.yes\\\")) {\\n1346 | \\t\\t\\t\\t\\tbreak\\n1347 | \\t\\t\\t\\t}\\n1348 | \\n1349 | \\t\\t\\t\\tawait provider.customModesManager.deleteCustomMode(message.slug)\\n1350 | \\t\\t\\t\\t// Switch back to default mode after deletion\\n1351 | \\t\\t\\t\\tawait updateGlobalState(\\\"mode\\\", defaultModeSlug)\\n1352 | \\t\\t\\t\\tawait provider.postStateToWebview()\\n1353 | \\t\\t\\t}\\n1354 | \\t\\t\\tbreak\\n1355 | \\t\\tcase \\\"humanRelayResponse\\\":\\n1356 | \\t\\t\\tif (message.requestId && message.text) {\\n1357 | \\t\\t\\t\\tvscode.commands.executeCommand(getCommand(\\\"handleHumanRelayResponse\\\"), {\\n1358 | \\t\\t\\t\\t\\trequestId: message.requestId,\\n1359 | \\t\\t\\t\\t\\ttext: message.text,\\n1360 | \\t\\t\\t\\t\\tcancelled: false,\\n1361 | \\t\\t\\t\\t})\\n1362 | \\t\\t\\t}\\n1363 | \\t\\t\\tbreak\\n1364 | \\n1365 | \\t\\tcase \\\"humanRelayCancel\\\":\\n1366 | \\t\\t\\tif (message.requestId) {\\n1367 | \\t\\t\\t\\tvscode.commands.executeCommand(getCommand(\\\"handleHumanRelayResponse\\\"), {\\n1368 | \\t\\t\\t\\t\\trequestId: message.requestId,\\n1369 | \\t\\t\\t\\t\\tcancelled: true,\\n1370 | \\t\\t\\t\\t})\\n1371 | \\t\\t\\t}\\n1372 | \\t\\t\\tbreak\\n1373 | \\n1374 | \\t\\tcase \\\"telemetrySetting\\\": {\\n1375 | \\t\\t\\tconst telemetrySetting = message.text as TelemetrySetting\\n1376 | \\t\\t\\tawait updateGlobalState(\\\"telemetrySetting\\\", telemetrySetting)\\n1377 | \\t\\t\\tconst isOptedIn = telemetrySetting === \\\"enabled\\\"\\n1378 | \\t\\t\\tTelemetryService.instance.updateTelemetryState(isOptedIn)\\n1379 | \\t\\t\\tawait provider.postStateToWebview()\\n1380 | \\t\\t\\tbreak\\n1381 | \\t\\t}\\n1382 | \\t\\tcase \\\"accountButtonClicked\\\": {\\n1383 | \\t\\t\\t// Navigate to the account tab.\\n1384 | \\t\\t\\tprovider.postMessageToWebview({ type: \\\"action\\\", action: \\\"accountButtonClicked\\\" })\\n1385 | \\t\\t\\tbreak\\n1386 | \\t\\t}\\n1387 | \\t\\tcase \\\"rooCloudSignIn\\\": {\\n1388 | \\t\\t\\ttry {\\n1389 | \\t\\t\\t\\tTelemetryService.instance.captureEvent(TelemetryEventName.AUTHENTICATION_INITIATED)\\n1390 | \\t\\t\\t\\tawait CloudService.instance.login()\\n1391 | \\t\\t\\t} catch (error) {\\n1392 | \\t\\t\\t\\tprovider.log(`AuthService#login failed: ${error}`)\\n1393 | \\t\\t\\t\\tvscode.window.showErrorMessage(\\\"Sign in failed.\\\")\\n1394 | \\t\\t\\t}\\n1395 | \\n1396 | \\t\\t\\tbreak\\n1397 | \\t\\t}\\n1398 | \\t\\tcase \\\"rooCloudSignOut\\\": {\\n1399 | \\t\\t\\ttry {\\n1400 | \\t\\t\\t\\tawait CloudService.instance.logout()\\n1401 | \\t\\t\\t\\tawait provider.postStateToWebview()\\n1402 | \\t\\t\\t\\tprovider.postMessageToWebview({ type: \\\"authenticatedUser\\\", userInfo: undefined })\\n1403 | \\t\\t\\t} catch (error) {\\n1404 | \\t\\t\\t\\tprovider.log(`AuthService#logout failed: ${error}`)\\n1405 | \\t\\t\\t\\tvscode.window.showErrorMessage(\\\"Sign out failed.\\\")\\n1406 | \\t\\t\\t}\\n1407 | \\n1408 | \\t\\t\\tbreak\\n1409 | \\t\\t}\\n1410 | \\t\\tcase \\\"codebaseIndexConfig\\\": {\\n1411 | \\t\\t\\tconst codebaseIndexConfig = message.values ?? {\\n1412 | \\t\\t\\t\\tcodebaseIndexEnabled: false,\\n1413 | \\t\\t\\t\\tcodebaseIndexQdrantUrl: \\\"http://localhost:6333\\\",\\n1414 | \\t\\t\\t\\tcodebaseIndexEmbedderProvider: \\\"openai\\\",\\n1415 | \\t\\t\\t\\tcodebaseIndexEmbedderBaseUrl: \\\"\\\",\\n1416 | \\t\\t\\t\\tcodebaseIndexEmbedderModelId: \\\"\\\",\\n1417 | \\t\\t\\t}\\n1418 | \\t\\t\\tawait updateGlobalState(\\\"codebaseIndexConfig\\\", codebaseIndexConfig)\\n1419 | \\n1420 | \\t\\t\\ttry {\\n1421 | \\t\\t\\t\\tif (provider.codeIndexManager) {\\n1422 | \\t\\t\\t\\t\\tawait provider.codeIndexManager.handleExternalSettingsChange()\\n1423 | \\n1424 | \\t\\t\\t\\t\\t// If now configured and enabled, start indexing automatically\\n1425 | \\t\\t\\t\\t\\tif (provider.codeIndexManager.isFeatureEnabled && provider.codeIndexManager.isFeatureConfigured) {\\n1426 | \\t\\t\\t\\t\\t\\tif (!provider.codeIndexManager.isInitialized) {\\n1427 | \\t\\t\\t\\t\\t\\t\\tawait provider.codeIndexManager.initialize(provider.contextProxy)\\n1428 | \\t\\t\\t\\t\\t\\t}\\n1429 | \\t\\t\\t\\t\\t\\t// Start indexing in background (no await)\\n1430 | \\t\\t\\t\\t\\t\\tprovider.codeIndexManager.startIndexing()\\n1431 | \\t\\t\\t\\t\\t}\\n1432 | \\t\\t\\t\\t}\\n1433 | \\t\\t\\t} catch (error) {\\n1434 | \\t\\t\\t\\tprovider.log(\\n1435 | \\t\\t\\t\\t\\t`[CodeIndexManager] Error during background CodeIndexManager configuration/indexing: ${error.message || error}`,\\n1436 | \\t\\t\\t\\t)\\n1437 | \\t\\t\\t}\\n1438 | \\n1439 | \\t\\t\\tawait provider.postStateToWebview()\\n1440 | \\t\\t\\tbreak\\n1441 | \\t\\t}\\n1442 | \\t\\tcase \\\"requestIndexingStatus\\\": {\\n1443 | \\t\\t\\tconst status = provider.codeIndexManager!.getCurrentStatus()\\n1444 | \\t\\t\\tprovider.postMessageToWebview({\\n1445 | \\t\\t\\t\\ttype: \\\"indexingStatusUpdate\\\",\\n1446 | \\t\\t\\t\\tvalues: status,\\n1447 | \\t\\t\\t})\\n1448 | \\t\\t\\tbreak\\n1449 | \\t\\t}\\n1450 | \\t\\tcase \\\"startIndexing\\\": {\\n1451 | \\t\\t\\ttry {\\n1452 | \\t\\t\\t\\tconst manager = provider.codeIndexManager!\\n1453 | \\t\\t\\t\\tif (manager.isFeatureEnabled && manager.isFeatureConfigured) {\\n1454 | \\t\\t\\t\\t\\tif (!manager.isInitialized) {\\n1455 | \\t\\t\\t\\t\\t\\tawait manager.initialize(provider.contextProxy)\\n1456 | \\t\\t\\t\\t\\t}\\n1457 | \\n1458 | \\t\\t\\t\\t\\tmanager.startIndexing()\\n1459 | \\t\\t\\t\\t}\\n1460 | \\t\\t\\t} catch (error) {\\n1461 | \\t\\t\\t\\tprovider.log(`Error starting indexing: ${error instanceof Error ? error.message : String(error)}`)\\n1462 | \\t\\t\\t}\\n1463 | \\t\\t\\tbreak\\n1464 | \\t\\t}\\n1465 | \\t\\tcase \\\"clearIndexData\\\": {\\n1466 | \\t\\t\\ttry {\\n1467 | \\t\\t\\t\\tconst manager = provider.codeIndexManager!\\n1468 | \\t\\t\\t\\tawait manager.clearIndexData()\\n1469 | \\t\\t\\t\\tprovider.postMessageToWebview({ type: \\\"indexCleared\\\", values: { success: true } })\\n1470 | \\t\\t\\t} catch (error) {\\n1471 | \\t\\t\\t\\tprovider.log(`Error clearing index data: ${error instanceof Error ? error.message : String(error)}`)\\n1472 | \\t\\t\\t\\tprovider.postMessageToWebview({\\n1473 | \\t\\t\\t\\t\\ttype: \\\"indexCleared\\\",\\n1474 | \\t\\t\\t\\t\\tvalues: {\\n1475 | \\t\\t\\t\\t\\t\\tsuccess: false,\\n1476 | \\t\\t\\t\\t\\t\\terror: error instanceof Error ? error.message : String(error),\\n1477 | \\t\\t\\t\\t\\t},\\n1478 | \\t\\t\\t\\t})\\n1479 | \\t\\t\\t}\\n1480 | \\t\\t\\tbreak\\n1481 | \\t\\t}\\n1482 | \\t\\tcase \\\"filterMarketplaceItems\\\": {\\n1483 | \\t\\t\\t// Check if marketplace is enabled before making API calls\\n1484 | \\t\\t\\tconst { experiments } = await provider.getState()\\n1485 | \\t\\t\\tif (!experiments.marketplace) {\\n1486 | \\t\\t\\t\\tconsole.log(\\\"Marketplace: Feature disabled, skipping API call\\\")\\n1487 | \\t\\t\\t\\tbreak\\n1488 | \\t\\t\\t}\\n1489 | \\n1490 | \\t\\t\\tif (marketplaceManager && message.filters) {\\n1491 | \\t\\t\\t\\ttry {\\n1492 | \\t\\t\\t\\t\\tawait marketplaceManager.updateWithFilteredItems({\\n1493 | \\t\\t\\t\\t\\t\\ttype: message.filters.type as MarketplaceItemType | undefined,\\n1494 | \\t\\t\\t\\t\\t\\tsearch: message.filters.search,\\n1495 | \\t\\t\\t\\t\\t\\ttags: message.filters.tags,\\n1496 | \\t\\t\\t\\t\\t})\\n1497 | \\t\\t\\t\\t\\tawait provider.postStateToWebview()\\n1498 | \\t\\t\\t\\t} catch (error) {\\n1499 | \\t\\t\\t\\t\\tconsole.error(\\\"Marketplace: Error filtering items:\\\", error)\\n1500 | \\t\\t\\t\\t\\tvscode.window.showErrorMessage(\\\"Failed to filter marketplace items\\\")\\n1501 | \\t\\t\\t\\t}\\n1502 | \\t\\t\\t}\\n1503 | \\t\\t\\tbreak\\n1504 | \\t\\t}\\n1505 | \\n1506 | \\t\\tcase \\\"installMarketplaceItem\\\": {\\n1507 | \\t\\t\\t// Check if marketplace is enabled before installing\\n1508 | \\t\\t\\tconst { experiments } = await provider.getState()\\n1509 | \\t\\t\\tif (!experiments.marketplace) {\\n1510 | \\t\\t\\t\\tconsole.log(\\\"Marketplace: Feature disabled, skipping installation\\\")\\n1511 | \\t\\t\\t\\tbreak\\n1512 | \\t\\t\\t}\\n1513 | \\n1514 | \\t\\t\\tif (marketplaceManager && message.mpItem && message.mpInstallOptions) {\\n1515 | \\t\\t\\t\\ttry {\\n1516 | \\t\\t\\t\\t\\tconst configFilePath = await marketplaceManager.installMarketplaceItem(\\n1517 | \\t\\t\\t\\t\\t\\tmessage.mpItem,\\n1518 | \\t\\t\\t\\t\\t\\tmessage.mpInstallOptions,\\n1519 | \\t\\t\\t\\t\\t)\\n1520 | \\t\\t\\t\\t\\tawait provider.postStateToWebview()\\n1521 | \\t\\t\\t\\t\\tconsole.log(`Marketplace item installed and config file opened: ${configFilePath}`)\\n1522 | \\t\\t\\t\\t\\t// Send success message to webview\\n1523 | \\t\\t\\t\\t\\tprovider.postMessageToWebview({\\n1524 | \\t\\t\\t\\t\\t\\ttype: \\\"marketplaceInstallResult\\\",\\n1525 | \\t\\t\\t\\t\\t\\tsuccess: true,\\n1526 | \\t\\t\\t\\t\\t\\tslug: message.mpItem.id,\\n1527 | \\t\\t\\t\\t\\t})\\n1528 | \\t\\t\\t\\t} catch (error) {\\n1529 | \\t\\t\\t\\t\\tconsole.error(`Error installing marketplace item: ${error}`)\\n1530 | \\t\\t\\t\\t\\t// Send error message to webview\\n1531 | \\t\\t\\t\\t\\tprovider.postMessageToWebview({\\n1532 | \\t\\t\\t\\t\\t\\ttype: \\\"marketplaceInstallResult\\\",\\n1533 | \\t\\t\\t\\t\\t\\tsuccess: false,\\n1534 | \\t\\t\\t\\t\\t\\terror: error instanceof Error ? error.message : String(error),\\n1535 | \\t\\t\\t\\t\\t\\tslug: message.mpItem.id,\\n1536 | \\t\\t\\t\\t\\t})\\n1537 | \\t\\t\\t\\t}\\n1538 | \\t\\t\\t}\\n1539 | \\t\\t\\tbreak\\n1540 | \\t\\t}\\n1541 | \\n1542 | \\t\\tcase \\\"removeInstalledMarketplaceItem\\\": {\\n1543 | \\t\\t\\t// Check if marketplace is enabled before removing\\n1544 | \\t\\t\\tconst { experiments } = await provider.getState()\\n1545 | \\t\\t\\tif (!experiments.marketplace) {\\n1546 | \\t\\t\\t\\tconsole.log(\\\"Marketplace: Feature disabled, skipping removal\\\")\\n1547 | \\t\\t\\t\\tbreak\\n1548 | \\t\\t\\t}\\n1549 | \\n1550 | \\t\\t\\tif (marketplaceManager && message.mpItem && message.mpInstallOptions) {\\n1551 | \\t\\t\\t\\ttry {\\n1552 | \\t\\t\\t\\t\\tawait marketplaceManager.removeInstalledMarketplaceItem(message.mpItem, message.mpInstallOptions)\\n1553 | \\t\\t\\t\\t\\tawait provider.postStateToWebview()\\n1554 | \\t\\t\\t\\t} catch (error) {\\n1555 | \\t\\t\\t\\t\\tconsole.error(`Error removing marketplace item: ${error}`)\\n1556 | \\t\\t\\t\\t}\\n1557 | \\t\\t\\t}\\n1558 | \\t\\t\\tbreak\\n1559 | \\t\\t}\\n1560 | \\n1561 | \\t\\tcase \\\"installMarketplaceItemWithParameters\\\": {\\n1562 | \\t\\t\\t// Check if marketplace is enabled before installing with parameters\\n1563 | \\t\\t\\tconst { experiments } = await provider.getState()\\n1564 | \\t\\t\\tif (!experiments.marketplace) {\\n1565 | \\t\\t\\t\\tconsole.log(\\\"Marketplace: Feature disabled, skipping installation with parameters\\\")\\n1566 | \\t\\t\\t\\tbreak\\n1567 | \\t\\t\\t}\\n1568 | \\n1569 | \\t\\t\\tif (marketplaceManager && message.payload && \\\"item\\\" in message.payload && \\\"parameters\\\" in message.payload) {\\n1570 | \\t\\t\\t\\ttry {\\n1571 | \\t\\t\\t\\t\\tconst configFilePath = await marketplaceManager.installMarketplaceItem(message.payload.item, {\\n1572 | \\t\\t\\t\\t\\t\\tparameters: message.payload.parameters,\\n1573 | \\t\\t\\t\\t\\t})\\n1574 | \\t\\t\\t\\t\\tawait provider.postStateToWebview()\\n1575 | \\t\\t\\t\\t\\tconsole.log(`Marketplace item with parameters installed and config file opened: ${configFilePath}`)\\n1576 | \\t\\t\\t\\t} catch (error) {\\n1577 | \\t\\t\\t\\t\\tconsole.error(`Error installing marketplace item with parameters: ${error}`)\\n1578 | \\t\\t\\t\\t\\tvscode.window.showErrorMessage(\\n1579 | \\t\\t\\t\\t\\t\\t`Failed to install marketplace item: ${error instanceof Error ? error.message : String(error)}`,\\n1580 | \\t\\t\\t\\t\\t)\\n1581 | \\t\\t\\t\\t}\\n1582 | \\t\\t\\t}\\n1583 | \\t\\t\\tbreak\\n1584 | \\t\\t}\\n1585 | \\n1586 | \\t\\tcase \\\"switchTab\\\": {\\n1587 | \\t\\t\\tif (message.tab) {\\n1588 | \\t\\t\\t\\t// Send a message to the webview to switch to the specified tab\\n1589 | \\t\\t\\t\\tawait provider.postMessageToWebview({ type: \\\"action\\\", action: \\\"switchTab\\\", tab: message.tab })\\n1590 | \\t\\t\\t}\\n1591 | \\t\\t\\tbreak\\n1592 | \\t\\t}\\n1593 | \\t}\\n1594 | }\\n\\n\\n\\n\\nLoading...\"}" -} -[2025-06-13T05:00:01.323Z] { - "ts": 1749790800308, - "type": "say", - "say": "text", - "text": "Now I can see the authentication implementation! I found the Clerk authentication callback handler and the CloudService integration. Let me search for more authentication-related files to understand the token refresh mechanism.", - "partial": false -} -[2025-06-13T05:00:01.942Z] { - "ts": 1749790801323, - "type": "ask", - "ask": "tool", - "text": "{\"tool\":\"searchFiles\",\"path\":\"Roo-Code-Cloud-Agents\",\"regex\":\"CloudService|refresh|token|auth\",\"filePattern\":\"*.ts\",\"content\":\"Showing first 300 of 300+ results. Use a more specific search if necessary.\\n\\n# apps/web-evals/src/lib/formatters.ts\\n 30 | \\n 31 | export const formatTokens = (tokens: number) => {\\n 32 | \\tif (tokens < 1000) {\\n 33 | \\t\\treturn tokens.toString()\\n 34 | \\t}\\n 35 | \\n 36 | \\tif (tokens < 1000000) {\\n 37 | \\t\\treturn `${(tokens / 1000).toFixed(1)}k`\\n 38 | \\t}\\n 39 | \\n 40 | \\tif (tokens < 1000000000) {\\n 41 | \\t\\treturn `${(tokens / 1000000).toFixed(1)}M`\\n 42 | \\t}\\n 43 | \\n 44 | \\treturn `${(tokens / 1000000000).toFixed(1)}B`\\n 45 | }\\n----\\n\\n# src/services/ripgrep/index.ts\\n101 | \\t\\tconst rgProcess = childProcess.spawn(bin, args)\\n102 | \\t\\t// cross-platform alternative to head, which is ripgrep author's recommendation for limiting output.\\n103 | \\t\\tconst rl = readline.createInterface({\\n----\\n\\n# apps/web-roo-code/src/lib/index.ts\\n 3 | export * from \\\"./format-score\\\"\\n 4 | export * from \\\"./format-tokens\\\"\\n----\\n\\n# apps/web-roo-code/src/lib/format-tokens.ts\\n 1 | export const formatTokens = (tokens: number, decimals = 0) => {\\n 2 | \\tif (tokens < 1000) {\\n 3 | \\t\\treturn tokens.toString()\\n 4 | \\t}\\n 5 | \\n 6 | \\tif (tokens < 1000000) {\\n 7 | \\t\\treturn `${(tokens / 1000).toFixed(decimals)}K`\\n 8 | \\t}\\n 9 | \\n 10 | \\tif (tokens < 1000000000) {\\n 11 | \\t\\treturn `${(tokens / 1000000).toFixed(decimals)}M`\\n 12 | \\t}\\n 13 | \\n 14 | \\treturn `${(tokens / 1000000000).toFixed(decimals)}B`\\n 15 | }\\n----\\n\\n# apps/web-roo-code/src/lib/hooks/use-open-router-models.ts\\n 19 | \\t\\t.object({\\n 20 | \\t\\t\\tmax_completion_tokens: z.number().nullish(),\\n 21 | \\t\\t})\\n----\\n 53 | \\t\\t\\tmodelInfo: {\\n 54 | \\t\\t\\t\\tmaxTokens: rawModel.top_provider?.max_completion_tokens ?? undefined,\\n 55 | \\t\\t\\t\\tcontextWindow: rawModel.context_length,\\n----\\n\\n# src/services/mcp/__tests__/McpHub.test.ts\\n 50 | \\t\\t\\tscheme: \\\"file\\\",\\n 51 | \\t\\t\\tauthority: \\\"\\\",\\n 52 | \\t\\t\\tpath: \\\"/test/path\\\",\\n----\\n\\n# src/services/mcp/McpHub.ts\\n1092 | \\n1093 | \\tpublic async refreshAllConnections(): Promise {\\n1094 | \\t\\tif (this.isConnecting) {\\n1095 | \\t\\t\\tvscode.window.showInformationMessage(t(\\\"mcp:info.already_refreshing\\\"))\\n1096 | \\t\\t\\treturn\\n----\\n1099 | \\t\\tthis.isConnecting = true\\n1100 | \\t\\tvscode.window.showInformationMessage(t(\\\"mcp:info.refreshing_all\\\"))\\n1101 | \\n----\\n1151 | \\n1152 | \\t\\t\\tvscode.window.showInformationMessage(t(\\\"mcp:info.all_refreshed\\\"))\\n1153 | \\t\\t} catch (error) {\\n1154 | \\t\\t\\tthis.showErrorMessage(\\\"Failed to refresh MCP servers\\\", error)\\n1155 | \\t\\t} finally {\\n----\\n1244 | \\n1245 | \\t\\t\\t\\t\\t// Only refresh capabilities if connected\\n1246 | \\t\\t\\t\\t\\tif (connection.server.status === \\\"connected\\\") {\\n----\\n1254 | \\t\\t\\t\\t} catch (error) {\\n1255 | \\t\\t\\t\\t\\tconsole.error(`Failed to refresh capabilities for ${serverName}:`, error)\\n1256 | \\t\\t\\t\\t}\\n----\\n\\n# src/services/code-index/embedders/openai.ts\\n 52 | \\t\\t\\t\\t\\tconsole.warn(\\n 53 | \\t\\t\\t\\t\\t\\t`Text at index ${i} exceeds maximum token limit (${itemTokens} > ${MAX_ITEM_TOKENS}). Skipping.`,\\n 54 | \\t\\t\\t\\t\\t)\\n----\\n108 | \\t\\t\\t\\t\\tusage: {\\n109 | \\t\\t\\t\\t\\t\\tpromptTokens: response.usage?.prompt_tokens || 0,\\n110 | \\t\\t\\t\\t\\t\\ttotalTokens: response.usage?.total_tokens || 0,\\n111 | \\t\\t\\t\\t\\t},\\n----\\n\\n# src/services/code-index/embedders/openai-compatible.ts\\n 18 | \\tusage?: {\\n 19 | \\t\\tprompt_tokens?: number\\n 20 | \\t\\ttotal_tokens?: number\\n 21 | \\t}\\n----\\n 34 | \\t * @param baseUrl The base URL for the OpenAI-compatible API endpoint\\n 35 | \\t * @param apiKey The API key for authentication\\n 36 | \\t * @param modelId Optional model identifier (defaults to \\\"text-embedding-3-small\\\")\\n----\\n 75 | \\t\\t\\t\\t\\tconsole.warn(\\n 76 | \\t\\t\\t\\t\\t\\t`Text at index ${i} exceeds maximum token limit (${itemTokens} > ${MAX_ITEM_TOKENS}). Skipping.`,\\n 77 | \\t\\t\\t\\t\\t)\\n----\\n156 | \\t\\t\\t\\t\\tusage: {\\n157 | \\t\\t\\t\\t\\t\\tpromptTokens: response.usage?.prompt_tokens || 0,\\n158 | \\t\\t\\t\\t\\t\\ttotalTokens: response.usage?.total_tokens || 0,\\n159 | \\t\\t\\t\\t\\t},\\n----\\n\\n# src/services/code-index/embedders/__tests__/openai-compatible.spec.ts\\n103 | \\t\\t\\t\\tdata: [{ embedding: [0.1, 0.2, 0.3] }],\\n104 | \\t\\t\\t\\tusage: { prompt_tokens: 10, total_tokens: 15 },\\n105 | \\t\\t\\t}\\n----\\n124 | \\t\\t\\t\\tdata: [{ embedding: [0.1, 0.2, 0.3] }, { embedding: [0.4, 0.5, 0.6] }],\\n125 | \\t\\t\\t\\tusage: { prompt_tokens: 20, total_tokens: 30 },\\n126 | \\t\\t\\t}\\n----\\n149 | \\t\\t\\t\\tdata: [{ embedding: [0.1, 0.2, 0.3] }],\\n150 | \\t\\t\\t\\tusage: { prompt_tokens: 10, total_tokens: 15 },\\n151 | \\t\\t\\t}\\n----\\n194 | \\t\\t\\t\\t\\tdata: [{ embedding: base64String }], // Base64 string instead of array\\n195 | \\t\\t\\t\\t\\tusage: { prompt_tokens: 10, total_tokens: 15 },\\n196 | \\t\\t\\t\\t}\\n----\\n226 | \\t\\t\\t\\t\\tdata: [{ embedding: base64String1 }, { embedding: base64String2 }],\\n227 | \\t\\t\\t\\t\\tusage: { prompt_tokens: 20, total_tokens: 30 },\\n228 | \\t\\t\\t\\t}\\n----\\n253 | \\t\\t\\t\\t\\t],\\n254 | \\t\\t\\t\\t\\tusage: { prompt_tokens: 20, total_tokens: 30 },\\n255 | \\t\\t\\t\\t}\\n----\\n270 | \\t\\t/**\\n271 | \\t\\t * Test batching logic when texts exceed token limits\\n272 | \\t\\t */\\n----\\n283 | \\t\\t\\t\\t\\t],\\n284 | \\t\\t\\t\\t\\tusage: { prompt_tokens: 10, total_tokens: 15 },\\n285 | \\t\\t\\t\\t})\\n----\\n299 | \\t\\t\\t\\t\\tdata: [{ embedding: [0.1, 0.2, 0.3] }, { embedding: [0.4, 0.5, 0.6] }],\\n300 | \\t\\t\\t\\t\\tusage: { prompt_tokens: 10, total_tokens: 15 },\\n301 | \\t\\t\\t\\t}\\n----\\n306 | \\t\\t\\t\\t// Should warn about oversized text\\n307 | \\t\\t\\t\\texpect(console.warn).toHaveBeenCalledWith(expect.stringContaining(\\\"exceeds maximum token limit\\\"))\\n308 | \\n----\\n317 | \\t\\t\\t\\t\\tdata: [{ embedding: [0.1, 0.2, 0.3] }, { embedding: [0.4, 0.5, 0.6] }],\\n318 | \\t\\t\\t\\t\\tusage: { prompt_tokens: 10, total_tokens: 15 },\\n319 | \\t\\t\\t\\t})\\n----\\n354 | \\t\\t\\t\\t\\t\\tdata: [{ embedding: base64String }],\\n355 | \\t\\t\\t\\t\\t\\tusage: { prompt_tokens: 10, total_tokens: 15 },\\n356 | \\t\\t\\t\\t\\t})\\n----\\n375 | \\t\\t\\t\\tconst testTexts = [\\\"Hello world\\\"]\\n376 | \\t\\t\\t\\tconst authError = new Error(\\\"Unauthorized\\\")\\n377 | \\t\\t\\t\\t;(authError as any).status = 401\\n378 | \\n379 | \\t\\t\\t\\tmockEmbeddingsCreate.mockRejectedValue(authError)\\n380 | \\n----\\n452 | \\t\\t\\t\\t\\tdata: null,\\n453 | \\t\\t\\t\\t\\tusage: { prompt_tokens: 10, total_tokens: 15 },\\n454 | \\t\\t\\t\\t}\\n----\\n494 | \\t\\t\\t\\t\\tusage: {\\n495 | \\t\\t\\t\\t\\t\\tprompt_tokens: 2,\\n496 | \\t\\t\\t\\t\\t\\ttotal_tokens: 2,\\n497 | \\t\\t\\t\\t\\t},\\n----\\n\\n# apps/web-evals/src/hooks/use-run-status.ts\\n 16 | \\ttasks: (Task & { taskMetrics: TaskMetrics | null })[] | undefined\\n 17 | \\ttokenUsage: Map\\n 18 | \\tusageUpdatedAt: number | undefined\\n----\\n 24 | \\n 25 | \\tconst tokenUsage = useRef>(new Map())\\n 26 | \\tconst startTimes = useRef>(new Map())\\n----\\n 79 | \\t\\t\\t\\tconst duration = startTime ? Date.now() - startTime : undefined\\n 80 | \\t\\t\\t\\ttokenUsage.current.set(taskId, { ...payload[1], duration })\\n 81 | \\t\\t\\t\\tsetUsageUpdatedAt(Date.now())\\n----\\n 97 | \\t\\ttasks,\\n 98 | \\t\\ttokenUsage: tokenUsage.current,\\n 99 | \\t\\tusageUpdatedAt,\\n----\\n\\n# src/activate/handleUri.ts\\n 2 | \\n 3 | import { CloudService } from \\\"@roo-code/cloud\\\"\\n 4 | \\n----\\n 37 | \\t\\t}\\n 38 | \\t\\tcase \\\"/auth/clerk/callback\\\": {\\n 39 | \\t\\t\\tconst code = query.get(\\\"code\\\")\\n 40 | \\t\\t\\tconst state = query.get(\\\"state\\\")\\n 41 | \\t\\t\\tawait CloudService.instance.handleAuthCallback(code, state)\\n 42 | \\t\\t\\tbreak\\n----\\n\\n# src/services/marketplace/types.ts\\n 20 | \\tdescription: string\\n 21 | \\tauthor?: string\\n 22 | \\tauthorUrl?: string\\n 23 | \\ttags?: string[]\\n----\\n 45 | \\tdescription: string\\n 46 | \\tauthor?: string\\n 47 | \\tauthorUrl?: string\\n 48 | \\turl: string // Required url field\\n----\\n 62 | \\ttype: MarketplaceItemType\\n 63 | \\tauthor?: string\\n 64 | \\tauthorUrl?: string\\n 65 | \\turl?: string // Optional - only MCPs have url\\n----\\n\\n# src/services/tree-sitter/__tests__/fixtures/sample-embedded_template.ts\\n 35 |
\\n 36 | <% if user.authenticated? %>\\n 37 |

Welcome, <%= user.name %>

\\n----\\n\\n# src/services/tree-sitter/__tests__/parseSourceCodeDefinitions.embedded_template.test.ts\\n 43 | \\tit(\\\"should detect control structures\\\", () => {\\n 44 | \\t\\texpect(parseResult).toMatch(/\\\\d+--\\\\d+ \\\\|\\\\s+<% if user\\\\.authenticated\\\\? %>/)\\n 45 | \\t\\texpect(parseResult).toMatch(/\\\\d+--\\\\d+ \\\\|\\\\s+<% user\\\\.posts\\\\.each do \\\\|post\\\\| %>/)\\n----\\n\\n# src/services/marketplace/schemas.ts\\n 35 | \\ttype: marketplaceItemTypeSchema,\\n 36 | \\tauthor: z.string().optional(),\\n 37 | \\tauthorUrl: z.string().url(\\\"Author URL must be a valid URL\\\").optional(),\\n 38 | \\ttags: z.array(z.string()).optional(),\\n----\\n 61 | \\tdescription: z.string(),\\n 62 | \\tauthor: z.string().optional(),\\n 63 | \\tauthorUrl: z.string().url().optional(),\\n 64 | \\ttags: z.array(z.string()).optional(),\\n----\\n 72 | \\tdescription: z.string(),\\n 73 | \\tauthor: z.string().optional(),\\n 74 | \\tauthorUrl: z.string().url().optional(),\\n 75 | \\turl: z.string().url(), // Required url field\\n----\\n\\n# src/extension.ts\\n 14 | \\n 15 | import { CloudService } from \\\"@roo-code/cloud\\\"\\n 16 | import { TelemetryService, PostHogTelemetryClient } from \\\"@roo-code/telemetry\\\"\\n----\\n 75 | \\t// Initialize Roo Code Cloud service.\\n 76 | \\tawait CloudService.createInstance(context, {\\n 77 | \\t\\tstateChanged: () => ClineProvider.getVisibleInstance()?.postStateToWebview(),\\n----\\n\\n# src/workers/countTokens.ts\\n 4 | \\n 5 | import { tiktoken } from \\\"../utils/tiktoken\\\"\\n 6 | \\n----\\n 10 | \\ttry {\\n 11 | \\t\\tconst count = await tiktoken(content)\\n 12 | \\t\\treturn { success: true, count }\\n----\\n\\n# src/api/index.ts\\n 50 | \\t/**\\n 51 | \\t * Counts tokens for content blocks\\n 52 | \\t * All providers extend BaseProvider which provides a default tiktoken implementation,\\n 53 | \\t * but they can override this to use their native token counting endpoints\\n 54 | \\t *\\n 55 | \\t * @param content The content to count tokens for\\n 56 | \\t * @returns A promise resolving to the token count\\n 57 | \\t */\\n----\\n\\n# src/extension/api.ts\\n219 | \\n220 | \\t\\t\\tcline.on(\\\"taskCompleted\\\", async (_, tokenUsage, toolUsage) => {\\n221 | \\t\\t\\t\\tthis.emit(RooCodeEventName.TaskCompleted, cline.taskId, tokenUsage, toolUsage)\\n222 | \\t\\t\\t\\tthis.taskMap.delete(cline.taskId)\\n----\\n224 | \\t\\t\\t\\tawait this.fileLog(\\n225 | \\t\\t\\t\\t\\t`[${new Date().toISOString()}] taskCompleted -> ${cline.taskId} | ${JSON.stringify(tokenUsage, null, 2)} | ${JSON.stringify(toolUsage, null, 2)}\\\\n`,\\n226 | \\t\\t\\t\\t)\\n----\\n\\n# src/api/providers/ollama.ts\\n 79 | \\t\\t\\t\\ttype: \\\"usage\\\",\\n 80 | \\t\\t\\t\\tinputTokens: lastUsage?.prompt_tokens || 0,\\n 81 | \\t\\t\\t\\toutputTokens: lastUsage?.completion_tokens || 0,\\n 82 | \\t\\t\\t}\\n----\\n\\n# src/core/checkpoints/index.ts\\n215 | \\t\\t\\t\\tJSON.stringify({\\n216 | \\t\\t\\t\\t\\ttokensIn: totalTokensIn,\\n217 | \\t\\t\\t\\t\\ttokensOut: totalTokensOut,\\n218 | \\t\\t\\t\\t\\tcacheWrites: totalCacheWrites,\\n----\\n\\n# src/utils/git.ts\\n 11 | \\tsubject: string\\n 12 | \\tauthor: string\\n 13 | \\tdate: string\\n----\\n 57 | \\t\\t\\tconst { stdout: hashStdout } = await execAsync(\\n 58 | \\t\\t\\t\\t`git log -n 10 --format=\\\"%H%n%h%n%s%n%an%n%ad\\\" --date=short ` + `--author-date-order ${query}`,\\n 59 | \\t\\t\\t\\t{ cwd },\\n----\\n 79 | \\t\\t\\t\\tsubject: lines[i + 2],\\n 80 | \\t\\t\\t\\tauthor: lines[i + 3],\\n 81 | \\t\\t\\t\\tdate: lines[i + 4],\\n----\\n107 | \\t\\t})\\n108 | \\t\\tconst [fullHash, shortHash, subject, author, date, body] = info.trim().split(\\\"\\\\n\\\")\\n109 | \\n----\\n115 | \\t\\t\\t`Commit: ${shortHash} (${fullHash})`,\\n116 | \\t\\t\\t`Author: ${author}`,\\n117 | \\t\\t\\t`Date: ${date}`,\\n----\\n\\n# src/api/providers/__tests__/bedrock-invokedModelId.spec.ts\\n176 | \\n177 | \\t\\t// The last usage event should have the token counts from the metadata\\n178 | \\t\\tconst lastUsageEvent = usageEvents[usageEvents.length - 1]\\n179 | \\t\\t// Expect the usage event to include all token information\\n180 | \\t\\texpect(lastUsageEvent).toMatchObject({\\n----\\n183 | \\t\\t\\toutputTokens: 200,\\n184 | \\t\\t\\t// Cache tokens may be present with default values\\n185 | \\t\\t\\tcacheReadTokens: expect.any(Number),\\n----\\n\\n# src/api/providers/__tests__/groq.spec.ts\\n101 | \\t\\t\\t\\t\\t\\t\\tdone: false,\\n102 | \\t\\t\\t\\t\\t\\t\\tvalue: { choices: [{ delta: {} }], usage: { prompt_tokens: 10, completion_tokens: 20 } },\\n103 | \\t\\t\\t\\t\\t\\t})\\n----\\n139 | \\t\\t\\t\\tmodel: modelId,\\n140 | \\t\\t\\t\\tmax_tokens: modelInfo.maxTokens,\\n141 | \\t\\t\\t\\ttemperature: 0.5,\\n----\\n\\n# src/api/providers/__tests__/gemini.spec.ts\\n167 | \\tdescribe(\\\"calculateCost\\\", () => {\\n168 | \\t\\t// Mock ModelInfo based on gemini-1.5-flash-latest pricing (per 1M tokens)\\n169 | \\t\\t// Removed 'id' and 'name' as they are not part of ModelInfo type directly\\n170 | \\t\\tconst mockInfo: ModelInfo = {\\n171 | \\t\\t\\tinputPrice: 0.125, // $/1M tokens\\n172 | \\t\\t\\toutputPrice: 0.375, // $/1M tokens\\n173 | \\t\\t\\tcacheWritesPrice: 0.125, // Assume same as input for test\\n----\\n179 | \\n180 | \\t\\tit(\\\"should calculate cost correctly based on input and output tokens\\\", () => {\\n181 | \\t\\t\\tconst inputTokens = 10000 // Use larger numbers for per-million pricing\\n----\\n190 | \\n191 | \\t\\tit(\\\"should return 0 if token counts are zero\\\", () => {\\n192 | \\t\\t\\t// Note: The method expects numbers, not undefined. Passing undefined would be a type error.\\n193 | \\t\\t\\t// The calculateCost method itself returns undefined if prices are missing, but 0 if tokens are 0 and prices exist.\\n194 | \\t\\t\\texpect(handler.calculateCost({ info: mockInfo, inputTokens: 0, outputTokens: 0 })).toBe(0)\\n----\\n196 | \\n197 | \\t\\tit(\\\"should handle only input tokens\\\", () => {\\n198 | \\t\\t\\tconst inputTokens = 5000\\n----\\n203 | \\n204 | \\t\\tit(\\\"should handle only output tokens\\\", () => {\\n205 | \\t\\t\\tconst outputTokens = 15000\\n----\\n210 | \\n211 | \\t\\tit(\\\"should calculate cost with cache write tokens\\\", () => {\\n212 | \\t\\t\\tconst inputTokens = 10000\\n----\\n227 | \\n228 | \\t\\tit(\\\"should calculate cost with cache read tokens\\\", () => {\\n229 | \\t\\t\\tconst inputTokens = 10000 // Total logical input\\n----\\n\\n# src/core/condense/index.ts\\n 55 | \\tcost: number // The cost of the summarization operation\\n 56 | \\tnewContextTokens?: number // The number of tokens in the context for the next API request\\n 57 | \\terror?: string // Populated iff the operation fails: error message shown to the user on failure (see Task.ts)\\n----\\n 63 | * @param {ApiMessage[]} messages - The conversation messages\\n 64 | * @param {ApiHandler} apiHandler - The API handler to use for token counting.\\n 65 | * @param {string} systemPrompt - The system prompt for API requests, which should be considered in the context token count\\n 66 | * @param {string} taskId - The task ID for the conversation, used for telemetry\\n----\\n 73 | * @param {ApiMessage[]} messages - The conversation messages\\n 74 | * @param {ApiHandler} apiHandler - The API handler to use for token counting (fallback if condensingApiHandler not provided)\\n 75 | * @param {string} systemPrompt - The system prompt for API requests (fallback if customCondensingPrompt not provided)\\n 76 | * @param {string} taskId - The task ID for the conversation, used for telemetry\\n 77 | * @param {number} prevContextTokens - The number of tokens currently in the context, used to ensure we don't grow the context\\n 78 | * @param {boolean} isAutomaticTrigger - Whether the summarization is triggered automatically\\n----\\n186 | \\n187 | \\t// Count the tokens in the context for the next API request\\n188 | \\t// We only estimate the tokens in summaryMesage if outputTokens is 0, otherwise we use outputTokens\\n189 | \\tconst systemPromptMessage: ApiMessage = { role: \\\"user\\\", content: systemPrompt }\\n----\\n\\n# src/api/providers/__tests__/vertex.spec.ts\\n 49 | \\t\\t\\t// The test expects 4 chunks:\\n 50 | \\t\\t\\t// 1. Usage chunk with input tokens\\n 51 | \\t\\t\\t// 2. Text chunk with \\\"Gemini response part 1\\\"\\n 52 | \\t\\t\\t// 3. Text chunk with \\\" part 2\\\"\\n 53 | \\t\\t\\t// 4. Usage chunk with output tokens\\n 54 | \\n----\\n\\n# src/api/providers/__tests__/ollama.spec.ts\\n 28 | \\t\\t\\t\\t\\t\\t\\t\\tusage: {\\n 29 | \\t\\t\\t\\t\\t\\t\\t\\t\\tprompt_tokens: 10,\\n 30 | \\t\\t\\t\\t\\t\\t\\t\\t\\tcompletion_tokens: 5,\\n 31 | \\t\\t\\t\\t\\t\\t\\t\\t\\ttotal_tokens: 15,\\n 32 | \\t\\t\\t\\t\\t\\t\\t\\t},\\n----\\n 54 | \\t\\t\\t\\t\\t\\t\\t\\t\\tusage: {\\n 55 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\tprompt_tokens: 10,\\n 56 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\tcompletion_tokens: 5,\\n 57 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\ttotal_tokens: 15,\\n 58 | \\t\\t\\t\\t\\t\\t\\t\\t\\t},\\n----\\n\\n# src/core/environment/getEnvironmentDetails.ts\\n192 | \\n193 | \\t// Add context tokens information.\\n194 | \\tconst { contextTokens, totalCost } = getApiMetrics(cline.clineMessages)\\n----\\n\\n# src/api/providers/__tests__/openai.spec.ts\\n 29 | \\t\\t\\t\\t\\t\\t\\t\\tusage: {\\n 30 | \\t\\t\\t\\t\\t\\t\\t\\t\\tprompt_tokens: 10,\\n 31 | \\t\\t\\t\\t\\t\\t\\t\\t\\tcompletion_tokens: 5,\\n 32 | \\t\\t\\t\\t\\t\\t\\t\\t\\ttotal_tokens: 15,\\n 33 | \\t\\t\\t\\t\\t\\t\\t\\t},\\n----\\n 55 | \\t\\t\\t\\t\\t\\t\\t\\t\\tusage: {\\n 56 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\tprompt_tokens: 10,\\n 57 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\tcompletion_tokens: 5,\\n 58 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\ttotal_tokens: 15,\\n 59 | \\t\\t\\t\\t\\t\\t\\t\\t\\t},\\n----\\n\\n# src/api/providers/__tests__/anthropic.spec.ts\\n 19 | \\t\\t\\t\\t\\t\\tusage: {\\n 20 | \\t\\t\\t\\t\\t\\t\\tinput_tokens: 10,\\n 21 | \\t\\t\\t\\t\\t\\t\\toutput_tokens: 5,\\n 22 | \\t\\t\\t\\t\\t\\t},\\n----\\n 30 | \\t\\t\\t\\t\\t\\t\\t\\tusage: {\\n 31 | \\t\\t\\t\\t\\t\\t\\t\\t\\tinput_tokens: 100,\\n 32 | \\t\\t\\t\\t\\t\\t\\t\\t\\toutput_tokens: 50,\\n 33 | \\t\\t\\t\\t\\t\\t\\t\\t\\tcache_creation_input_tokens: 20,\\n 34 | \\t\\t\\t\\t\\t\\t\\t\\t\\tcache_read_input_tokens: 10,\\n 35 | \\t\\t\\t\\t\\t\\t\\t\\t},\\n----\\n105 | \\n106 | \\t\\tit(\\\"use apiKey for passing token if anthropicUseAuthToken is not set\\\", () => {\\n107 | \\t\\t\\tconst handlerWithCustomUrl = new AnthropicHandler({\\n----\\n112 | \\t\\t\\texpect(mockAnthropicConstructor.mock.calls[0]![0]!.apiKey).toEqual(\\\"test-api-key\\\")\\n113 | \\t\\t\\texpect(mockAnthropicConstructor.mock.calls[0]![0]!.authToken).toBeUndefined()\\n114 | \\t\\t})\\n115 | \\n116 | \\t\\tit(\\\"use apiKey for passing token if anthropicUseAuthToken is set but custom base URL is not given\\\", () => {\\n117 | \\t\\t\\tconst handlerWithCustomUrl = new AnthropicHandler({\\n----\\n123 | \\t\\t\\texpect(mockAnthropicConstructor.mock.calls[0]![0]!.apiKey).toEqual(\\\"test-api-key\\\")\\n124 | \\t\\t\\texpect(mockAnthropicConstructor.mock.calls[0]![0]!.authToken).toBeUndefined()\\n125 | \\t\\t})\\n126 | \\n127 | \\t\\tit(\\\"use authToken for passing token if both of anthropicBaseUrl and anthropicUseAuthToken are set\\\", () => {\\n128 | \\t\\t\\tconst customBaseUrl = \\\"https://custom.anthropic.com\\\"\\n----\\n135 | \\t\\t\\texpect(mockAnthropicConstructor).toHaveBeenCalledTimes(1)\\n136 | \\t\\t\\texpect(mockAnthropicConstructor.mock.calls[0]![0]!.authToken).toEqual(\\\"test-api-key\\\")\\n137 | \\t\\t\\texpect(mockAnthropicConstructor.mock.calls[0]![0]!.apiKey).toBeUndefined()\\n----\\n190 | \\t\\t\\t\\tmessages: [{ role: \\\"user\\\", content: \\\"Test prompt\\\" }],\\n191 | \\t\\t\\t\\tmax_tokens: 8192,\\n192 | \\t\\t\\t\\ttemperature: 0,\\n----\\n\\n# src/api/providers/__tests__/xai.test.ts\\n228 | \\t\\t\\t\\t\\t\\t\\t\\tusage: {\\n229 | \\t\\t\\t\\t\\t\\t\\t\\t\\tprompt_tokens: 10,\\n230 | \\t\\t\\t\\t\\t\\t\\t\\t\\tcompletion_tokens: 20,\\n231 | \\t\\t\\t\\t\\t\\t\\t\\t\\tcache_read_input_tokens: 5,\\n232 | \\t\\t\\t\\t\\t\\t\\t\\t\\tcache_creation_input_tokens: 15,\\n233 | \\t\\t\\t\\t\\t\\t\\t\\t},\\n----\\n284 | \\t\\t\\t\\tmodel: modelId,\\n285 | \\t\\t\\t\\tmax_tokens: modelInfo.maxTokens,\\n286 | \\t\\t\\t\\ttemperature: 0,\\n----\\n\\n# src/api/providers/__tests__/openrouter.spec.ts\\n132 | \\t\\t\\t\\t\\t\\tchoices: [{ delta: {} }],\\n133 | \\t\\t\\t\\t\\t\\tusage: { prompt_tokens: 10, completion_tokens: 20, cost: 0.001 },\\n134 | \\t\\t\\t\\t\\t}\\n----\\n162 | \\t\\t\\t\\texpect.objectContaining({\\n163 | \\t\\t\\t\\t\\tmax_tokens: 8192,\\n164 | \\t\\t\\t\\t\\tmessages: [\\n----\\n285 | \\t\\t\\t\\tmodel: mockOptions.openRouterModelId,\\n286 | \\t\\t\\t\\tmax_tokens: 8192,\\n287 | \\t\\t\\t\\tthinking: undefined,\\n----\\n\\n# src/api/providers/unbound.ts\\n 22 | interface UnboundUsage extends OpenAI.CompletionUsage {\\n 23 | \\tcache_creation_input_tokens?: number\\n 24 | \\tcache_read_input_tokens?: number\\n 25 | }\\n----\\n 73 | \\n 74 | \\t\\t// Required by Anthropic; other providers default to max tokens allowed.\\n 75 | \\t\\tlet maxTokens: number | undefined\\n----\\n 82 | \\t\\t\\tmodel: modelId.split(\\\"/\\\")[1],\\n 83 | \\t\\t\\tmax_tokens: maxTokens,\\n 84 | \\t\\t\\tmessages: openAiMessages,\\n----\\n111 | \\t\\t\\t\\t\\ttype: \\\"usage\\\",\\n112 | \\t\\t\\t\\t\\tinputTokens: usage.prompt_tokens || 0,\\n113 | \\t\\t\\t\\t\\toutputTokens: usage.completion_tokens || 0,\\n114 | \\t\\t\\t\\t}\\n115 | \\n116 | \\t\\t\\t\\t// Only add cache tokens if they exist.\\n117 | \\t\\t\\t\\tif (usage.cache_creation_input_tokens) {\\n118 | \\t\\t\\t\\t\\tusageData.cacheWriteTokens = usage.cache_creation_input_tokens\\n119 | \\t\\t\\t\\t}\\n120 | \\n121 | \\t\\t\\t\\tif (usage.cache_read_input_tokens) {\\n122 | \\t\\t\\t\\t\\tusageData.cacheReadTokens = usage.cache_read_input_tokens\\n123 | \\t\\t\\t\\t}\\n----\\n146 | \\t\\t\\tif (modelId.startsWith(\\\"anthropic/\\\")) {\\n147 | \\t\\t\\t\\trequestOptions.max_tokens = info.maxTokens\\n148 | \\t\\t\\t}\\n----\\n\\n# src/api/providers/vscode-lm.ts\\n128 | \\t\\t\\t\\tmaxInputTokens: 8192,\\n129 | \\t\\t\\t\\tsendRequest: async (_messages, _options, _token) => {\\n130 | \\t\\t\\t\\t\\t// Provide a minimal implementation\\n----\\n179 | \\t * Implements the ApiHandler countTokens interface method\\n180 | \\t * Provides token counting for Anthropic content blocks\\n181 | \\t *\\n182 | \\t * @param content The content blocks to count tokens for\\n183 | \\t * @returns A promise resolving to the token count\\n184 | \\t */\\n185 | \\toverride async countTokens(content: Array): Promise {\\n186 | \\t\\t// Convert Anthropic content blocks to a string for VSCode LM token counting\\n187 | \\t\\tlet textContent = \\\"\\\"\\n----\\n201 | \\t/**\\n202 | \\t * Private implementation of token counting used internally by VsCodeLmHandler\\n203 | \\t */\\n----\\n206 | \\t\\tif (!this.client) {\\n207 | \\t\\t\\tconsole.warn(\\\"Roo Code : No client available for token counting\\\")\\n208 | \\t\\t\\treturn 0\\n----\\n211 | \\t\\tif (!this.currentRequestCancellation) {\\n212 | \\t\\t\\tconsole.warn(\\\"Roo Code : No cancellation token available for token counting\\\")\\n213 | \\t\\t\\treturn 0\\n----\\n217 | \\t\\tif (!text) {\\n218 | \\t\\t\\tconsole.debug(\\\"Roo Code : Empty text provided for token counting\\\")\\n219 | \\t\\t\\treturn 0\\n----\\n223 | \\t\\t\\t// Handle different input types\\n224 | \\t\\t\\tlet tokenCount: number\\n225 | \\n226 | \\t\\t\\tif (typeof text === \\\"string\\\") {\\n227 | \\t\\t\\t\\ttokenCount = await this.client.countTokens(text, this.currentRequestCancellation.token)\\n228 | \\t\\t\\t} else if (text instanceof vscode.LanguageModelChatMessage) {\\n----\\n233 | \\t\\t\\t\\t}\\n234 | \\t\\t\\t\\ttokenCount = await this.client.countTokens(text, this.currentRequestCancellation.token)\\n235 | \\t\\t\\t} else {\\n236 | \\t\\t\\t\\tconsole.warn(\\\"Roo Code : Invalid input type for token counting\\\")\\n237 | \\t\\t\\t\\treturn 0\\n----\\n240 | \\t\\t\\t// Validate the result\\n241 | \\t\\t\\tif (typeof tokenCount !== \\\"number\\\") {\\n242 | \\t\\t\\t\\tconsole.warn(\\\"Roo Code : Non-numeric token count received:\\\", tokenCount)\\n243 | \\t\\t\\t\\treturn 0\\n----\\n245 | \\n246 | \\t\\t\\tif (tokenCount < 0) {\\n247 | \\t\\t\\t\\tconsole.warn(\\\"Roo Code : Negative token count received:\\\", tokenCount)\\n248 | \\t\\t\\t\\treturn 0\\n----\\n250 | \\n251 | \\t\\t\\treturn tokenCount\\n252 | \\t\\t} catch (error) {\\n----\\n278 | \\n279 | \\t\\treturn systemTokens + messageTokens.reduce((sum: number, tokens: number): number => sum + tokens, 0)\\n280 | \\t}\\n----\\n357 | \\n358 | \\t\\t// Initialize cancellation token for the request\\n359 | \\t\\tthis.currentRequestCancellation = new vscode.CancellationTokenSource()\\n360 | \\n361 | \\t\\t// Calculate input tokens before starting the stream\\n362 | \\t\\tconst totalInputTokens: number = await this.calculateTotalInputTokens(systemPrompt, vsCodeLmMessages)\\n363 | \\n364 | \\t\\t// Accumulate the text and count at the end of the stream to reduce token counting overhead.\\n365 | \\t\\tlet accumulatedText: string = \\\"\\\"\\n----\\n378 | \\t\\t\\t\\trequestOptions,\\n379 | \\t\\t\\t\\tthis.currentRequestCancellation.token,\\n380 | \\t\\t\\t)\\n----\\n446 | \\n447 | \\t\\t\\t// Count tokens in the accumulated text after stream completion\\n448 | \\t\\t\\tconst totalOutputTokens: number = await this.internalCountTokens(accumulatedText)\\n----\\n511 | \\t\\t\\tconst modelInfo: ModelInfo = {\\n512 | \\t\\t\\t\\tmaxTokens: -1, // Unlimited tokens by default\\n513 | \\t\\t\\t\\tcontextWindow:\\n----\\n548 | \\t\\t\\t\\t{},\\n549 | \\t\\t\\t\\tnew vscode.CancellationTokenSource().token,\\n550 | \\t\\t\\t)\\n----\\n\\n# src/core/condense/__tests__/index.test.ts\\n203 | \\n204 | \\t\\t// Check the cost and token counts\\n205 | \\t\\texpect(result.cost).toBe(0.05)\\n206 | \\t\\texpect(result.summary).toBe(\\\"This is a summary\\\")\\n207 | \\t\\texpect(result.newContextTokens).toBe(250) // 150 output tokens + 100 from countTokens\\n208 | \\t\\texpect(result.error).toBeUndefined()\\n----\\n317 | \\t\\t// Check the newContextTokens calculation includes system prompt\\n318 | \\t\\texpect(result.newContextTokens).toBe(300) // 200 output tokens + 100 from countTokens\\n319 | \\t\\texpect(result.cost).toBe(0.06)\\n----\\n323 | \\n324 | \\tit(\\\"should return error when new context tokens >= previous context tokens\\\", async () => {\\n325 | \\t\\tconst messages: ApiMessage[] = [\\n----\\n336 | \\t\\tconst streamWithLargeTokens = (async function* () {\\n337 | \\t\\t\\tyield { type: \\\"text\\\" as const, text: \\\"This is a very long summary that uses many tokens\\\" }\\n338 | \\t\\t\\tyield { type: \\\"usage\\\" as const, totalCost: 0.08, outputTokens: 500 }\\n----\\n364 | \\n365 | \\tit(\\\"should successfully summarize when new context tokens < previous context tokens\\\", async () => {\\n366 | \\t\\tconst messages: ApiMessage[] = [\\n----\\n375 | \\n376 | \\t\\t// Create a stream that produces a summary with reasonable token count\\n377 | \\t\\tconst streamWithSmallTokens = (async function* () {\\n----\\n401 | \\t\\texpect(result.error).toBeUndefined()\\n402 | \\t\\texpect(result.newContextTokens).toBe(80) // 50 output tokens + 30 from countTokens\\n403 | \\t\\texpect(result.newContextTokens).toBeLessThan(prevContextTokens)\\n----\\n\\n# src/api/providers/__tests__/lmstudio.test.ts\\n 25 | \\t\\t\\t\\t\\t\\t\\t\\tusage: {\\n 26 | \\t\\t\\t\\t\\t\\t\\t\\t\\tprompt_tokens: 10,\\n 27 | \\t\\t\\t\\t\\t\\t\\t\\t\\tcompletion_tokens: 5,\\n 28 | \\t\\t\\t\\t\\t\\t\\t\\t\\ttotal_tokens: 15,\\n 29 | \\t\\t\\t\\t\\t\\t\\t\\t},\\n----\\n 51 | \\t\\t\\t\\t\\t\\t\\t\\t\\tusage: {\\n 52 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\tprompt_tokens: 10,\\n 53 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\tcompletion_tokens: 5,\\n 54 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\ttotal_tokens: 15,\\n 55 | \\t\\t\\t\\t\\t\\t\\t\\t\\t},\\n----\\n\\n# src/api/providers/__tests__/bedrock-reasoning.test.ts\\n 94 | \\t\\t\\t\\ttype: \\\"enabled\\\",\\n 95 | \\t\\t\\t\\tbudget_tokens: 4096, // Uses the full modelMaxThinkingTokens value\\n 96 | \\t\\t\\t})\\n----\\n143 | \\t\\t\\t\\ttype: \\\"enabled\\\",\\n144 | \\t\\t\\t\\tbudget_tokens: 8192,\\n145 | \\t\\t\\t})\\n----\\n266 | \\t\\t\\t\\ttype: \\\"enabled\\\",\\n267 | \\t\\t\\t\\tbudget_tokens: 4096,\\n268 | \\t\\t\\t})\\n----\\n\\n# src/api/providers/lm-studio.ts\\n 40 | \\t\\t// -------------------------\\n 41 | \\t\\t// Track token usage\\n 42 | \\t\\t// -------------------------\\n----\\n 68 | \\t\\t} catch (err) {\\n 69 | \\t\\t\\tconsole.error(\\\"[LmStudio] Failed to count input tokens:\\\", err)\\n 70 | \\t\\t\\tinputTokens = 0\\n----\\n116 | \\t\\t\\t} catch (err) {\\n117 | \\t\\t\\t\\tconsole.error(\\\"[LmStudio] Failed to count output tokens:\\\", err)\\n118 | \\t\\t\\t\\toutputTokens = 0\\n----\\n\\n# src/utils/countTokens.ts\\n 4 | import { countTokensResultSchema } from \\\"../workers/types\\\"\\n 5 | import { tiktoken } from \\\"./tiktoken\\\"\\n 6 | \\n----\\n 27 | \\tif (!useWorker || !pool) {\\n 28 | \\t\\treturn tiktoken(content)\\n 29 | \\t}\\n----\\n 42 | \\t\\tconsole.error(error)\\n 43 | \\t\\treturn tiktoken(content)\\n 44 | \\t}\\n----\\n\\n# src/api/providers/__tests__/requesty.spec.ts\\n124 | \\t\\t\\t\\t\\t\\tusage: {\\n125 | \\t\\t\\t\\t\\t\\t\\tprompt_tokens: 10,\\n126 | \\t\\t\\t\\t\\t\\t\\tcompletion_tokens: 20,\\n127 | \\t\\t\\t\\t\\t\\t\\tprompt_tokens_details: {\\n128 | \\t\\t\\t\\t\\t\\t\\t\\tcaching_tokens: 5,\\n129 | \\t\\t\\t\\t\\t\\t\\t\\tcached_tokens: 2,\\n130 | \\t\\t\\t\\t\\t\\t\\t},\\n----\\n162 | \\t\\t\\t\\texpect.objectContaining({\\n163 | \\t\\t\\t\\t\\tmax_tokens: 8192,\\n164 | \\t\\t\\t\\t\\tmessages: [\\n----\\n204 | \\t\\t\\t\\tmodel: mockOptions.requestyModelId,\\n205 | \\t\\t\\t\\tmax_tokens: 8192,\\n206 | \\t\\t\\t\\tmessages: [{ role: \\\"system\\\", content: \\\"test prompt\\\" }],\\n----\\n\\n# src/api/providers/__tests__/vscode-lm.test.ts\\n 28 | \\t\\tCancellationTokenSource: jest.fn(() => ({\\n 29 | \\t\\t\\ttoken: {\\n 30 | \\t\\t\\t\\tisCancellationRequested: false,\\n----\\n\\n# src/api/providers/__tests__/chutes.spec.ts\\n 49 | \\t\\t\\t\\t\\tusage: {\\n 50 | \\t\\t\\t\\t\\t\\tprompt_tokens: 10,\\n 51 | \\t\\t\\t\\t\\t\\tcompletion_tokens: 5,\\n 52 | \\t\\t\\t\\t\\t\\ttotal_tokens: 15,\\n 53 | \\t\\t\\t\\t\\t},\\n----\\n103 | \\t\\t\\t\\t\\t],\\n104 | \\t\\t\\t\\t\\tusage: { prompt_tokens: 10, completion_tokens: 5 },\\n105 | \\t\\t\\t\\t}\\n----\\n211 | \\t\\t\\t\\t\\t\\t\\tdone: false,\\n212 | \\t\\t\\t\\t\\t\\t\\tvalue: { choices: [{ delta: {} }], usage: { prompt_tokens: 10, completion_tokens: 20 } },\\n213 | \\t\\t\\t\\t\\t\\t})\\n----\\n284 | \\t\\t\\t\\tmodel: modelId,\\n285 | \\t\\t\\t\\tmax_tokens: modelInfo.maxTokens,\\n286 | \\t\\t\\t\\ttemperature: 0.5,\\n----\\n\\n# src/api/providers/__tests__/openai-native.spec.ts\\n 29 | \\t\\t\\t\\t\\t\\t\\t\\tusage: {\\n 30 | \\t\\t\\t\\t\\t\\t\\t\\t\\tprompt_tokens: 10,\\n 31 | \\t\\t\\t\\t\\t\\t\\t\\t\\tcompletion_tokens: 5,\\n 32 | \\t\\t\\t\\t\\t\\t\\t\\t\\ttotal_tokens: 15,\\n 33 | \\t\\t\\t\\t\\t\\t\\t\\t},\\n----\\n 55 | \\t\\t\\t\\t\\t\\t\\t\\t\\tusage: {\\n 56 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\tprompt_tokens: 10,\\n 57 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\tcompletion_tokens: 5,\\n 58 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\ttotal_tokens: 15,\\n 59 | \\t\\t\\t\\t\\t\\t\\t\\t\\t},\\n----\\n145 | \\t\\t\\t\\t\\t\\tusage: {\\n146 | \\t\\t\\t\\t\\t\\t\\tprompt_tokens: 0,\\n147 | \\t\\t\\t\\t\\t\\t\\tcompletion_tokens: 0,\\n148 | \\t\\t\\t\\t\\t\\t\\ttotal_tokens: 0,\\n149 | \\t\\t\\t\\t\\t\\t},\\n----\\n215 | \\t\\t\\t\\t{ choices: [{ delta: { content: \\\" there\\\" } }], usage: null },\\n216 | \\t\\t\\t\\t{ choices: [{ delta: { content: \\\"!\\\" } }], usage: { prompt_tokens: 10, completion_tokens: 5 } },\\n217 | \\t\\t\\t]\\n----\\n261 | \\t\\t\\t\\t{ choices: [{ delta: { content: null } }], usage: null },\\n262 | \\t\\t\\t\\t{ choices: [{ delta: { content: \\\"Hello\\\" } }], usage: { prompt_tokens: 10, completion_tokens: 5 } },\\n263 | \\t\\t\\t]\\n----\\n\\n# src/api/providers/__tests__/deepseek.test.ts\\n 28 | \\t\\t\\t\\t\\t\\t\\t\\tusage: {\\n 29 | \\t\\t\\t\\t\\t\\t\\t\\t\\tprompt_tokens: 10,\\n 30 | \\t\\t\\t\\t\\t\\t\\t\\t\\tcompletion_tokens: 5,\\n 31 | \\t\\t\\t\\t\\t\\t\\t\\t\\ttotal_tokens: 15,\\n 32 | \\t\\t\\t\\t\\t\\t\\t\\t\\tprompt_tokens_details: {\\n 33 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\tcache_miss_tokens: 8,\\n 34 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\tcached_tokens: 2,\\n 35 | \\t\\t\\t\\t\\t\\t\\t\\t\\t},\\n----\\n 59 | \\t\\t\\t\\t\\t\\t\\t\\t\\tusage: {\\n 60 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\tprompt_tokens: 10,\\n 61 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\tcompletion_tokens: 5,\\n 62 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\ttotal_tokens: 15,\\n 63 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\tprompt_tokens_details: {\\n 64 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\t\\tcache_miss_tokens: 8,\\n 65 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\t\\tcached_tokens: 2,\\n 66 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\t},\\n----\\n261 | \\t\\t\\tconst usage = {\\n262 | \\t\\t\\t\\tprompt_tokens: 100,\\n263 | \\t\\t\\t\\tcompletion_tokens: 50,\\n264 | \\t\\t\\t\\ttotal_tokens: 150,\\n265 | \\t\\t\\t\\tprompt_tokens_details: {\\n266 | \\t\\t\\t\\t\\tcache_miss_tokens: 80,\\n267 | \\t\\t\\t\\t\\tcached_tokens: 20,\\n268 | \\t\\t\\t\\t},\\n----\\n289 | \\t\\t\\tconst usage = {\\n290 | \\t\\t\\t\\tprompt_tokens: 100,\\n291 | \\t\\t\\t\\tcompletion_tokens: 50,\\n292 | \\t\\t\\t\\ttotal_tokens: 150,\\n293 | \\t\\t\\t\\t// No prompt_tokens_details\\n294 | \\t\\t\\t}\\n----\\n\\n# src/api/transform/cache-strategy/multi-point-strategy.ts\\n 7 | * Strategy for handling multiple cache points.\\n 8 | * Creates cache points after messages as soon as uncached tokens exceed minimumTokenCount.\\n 9 | */\\n----\\n 58 | \\t *\\n 59 | \\t * @param minTokensPerPoint Minimum tokens required per cache point\\n 60 | \\t * @param remainingCachePoints Number of cache points available\\n----\\n 97 | \\n 98 | \\t\\t// Calculate tokens in new messages (added since last cache point placement)\\n 99 | \\t\\tconst lastPreviousIndex = previousPlacements[previousPlacements.length - 1].index\\n----\\n103 | \\n104 | \\t\\t// If new messages have enough tokens for a cache point, we need to decide\\n105 | \\t\\t// whether to keep all previous cache points or combine some\\n----\\n127 | \\t\\t\\t\\t// We need to decide which previous cache points to keep and which to combine\\n128 | \\t\\t\\t\\t// Strategy: Compare the token count of new messages with the smallest combined token gap\\n129 | \\n130 | \\t\\t\\t\\t// First, analyze the token distribution between previous cache points\\n131 | \\t\\t\\t\\tconst tokensBetweenPlacements: number[] = []\\n132 | \\t\\t\\t\\tlet startIdx = 0\\n----\\n134 | \\t\\t\\t\\tfor (const placement of previousPlacements) {\\n135 | \\t\\t\\t\\t\\tconst tokens = this.config.messages\\n136 | \\t\\t\\t\\t\\t\\t.slice(startIdx, placement.index + 1)\\n----\\n138 | \\n139 | \\t\\t\\t\\t\\ttokensBetweenPlacements.push(tokens)\\n140 | \\t\\t\\t\\t\\tstartIdx = placement.index + 1\\n----\\n142 | \\n143 | \\t\\t\\t\\t// Find the two consecutive placements with the smallest token gap\\n144 | \\t\\t\\t\\tlet smallestGapIndex = 0\\n----\\n146 | \\n147 | \\t\\t\\t\\tfor (let i = 0; i < tokensBetweenPlacements.length - 1; i++) {\\n148 | \\t\\t\\t\\t\\tconst gap = tokensBetweenPlacements[i] + tokensBetweenPlacements[i + 1]\\n149 | \\t\\t\\t\\t\\tif (gap < smallestGap) {\\n----\\n155 | \\t\\t\\t\\t// Only combine cache points if it's beneficial\\n156 | \\t\\t\\t\\t// Compare the token count of new messages with the smallest combined token gap\\n157 | \\t\\t\\t\\t// Apply a required percentage increase to ensure reallocation is worth it\\n----\\n161 | \\t\\t\\t\\tif (newMessagesTokens >= requiredTokenThreshold) {\\n162 | \\t\\t\\t\\t\\t// It's beneficial to combine cache points since new messages have significantly more tokens\\n163 | \\t\\t\\t\\t\\tlogger.info(\\\"Combining cache points is beneficial\\\", {\\n----\\n231 | \\t\\t} else {\\n232 | \\t\\t\\t// New messages don't have enough tokens for a cache point\\n233 | \\t\\t\\t// Keep all previous placements that are still valid\\n----\\n266 | \\t\\tif (lastUserMessageIndex >= 0) {\\n267 | \\t\\t\\t// Calculate the total tokens covered from the previous cache point (or start of conversation)\\n268 | \\t\\t\\t// to this cache point. This ensures tokensCovered represents the full span of tokens\\n269 | \\t\\t\\t// that will be cached by this cache point.\\n----\\n281 | \\n282 | \\t\\t\\t// Calculate tokens from previous cache point (or start) to this cache point\\n283 | \\t\\t\\tconst tokenStartIndex = previousCachePointIndex + 1\\n284 | \\t\\t\\ttotalTokensCovered = this.config.messages\\n285 | \\t\\t\\t\\t.slice(tokenStartIndex, lastUserMessageIndex + 1)\\n286 | \\t\\t\\t\\t.reduce((acc, curr) => acc + this.estimateTokenCount(curr), 0)\\n287 | \\n288 | \\t\\t\\t// Guard clause: ensure we have enough tokens to justify a cache point\\n289 | \\t\\t\\tif (totalTokensCovered < minTokensPerPoint) {\\n----\\n294 | \\t\\t\\t\\ttype: \\\"message\\\",\\n295 | \\t\\t\\t\\ttokensCovered: totalTokensCovered,\\n296 | \\t\\t\\t}\\n----\\n\\n# src/api/transform/model-params.ts\\n 95 | \\n 96 | \\t\\t// Reasoning cannot be less than 1024 tokens.\\n 97 | \\t\\tif (reasoningBudget < 1024) {\\n----\\n\\n# src/api/transform/cache-strategy/types.ts\\n 7 | export interface ModelInfo {\\n 8 | \\t/** Maximum number of tokens the model can generate */\\n 9 | \\tmaxTokens: number\\n 10 | \\t/** Maximum context window size in tokens */\\n 11 | \\tcontextWindow: number\\n----\\n 15 | \\tmaxCachePoints: number\\n 16 | \\t/** Minimum number of tokens required for a cache point */\\n 17 | \\tminTokensPerCachePoint: number\\n----\\n 49 | \\ttype: \\\"system\\\" | \\\"message\\\"\\n 50 | \\t/** Number of tokens this cache point covers */\\n 51 | \\ttokensCovered: number\\n 52 | }\\n----\\n\\n# src/api/transform/__tests__/reasoning.spec.ts\\n 40 | \\n 41 | \\t\\t\\texpect(result).toEqual({ max_tokens: 1000 })\\n 42 | \\t\\t})\\n----\\n 61 | \\n 62 | \\t\\t\\texpect(result).toEqual({ max_tokens: 1000 })\\n 63 | \\t\\t})\\n----\\n123 | \\n124 | \\t\\t\\texpect(result).toEqual({ max_tokens: 1000 })\\n125 | \\t\\t})\\n----\\n140 | \\n141 | \\t\\t\\texpect(result).toEqual({ max_tokens: undefined })\\n142 | \\t\\t})\\n----\\n189 | \\n190 | \\t\\t\\texpect(result).toEqual({ max_tokens: 0 })\\n191 | \\t\\t})\\n----\\n244 | \\t\\t\\t\\ttype: \\\"enabled\\\",\\n245 | \\t\\t\\t\\tbudget_tokens: 1000,\\n246 | \\t\\t\\t})\\n----\\n268 | \\t\\t\\t\\ttype: \\\"enabled\\\",\\n269 | \\t\\t\\t\\tbudget_tokens: 1000,\\n270 | \\t\\t\\t})\\n----\\n314 | \\t\\t\\t\\ttype: \\\"enabled\\\",\\n315 | \\t\\t\\t\\tbudget_tokens: undefined,\\n316 | \\t\\t\\t})\\n----\\n334 | \\t\\t\\t\\ttype: \\\"enabled\\\",\\n335 | \\t\\t\\t\\tbudget_tokens: 0,\\n336 | \\t\\t\\t})\\n----\\n354 | \\t\\t\\t\\ttype: \\\"enabled\\\",\\n355 | \\t\\t\\t\\tbudget_tokens: 100000,\\n356 | \\t\\t\\t})\\n----\\n374 | \\t\\t\\t\\ttype: \\\"enabled\\\",\\n375 | \\t\\t\\t\\tbudget_tokens: 1000,\\n376 | \\t\\t\\t})\\n----\\n547 | \\n548 | \\t\\t\\texpect(openRouterResult).toEqual({ max_tokens: 1000 })\\n549 | \\t\\t\\texpect(anthropicResult).toEqual({ type: \\\"enabled\\\", budget_tokens: 1000 })\\n550 | \\t\\t\\texpect(openAiResult).toBeUndefined()\\n----\\n601 | \\t\\t\\t// Budget should take precedence for OpenRouter and Anthropic\\n602 | \\t\\t\\texpect(openRouterResult).toEqual({ max_tokens: 1000 })\\n603 | \\t\\t\\texpect(anthropicResult).toEqual({ type: \\\"enabled\\\", budget_tokens: 1000 })\\n604 | \\t\\t\\t// OpenAI should still use effort since it doesn't support budget\\n----\\n672 | \\t\\t\\t\\texpect(typeof result).toBe(\\\"object\\\")\\n673 | \\t\\t\\t\\texpect(\\\"max_tokens\\\" in result || \\\"effort\\\" in result || \\\"exclude\\\" in result).toBe(true)\\n674 | \\t\\t\\t}\\n----\\n688 | \\t\\t\\t\\texpect(result).toHaveProperty(\\\"type\\\", \\\"enabled\\\")\\n689 | \\t\\t\\t\\texpect(result).toHaveProperty(\\\"budget_tokens\\\")\\n690 | \\t\\t\\t}\\n----\\n\\n# src/api/transform/__tests__/model-params.spec.ts\\n205 | \\t\\t\\t\\t\\ttype: \\\"enabled\\\",\\n206 | \\t\\t\\t\\t\\tbudget_tokens: 1600,\\n207 | \\t\\t\\t\\t},\\n----\\n226 | \\t\\t\\t\\ttype: \\\"enabled\\\",\\n227 | \\t\\t\\t\\tbudget_tokens: 1600,\\n228 | \\t\\t\\t})\\n----\\n262 | \\t\\t\\t\\t\\ttype: \\\"enabled\\\",\\n263 | \\t\\t\\t\\t\\tbudget_tokens: 2400,\\n264 | \\t\\t\\t\\t},\\n----\\n287 | \\t\\t\\t\\t\\ttype: \\\"enabled\\\",\\n288 | \\t\\t\\t\\t\\tbudget_tokens: 1500,\\n289 | \\t\\t\\t\\t},\\n----\\n308 | \\n309 | \\t\\tit(\\\"should clamp thinking budget to at least 1024 tokens\\\", () => {\\n310 | \\t\\t\\tconst model: ModelInfo = {\\n----\\n328 | \\t\\t\\t\\t\\ttype: \\\"enabled\\\",\\n329 | \\t\\t\\t\\t\\tbudget_tokens: 1024,\\n330 | \\t\\t\\t\\t},\\n----\\n333 | \\n334 | \\t\\tit(\\\"should clamp thinking budget to at most 80% of max tokens\\\", () => {\\n335 | \\t\\t\\tconst model: ModelInfo = {\\n----\\n353 | \\t\\t\\t\\t\\ttype: \\\"enabled\\\",\\n354 | \\t\\t\\t\\t\\tbudget_tokens: 3200,\\n355 | \\t\\t\\t\\t},\\n----\\n372 | \\t\\t\\t\\t\\ttype: \\\"enabled\\\",\\n373 | \\t\\t\\t\\t\\tbudget_tokens: DEFAULT_HYBRID_REASONING_MODEL_THINKING_TOKENS,\\n374 | \\t\\t\\t\\t},\\n----\\n390 | \\t\\t\\texpect(result.maxTokens).toBe(5000)\\n391 | \\t\\t\\texpect(result.reasoningBudget).toBe(2000) // Custom thinking tokens takes precedence\\n392 | \\t\\t})\\n393 | \\n394 | \\t\\tit(\\\"should clamp custom thinking tokens even when custom max tokens is provided\\\", () => {\\n395 | \\t\\t\\tconst model: ModelInfo = {\\n----\\n719 | \\n720 | \\t\\t\\texpect(result.reasoning).toEqual({ max_tokens: 3200 })\\n721 | \\t\\t})\\n----\\n\\n# src/api/transform/cache-strategy/base-strategy.ts\\n 27 | \\t/**\\n 28 | \\t * Calculate token count for system prompt using a more accurate approach\\n 29 | \\t */\\n----\\n 33 | \\n 34 | \\t\\t\\t// Use a more accurate token estimation than simple character count\\n 35 | \\t\\t\\t// Count words and add overhead for punctuation and special tokens\\n 36 | \\t\\t\\tconst words = text.split(/\\\\s+/).filter((word) => word.length > 0)\\n 37 | \\t\\t\\t// Average English word is ~1.3 tokens\\n 38 | \\t\\t\\tlet tokenCount = words.length * 1.3\\n 39 | \\t\\t\\t// Add overhead for punctuation and special characters\\n 40 | \\t\\t\\ttokenCount += (text.match(/[.,!?;:()[\\\\]{}\\\"\\\"''`]/g) || []).length * 0.3\\n 41 | \\t\\t\\t// Add overhead for newlines\\n 42 | \\t\\t\\ttokenCount += (text.match(/\\\\n/g) || []).length * 0.5\\n 43 | \\t\\t\\t// Add a small overhead for system prompt structure\\n 44 | \\t\\t\\ttokenCount += 5\\n 45 | \\n 46 | \\t\\t\\tthis.systemTokenCount = Math.ceil(tokenCount)\\n 47 | \\t\\t}\\n----\\n 84 | \\t/**\\n 85 | \\t * Check if a token count meets the minimum threshold for caching\\n 86 | \\t */\\n 87 | \\tprotected meetsMinTokenThreshold(tokenCount: number): boolean {\\n 88 | \\t\\tconst minTokens = this.config.modelInfo.minTokensPerCachePoint\\n----\\n 91 | \\t\\t}\\n 92 | \\t\\treturn tokenCount >= minTokens\\n 93 | \\t}\\n----\\n 95 | \\t/**\\n 96 | \\t * Estimate token count for a message using a more accurate approach\\n 97 | \\t * This implementation is based on the BaseProvider's countTokens method\\n----\\n100 | \\tprotected estimateTokenCount(message: Anthropic.Messages.MessageParam): number {\\n101 | \\t\\t// Use a more sophisticated token counting approach\\n102 | \\t\\tif (!message.content) return 0\\n----\\n108 | \\t\\t\\t\\tif (block.type === \\\"text\\\") {\\n109 | \\t\\t\\t\\t\\t// Use a more accurate token estimation than simple character count\\n110 | \\t\\t\\t\\t\\t// This is still an approximation but better than character/4\\n----\\n112 | \\t\\t\\t\\t\\tif (text.length > 0) {\\n113 | \\t\\t\\t\\t\\t\\t// Count words and add overhead for punctuation and special tokens\\n114 | \\t\\t\\t\\t\\t\\tconst words = text.split(/\\\\s+/).filter((word) => word.length > 0)\\n115 | \\t\\t\\t\\t\\t\\t// Average English word is ~1.3 tokens\\n116 | \\t\\t\\t\\t\\t\\ttotalTokens += words.length * 1.3\\n----\\n128 | \\t\\t\\tconst text = message.content\\n129 | \\t\\t\\t// Count words and add overhead for punctuation and special tokens\\n130 | \\t\\t\\tconst words = text.split(/\\\\s+/).filter((word) => word.length > 0)\\n131 | \\t\\t\\t// Average English word is ~1.3 tokens\\n132 | \\t\\t\\ttotalTokens += words.length * 1.3\\n----\\n\\n# src/api/providers/__tests__/glama.spec.ts\\n 57 | \\t\\t\\t\\t\\t\\t\\t\\t\\tchoices: [{ delta: {}, index: 0 }],\\n 58 | \\t\\t\\t\\t\\t\\t\\t\\t\\tusage: { prompt_tokens: 10, completion_tokens: 5, total_tokens: 15 },\\n 59 | \\t\\t\\t\\t\\t\\t\\t\\t}\\n----\\n112 | \\t\\t\\tusage: {\\n113 | \\t\\t\\t\\tprompt_tokens: 10,\\n114 | \\t\\t\\t\\tcompletion_tokens: 5,\\n115 | \\t\\t\\t\\ttotal_tokens: 15,\\n116 | \\t\\t\\t},\\n----\\n176 | \\t\\t\\t\\t\\ttemperature: 0,\\n177 | \\t\\t\\t\\t\\tmax_tokens: 8192,\\n178 | \\t\\t\\t\\t}),\\n----\\n194 | \\n195 | \\t\\tit(\\\"should not set max_tokens for non-Anthropic models\\\", async () => {\\n196 | \\t\\t\\t// Reset mock to clear any previous calls\\n----\\n213 | \\t\\t\\t)\\n214 | \\t\\t\\texpect(mockCreate.mock.calls[0][0]).not.toHaveProperty(\\\"max_tokens\\\")\\n215 | \\t\\t})\\n----\\n\\n# src/api/transform/__tests__/stream.spec.ts\\n 33 | \\n 34 | \\t\\tit(\\\"should handle usage chunks without cache tokens\\\", () => {\\n 35 | \\t\\t\\tconst usageChunk: ApiStreamChunk = {\\n----\\n 57 | \\n 58 | \\t\\tit(\\\"should handle usage chunks with zero tokens\\\", () => {\\n 59 | \\t\\t\\tconst zeroUsageChunk: ApiStreamChunk = {\\n----\\n 69 | \\n 70 | \\t\\tit(\\\"should handle usage chunks with large token counts\\\", () => {\\n 71 | \\t\\t\\tconst largeUsageChunk: ApiStreamChunk = {\\n----\\n\\n# src/api/transform/cache-strategy/__tests__/cache-strategy.spec.ts\\n 29 | \\n 30 | const createMessageWithTokens = (role: \\\"user\\\" | \\\"assistant\\\", tokenCount: number) => ({\\n 31 | \\trole,\\n 32 | \\tcontent: \\\"x\\\".repeat(tokenCount * 4), // Approximate 4 chars per token\\n 33 | })\\n----\\n130 | \\t\\t\\t\\tit(\\\"adds system cache block when prompt caching is enabled, messages exist, and system prompt is long enough\\\", () => {\\n131 | \\t\\t\\t\\t\\t// Create a system prompt that's at least 50 tokens (200+ characters)\\n132 | \\t\\t\\t\\t\\tconst longSystemPrompt =\\n----\\n236 | \\t\\t\\t\\t\\t\\t\\t\\t(i + 1) +\\n237 | \\t\\t\\t\\t\\t\\t\\t\\t\\\" with some additional text to increase token count. \\\" +\\n238 | \\t\\t\\t\\t\\t\\t\\t\\t\\\"Adding more text to ensure we exceed the token threshold for cache block insertion.\\\",\\n239 | \\t\\t\\t\\t\\t\\t}))\\n----\\n579 | \\n580 | \\t\\tit(\\\"should yield usage results with cache tokens when using MultiPointStrategy\\\", async () => {\\n581 | \\t\\t\\t// Mock the convertToBedrockConverseMessages method to return a result with cache points\\n----\\n589 | \\n590 | \\t\\t\\t// Create a mock stream that includes cache token fields\\n591 | \\t\\t\\tconst mockApiResponse = {\\n----\\n626 | \\n627 | \\t\\t\\t// Verify that usage results with cache tokens are yielded\\n628 | \\t\\t\\texpect(chunks.length).toBeGreaterThan(0)\\n629 | \\t\\t\\t// The test already expects cache tokens, but the implementation might not be including them\\n630 | \\t\\t\\t// Let's make the test more flexible to accept either format\\n----\\n652 | \\n653 | \\t\\t// Helper function to create a message with approximate token count\\n654 | \\t\\tconst createMessage = (role: \\\"user\\\" | \\\"assistant\\\", content: string, tokenCount: number) => {\\n655 | \\t\\t\\t// Pad the content to reach the desired token count (approx 4 chars per token)\\n656 | \\t\\t\\tconst paddingNeeded = Math.max(0, tokenCount * 4 - content.length)\\n657 | \\t\\t\\tconst padding = \\\" \\\".repeat(paddingNeeded)\\n----\\n667 | \\t\\t\\t\\t\\\"Cache point placements:\\\",\\n668 | \\t\\t\\t\\tplacements.map((p) => `index: ${p.index}, tokens: ${p.tokensCovered}`),\\n669 | \\t\\t\\t)\\n----\\n683 | \\t\\t\\t\\t\\tmodelInfo: multiPointModelInfo,\\n684 | \\t\\t\\t\\t\\tsystemPrompt: \\\"You are a helpful assistant.\\\", // ~10 tokens\\n685 | \\t\\t\\t\\t\\tmessages,\\n----\\n730 | \\t\\t\\t\\t\\t\\ttype: \\\"message\\\",\\n731 | \\t\\t\\t\\t\\t\\ttokensCovered: 300,\\n732 | \\t\\t\\t\\t\\t},\\n----\\n736 | \\t\\t\\t\\t\\tmodelInfo: multiPointModelInfo,\\n737 | \\t\\t\\t\\t\\tsystemPrompt: \\\"You are a helpful assistant.\\\", // ~10 tokens\\n738 | \\t\\t\\t\\t\\tmessages,\\n----\\n759 | \\n760 | \\t\\t\\t\\t// Check if we have a second cache point (may not always be added depending on token distribution)\\n761 | \\t\\t\\t\\tif (result.messageCachePointPlacements && result.messageCachePointPlacements.length > 1) {\\n----\\n789 | \\t\\t\\t\\t\\t\\ttype: \\\"message\\\",\\n790 | \\t\\t\\t\\t\\t\\ttokensCovered: 300,\\n791 | \\t\\t\\t\\t\\t},\\n----\\n794 | \\t\\t\\t\\t\\t\\ttype: \\\"message\\\",\\n795 | \\t\\t\\t\\t\\t\\ttokensCovered: 300,\\n796 | \\t\\t\\t\\t\\t},\\n----\\n800 | \\t\\t\\t\\t\\tmodelInfo: multiPointModelInfo,\\n801 | \\t\\t\\t\\t\\tsystemPrompt: \\\"You are a helpful assistant.\\\", // ~10 tokens\\n802 | \\t\\t\\t\\t\\tmessages,\\n----\\n863 | \\t\\t\\t\\t\\t\\ttype: \\\"message\\\",\\n864 | \\t\\t\\t\\t\\t\\ttokensCovered: 300,\\n865 | \\t\\t\\t\\t\\t},\\n----\\n868 | \\t\\t\\t\\t\\t\\ttype: \\\"message\\\",\\n869 | \\t\\t\\t\\t\\t\\ttokensCovered: 300,\\n870 | \\t\\t\\t\\t\\t},\\n----\\n873 | \\t\\t\\t\\t\\t\\ttype: \\\"message\\\",\\n874 | \\t\\t\\t\\t\\t\\ttokensCovered: 300,\\n875 | \\t\\t\\t\\t\\t},\\n----\\n879 | \\t\\t\\t\\t\\tmodelInfo: multiPointModelInfo,\\n880 | \\t\\t\\t\\t\\tsystemPrompt: \\\"You are a helpful assistant.\\\", // ~10 tokens\\n881 | \\t\\t\\t\\t\\tmessages,\\n----\\n929 | \\t\\t\\t\\t// This test verifies that the documentation for Example 5 is correct\\n930 | \\t\\t\\t\\t// In Example 5, the third cache point at index 10 should cover 660 tokens\\n931 | \\t\\t\\t\\t// (260 tokens from messages 7-8 plus 400 tokens from the new messages)\\n932 | \\n----\\n944 | \\t\\t\\t\\t\\tcreateMessage(\\\"assistant\\\", \\\"Deep learning has many applications including...\\\", 160),\\n945 | \\t\\t\\t\\t\\t// New messages with 400 tokens total\\n946 | \\t\\t\\t\\t\\tcreateMessage(\\\"user\\\", \\\"Can you provide a detailed example?\\\", 100),\\n----\\n954 | \\t\\t\\t\\t\\t\\ttype: \\\"message\\\",\\n955 | \\t\\t\\t\\t\\t\\ttokensCovered: 240,\\n956 | \\t\\t\\t\\t\\t},\\n----\\n959 | \\t\\t\\t\\t\\t\\ttype: \\\"message\\\",\\n960 | \\t\\t\\t\\t\\t\\ttokensCovered: 440,\\n961 | \\t\\t\\t\\t\\t},\\n----\\n964 | \\t\\t\\t\\t\\t\\ttype: \\\"message\\\",\\n965 | \\t\\t\\t\\t\\t\\ttokensCovered: 260,\\n966 | \\t\\t\\t\\t\\t},\\n----\\n969 | \\t\\t\\t\\t// In the documentation, the algorithm decides to replace the cache point at index 8\\n970 | \\t\\t\\t\\t// with a new one at index 10, and the tokensCovered value should be 660 tokens\\n971 | \\t\\t\\t\\t// (260 tokens from messages 7-8 plus 400 tokens from the new messages)\\n972 | \\n973 | \\t\\t\\t\\t// However, the actual implementation may behave differently depending on how\\n974 | \\t\\t\\t\\t// it calculates token counts and makes decisions about cache point placement\\n975 | \\n976 | \\t\\t\\t\\t// The important part is that our fix ensures that when a cache point is created,\\n977 | \\t\\t\\t\\t// the tokensCovered value represents all tokens from the previous cache point\\n978 | \\t\\t\\t\\t// to the current cache point, not just the tokens in the new messages\\n979 | \\t\\t\\t})\\n980 | \\n981 | \\t\\t\\tit(\\\"should not combine cache points when new messages have fewer tokens than the smallest combined gap\\\", () => {\\n982 | \\t\\t\\t\\t// This test verifies that when new messages have fewer tokens than the smallest combined gap,\\n983 | \\t\\t\\t\\t// the algorithm keeps all existing cache points and doesn't add a new one\\n----\\n1004 | \\t\\t\\t\\t\\t\\t),\\n1005 | \\t\\t\\t\\t\\t\\t// Small addition (only 50 tokens total)\\n1006 | \\t\\t\\t\\t\\t\\tcreateMessage(\\\"user\\\", \\\"Thanks for the explanation.\\\", 20),\\n----\\n1009 | \\n1010 | \\t\\t\\t\\t\\t// Previous cache point placements with significant token coverage\\n1011 | \\t\\t\\t\\t\\tconst previousCachePointPlacements: CachePointPlacement[] = [\\n----\\n1014 | \\t\\t\\t\\t\\t\\t\\ttype: \\\"message\\\",\\n1015 | \\t\\t\\t\\t\\t\\t\\ttokensCovered: 400, // Significant token coverage\\n1016 | \\t\\t\\t\\t\\t\\t},\\n----\\n1019 | \\t\\t\\t\\t\\t\\t\\ttype: \\\"message\\\",\\n1020 | \\t\\t\\t\\t\\t\\t\\ttokensCovered: 300, // Significant token coverage\\n1021 | \\t\\t\\t\\t\\t\\t},\\n----\\n1024 | \\t\\t\\t\\t\\t\\t\\ttype: \\\"message\\\",\\n1025 | \\t\\t\\t\\t\\t\\t\\ttokensCovered: 300, // Significant token coverage\\n1026 | \\t\\t\\t\\t\\t\\t},\\n----\\n1030 | \\t\\t\\t\\t\\t\\tmodelInfo: multiPointModelInfo,\\n1031 | \\t\\t\\t\\t\\t\\tsystemPrompt: \\\"You are a helpful assistant.\\\", // ~10 tokens\\n1032 | \\t\\t\\t\\t\\t\\tmessages,\\n----\\n1057 | \\n1058 | \\t\\t\\tit(\\\"should make correct decisions based on token counts\\\", () => {\\n1059 | \\t\\t\\t\\t// This test verifies that the algorithm correctly compares token counts\\n1060 | \\t\\t\\t\\t// and makes the right decision about combining cache points\\n1061 | \\n1062 | \\t\\t\\t\\t// Create messages with a variety of token counts\\n1063 | \\t\\t\\t\\tconst messages = [\\n----\\n1081 | \\t\\t\\t\\t\\t\\ttype: \\\"message\\\",\\n1082 | \\t\\t\\t\\t\\t\\ttokensCovered: 400,\\n1083 | \\t\\t\\t\\t\\t},\\n----\\n1086 | \\t\\t\\t\\t\\t\\ttype: \\\"message\\\",\\n1087 | \\t\\t\\t\\t\\t\\ttokensCovered: 150,\\n1088 | \\t\\t\\t\\t\\t},\\n----\\n1091 | \\t\\t\\t\\t\\t\\ttype: \\\"message\\\",\\n1092 | \\t\\t\\t\\t\\t\\ttokensCovered: 150,\\n1093 | \\t\\t\\t\\t\\t},\\n----\\n\\n# src/api/providers/__tests__/openai-usage-tracking.spec.ts\\n 28 | \\t\\t\\t\\t\\t\\t\\t\\tusage: {\\n 29 | \\t\\t\\t\\t\\t\\t\\t\\t\\tprompt_tokens: 10,\\n 30 | \\t\\t\\t\\t\\t\\t\\t\\t\\tcompletion_tokens: 5,\\n 31 | \\t\\t\\t\\t\\t\\t\\t\\t\\ttotal_tokens: 15,\\n 32 | \\t\\t\\t\\t\\t\\t\\t\\t},\\n----\\n 47 | \\t\\t\\t\\t\\t\\t\\t\\t\\tusage: {\\n 48 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\tprompt_tokens: 10,\\n 49 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\tcompletion_tokens: 2,\\n 50 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\ttotal_tokens: 12,\\n 51 | \\t\\t\\t\\t\\t\\t\\t\\t\\t},\\n----\\n 62 | \\t\\t\\t\\t\\t\\t\\t\\t\\tusage: {\\n 63 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\tprompt_tokens: 10,\\n 64 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\tcompletion_tokens: 4,\\n 65 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\ttotal_tokens: 14,\\n 66 | \\t\\t\\t\\t\\t\\t\\t\\t\\t},\\n----\\n 77 | \\t\\t\\t\\t\\t\\t\\t\\t\\tusage: {\\n 78 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\tprompt_tokens: 10,\\n 79 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\tcompletion_tokens: 5,\\n 80 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\ttotal_tokens: 15,\\n 81 | \\t\\t\\t\\t\\t\\t\\t\\t\\t},\\n----\\n155 | \\t\\t\\t\\t\\t\\tchoices: [{ message: { role: \\\"assistant\\\", content: \\\"Test response\\\" } }],\\n156 | \\t\\t\\t\\t\\t\\tusage: { prompt_tokens: 10, completion_tokens: 5, total_tokens: 15 },\\n157 | \\t\\t\\t\\t\\t}\\n----\\n177 | \\t\\t\\t\\t\\t\\t\\tusage: {\\n178 | \\t\\t\\t\\t\\t\\t\\t\\tprompt_tokens: 10,\\n179 | \\t\\t\\t\\t\\t\\t\\t\\tcompletion_tokens: 5,\\n180 | \\t\\t\\t\\t\\t\\t\\t\\ttotal_tokens: 15,\\n181 | \\t\\t\\t\\t\\t\\t\\t},\\n----\\n\\n# src/api/providers/__tests__/unbound.spec.ts\\n 82 | \\t\\t\\t\\t\\t\\t\\t\\t\\tusage: {\\n 83 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\tprompt_tokens: 10,\\n 84 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\tcompletion_tokens: 5,\\n 85 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\ttotal_tokens: 15,\\n 86 | \\t\\t\\t\\t\\t\\t\\t\\t\\t},\\n----\\n 91 | \\t\\t\\t\\t\\t\\t\\t\\t\\tusage: {\\n 92 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\tprompt_tokens: 8,\\n 93 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\tcompletion_tokens: 4,\\n 94 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\ttotal_tokens: 12,\\n 95 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\tcache_creation_input_tokens: 3,\\n 96 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\tcache_read_input_tokens: 2,\\n 97 | \\t\\t\\t\\t\\t\\t\\t\\t\\t},\\n----\\n232 | \\t\\t\\t\\t\\ttemperature: 0,\\n233 | \\t\\t\\t\\t\\tmax_tokens: 8192,\\n234 | \\t\\t\\t\\t}),\\n----\\n253 | \\n254 | \\t\\tit(\\\"should not set max_tokens for non-Anthropic models\\\", async () => {\\n255 | \\t\\t\\tmockCreate.mockClear()\\n----\\n277 | \\n278 | \\t\\t\\texpect(mockCreate.mock.calls[0][0]).not.toHaveProperty(\\\"max_tokens\\\")\\n279 | \\t\\t})\\n----\\n\\n# src/api/providers/__tests__/anthropic-vertex.spec.ts\\n 21 | \\t\\t\\t\\t\\t\\tusage: {\\n 22 | \\t\\t\\t\\t\\t\\t\\tinput_tokens: 10,\\n 23 | \\t\\t\\t\\t\\t\\t\\toutput_tokens: 5,\\n 24 | \\t\\t\\t\\t\\t\\t},\\n----\\n 32 | \\t\\t\\t\\t\\t\\t\\t\\tusage: {\\n 33 | \\t\\t\\t\\t\\t\\t\\t\\t\\tinput_tokens: 10,\\n 34 | \\t\\t\\t\\t\\t\\t\\t\\t\\toutput_tokens: 5,\\n 35 | \\t\\t\\t\\t\\t\\t\\t\\t},\\n----\\n 95 | \\t\\t\\t\\t\\t\\tusage: {\\n 96 | \\t\\t\\t\\t\\t\\t\\tinput_tokens: 10,\\n 97 | \\t\\t\\t\\t\\t\\t\\toutput_tokens: 0,\\n 98 | \\t\\t\\t\\t\\t\\t},\\n----\\n118 | \\t\\t\\t\\t\\tusage: {\\n119 | \\t\\t\\t\\t\\t\\toutput_tokens: 5,\\n120 | \\t\\t\\t\\t\\t},\\n----\\n164 | \\t\\t\\t\\tmodel: \\\"claude-3-5-sonnet-v2@20241022\\\",\\n165 | \\t\\t\\t\\tmax_tokens: 8192,\\n166 | \\t\\t\\t\\ttemperature: 0,\\n----\\n284 | \\t\\t\\t\\t\\t\\tusage: {\\n285 | \\t\\t\\t\\t\\t\\t\\tinput_tokens: 10,\\n286 | \\t\\t\\t\\t\\t\\t\\toutput_tokens: 0,\\n287 | \\t\\t\\t\\t\\t\\t\\tcache_creation_input_tokens: 3,\\n288 | \\t\\t\\t\\t\\t\\t\\tcache_read_input_tokens: 2,\\n289 | \\t\\t\\t\\t\\t\\t},\\n----\\n309 | \\t\\t\\t\\t\\tusage: {\\n310 | \\t\\t\\t\\t\\t\\toutput_tokens: 5,\\n311 | \\t\\t\\t\\t\\t},\\n----\\n419 | \\t\\t\\t\\t\\t\\tusage: {\\n420 | \\t\\t\\t\\t\\t\\t\\tinput_tokens: 10,\\n421 | \\t\\t\\t\\t\\t\\t\\toutput_tokens: 0,\\n422 | \\t\\t\\t\\t\\t\\t\\tcache_creation_input_tokens: 5,\\n423 | \\t\\t\\t\\t\\t\\t\\tcache_read_input_tokens: 3,\\n424 | \\t\\t\\t\\t\\t\\t},\\n----\\n484 | \\t\\t\\t\\t\\t\\tusage: {\\n485 | \\t\\t\\t\\t\\t\\t\\tinput_tokens: 10,\\n486 | \\t\\t\\t\\t\\t\\t\\toutput_tokens: 0,\\n487 | \\t\\t\\t\\t\\t\\t},\\n----\\n618 | \\t\\t\\t\\tmodel: \\\"claude-3-5-sonnet-v2@20241022\\\",\\n619 | \\t\\t\\t\\tmax_tokens: 8192,\\n620 | \\t\\t\\t\\ttemperature: 0,\\n----\\n752 | \\n753 | \\t\\t\\t// Test with default thinking budget (80% of max tokens)\\n754 | \\t\\t\\tconst handlerWithDefaultBudget = new AnthropicVertexHandler({\\n----\\n767 | \\t\\t\\t\\tvertexRegion: \\\"us-central1\\\",\\n768 | \\t\\t\\t\\tmodelMaxTokens: 1000, // This would result in 800 tokens for thinking, but minimum is 1024\\n769 | \\t\\t\\t})\\n----\\n789 | \\t\\t\\t\\t\\t\\tmodel: options.model,\\n790 | \\t\\t\\t\\t\\t\\tusage: { input_tokens: 10, output_tokens: 5 },\\n791 | \\t\\t\\t\\t\\t}\\n----\\n794 | \\t\\t\\t\\t\\tasync *[Symbol.asyncIterator]() {\\n795 | \\t\\t\\t\\t\\t\\tyield { type: \\\"message_start\\\", message: { usage: { input_tokens: 10, output_tokens: 5 } } }\\n796 | \\t\\t\\t\\t\\t},\\n----\\n806 | \\t\\t\\t\\texpect.objectContaining({\\n807 | \\t\\t\\t\\t\\tthinking: { type: \\\"enabled\\\", budget_tokens: 4096 },\\n808 | \\t\\t\\t\\t\\ttemperature: 1.0, // Thinking requires temperature 1.0\\n----\\n\\n# src/core/prompts/tools/codebase-search.ts\\n 12 | \\n 13 | Example: Searching for functions related to user authentication\\n 14 | \\n----\\n\\n# src/core/task/__tests__/Task.test.ts\\n185 | \\t\\t\\t\\t\\t\\t\\t\\ttask: \\\"historical task\\\",\\n186 | \\t\\t\\t\\t\\t\\t\\t\\ttokensIn: 100,\\n187 | \\t\\t\\t\\t\\t\\t\\t\\ttokensOut: 200,\\n188 | \\t\\t\\t\\t\\t\\t\\t\\tcacheWrites: 0,\\n----\\n253 | \\t\\t\\t\\ttask: \\\"historical task\\\",\\n254 | \\t\\t\\t\\ttokensIn: 100,\\n255 | \\t\\t\\t\\ttokensOut: 200,\\n256 | \\t\\t\\t\\tcacheWrites: 0,\\n----\\n615 | \\t\\t\\t\\t\\t\\ttext: JSON.stringify({\\n616 | \\t\\t\\t\\t\\t\\t\\ttokensIn: 100,\\n617 | \\t\\t\\t\\t\\t\\t\\ttokensOut: 50,\\n618 | \\t\\t\\t\\t\\t\\t\\tcacheWrites: 0,\\n----\\n740 | \\t\\t\\t\\t\\t\\ttext: JSON.stringify({\\n741 | \\t\\t\\t\\t\\t\\t\\ttokensIn: 100,\\n742 | \\t\\t\\t\\t\\t\\t\\ttokensOut: 50,\\n743 | \\t\\t\\t\\t\\t\\t\\tcacheWrites: 0,\\n----\\n\\n# src/core/prompts/instructions/create-mcp-server.ts\\n 11 | \\n 12 | When creating MCP servers, it's important to understand that they operate in a non-interactive environment. The server cannot initiate OAuth flows, open browser windows, or prompt for user input during runtime. All credentials and authentication tokens must be provided upfront through environment variables in the MCP settings configuration. For example, Spotify's API uses OAuth to get a refresh token for the user, but the MCP server cannot initiate this flow. While you can walk the user through [truncated...]\\n 13 | \\n----\\n 76 | \\t\\t\\t\\t...\\n 77 | \\t\\t\\t\\t\\\"type\\\": \\\"module\\\", // added by default, uses ES module syntax (import/export) rather than CommonJS (require/module.exports) (Important to know if you create additional scripts in this server repository like a get-refresh-token.js script)\\n 78 | \\t\\t\\t\\t\\\"scripts\\\": {\\n----\\n\\n# src/core/task/Task.ts\\n 24 | import { TelemetryService } from \\\"@roo-code/telemetry\\\"\\n 25 | import { CloudService } from \\\"@roo-code/cloud\\\"\\n 26 | \\n----\\n 96 | \\ttaskSpawned: [taskId: string]\\n 97 | \\ttaskCompleted: [taskId: string, tokenUsage: TokenUsage, toolUsage: ToolUsage]\\n 98 | \\ttaskTokenUsageUpdated: [taskId: string, tokenUsage: TokenUsage]\\n 99 | \\ttaskToolFailed: [taskId: string, tool: ToolName, error: string]\\n----\\n346 | \\n347 | \\t\\tconst shouldCaptureMessage = message.partial !== true && CloudService.isEnabled()\\n348 | \\n349 | \\t\\tif (shouldCaptureMessage) {\\n350 | \\t\\t\\tCloudService.instance.captureEvent({\\n351 | \\t\\t\\t\\tevent: TelemetryEventName.TASK_MESSAGE,\\n----\\n366 | \\n367 | \\t\\tconst shouldCaptureMessage = message.partial !== true && CloudService.isEnabled()\\n368 | \\n369 | \\t\\tif (shouldCaptureMessage) {\\n370 | \\t\\t\\tCloudService.instance.captureEvent({\\n371 | \\t\\t\\t\\tevent: TelemetryEventName.TASK_MESSAGE,\\n----\\n384 | \\n385 | \\t\\t\\tconst { historyItem, tokenUsage } = await taskMetadata({\\n386 | \\t\\t\\t\\tmessages: this.clineMessages,\\n----\\n392 | \\n393 | \\t\\t\\tthis.emit(\\\"taskTokenUsageUpdated\\\", this.taskId, tokenUsage)\\n394 | \\n----\\n1246 | \\t\\t\\t\\t\\t...JSON.parse(this.clineMessages[lastApiReqIndex].text || \\\"{}\\\"),\\n1247 | \\t\\t\\t\\t\\ttokensIn: inputTokens,\\n1248 | \\t\\t\\t\\t\\ttokensOut: outputTokens,\\n1249 | \\t\\t\\t\\t\\tcacheWrites: cacheWriteTokens,\\n----\\n1678 | \\t\\tif (contextTokens) {\\n1679 | \\t\\t\\t// Default max tokens value for thinking models when no specific\\n1680 | \\t\\t\\t// value is set.\\n----\\n\\n# src/api/providers/glama.ts\\n 51 | \\n 52 | \\t\\t// Required by Anthropic; other providers default to max tokens allowed.\\n 53 | \\t\\tlet maxTokens: number | undefined\\n----\\n 60 | \\t\\t\\tmodel: modelId,\\n 61 | \\t\\t\\tmax_tokens: maxTokens,\\n 62 | \\t\\t\\tmessages: openAiMessages,\\n----\\n 90 | \\t\\t\\t\\t// In case of an interrupted request, we need to wait for the upstream API to finish processing the request\\n 91 | \\t\\t\\t\\t// before we can fetch information about the token usage and cost.\\n 92 | \\t\\t\\t\\tconst response = await axios.get(\\n----\\n 98 | \\n 99 | \\t\\t\\t\\tif (completionRequest.tokenUsage && completionRequest.totalCostUsd) {\\n100 | \\t\\t\\t\\t\\tyield {\\n101 | \\t\\t\\t\\t\\t\\ttype: \\\"usage\\\",\\n102 | \\t\\t\\t\\t\\t\\tcacheWriteTokens: completionRequest.tokenUsage.cacheCreationInputTokens,\\n103 | \\t\\t\\t\\t\\t\\tcacheReadTokens: completionRequest.tokenUsage.cacheReadInputTokens,\\n104 | \\t\\t\\t\\t\\t\\tinputTokens: completionRequest.tokenUsage.promptTokens,\\n105 | \\t\\t\\t\\t\\t\\toutputTokens: completionRequest.tokenUsage.completionTokens,\\n106 | \\t\\t\\t\\t\\t\\ttotalCost: parseFloat(completionRequest.totalCostUsd),\\n----\\n132 | \\t\\t\\tif (modelId.startsWith(\\\"anthropic/\\\")) {\\n133 | \\t\\t\\t\\trequestOptions.max_tokens = info.maxTokens\\n134 | \\t\\t\\t}\\n----\\n\\n# src/api/transform/reasoning.ts\\n 12 | \\teffort?: ReasoningEffort\\n 13 | \\tmax_tokens?: number\\n 14 | \\texclude?: boolean\\n----\\n 36 | \\tshouldUseReasoningBudget({ model, settings })\\n 37 | \\t\\t? { max_tokens: reasoningBudget }\\n 38 | \\t\\t: shouldUseReasoningEffort({ model, settings })\\n----\\n 46 | }: GetModelReasoningOptions): AnthropicReasoningParams | undefined =>\\n 47 | \\tshouldUseReasoningBudget({ model, settings }) ? { type: \\\"enabled\\\", budget_tokens: reasoningBudget! } : undefined\\n 48 | \\n----\\n\\n# src/api/providers/openrouter.ts\\n 32 | \\tinclude_reasoning?: boolean\\n 33 | \\t// https://openrouter.ai/docs/use-cases/reasoning-tokens\\n 34 | \\treasoning?: OpenRouterReasoningParams\\n----\\n 40 | interface CompletionUsage {\\n 41 | \\tcompletion_tokens?: number\\n 42 | \\tcompletion_tokens_details?: {\\n 43 | \\t\\treasoning_tokens?: number\\n 44 | \\t}\\n 45 | \\tprompt_tokens?: number\\n 46 | \\tprompt_tokens_details?: {\\n 47 | \\t\\tcached_tokens?: number\\n 48 | \\t}\\n 49 | \\ttotal_tokens?: number\\n 50 | \\tcost?: number\\n----\\n 81 | \\n 82 | \\t\\t// OpenRouter sends reasoning tokens by default for Gemini 2.5 Pro\\n 83 | \\t\\t// Preview even if you don't request them. This is not the default for\\n----\\n116 | \\t\\t\\tmodel: modelId,\\n117 | \\t\\t\\t...(maxTokens && maxTokens > 0 && { max_tokens: maxTokens }),\\n118 | \\t\\t\\ttemperature,\\n----\\n165 | \\t\\t\\t\\ttype: \\\"usage\\\",\\n166 | \\t\\t\\t\\tinputTokens: lastUsage.prompt_tokens || 0,\\n167 | \\t\\t\\t\\toutputTokens: lastUsage.completion_tokens || 0,\\n168 | \\t\\t\\t\\t// Waiting on OpenRouter to figure out what this represents in the Gemini case\\n169 | \\t\\t\\t\\t// and how to best support it.\\n170 | \\t\\t\\t\\t// cacheReadTokens: lastUsage.prompt_tokens_details?.cached_tokens,\\n171 | \\t\\t\\t\\treasoningTokens: lastUsage.completion_tokens_details?.reasoning_tokens,\\n172 | \\t\\t\\t\\ttotalCost: (lastUsage.is_byok ? BYOK_COST_MULTIPLIER : 1) * (lastUsage.cost || 0),\\n----\\n219 | \\t\\t\\tmodel: modelId,\\n220 | \\t\\t\\tmax_tokens: maxTokens,\\n221 | \\t\\t\\ttemperature,\\n----\\n\\n# src/utils/__tests__/cost.spec.ts\\n 13 | \\t\\t\\tsupportsPromptCache: true,\\n 14 | \\t\\t\\tinputPrice: 3.0, // $3 per million tokens\\n 15 | \\t\\t\\toutputPrice: 15.0, // $15 per million tokens\\n 16 | \\t\\t\\tcacheWritesPrice: 3.75, // $3.75 per million tokens\\n 17 | \\t\\t\\tcacheReadsPrice: 0.3, // $0.30 per million tokens\\n 18 | \\t\\t}\\n----\\n 70 | \\n 71 | \\t\\tit(\\\"should handle zero tokens\\\", () => {\\n 72 | \\t\\t\\tconst cost = calculateApiCostAnthropic(mockModelInfo, 0, 0, 0, 0)\\n----\\n106 | \\t\\t\\tsupportsPromptCache: true,\\n107 | \\t\\t\\tinputPrice: 3.0, // $3 per million tokens\\n108 | \\t\\t\\toutputPrice: 15.0, // $15 per million tokens\\n109 | \\t\\t\\tcacheWritesPrice: 3.75, // $3.75 per million tokens\\n110 | \\t\\t\\tcacheReadsPrice: 0.3, // $0.30 per million tokens\\n111 | \\t\\t}\\n----\\n163 | \\n164 | \\t\\tit(\\\"should handle zero tokens\\\", () => {\\n165 | \\t\\t\\tconst cost = calculateApiCostOpenAI(mockModelInfo, 0, 0, 0, 0)\\n----\\n\\n# src/utils/__tests__/git.spec.ts\\n 98 | \\t\\t\\t\\tsubject: \\\"fix: test commit\\\",\\n 99 | \\t\\t\\t\\tauthor: \\\"John Doe\\\",\\n100 | \\t\\t\\t\\tdate: \\\"2024-01-06\\\",\\n----\\n162 | \\t\\t\\t\\t[\\n163 | \\t\\t\\t\\t\\t'git log -n 10 --format=\\\"%H%n%h%n%s%n%an%n%ad\\\" --date=short --author-date-order abc123',\\n164 | \\t\\t\\t\\t\\t{ stdout: mockCommitData, stderr: \\\"\\\" },\\n----\\n185 | \\t\\t\\t\\tsubject: \\\"fix: test commit\\\",\\n186 | \\t\\t\\t\\tauthor: \\\"John Doe\\\",\\n187 | \\t\\t\\t\\tdate: \\\"2024-01-06\\\",\\n----\\n\\n# src/api/providers/gemini.ts\\n 7 | } from \\\"@google/genai\\\"\\n 8 | import type { JWTInput } from \\\"google-auth-library\\\"\\n 9 | \\n----\\n179 | \\t\\t\\tif (response.totalTokens === undefined) {\\n180 | \\t\\t\\t\\tconsole.warn(\\\"Gemini token counting returned undefined, using fallback\\\")\\n181 | \\t\\t\\t\\treturn super.countTokens(content)\\n----\\n185 | \\t\\t} catch (error) {\\n186 | \\t\\t\\tconsole.warn(\\\"Gemini token counting failed, using fallback\\\", error)\\n187 | \\t\\t\\treturn super.countTokens(content)\\n----\\n209 | \\n210 | \\t\\t// If there's tiered pricing then adjust the input and output token prices\\n211 | \\t\\t// based on the input tokens used.\\n212 | \\t\\tif (info.tiers) {\\n----\\n221 | \\n222 | \\t\\t// Subtract the cached input tokens from the total input tokens.\\n223 | \\t\\tconst uncachedInputTokens = inputTokens - cacheReadTokens\\n----\\n230 | \\n231 | \\t\\tconst trace: Record = {\\n232 | \\t\\t\\tinput: { price: inputPrice, tokens: uncachedInputTokens, cost: inputTokensCost },\\n233 | \\t\\t\\toutput: { price: outputPrice, tokens: outputTokens, cost: outputTokensCost },\\n234 | \\t\\t}\\n----\\n236 | \\t\\tif (cacheReadTokens > 0) {\\n237 | \\t\\t\\ttrace.cacheRead = { price: cacheReadsPrice, tokens: cacheReadTokens, cost: cacheReadCost }\\n238 | \\t\\t}\\n----\\n\\n# src/api/providers/lite-llm.ts\\n 46 | \\n 47 | \\t\\t// Required by some providers; others default to max tokens allowed\\n 48 | \\t\\tlet maxTokens: number | undefined = info.maxTokens ?? undefined\\n----\\n 51 | \\t\\t\\tmodel: modelId,\\n 52 | \\t\\t\\tmax_tokens: maxTokens,\\n 53 | \\t\\t\\tmessages: openAiMessages,\\n----\\n 84 | \\t\\t\\t\\t\\ttype: \\\"usage\\\",\\n 85 | \\t\\t\\t\\t\\tinputTokens: lastUsage.prompt_tokens || 0,\\n 86 | \\t\\t\\t\\t\\toutputTokens: lastUsage.completion_tokens || 0,\\n 87 | \\t\\t\\t\\t\\tcacheWriteTokens: lastUsage.cache_creation_input_tokens || 0,\\n 88 | \\t\\t\\t\\t\\tcacheReadTokens: lastUsage.prompt_tokens_details?.cached_tokens || 0,\\n 89 | \\t\\t\\t\\t}\\n----\\n121 | \\n122 | \\t\\t\\trequestOptions.max_tokens = info.maxTokens\\n123 | \\n----\\n136 | interface LiteLLMUsage extends OpenAI.CompletionUsage {\\n137 | \\tcache_creation_input_tokens?: number\\n138 | }\\n----\\n\\n# src/api/providers/requesty.ts\\n 19 | // Requesty usage includes an extra field for Anthropic use cases.\\n 20 | // Safely cast the prompt token details section to the appropriate structure.\\n 21 | interface RequestyUsage extends OpenAI.CompletionUsage {\\n 22 | \\tprompt_tokens_details?: {\\n 23 | \\t\\tcaching_tokens?: number\\n 24 | \\t\\tcached_tokens?: number\\n 25 | \\t}\\n----\\n 76 | \\t\\tconst requestyUsage = usage as RequestyUsage\\n 77 | \\t\\tconst inputTokens = requestyUsage?.prompt_tokens || 0\\n 78 | \\t\\tconst outputTokens = requestyUsage?.completion_tokens || 0\\n 79 | \\t\\tconst cacheWriteTokens = requestyUsage?.prompt_tokens_details?.caching_tokens || 0\\n 80 | \\t\\tconst cacheReadTokens = requestyUsage?.prompt_tokens_details?.cached_tokens || 0\\n 81 | \\t\\tconst totalCost = modelInfo\\n----\\n102 | \\t\\t\\tinfo,\\n103 | \\t\\t\\tmaxTokens: max_tokens,\\n104 | \\t\\t\\ttemperature,\\n----\\n116 | \\t\\t\\tmodel,\\n117 | \\t\\t\\tmax_tokens,\\n118 | \\t\\t\\ttemperature,\\n----\\n150 | \\tasync completePrompt(prompt: string): Promise {\\n151 | \\t\\tconst { id: model, maxTokens: max_tokens, temperature } = await this.fetchModel()\\n152 | \\n----\\n156 | \\t\\t\\tmodel,\\n157 | \\t\\t\\tmax_tokens,\\n158 | \\t\\t\\tmessages: openAiMessages,\\n----\\n\\n# src/api/providers/anthropic.ts\\n 29 | \\t\\tconst apiKeyFieldName =\\n 30 | \\t\\t\\tthis.options.anthropicBaseUrl && this.options.anthropicUseAuthToken ? \\\"authToken\\\" : \\\"apiKey\\\"\\n 31 | \\n----\\n 75 | \\t\\t\\t\\t\\t\\tmodel: modelId,\\n 76 | \\t\\t\\t\\t\\t\\tmax_tokens: maxTokens ?? ANTHROPIC_DEFAULT_MAX_TOKENS,\\n 77 | \\t\\t\\t\\t\\t\\ttemperature,\\n----\\n124 | \\t\\t\\t\\t\\tmodel: modelId,\\n125 | \\t\\t\\t\\t\\tmax_tokens: maxTokens ?? ANTHROPIC_DEFAULT_MAX_TOKENS,\\n126 | \\t\\t\\t\\t\\ttemperature,\\n----\\n142 | \\t\\t\\t\\t\\t\\ttype: \\\"usage\\\",\\n143 | \\t\\t\\t\\t\\t\\tinputTokens: usage.input_tokens || 0,\\n144 | \\t\\t\\t\\t\\t\\toutputTokens: usage.output_tokens || 0,\\n145 | \\t\\t\\t\\t\\t\\tcacheWriteTokens: usage.cache_creation_input_tokens || undefined,\\n146 | \\t\\t\\t\\t\\t\\tcacheReadTokens: usage.cache_read_input_tokens || undefined,\\n147 | \\t\\t\\t\\t\\t}\\n----\\n151 | \\t\\t\\t\\tcase \\\"message_delta\\\":\\n152 | \\t\\t\\t\\t\\t// Tells us stop_reason, stop_sequence, and output tokens\\n153 | \\t\\t\\t\\t\\t// along the way and at the end of the message.\\n----\\n156 | \\t\\t\\t\\t\\t\\tinputTokens: 0,\\n157 | \\t\\t\\t\\t\\t\\toutputTokens: chunk.usage.output_tokens || 0,\\n158 | \\t\\t\\t\\t\\t}\\n----\\n231 | \\t\\t\\tmodel,\\n232 | \\t\\t\\tmax_tokens: ANTHROPIC_DEFAULT_MAX_TOKENS,\\n233 | \\t\\t\\tthinking: undefined,\\n----\\n243 | \\t/**\\n244 | \\t * Counts tokens for the given content using Anthropic's API\\n245 | \\t *\\n246 | \\t * @param content The content blocks to count tokens for\\n247 | \\t * @returns A promise resolving to the token count\\n248 | \\t */\\n----\\n258 | \\n259 | \\t\\t\\treturn response.input_tokens\\n260 | \\t\\t} catch (error) {\\n261 | \\t\\t\\t// Log error but fallback to tiktoken estimation\\n262 | \\t\\t\\tconsole.warn(\\\"Anthropic token counting failed, using fallback\\\", error)\\n263 | \\n----\\n\\n# src/api/providers/deepseek.ts\\n 32 | \\t\\t\\ttype: \\\"usage\\\",\\n 33 | \\t\\t\\tinputTokens: usage?.prompt_tokens || 0,\\n 34 | \\t\\t\\toutputTokens: usage?.completion_tokens || 0,\\n 35 | \\t\\t\\tcacheWriteTokens: usage?.prompt_tokens_details?.cache_miss_tokens,\\n 36 | \\t\\t\\tcacheReadTokens: usage?.prompt_tokens_details?.cached_tokens,\\n 37 | \\t\\t}\\n----\\n\\n# src/api/providers/openai.ts\\n162 | \\t\\t\\tif (this.options.includeMaxTokens) {\\n163 | \\t\\t\\t\\trequestOptions.max_tokens = modelInfo.maxTokens\\n164 | \\t\\t\\t}\\n----\\n241 | \\t\\t\\ttype: \\\"usage\\\",\\n242 | \\t\\t\\tinputTokens: usage?.prompt_tokens || 0,\\n243 | \\t\\t\\toutputTokens: usage?.completion_tokens || 0,\\n244 | \\t\\t\\tcacheWriteTokens: usage?.cache_creation_input_tokens || undefined,\\n245 | \\t\\t\\tcacheReadTokens: usage?.cache_read_input_tokens || undefined,\\n246 | \\t\\t}\\n----\\n347 | \\t\\t\\t\\t\\ttype: \\\"usage\\\",\\n348 | \\t\\t\\t\\t\\tinputTokens: chunk.usage.prompt_tokens || 0,\\n349 | \\t\\t\\t\\t\\toutputTokens: chunk.usage.completion_tokens || 0,\\n350 | \\t\\t\\t\\t}\\n----\\n\\n# src/api/providers/xai.ts\\n 52 | \\t\\t\\tmodel: modelId,\\n 53 | \\t\\t\\tmax_tokens: modelInfo.maxTokens,\\n 54 | \\t\\t\\ttemperature: this.options.modelTemperature ?? XAI_DEFAULT_TEMPERATURE,\\n----\\n 78 | \\t\\t\\tif (chunk.usage) {\\n 79 | \\t\\t\\t\\t// Extract detailed token information if available\\n 80 | \\t\\t\\t\\t// First check for prompt_tokens_details structure (real API response)\\n 81 | \\t\\t\\t\\tconst promptDetails = \\\"prompt_tokens_details\\\" in chunk.usage ? chunk.usage.prompt_tokens_details : null;\\n 82 | \\t\\t\\t\\tconst cachedTokens = promptDetails && \\\"cached_tokens\\\" in promptDetails ? promptDetails.cached_tokens : 0;\\n 83 | \\n 84 | \\t\\t\\t\\t// Fall back to direct fields in usage (used in test mocks)\\n 85 | \\t\\t\\t\\tconst readTokens = cachedTokens || (\\\"cache_read_input_tokens\\\" in chunk.usage ? (chunk.usage as any).cache_read_input_tokens : 0);\\n 86 | \\t\\t\\t\\tconst writeTokens = \\\"cache_creation_input_tokens\\\" in chunk.usage ? (chunk.usage as any).cache_creation_input_tokens : 0;\\n 87 | \\n----\\n 89 | \\t\\t\\t\\t\\ttype: \\\"usage\\\",\\n 90 | \\t\\t\\t\\t\\tinputTokens: chunk.usage.prompt_tokens || 0,\\n 91 | \\t\\t\\t\\t\\toutputTokens: chunk.usage.completion_tokens || 0,\\n 92 | \\t\\t\\t\\t\\tcacheReadTokens: readTokens,\\n----\"}", - "partial": false -} -[2025-06-13T05:00:02.337Z] { - "ts": 1749790802303, - "type": "say", - "say": "api_req_started", - "text": "{\"request\":\"[search_files for 'CloudService|refresh|token|auth' in '*.ts'] Result:\\n\\nShowing first 300 of 300+ results. Use a more specific search if necessary.\\n\\n# apps/web-evals/src/lib/formatters.ts\\n 30 | \\n 31 | export const formatTokens = (tokens: number) => {\\n 32 | \\tif (tokens < 1000) {\\n 33 | \\t\\treturn tokens.toString()\\n 34 | \\t}\\n 35 | \\n 36 | \\tif (tokens < 1000000) {\\n 37 | \\t\\treturn `${(tokens / 1000).toFixed(1)}k`\\n 38 | \\t}\\n 39 | \\n 40 | \\tif (tokens < 1000000000) {\\n 41 | \\t\\treturn `${(tokens / 1000000).toFixed(1)}M`\\n 42 | \\t}\\n 43 | \\n 44 | \\treturn `${(tokens / 1000000000).toFixed(1)}B`\\n 45 | }\\n----\\n\\n# src/services/ripgrep/index.ts\\n101 | \\t\\tconst rgProcess = childProcess.spawn(bin, args)\\n102 | \\t\\t// cross-platform alternative to head, which is ripgrep author's recommendation for limiting output.\\n103 | \\t\\tconst rl = readline.createInterface({\\n----\\n\\n# apps/web-roo-code/src/lib/index.ts\\n 3 | export * from \\\"./format-score\\\"\\n 4 | export * from \\\"./format-tokens\\\"\\n----\\n\\n# apps/web-roo-code/src/lib/format-tokens.ts\\n 1 | export const formatTokens = (tokens: number, decimals = 0) => {\\n 2 | \\tif (tokens < 1000) {\\n 3 | \\t\\treturn tokens.toString()\\n 4 | \\t}\\n 5 | \\n 6 | \\tif (tokens < 1000000) {\\n 7 | \\t\\treturn `${(tokens / 1000).toFixed(decimals)}K`\\n 8 | \\t}\\n 9 | \\n 10 | \\tif (tokens < 1000000000) {\\n 11 | \\t\\treturn `${(tokens / 1000000).toFixed(decimals)}M`\\n 12 | \\t}\\n 13 | \\n 14 | \\treturn `${(tokens / 1000000000).toFixed(decimals)}B`\\n 15 | }\\n----\\n\\n# apps/web-roo-code/src/lib/hooks/use-open-router-models.ts\\n 19 | \\t\\t.object({\\n 20 | \\t\\t\\tmax_completion_tokens: z.number().nullish(),\\n 21 | \\t\\t})\\n----\\n 53 | \\t\\t\\tmodelInfo: {\\n 54 | \\t\\t\\t\\tmaxTokens: rawModel.top_provider?.max_completion_tokens ?? undefined,\\n 55 | \\t\\t\\t\\tcontextWindow: rawModel.context_length,\\n----\\n\\n# src/services/mcp/__tests__/McpHub.test.ts\\n 50 | \\t\\t\\tscheme: \\\"file\\\",\\n 51 | \\t\\t\\tauthority: \\\"\\\",\\n 52 | \\t\\t\\tpath: \\\"/test/path\\\",\\n----\\n\\n# src/services/mcp/McpHub.ts\\n1092 | \\n1093 | \\tpublic async refreshAllConnections(): Promise {\\n1094 | \\t\\tif (this.isConnecting) {\\n1095 | \\t\\t\\tvscode.window.showInformationMessage(t(\\\"mcp:info.already_refreshing\\\"))\\n1096 | \\t\\t\\treturn\\n----\\n1099 | \\t\\tthis.isConnecting = true\\n1100 | \\t\\tvscode.window.showInformationMessage(t(\\\"mcp:info.refreshing_all\\\"))\\n1101 | \\n----\\n1151 | \\n1152 | \\t\\t\\tvscode.window.showInformationMessage(t(\\\"mcp:info.all_refreshed\\\"))\\n1153 | \\t\\t} catch (error) {\\n1154 | \\t\\t\\tthis.showErrorMessage(\\\"Failed to refresh MCP servers\\\", error)\\n1155 | \\t\\t} finally {\\n----\\n1244 | \\n1245 | \\t\\t\\t\\t\\t// Only refresh capabilities if connected\\n1246 | \\t\\t\\t\\t\\tif (connection.server.status === \\\"connected\\\") {\\n----\\n1254 | \\t\\t\\t\\t} catch (error) {\\n1255 | \\t\\t\\t\\t\\tconsole.error(`Failed to refresh capabilities for ${serverName}:`, error)\\n1256 | \\t\\t\\t\\t}\\n----\\n\\n# src/services/code-index/embedders/openai.ts\\n 52 | \\t\\t\\t\\t\\tconsole.warn(\\n 53 | \\t\\t\\t\\t\\t\\t`Text at index ${i} exceeds maximum token limit (${itemTokens} > ${MAX_ITEM_TOKENS}). Skipping.`,\\n 54 | \\t\\t\\t\\t\\t)\\n----\\n108 | \\t\\t\\t\\t\\tusage: {\\n109 | \\t\\t\\t\\t\\t\\tpromptTokens: response.usage?.prompt_tokens || 0,\\n110 | \\t\\t\\t\\t\\t\\ttotalTokens: response.usage?.total_tokens || 0,\\n111 | \\t\\t\\t\\t\\t},\\n----\\n\\n# src/services/code-index/embedders/openai-compatible.ts\\n 18 | \\tusage?: {\\n 19 | \\t\\tprompt_tokens?: number\\n 20 | \\t\\ttotal_tokens?: number\\n 21 | \\t}\\n----\\n 34 | \\t * @param baseUrl The base URL for the OpenAI-compatible API endpoint\\n 35 | \\t * @param apiKey The API key for authentication\\n 36 | \\t * @param modelId Optional model identifier (defaults to \\\"text-embedding-3-small\\\")\\n----\\n 75 | \\t\\t\\t\\t\\tconsole.warn(\\n 76 | \\t\\t\\t\\t\\t\\t`Text at index ${i} exceeds maximum token limit (${itemTokens} > ${MAX_ITEM_TOKENS}). Skipping.`,\\n 77 | \\t\\t\\t\\t\\t)\\n----\\n156 | \\t\\t\\t\\t\\tusage: {\\n157 | \\t\\t\\t\\t\\t\\tpromptTokens: response.usage?.prompt_tokens || 0,\\n158 | \\t\\t\\t\\t\\t\\ttotalTokens: response.usage?.total_tokens || 0,\\n159 | \\t\\t\\t\\t\\t},\\n----\\n\\n# src/services/code-index/embedders/__tests__/openai-compatible.spec.ts\\n103 | \\t\\t\\t\\tdata: [{ embedding: [0.1, 0.2, 0.3] }],\\n104 | \\t\\t\\t\\tusage: { prompt_tokens: 10, total_tokens: 15 },\\n105 | \\t\\t\\t}\\n----\\n124 | \\t\\t\\t\\tdata: [{ embedding: [0.1, 0.2, 0.3] }, { embedding: [0.4, 0.5, 0.6] }],\\n125 | \\t\\t\\t\\tusage: { prompt_tokens: 20, total_tokens: 30 },\\n126 | \\t\\t\\t}\\n----\\n149 | \\t\\t\\t\\tdata: [{ embedding: [0.1, 0.2, 0.3] }],\\n150 | \\t\\t\\t\\tusage: { prompt_tokens: 10, total_tokens: 15 },\\n151 | \\t\\t\\t}\\n----\\n194 | \\t\\t\\t\\t\\tdata: [{ embedding: base64String }], // Base64 string instead of array\\n195 | \\t\\t\\t\\t\\tusage: { prompt_tokens: 10, total_tokens: 15 },\\n196 | \\t\\t\\t\\t}\\n----\\n226 | \\t\\t\\t\\t\\tdata: [{ embedding: base64String1 }, { embedding: base64String2 }],\\n227 | \\t\\t\\t\\t\\tusage: { prompt_tokens: 20, total_tokens: 30 },\\n228 | \\t\\t\\t\\t}\\n----\\n253 | \\t\\t\\t\\t\\t],\\n254 | \\t\\t\\t\\t\\tusage: { prompt_tokens: 20, total_tokens: 30 },\\n255 | \\t\\t\\t\\t}\\n----\\n270 | \\t\\t/**\\n271 | \\t\\t * Test batching logic when texts exceed token limits\\n272 | \\t\\t */\\n----\\n283 | \\t\\t\\t\\t\\t],\\n284 | \\t\\t\\t\\t\\tusage: { prompt_tokens: 10, total_tokens: 15 },\\n285 | \\t\\t\\t\\t})\\n----\\n299 | \\t\\t\\t\\t\\tdata: [{ embedding: [0.1, 0.2, 0.3] }, { embedding: [0.4, 0.5, 0.6] }],\\n300 | \\t\\t\\t\\t\\tusage: { prompt_tokens: 10, total_tokens: 15 },\\n301 | \\t\\t\\t\\t}\\n----\\n306 | \\t\\t\\t\\t// Should warn about oversized text\\n307 | \\t\\t\\t\\texpect(console.warn).toHaveBeenCalledWith(expect.stringContaining(\\\"exceeds maximum token limit\\\"))\\n308 | \\n----\\n317 | \\t\\t\\t\\t\\tdata: [{ embedding: [0.1, 0.2, 0.3] }, { embedding: [0.4, 0.5, 0.6] }],\\n318 | \\t\\t\\t\\t\\tusage: { prompt_tokens: 10, total_tokens: 15 },\\n319 | \\t\\t\\t\\t})\\n----\\n354 | \\t\\t\\t\\t\\t\\tdata: [{ embedding: base64String }],\\n355 | \\t\\t\\t\\t\\t\\tusage: { prompt_tokens: 10, total_tokens: 15 },\\n356 | \\t\\t\\t\\t\\t})\\n----\\n375 | \\t\\t\\t\\tconst testTexts = [\\\"Hello world\\\"]\\n376 | \\t\\t\\t\\tconst authError = new Error(\\\"Unauthorized\\\")\\n377 | \\t\\t\\t\\t;(authError as any).status = 401\\n378 | \\n379 | \\t\\t\\t\\tmockEmbeddingsCreate.mockRejectedValue(authError)\\n380 | \\n----\\n452 | \\t\\t\\t\\t\\tdata: null,\\n453 | \\t\\t\\t\\t\\tusage: { prompt_tokens: 10, total_tokens: 15 },\\n454 | \\t\\t\\t\\t}\\n----\\n494 | \\t\\t\\t\\t\\tusage: {\\n495 | \\t\\t\\t\\t\\t\\tprompt_tokens: 2,\\n496 | \\t\\t\\t\\t\\t\\ttotal_tokens: 2,\\n497 | \\t\\t\\t\\t\\t},\\n----\\n\\n# apps/web-evals/src/hooks/use-run-status.ts\\n 16 | \\ttasks: (Task & { taskMetrics: TaskMetrics | null })[] | undefined\\n 17 | \\ttokenUsage: Map\\n 18 | \\tusageUpdatedAt: number | undefined\\n----\\n 24 | \\n 25 | \\tconst tokenUsage = useRef>(new Map())\\n 26 | \\tconst startTimes = useRef>(new Map())\\n----\\n 79 | \\t\\t\\t\\tconst duration = startTime ? Date.now() - startTime : undefined\\n 80 | \\t\\t\\t\\ttokenUsage.current.set(taskId, { ...payload[1], duration })\\n 81 | \\t\\t\\t\\tsetUsageUpdatedAt(Date.now())\\n----\\n 97 | \\t\\ttasks,\\n 98 | \\t\\ttokenUsage: tokenUsage.current,\\n 99 | \\t\\tusageUpdatedAt,\\n----\\n\\n# src/activate/handleUri.ts\\n 2 | \\n 3 | import { CloudService } from \\\"@roo-code/cloud\\\"\\n 4 | \\n----\\n 37 | \\t\\t}\\n 38 | \\t\\tcase \\\"/auth/clerk/callback\\\": {\\n 39 | \\t\\t\\tconst code = query.get(\\\"code\\\")\\n 40 | \\t\\t\\tconst state = query.get(\\\"state\\\")\\n 41 | \\t\\t\\tawait CloudService.instance.handleAuthCallback(code, state)\\n 42 | \\t\\t\\tbreak\\n----\\n\\n# src/services/marketplace/types.ts\\n 20 | \\tdescription: string\\n 21 | \\tauthor?: string\\n 22 | \\tauthorUrl?: string\\n 23 | \\ttags?: string[]\\n----\\n 45 | \\tdescription: string\\n 46 | \\tauthor?: string\\n 47 | \\tauthorUrl?: string\\n 48 | \\turl: string // Required url field\\n----\\n 62 | \\ttype: MarketplaceItemType\\n 63 | \\tauthor?: string\\n 64 | \\tauthorUrl?: string\\n 65 | \\turl?: string // Optional - only MCPs have url\\n----\\n\\n# src/services/tree-sitter/__tests__/fixtures/sample-embedded_template.ts\\n 35 |
\\n 36 | <% if user.authenticated? %>\\n 37 |

Welcome, <%= user.name %>

\\n----\\n\\n# src/services/tree-sitter/__tests__/parseSourceCodeDefinitions.embedded_template.test.ts\\n 43 | \\tit(\\\"should detect control structures\\\", () => {\\n 44 | \\t\\texpect(parseResult).toMatch(/\\\\d+--\\\\d+ \\\\|\\\\s+<% if user\\\\.authenticated\\\\? %>/)\\n 45 | \\t\\texpect(parseResult).toMatch(/\\\\d+--\\\\d+ \\\\|\\\\s+<% user\\\\.posts\\\\.each do \\\\|post\\\\| %>/)\\n----\\n\\n# src/services/marketplace/schemas.ts\\n 35 | \\ttype: marketplaceItemTypeSchema,\\n 36 | \\tauthor: z.string().optional(),\\n 37 | \\tauthorUrl: z.string().url(\\\"Author URL must be a valid URL\\\").optional(),\\n 38 | \\ttags: z.array(z.string()).optional(),\\n----\\n 61 | \\tdescription: z.string(),\\n 62 | \\tauthor: z.string().optional(),\\n 63 | \\tauthorUrl: z.string().url().optional(),\\n 64 | \\ttags: z.array(z.string()).optional(),\\n----\\n 72 | \\tdescription: z.string(),\\n 73 | \\tauthor: z.string().optional(),\\n 74 | \\tauthorUrl: z.string().url().optional(),\\n 75 | \\turl: z.string().url(), // Required url field\\n----\\n\\n# src/extension.ts\\n 14 | \\n 15 | import { CloudService } from \\\"@roo-code/cloud\\\"\\n 16 | import { TelemetryService, PostHogTelemetryClient } from \\\"@roo-code/telemetry\\\"\\n----\\n 75 | \\t// Initialize Roo Code Cloud service.\\n 76 | \\tawait CloudService.createInstance(context, {\\n 77 | \\t\\tstateChanged: () => ClineProvider.getVisibleInstance()?.postStateToWebview(),\\n----\\n\\n# src/workers/countTokens.ts\\n 4 | \\n 5 | import { tiktoken } from \\\"../utils/tiktoken\\\"\\n 6 | \\n----\\n 10 | \\ttry {\\n 11 | \\t\\tconst count = await tiktoken(content)\\n 12 | \\t\\treturn { success: true, count }\\n----\\n\\n# src/api/index.ts\\n 50 | \\t/**\\n 51 | \\t * Counts tokens for content blocks\\n 52 | \\t * All providers extend BaseProvider which provides a default tiktoken implementation,\\n 53 | \\t * but they can override this to use their native token counting endpoints\\n 54 | \\t *\\n 55 | \\t * @param content The content to count tokens for\\n 56 | \\t * @returns A promise resolving to the token count\\n 57 | \\t */\\n----\\n\\n# src/extension/api.ts\\n219 | \\n220 | \\t\\t\\tcline.on(\\\"taskCompleted\\\", async (_, tokenUsage, toolUsage) => {\\n221 | \\t\\t\\t\\tthis.emit(RooCodeEventName.TaskCompleted, cline.taskId, tokenUsage, toolUsage)\\n222 | \\t\\t\\t\\tthis.taskMap.delete(cline.taskId)\\n----\\n224 | \\t\\t\\t\\tawait this.fileLog(\\n225 | \\t\\t\\t\\t\\t`[${new Date().toISOString()}] taskCompleted -> ${cline.taskId} | ${JSON.stringify(tokenUsage, null, 2)} | ${JSON.stringify(toolUsage, null, 2)}\\\\n`,\\n226 | \\t\\t\\t\\t)\\n----\\n\\n# src/api/providers/ollama.ts\\n 79 | \\t\\t\\t\\ttype: \\\"usage\\\",\\n 80 | \\t\\t\\t\\tinputTokens: lastUsage?.prompt_tokens || 0,\\n 81 | \\t\\t\\t\\toutputTokens: lastUsage?.completion_tokens || 0,\\n 82 | \\t\\t\\t}\\n----\\n\\n# src/core/checkpoints/index.ts\\n215 | \\t\\t\\t\\tJSON.stringify({\\n216 | \\t\\t\\t\\t\\ttokensIn: totalTokensIn,\\n217 | \\t\\t\\t\\t\\ttokensOut: totalTokensOut,\\n218 | \\t\\t\\t\\t\\tcacheWrites: totalCacheWrites,\\n----\\n\\n# src/utils/git.ts\\n 11 | \\tsubject: string\\n 12 | \\tauthor: string\\n 13 | \\tdate: string\\n----\\n 57 | \\t\\t\\tconst { stdout: hashStdout } = await execAsync(\\n 58 | \\t\\t\\t\\t`git log -n 10 --format=\\\"%H%n%h%n%s%n%an%n%ad\\\" --date=short ` + `--author-date-order ${query}`,\\n 59 | \\t\\t\\t\\t{ cwd },\\n----\\n 79 | \\t\\t\\t\\tsubject: lines[i + 2],\\n 80 | \\t\\t\\t\\tauthor: lines[i + 3],\\n 81 | \\t\\t\\t\\tdate: lines[i + 4],\\n----\\n107 | \\t\\t})\\n108 | \\t\\tconst [fullHash, shortHash, subject, author, date, body] = info.trim().split(\\\"\\\\n\\\")\\n109 | \\n----\\n115 | \\t\\t\\t`Commit: ${shortHash} (${fullHash})`,\\n116 | \\t\\t\\t`Author: ${author}`,\\n117 | \\t\\t\\t`Date: ${date}`,\\n----\\n\\n# src/api/providers/__tests__/bedrock-invokedModelId.spec.ts\\n176 | \\n177 | \\t\\t// The last usage event should have the token counts from the metadata\\n178 | \\t\\tconst lastUsageEvent = usageEvents[usageEvents.length - 1]\\n179 | \\t\\t// Expect the usage event to include all token information\\n180 | \\t\\texpect(lastUsageEvent).toMatchObject({\\n----\\n183 | \\t\\t\\toutputTokens: 200,\\n184 | \\t\\t\\t// Cache tokens may be present with default values\\n185 | \\t\\t\\tcacheReadTokens: expect.any(Number),\\n----\\n\\n# src/api/providers/__tests__/groq.spec.ts\\n101 | \\t\\t\\t\\t\\t\\t\\tdone: false,\\n102 | \\t\\t\\t\\t\\t\\t\\tvalue: { choices: [{ delta: {} }], usage: { prompt_tokens: 10, completion_tokens: 20 } },\\n103 | \\t\\t\\t\\t\\t\\t})\\n----\\n139 | \\t\\t\\t\\tmodel: modelId,\\n140 | \\t\\t\\t\\tmax_tokens: modelInfo.maxTokens,\\n141 | \\t\\t\\t\\ttemperature: 0.5,\\n----\\n\\n# src/api/providers/__tests__/gemini.spec.ts\\n167 | \\tdescribe(\\\"calculateCost\\\", () => {\\n168 | \\t\\t// Mock ModelInfo based on gemini-1.5-flash-latest pricing (per 1M tokens)\\n169 | \\t\\t// Removed 'id' and 'name' as they are not part of ModelInfo type directly\\n170 | \\t\\tconst mockInfo: ModelInfo = {\\n171 | \\t\\t\\tinputPrice: 0.125, // $/1M tokens\\n172 | \\t\\t\\toutputPrice: 0.375, // $/1M tokens\\n173 | \\t\\t\\tcacheWritesPrice: 0.125, // Assume same as input for test\\n----\\n179 | \\n180 | \\t\\tit(\\\"should calculate cost correctly based on input and output tokens\\\", () => {\\n181 | \\t\\t\\tconst inputTokens = 10000 // Use larger numbers for per-million pricing\\n----\\n190 | \\n191 | \\t\\tit(\\\"should return 0 if token counts are zero\\\", () => {\\n192 | \\t\\t\\t// Note: The method expects numbers, not undefined. Passing undefined would be a type error.\\n193 | \\t\\t\\t// The calculateCost method itself returns undefined if prices are missing, but 0 if tokens are 0 and prices exist.\\n194 | \\t\\t\\texpect(handler.calculateCost({ info: mockInfo, inputTokens: 0, outputTokens: 0 })).toBe(0)\\n----\\n196 | \\n197 | \\t\\tit(\\\"should handle only input tokens\\\", () => {\\n198 | \\t\\t\\tconst inputTokens = 5000\\n----\\n203 | \\n204 | \\t\\tit(\\\"should handle only output tokens\\\", () => {\\n205 | \\t\\t\\tconst outputTokens = 15000\\n----\\n210 | \\n211 | \\t\\tit(\\\"should calculate cost with cache write tokens\\\", () => {\\n212 | \\t\\t\\tconst inputTokens = 10000\\n----\\n227 | \\n228 | \\t\\tit(\\\"should calculate cost with cache read tokens\\\", () => {\\n229 | \\t\\t\\tconst inputTokens = 10000 // Total logical input\\n----\\n\\n# src/core/condense/index.ts\\n 55 | \\tcost: number // The cost of the summarization operation\\n 56 | \\tnewContextTokens?: number // The number of tokens in the context for the next API request\\n 57 | \\terror?: string // Populated iff the operation fails: error message shown to the user on failure (see Task.ts)\\n----\\n 63 | * @param {ApiMessage[]} messages - The conversation messages\\n 64 | * @param {ApiHandler} apiHandler - The API handler to use for token counting.\\n 65 | * @param {string} systemPrompt - The system prompt for API requests, which should be considered in the context token count\\n 66 | * @param {string} taskId - The task ID for the conversation, used for telemetry\\n----\\n 73 | * @param {ApiMessage[]} messages - The conversation messages\\n 74 | * @param {ApiHandler} apiHandler - The API handler to use for token counting (fallback if condensingApiHandler not provided)\\n 75 | * @param {string} systemPrompt - The system prompt for API requests (fallback if customCondensingPrompt not provided)\\n 76 | * @param {string} taskId - The task ID for the conversation, used for telemetry\\n 77 | * @param {number} prevContextTokens - The number of tokens currently in the context, used to ensure we don't grow the context\\n 78 | * @param {boolean} isAutomaticTrigger - Whether the summarization is triggered automatically\\n----\\n186 | \\n187 | \\t// Count the tokens in the context for the next API request\\n188 | \\t// We only estimate the tokens in summaryMesage if outputTokens is 0, otherwise we use outputTokens\\n189 | \\tconst systemPromptMessage: ApiMessage = { role: \\\"user\\\", content: systemPrompt }\\n----\\n\\n# src/api/providers/__tests__/vertex.spec.ts\\n 49 | \\t\\t\\t// The test expects 4 chunks:\\n 50 | \\t\\t\\t// 1. Usage chunk with input tokens\\n 51 | \\t\\t\\t// 2. Text chunk with \\\"Gemini response part 1\\\"\\n 52 | \\t\\t\\t// 3. Text chunk with \\\" part 2\\\"\\n 53 | \\t\\t\\t// 4. Usage chunk with output tokens\\n 54 | \\n----\\n\\n# src/api/providers/__tests__/ollama.spec.ts\\n 28 | \\t\\t\\t\\t\\t\\t\\t\\tusage: {\\n 29 | \\t\\t\\t\\t\\t\\t\\t\\t\\tprompt_tokens: 10,\\n 30 | \\t\\t\\t\\t\\t\\t\\t\\t\\tcompletion_tokens: 5,\\n 31 | \\t\\t\\t\\t\\t\\t\\t\\t\\ttotal_tokens: 15,\\n 32 | \\t\\t\\t\\t\\t\\t\\t\\t},\\n----\\n 54 | \\t\\t\\t\\t\\t\\t\\t\\t\\tusage: {\\n 55 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\tprompt_tokens: 10,\\n 56 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\tcompletion_tokens: 5,\\n 57 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\ttotal_tokens: 15,\\n 58 | \\t\\t\\t\\t\\t\\t\\t\\t\\t},\\n----\\n\\n# src/core/environment/getEnvironmentDetails.ts\\n192 | \\n193 | \\t// Add context tokens information.\\n194 | \\tconst { contextTokens, totalCost } = getApiMetrics(cline.clineMessages)\\n----\\n\\n# src/api/providers/__tests__/openai.spec.ts\\n 29 | \\t\\t\\t\\t\\t\\t\\t\\tusage: {\\n 30 | \\t\\t\\t\\t\\t\\t\\t\\t\\tprompt_tokens: 10,\\n 31 | \\t\\t\\t\\t\\t\\t\\t\\t\\tcompletion_tokens: 5,\\n 32 | \\t\\t\\t\\t\\t\\t\\t\\t\\ttotal_tokens: 15,\\n 33 | \\t\\t\\t\\t\\t\\t\\t\\t},\\n----\\n 55 | \\t\\t\\t\\t\\t\\t\\t\\t\\tusage: {\\n 56 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\tprompt_tokens: 10,\\n 57 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\tcompletion_tokens: 5,\\n 58 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\ttotal_tokens: 15,\\n 59 | \\t\\t\\t\\t\\t\\t\\t\\t\\t},\\n----\\n\\n# src/api/providers/__tests__/anthropic.spec.ts\\n 19 | \\t\\t\\t\\t\\t\\tusage: {\\n 20 | \\t\\t\\t\\t\\t\\t\\tinput_tokens: 10,\\n 21 | \\t\\t\\t\\t\\t\\t\\toutput_tokens: 5,\\n 22 | \\t\\t\\t\\t\\t\\t},\\n----\\n 30 | \\t\\t\\t\\t\\t\\t\\t\\tusage: {\\n 31 | \\t\\t\\t\\t\\t\\t\\t\\t\\tinput_tokens: 100,\\n 32 | \\t\\t\\t\\t\\t\\t\\t\\t\\toutput_tokens: 50,\\n 33 | \\t\\t\\t\\t\\t\\t\\t\\t\\tcache_creation_input_tokens: 20,\\n 34 | \\t\\t\\t\\t\\t\\t\\t\\t\\tcache_read_input_tokens: 10,\\n 35 | \\t\\t\\t\\t\\t\\t\\t\\t},\\n----\\n105 | \\n106 | \\t\\tit(\\\"use apiKey for passing token if anthropicUseAuthToken is not set\\\", () => {\\n107 | \\t\\t\\tconst handlerWithCustomUrl = new AnthropicHandler({\\n----\\n112 | \\t\\t\\texpect(mockAnthropicConstructor.mock.calls[0]![0]!.apiKey).toEqual(\\\"test-api-key\\\")\\n113 | \\t\\t\\texpect(mockAnthropicConstructor.mock.calls[0]![0]!.authToken).toBeUndefined()\\n114 | \\t\\t})\\n115 | \\n116 | \\t\\tit(\\\"use apiKey for passing token if anthropicUseAuthToken is set but custom base URL is not given\\\", () => {\\n117 | \\t\\t\\tconst handlerWithCustomUrl = new AnthropicHandler({\\n----\\n123 | \\t\\t\\texpect(mockAnthropicConstructor.mock.calls[0]![0]!.apiKey).toEqual(\\\"test-api-key\\\")\\n124 | \\t\\t\\texpect(mockAnthropicConstructor.mock.calls[0]![0]!.authToken).toBeUndefined()\\n125 | \\t\\t})\\n126 | \\n127 | \\t\\tit(\\\"use authToken for passing token if both of anthropicBaseUrl and anthropicUseAuthToken are set\\\", () => {\\n128 | \\t\\t\\tconst customBaseUrl = \\\"https://custom.anthropic.com\\\"\\n----\\n135 | \\t\\t\\texpect(mockAnthropicConstructor).toHaveBeenCalledTimes(1)\\n136 | \\t\\t\\texpect(mockAnthropicConstructor.mock.calls[0]![0]!.authToken).toEqual(\\\"test-api-key\\\")\\n137 | \\t\\t\\texpect(mockAnthropicConstructor.mock.calls[0]![0]!.apiKey).toBeUndefined()\\n----\\n190 | \\t\\t\\t\\tmessages: [{ role: \\\"user\\\", content: \\\"Test prompt\\\" }],\\n191 | \\t\\t\\t\\tmax_tokens: 8192,\\n192 | \\t\\t\\t\\ttemperature: 0,\\n----\\n\\n# src/api/providers/__tests__/xai.test.ts\\n228 | \\t\\t\\t\\t\\t\\t\\t\\tusage: {\\n229 | \\t\\t\\t\\t\\t\\t\\t\\t\\tprompt_tokens: 10,\\n230 | \\t\\t\\t\\t\\t\\t\\t\\t\\tcompletion_tokens: 20,\\n231 | \\t\\t\\t\\t\\t\\t\\t\\t\\tcache_read_input_tokens: 5,\\n232 | \\t\\t\\t\\t\\t\\t\\t\\t\\tcache_creation_input_tokens: 15,\\n233 | \\t\\t\\t\\t\\t\\t\\t\\t},\\n----\\n284 | \\t\\t\\t\\tmodel: modelId,\\n285 | \\t\\t\\t\\tmax_tokens: modelInfo.maxTokens,\\n286 | \\t\\t\\t\\ttemperature: 0,\\n----\\n\\n# src/api/providers/__tests__/openrouter.spec.ts\\n132 | \\t\\t\\t\\t\\t\\tchoices: [{ delta: {} }],\\n133 | \\t\\t\\t\\t\\t\\tusage: { prompt_tokens: 10, completion_tokens: 20, cost: 0.001 },\\n134 | \\t\\t\\t\\t\\t}\\n----\\n162 | \\t\\t\\t\\texpect.objectContaining({\\n163 | \\t\\t\\t\\t\\tmax_tokens: 8192,\\n164 | \\t\\t\\t\\t\\tmessages: [\\n----\\n285 | \\t\\t\\t\\tmodel: mockOptions.openRouterModelId,\\n286 | \\t\\t\\t\\tmax_tokens: 8192,\\n287 | \\t\\t\\t\\tthinking: undefined,\\n----\\n\\n# src/api/providers/unbound.ts\\n 22 | interface UnboundUsage extends OpenAI.CompletionUsage {\\n 23 | \\tcache_creation_input_tokens?: number\\n 24 | \\tcache_read_input_tokens?: number\\n 25 | }\\n----\\n 73 | \\n 74 | \\t\\t// Required by Anthropic; other providers default to max tokens allowed.\\n 75 | \\t\\tlet maxTokens: number | undefined\\n----\\n 82 | \\t\\t\\tmodel: modelId.split(\\\"/\\\")[1],\\n 83 | \\t\\t\\tmax_tokens: maxTokens,\\n 84 | \\t\\t\\tmessages: openAiMessages,\\n----\\n111 | \\t\\t\\t\\t\\ttype: \\\"usage\\\",\\n112 | \\t\\t\\t\\t\\tinputTokens: usage.prompt_tokens || 0,\\n113 | \\t\\t\\t\\t\\toutputTokens: usage.completion_tokens || 0,\\n114 | \\t\\t\\t\\t}\\n115 | \\n116 | \\t\\t\\t\\t// Only add cache tokens if they exist.\\n117 | \\t\\t\\t\\tif (usage.cache_creation_input_tokens) {\\n118 | \\t\\t\\t\\t\\tusageData.cacheWriteTokens = usage.cache_creation_input_tokens\\n119 | \\t\\t\\t\\t}\\n120 | \\n121 | \\t\\t\\t\\tif (usage.cache_read_input_tokens) {\\n122 | \\t\\t\\t\\t\\tusageData.cacheReadTokens = usage.cache_read_input_tokens\\n123 | \\t\\t\\t\\t}\\n----\\n146 | \\t\\t\\tif (modelId.startsWith(\\\"anthropic/\\\")) {\\n147 | \\t\\t\\t\\trequestOptions.max_tokens = info.maxTokens\\n148 | \\t\\t\\t}\\n----\\n\\n# src/api/providers/vscode-lm.ts\\n128 | \\t\\t\\t\\tmaxInputTokens: 8192,\\n129 | \\t\\t\\t\\tsendRequest: async (_messages, _options, _token) => {\\n130 | \\t\\t\\t\\t\\t// Provide a minimal implementation\\n----\\n179 | \\t * Implements the ApiHandler countTokens interface method\\n180 | \\t * Provides token counting for Anthropic content blocks\\n181 | \\t *\\n182 | \\t * @param content The content blocks to count tokens for\\n183 | \\t * @returns A promise resolving to the token count\\n184 | \\t */\\n185 | \\toverride async countTokens(content: Array): Promise {\\n186 | \\t\\t// Convert Anthropic content blocks to a string for VSCode LM token counting\\n187 | \\t\\tlet textContent = \\\"\\\"\\n----\\n201 | \\t/**\\n202 | \\t * Private implementation of token counting used internally by VsCodeLmHandler\\n203 | \\t */\\n----\\n206 | \\t\\tif (!this.client) {\\n207 | \\t\\t\\tconsole.warn(\\\"Roo Code : No client available for token counting\\\")\\n208 | \\t\\t\\treturn 0\\n----\\n211 | \\t\\tif (!this.currentRequestCancellation) {\\n212 | \\t\\t\\tconsole.warn(\\\"Roo Code : No cancellation token available for token counting\\\")\\n213 | \\t\\t\\treturn 0\\n----\\n217 | \\t\\tif (!text) {\\n218 | \\t\\t\\tconsole.debug(\\\"Roo Code : Empty text provided for token counting\\\")\\n219 | \\t\\t\\treturn 0\\n----\\n223 | \\t\\t\\t// Handle different input types\\n224 | \\t\\t\\tlet tokenCount: number\\n225 | \\n226 | \\t\\t\\tif (typeof text === \\\"string\\\") {\\n227 | \\t\\t\\t\\ttokenCount = await this.client.countTokens(text, this.currentRequestCancellation.token)\\n228 | \\t\\t\\t} else if (text instanceof vscode.LanguageModelChatMessage) {\\n----\\n233 | \\t\\t\\t\\t}\\n234 | \\t\\t\\t\\ttokenCount = await this.client.countTokens(text, this.currentRequestCancellation.token)\\n235 | \\t\\t\\t} else {\\n236 | \\t\\t\\t\\tconsole.warn(\\\"Roo Code : Invalid input type for token counting\\\")\\n237 | \\t\\t\\t\\treturn 0\\n----\\n240 | \\t\\t\\t// Validate the result\\n241 | \\t\\t\\tif (typeof tokenCount !== \\\"number\\\") {\\n242 | \\t\\t\\t\\tconsole.warn(\\\"Roo Code : Non-numeric token count received:\\\", tokenCount)\\n243 | \\t\\t\\t\\treturn 0\\n----\\n245 | \\n246 | \\t\\t\\tif (tokenCount < 0) {\\n247 | \\t\\t\\t\\tconsole.warn(\\\"Roo Code : Negative token count received:\\\", tokenCount)\\n248 | \\t\\t\\t\\treturn 0\\n----\\n250 | \\n251 | \\t\\t\\treturn tokenCount\\n252 | \\t\\t} catch (error) {\\n----\\n278 | \\n279 | \\t\\treturn systemTokens + messageTokens.reduce((sum: number, tokens: number): number => sum + tokens, 0)\\n280 | \\t}\\n----\\n357 | \\n358 | \\t\\t// Initialize cancellation token for the request\\n359 | \\t\\tthis.currentRequestCancellation = new vscode.CancellationTokenSource()\\n360 | \\n361 | \\t\\t// Calculate input tokens before starting the stream\\n362 | \\t\\tconst totalInputTokens: number = await this.calculateTotalInputTokens(systemPrompt, vsCodeLmMessages)\\n363 | \\n364 | \\t\\t// Accumulate the text and count at the end of the stream to reduce token counting overhead.\\n365 | \\t\\tlet accumulatedText: string = \\\"\\\"\\n----\\n378 | \\t\\t\\t\\trequestOptions,\\n379 | \\t\\t\\t\\tthis.currentRequestCancellation.token,\\n380 | \\t\\t\\t)\\n----\\n446 | \\n447 | \\t\\t\\t// Count tokens in the accumulated text after stream completion\\n448 | \\t\\t\\tconst totalOutputTokens: number = await this.internalCountTokens(accumulatedText)\\n----\\n511 | \\t\\t\\tconst modelInfo: ModelInfo = {\\n512 | \\t\\t\\t\\tmaxTokens: -1, // Unlimited tokens by default\\n513 | \\t\\t\\t\\tcontextWindow:\\n----\\n548 | \\t\\t\\t\\t{},\\n549 | \\t\\t\\t\\tnew vscode.CancellationTokenSource().token,\\n550 | \\t\\t\\t)\\n----\\n\\n# src/core/condense/__tests__/index.test.ts\\n203 | \\n204 | \\t\\t// Check the cost and token counts\\n205 | \\t\\texpect(result.cost).toBe(0.05)\\n206 | \\t\\texpect(result.summary).toBe(\\\"This is a summary\\\")\\n207 | \\t\\texpect(result.newContextTokens).toBe(250) // 150 output tokens + 100 from countTokens\\n208 | \\t\\texpect(result.error).toBeUndefined()\\n----\\n317 | \\t\\t// Check the newContextTokens calculation includes system prompt\\n318 | \\t\\texpect(result.newContextTokens).toBe(300) // 200 output tokens + 100 from countTokens\\n319 | \\t\\texpect(result.cost).toBe(0.06)\\n----\\n323 | \\n324 | \\tit(\\\"should return error when new context tokens >= previous context tokens\\\", async () => {\\n325 | \\t\\tconst messages: ApiMessage[] = [\\n----\\n336 | \\t\\tconst streamWithLargeTokens = (async function* () {\\n337 | \\t\\t\\tyield { type: \\\"text\\\" as const, text: \\\"This is a very long summary that uses many tokens\\\" }\\n338 | \\t\\t\\tyield { type: \\\"usage\\\" as const, totalCost: 0.08, outputTokens: 500 }\\n----\\n364 | \\n365 | \\tit(\\\"should successfully summarize when new context tokens < previous context tokens\\\", async () => {\\n366 | \\t\\tconst messages: ApiMessage[] = [\\n----\\n375 | \\n376 | \\t\\t// Create a stream that produces a summary with reasonable token count\\n377 | \\t\\tconst streamWithSmallTokens = (async function* () {\\n----\\n401 | \\t\\texpect(result.error).toBeUndefined()\\n402 | \\t\\texpect(result.newContextTokens).toBe(80) // 50 output tokens + 30 from countTokens\\n403 | \\t\\texpect(result.newContextTokens).toBeLessThan(prevContextTokens)\\n----\\n\\n# src/api/providers/__tests__/lmstudio.test.ts\\n 25 | \\t\\t\\t\\t\\t\\t\\t\\tusage: {\\n 26 | \\t\\t\\t\\t\\t\\t\\t\\t\\tprompt_tokens: 10,\\n 27 | \\t\\t\\t\\t\\t\\t\\t\\t\\tcompletion_tokens: 5,\\n 28 | \\t\\t\\t\\t\\t\\t\\t\\t\\ttotal_tokens: 15,\\n 29 | \\t\\t\\t\\t\\t\\t\\t\\t},\\n----\\n 51 | \\t\\t\\t\\t\\t\\t\\t\\t\\tusage: {\\n 52 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\tprompt_tokens: 10,\\n 53 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\tcompletion_tokens: 5,\\n 54 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\ttotal_tokens: 15,\\n 55 | \\t\\t\\t\\t\\t\\t\\t\\t\\t},\\n----\\n\\n# src/api/providers/__tests__/bedrock-reasoning.test.ts\\n 94 | \\t\\t\\t\\ttype: \\\"enabled\\\",\\n 95 | \\t\\t\\t\\tbudget_tokens: 4096, // Uses the full modelMaxThinkingTokens value\\n 96 | \\t\\t\\t})\\n----\\n143 | \\t\\t\\t\\ttype: \\\"enabled\\\",\\n144 | \\t\\t\\t\\tbudget_tokens: 8192,\\n145 | \\t\\t\\t})\\n----\\n266 | \\t\\t\\t\\ttype: \\\"enabled\\\",\\n267 | \\t\\t\\t\\tbudget_tokens: 4096,\\n268 | \\t\\t\\t})\\n----\\n\\n# src/api/providers/lm-studio.ts\\n 40 | \\t\\t// -------------------------\\n 41 | \\t\\t// Track token usage\\n 42 | \\t\\t// -------------------------\\n----\\n 68 | \\t\\t} catch (err) {\\n 69 | \\t\\t\\tconsole.error(\\\"[LmStudio] Failed to count input tokens:\\\", err)\\n 70 | \\t\\t\\tinputTokens = 0\\n----\\n116 | \\t\\t\\t} catch (err) {\\n117 | \\t\\t\\t\\tconsole.error(\\\"[LmStudio] Failed to count output tokens:\\\", err)\\n118 | \\t\\t\\t\\toutputTokens = 0\\n----\\n\\n# src/utils/countTokens.ts\\n 4 | import { countTokensResultSchema } from \\\"../workers/types\\\"\\n 5 | import { tiktoken } from \\\"./tiktoken\\\"\\n 6 | \\n----\\n 27 | \\tif (!useWorker || !pool) {\\n 28 | \\t\\treturn tiktoken(content)\\n 29 | \\t}\\n----\\n 42 | \\t\\tconsole.error(error)\\n 43 | \\t\\treturn tiktoken(content)\\n 44 | \\t}\\n----\\n\\n# src/api/providers/__tests__/requesty.spec.ts\\n124 | \\t\\t\\t\\t\\t\\tusage: {\\n125 | \\t\\t\\t\\t\\t\\t\\tprompt_tokens: 10,\\n126 | \\t\\t\\t\\t\\t\\t\\tcompletion_tokens: 20,\\n127 | \\t\\t\\t\\t\\t\\t\\tprompt_tokens_details: {\\n128 | \\t\\t\\t\\t\\t\\t\\t\\tcaching_tokens: 5,\\n129 | \\t\\t\\t\\t\\t\\t\\t\\tcached_tokens: 2,\\n130 | \\t\\t\\t\\t\\t\\t\\t},\\n----\\n162 | \\t\\t\\t\\texpect.objectContaining({\\n163 | \\t\\t\\t\\t\\tmax_tokens: 8192,\\n164 | \\t\\t\\t\\t\\tmessages: [\\n----\\n204 | \\t\\t\\t\\tmodel: mockOptions.requestyModelId,\\n205 | \\t\\t\\t\\tmax_tokens: 8192,\\n206 | \\t\\t\\t\\tmessages: [{ role: \\\"system\\\", content: \\\"test prompt\\\" }],\\n----\\n\\n# src/api/providers/__tests__/vscode-lm.test.ts\\n 28 | \\t\\tCancellationTokenSource: jest.fn(() => ({\\n 29 | \\t\\t\\ttoken: {\\n 30 | \\t\\t\\t\\tisCancellationRequested: false,\\n----\\n\\n# src/api/providers/__tests__/chutes.spec.ts\\n 49 | \\t\\t\\t\\t\\tusage: {\\n 50 | \\t\\t\\t\\t\\t\\tprompt_tokens: 10,\\n 51 | \\t\\t\\t\\t\\t\\tcompletion_tokens: 5,\\n 52 | \\t\\t\\t\\t\\t\\ttotal_tokens: 15,\\n 53 | \\t\\t\\t\\t\\t},\\n----\\n103 | \\t\\t\\t\\t\\t],\\n104 | \\t\\t\\t\\t\\tusage: { prompt_tokens: 10, completion_tokens: 5 },\\n105 | \\t\\t\\t\\t}\\n----\\n211 | \\t\\t\\t\\t\\t\\t\\tdone: false,\\n212 | \\t\\t\\t\\t\\t\\t\\tvalue: { choices: [{ delta: {} }], usage: { prompt_tokens: 10, completion_tokens: 20 } },\\n213 | \\t\\t\\t\\t\\t\\t})\\n----\\n284 | \\t\\t\\t\\tmodel: modelId,\\n285 | \\t\\t\\t\\tmax_tokens: modelInfo.maxTokens,\\n286 | \\t\\t\\t\\ttemperature: 0.5,\\n----\\n\\n# src/api/providers/__tests__/openai-native.spec.ts\\n 29 | \\t\\t\\t\\t\\t\\t\\t\\tusage: {\\n 30 | \\t\\t\\t\\t\\t\\t\\t\\t\\tprompt_tokens: 10,\\n 31 | \\t\\t\\t\\t\\t\\t\\t\\t\\tcompletion_tokens: 5,\\n 32 | \\t\\t\\t\\t\\t\\t\\t\\t\\ttotal_tokens: 15,\\n 33 | \\t\\t\\t\\t\\t\\t\\t\\t},\\n----\\n 55 | \\t\\t\\t\\t\\t\\t\\t\\t\\tusage: {\\n 56 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\tprompt_tokens: 10,\\n 57 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\tcompletion_tokens: 5,\\n 58 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\ttotal_tokens: 15,\\n 59 | \\t\\t\\t\\t\\t\\t\\t\\t\\t},\\n----\\n145 | \\t\\t\\t\\t\\t\\tusage: {\\n146 | \\t\\t\\t\\t\\t\\t\\tprompt_tokens: 0,\\n147 | \\t\\t\\t\\t\\t\\t\\tcompletion_tokens: 0,\\n148 | \\t\\t\\t\\t\\t\\t\\ttotal_tokens: 0,\\n149 | \\t\\t\\t\\t\\t\\t},\\n----\\n215 | \\t\\t\\t\\t{ choices: [{ delta: { content: \\\" there\\\" } }], usage: null },\\n216 | \\t\\t\\t\\t{ choices: [{ delta: { content: \\\"!\\\" } }], usage: { prompt_tokens: 10, completion_tokens: 5 } },\\n217 | \\t\\t\\t]\\n----\\n261 | \\t\\t\\t\\t{ choices: [{ delta: { content: null } }], usage: null },\\n262 | \\t\\t\\t\\t{ choices: [{ delta: { content: \\\"Hello\\\" } }], usage: { prompt_tokens: 10, completion_tokens: 5 } },\\n263 | \\t\\t\\t]\\n----\\n\\n# src/api/providers/__tests__/deepseek.test.ts\\n 28 | \\t\\t\\t\\t\\t\\t\\t\\tusage: {\\n 29 | \\t\\t\\t\\t\\t\\t\\t\\t\\tprompt_tokens: 10,\\n 30 | \\t\\t\\t\\t\\t\\t\\t\\t\\tcompletion_tokens: 5,\\n 31 | \\t\\t\\t\\t\\t\\t\\t\\t\\ttotal_tokens: 15,\\n 32 | \\t\\t\\t\\t\\t\\t\\t\\t\\tprompt_tokens_details: {\\n 33 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\tcache_miss_tokens: 8,\\n 34 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\tcached_tokens: 2,\\n 35 | \\t\\t\\t\\t\\t\\t\\t\\t\\t},\\n----\\n 59 | \\t\\t\\t\\t\\t\\t\\t\\t\\tusage: {\\n 60 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\tprompt_tokens: 10,\\n 61 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\tcompletion_tokens: 5,\\n 62 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\ttotal_tokens: 15,\\n 63 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\tprompt_tokens_details: {\\n 64 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\t\\tcache_miss_tokens: 8,\\n 65 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\t\\tcached_tokens: 2,\\n 66 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\t},\\n----\\n261 | \\t\\t\\tconst usage = {\\n262 | \\t\\t\\t\\tprompt_tokens: 100,\\n263 | \\t\\t\\t\\tcompletion_tokens: 50,\\n264 | \\t\\t\\t\\ttotal_tokens: 150,\\n265 | \\t\\t\\t\\tprompt_tokens_details: {\\n266 | \\t\\t\\t\\t\\tcache_miss_tokens: 80,\\n267 | \\t\\t\\t\\t\\tcached_tokens: 20,\\n268 | \\t\\t\\t\\t},\\n----\\n289 | \\t\\t\\tconst usage = {\\n290 | \\t\\t\\t\\tprompt_tokens: 100,\\n291 | \\t\\t\\t\\tcompletion_tokens: 50,\\n292 | \\t\\t\\t\\ttotal_tokens: 150,\\n293 | \\t\\t\\t\\t// No prompt_tokens_details\\n294 | \\t\\t\\t}\\n----\\n\\n# src/api/transform/cache-strategy/multi-point-strategy.ts\\n 7 | * Strategy for handling multiple cache points.\\n 8 | * Creates cache points after messages as soon as uncached tokens exceed minimumTokenCount.\\n 9 | */\\n----\\n 58 | \\t *\\n 59 | \\t * @param minTokensPerPoint Minimum tokens required per cache point\\n 60 | \\t * @param remainingCachePoints Number of cache points available\\n----\\n 97 | \\n 98 | \\t\\t// Calculate tokens in new messages (added since last cache point placement)\\n 99 | \\t\\tconst lastPreviousIndex = previousPlacements[previousPlacements.length - 1].index\\n----\\n103 | \\n104 | \\t\\t// If new messages have enough tokens for a cache point, we need to decide\\n105 | \\t\\t// whether to keep all previous cache points or combine some\\n----\\n127 | \\t\\t\\t\\t// We need to decide which previous cache points to keep and which to combine\\n128 | \\t\\t\\t\\t// Strategy: Compare the token count of new messages with the smallest combined token gap\\n129 | \\n130 | \\t\\t\\t\\t// First, analyze the token distribution between previous cache points\\n131 | \\t\\t\\t\\tconst tokensBetweenPlacements: number[] = []\\n132 | \\t\\t\\t\\tlet startIdx = 0\\n----\\n134 | \\t\\t\\t\\tfor (const placement of previousPlacements) {\\n135 | \\t\\t\\t\\t\\tconst tokens = this.config.messages\\n136 | \\t\\t\\t\\t\\t\\t.slice(startIdx, placement.index + 1)\\n----\\n138 | \\n139 | \\t\\t\\t\\t\\ttokensBetweenPlacements.push(tokens)\\n140 | \\t\\t\\t\\t\\tstartIdx = placement.index + 1\\n----\\n142 | \\n143 | \\t\\t\\t\\t// Find the two consecutive placements with the smallest token gap\\n144 | \\t\\t\\t\\tlet smallestGapIndex = 0\\n----\\n146 | \\n147 | \\t\\t\\t\\tfor (let i = 0; i < tokensBetweenPlacements.length - 1; i++) {\\n148 | \\t\\t\\t\\t\\tconst gap = tokensBetweenPlacements[i] + tokensBetweenPlacements[i + 1]\\n149 | \\t\\t\\t\\t\\tif (gap < smallestGap) {\\n----\\n155 | \\t\\t\\t\\t// Only combine cache points if it's beneficial\\n156 | \\t\\t\\t\\t// Compare the token count of new messages with the smallest combined token gap\\n157 | \\t\\t\\t\\t// Apply a required percentage increase to ensure reallocation is worth it\\n----\\n161 | \\t\\t\\t\\tif (newMessagesTokens >= requiredTokenThreshold) {\\n162 | \\t\\t\\t\\t\\t// It's beneficial to combine cache points since new messages have significantly more tokens\\n163 | \\t\\t\\t\\t\\tlogger.info(\\\"Combining cache points is beneficial\\\", {\\n----\\n231 | \\t\\t} else {\\n232 | \\t\\t\\t// New messages don't have enough tokens for a cache point\\n233 | \\t\\t\\t// Keep all previous placements that are still valid\\n----\\n266 | \\t\\tif (lastUserMessageIndex >= 0) {\\n267 | \\t\\t\\t// Calculate the total tokens covered from the previous cache point (or start of conversation)\\n268 | \\t\\t\\t// to this cache point. This ensures tokensCovered represents the full span of tokens\\n269 | \\t\\t\\t// that will be cached by this cache point.\\n----\\n281 | \\n282 | \\t\\t\\t// Calculate tokens from previous cache point (or start) to this cache point\\n283 | \\t\\t\\tconst tokenStartIndex = previousCachePointIndex + 1\\n284 | \\t\\t\\ttotalTokensCovered = this.config.messages\\n285 | \\t\\t\\t\\t.slice(tokenStartIndex, lastUserMessageIndex + 1)\\n286 | \\t\\t\\t\\t.reduce((acc, curr) => acc + this.estimateTokenCount(curr), 0)\\n287 | \\n288 | \\t\\t\\t// Guard clause: ensure we have enough tokens to justify a cache point\\n289 | \\t\\t\\tif (totalTokensCovered < minTokensPerPoint) {\\n----\\n294 | \\t\\t\\t\\ttype: \\\"message\\\",\\n295 | \\t\\t\\t\\ttokensCovered: totalTokensCovered,\\n296 | \\t\\t\\t}\\n----\\n\\n# src/api/transform/model-params.ts\\n 95 | \\n 96 | \\t\\t// Reasoning cannot be less than 1024 tokens.\\n 97 | \\t\\tif (reasoningBudget < 1024) {\\n----\\n\\n# src/api/transform/cache-strategy/types.ts\\n 7 | export interface ModelInfo {\\n 8 | \\t/** Maximum number of tokens the model can generate */\\n 9 | \\tmaxTokens: number\\n 10 | \\t/** Maximum context window size in tokens */\\n 11 | \\tcontextWindow: number\\n----\\n 15 | \\tmaxCachePoints: number\\n 16 | \\t/** Minimum number of tokens required for a cache point */\\n 17 | \\tminTokensPerCachePoint: number\\n----\\n 49 | \\ttype: \\\"system\\\" | \\\"message\\\"\\n 50 | \\t/** Number of tokens this cache point covers */\\n 51 | \\ttokensCovered: number\\n 52 | }\\n----\\n\\n# src/api/transform/__tests__/reasoning.spec.ts\\n 40 | \\n 41 | \\t\\t\\texpect(result).toEqual({ max_tokens: 1000 })\\n 42 | \\t\\t})\\n----\\n 61 | \\n 62 | \\t\\t\\texpect(result).toEqual({ max_tokens: 1000 })\\n 63 | \\t\\t})\\n----\\n123 | \\n124 | \\t\\t\\texpect(result).toEqual({ max_tokens: 1000 })\\n125 | \\t\\t})\\n----\\n140 | \\n141 | \\t\\t\\texpect(result).toEqual({ max_tokens: undefined })\\n142 | \\t\\t})\\n----\\n189 | \\n190 | \\t\\t\\texpect(result).toEqual({ max_tokens: 0 })\\n191 | \\t\\t})\\n----\\n244 | \\t\\t\\t\\ttype: \\\"enabled\\\",\\n245 | \\t\\t\\t\\tbudget_tokens: 1000,\\n246 | \\t\\t\\t})\\n----\\n268 | \\t\\t\\t\\ttype: \\\"enabled\\\",\\n269 | \\t\\t\\t\\tbudget_tokens: 1000,\\n270 | \\t\\t\\t})\\n----\\n314 | \\t\\t\\t\\ttype: \\\"enabled\\\",\\n315 | \\t\\t\\t\\tbudget_tokens: undefined,\\n316 | \\t\\t\\t})\\n----\\n334 | \\t\\t\\t\\ttype: \\\"enabled\\\",\\n335 | \\t\\t\\t\\tbudget_tokens: 0,\\n336 | \\t\\t\\t})\\n----\\n354 | \\t\\t\\t\\ttype: \\\"enabled\\\",\\n355 | \\t\\t\\t\\tbudget_tokens: 100000,\\n356 | \\t\\t\\t})\\n----\\n374 | \\t\\t\\t\\ttype: \\\"enabled\\\",\\n375 | \\t\\t\\t\\tbudget_tokens: 1000,\\n376 | \\t\\t\\t})\\n----\\n547 | \\n548 | \\t\\t\\texpect(openRouterResult).toEqual({ max_tokens: 1000 })\\n549 | \\t\\t\\texpect(anthropicResult).toEqual({ type: \\\"enabled\\\", budget_tokens: 1000 })\\n550 | \\t\\t\\texpect(openAiResult).toBeUndefined()\\n----\\n601 | \\t\\t\\t// Budget should take precedence for OpenRouter and Anthropic\\n602 | \\t\\t\\texpect(openRouterResult).toEqual({ max_tokens: 1000 })\\n603 | \\t\\t\\texpect(anthropicResult).toEqual({ type: \\\"enabled\\\", budget_tokens: 1000 })\\n604 | \\t\\t\\t// OpenAI should still use effort since it doesn't support budget\\n----\\n672 | \\t\\t\\t\\texpect(typeof result).toBe(\\\"object\\\")\\n673 | \\t\\t\\t\\texpect(\\\"max_tokens\\\" in result || \\\"effort\\\" in result || \\\"exclude\\\" in result).toBe(true)\\n674 | \\t\\t\\t}\\n----\\n688 | \\t\\t\\t\\texpect(result).toHaveProperty(\\\"type\\\", \\\"enabled\\\")\\n689 | \\t\\t\\t\\texpect(result).toHaveProperty(\\\"budget_tokens\\\")\\n690 | \\t\\t\\t}\\n----\\n\\n# src/api/transform/__tests__/model-params.spec.ts\\n205 | \\t\\t\\t\\t\\ttype: \\\"enabled\\\",\\n206 | \\t\\t\\t\\t\\tbudget_tokens: 1600,\\n207 | \\t\\t\\t\\t},\\n----\\n226 | \\t\\t\\t\\ttype: \\\"enabled\\\",\\n227 | \\t\\t\\t\\tbudget_tokens: 1600,\\n228 | \\t\\t\\t})\\n----\\n262 | \\t\\t\\t\\t\\ttype: \\\"enabled\\\",\\n263 | \\t\\t\\t\\t\\tbudget_tokens: 2400,\\n264 | \\t\\t\\t\\t},\\n----\\n287 | \\t\\t\\t\\t\\ttype: \\\"enabled\\\",\\n288 | \\t\\t\\t\\t\\tbudget_tokens: 1500,\\n289 | \\t\\t\\t\\t},\\n----\\n308 | \\n309 | \\t\\tit(\\\"should clamp thinking budget to at least 1024 tokens\\\", () => {\\n310 | \\t\\t\\tconst model: ModelInfo = {\\n----\\n328 | \\t\\t\\t\\t\\ttype: \\\"enabled\\\",\\n329 | \\t\\t\\t\\t\\tbudget_tokens: 1024,\\n330 | \\t\\t\\t\\t},\\n----\\n333 | \\n334 | \\t\\tit(\\\"should clamp thinking budget to at most 80% of max tokens\\\", () => {\\n335 | \\t\\t\\tconst model: ModelInfo = {\\n----\\n353 | \\t\\t\\t\\t\\ttype: \\\"enabled\\\",\\n354 | \\t\\t\\t\\t\\tbudget_tokens: 3200,\\n355 | \\t\\t\\t\\t},\\n----\\n372 | \\t\\t\\t\\t\\ttype: \\\"enabled\\\",\\n373 | \\t\\t\\t\\t\\tbudget_tokens: DEFAULT_HYBRID_REASONING_MODEL_THINKING_TOKENS,\\n374 | \\t\\t\\t\\t},\\n----\\n390 | \\t\\t\\texpect(result.maxTokens).toBe(5000)\\n391 | \\t\\t\\texpect(result.reasoningBudget).toBe(2000) // Custom thinking tokens takes precedence\\n392 | \\t\\t})\\n393 | \\n394 | \\t\\tit(\\\"should clamp custom thinking tokens even when custom max tokens is provided\\\", () => {\\n395 | \\t\\t\\tconst model: ModelInfo = {\\n----\\n719 | \\n720 | \\t\\t\\texpect(result.reasoning).toEqual({ max_tokens: 3200 })\\n721 | \\t\\t})\\n----\\n\\n# src/api/transform/cache-strategy/base-strategy.ts\\n 27 | \\t/**\\n 28 | \\t * Calculate token count for system prompt using a more accurate approach\\n 29 | \\t */\\n----\\n 33 | \\n 34 | \\t\\t\\t// Use a more accurate token estimation than simple character count\\n 35 | \\t\\t\\t// Count words and add overhead for punctuation and special tokens\\n 36 | \\t\\t\\tconst words = text.split(/\\\\s+/).filter((word) => word.length > 0)\\n 37 | \\t\\t\\t// Average English word is ~1.3 tokens\\n 38 | \\t\\t\\tlet tokenCount = words.length * 1.3\\n 39 | \\t\\t\\t// Add overhead for punctuation and special characters\\n 40 | \\t\\t\\ttokenCount += (text.match(/[.,!?;:()[\\\\]{}\\\"\\\"''`]/g) || []).length * 0.3\\n 41 | \\t\\t\\t// Add overhead for newlines\\n 42 | \\t\\t\\ttokenCount += (text.match(/\\\\n/g) || []).length * 0.5\\n 43 | \\t\\t\\t// Add a small overhead for system prompt structure\\n 44 | \\t\\t\\ttokenCount += 5\\n 45 | \\n 46 | \\t\\t\\tthis.systemTokenCount = Math.ceil(tokenCount)\\n 47 | \\t\\t}\\n----\\n 84 | \\t/**\\n 85 | \\t * Check if a token count meets the minimum threshold for caching\\n 86 | \\t */\\n 87 | \\tprotected meetsMinTokenThreshold(tokenCount: number): boolean {\\n 88 | \\t\\tconst minTokens = this.config.modelInfo.minTokensPerCachePoint\\n----\\n 91 | \\t\\t}\\n 92 | \\t\\treturn tokenCount >= minTokens\\n 93 | \\t}\\n----\\n 95 | \\t/**\\n 96 | \\t * Estimate token count for a message using a more accurate approach\\n 97 | \\t * This implementation is based on the BaseProvider's countTokens method\\n----\\n100 | \\tprotected estimateTokenCount(message: Anthropic.Messages.MessageParam): number {\\n101 | \\t\\t// Use a more sophisticated token counting approach\\n102 | \\t\\tif (!message.content) return 0\\n----\\n108 | \\t\\t\\t\\tif (block.type === \\\"text\\\") {\\n109 | \\t\\t\\t\\t\\t// Use a more accurate token estimation than simple character count\\n110 | \\t\\t\\t\\t\\t// This is still an approximation but better than character/4\\n----\\n112 | \\t\\t\\t\\t\\tif (text.length > 0) {\\n113 | \\t\\t\\t\\t\\t\\t// Count words and add overhead for punctuation and special tokens\\n114 | \\t\\t\\t\\t\\t\\tconst words = text.split(/\\\\s+/).filter((word) => word.length > 0)\\n115 | \\t\\t\\t\\t\\t\\t// Average English word is ~1.3 tokens\\n116 | \\t\\t\\t\\t\\t\\ttotalTokens += words.length * 1.3\\n----\\n128 | \\t\\t\\tconst text = message.content\\n129 | \\t\\t\\t// Count words and add overhead for punctuation and special tokens\\n130 | \\t\\t\\tconst words = text.split(/\\\\s+/).filter((word) => word.length > 0)\\n131 | \\t\\t\\t// Average English word is ~1.3 tokens\\n132 | \\t\\t\\ttotalTokens += words.length * 1.3\\n----\\n\\n# src/api/providers/__tests__/glama.spec.ts\\n 57 | \\t\\t\\t\\t\\t\\t\\t\\t\\tchoices: [{ delta: {}, index: 0 }],\\n 58 | \\t\\t\\t\\t\\t\\t\\t\\t\\tusage: { prompt_tokens: 10, completion_tokens: 5, total_tokens: 15 },\\n 59 | \\t\\t\\t\\t\\t\\t\\t\\t}\\n----\\n112 | \\t\\t\\tusage: {\\n113 | \\t\\t\\t\\tprompt_tokens: 10,\\n114 | \\t\\t\\t\\tcompletion_tokens: 5,\\n115 | \\t\\t\\t\\ttotal_tokens: 15,\\n116 | \\t\\t\\t},\\n----\\n176 | \\t\\t\\t\\t\\ttemperature: 0,\\n177 | \\t\\t\\t\\t\\tmax_tokens: 8192,\\n178 | \\t\\t\\t\\t}),\\n----\\n194 | \\n195 | \\t\\tit(\\\"should not set max_tokens for non-Anthropic models\\\", async () => {\\n196 | \\t\\t\\t// Reset mock to clear any previous calls\\n----\\n213 | \\t\\t\\t)\\n214 | \\t\\t\\texpect(mockCreate.mock.calls[0][0]).not.toHaveProperty(\\\"max_tokens\\\")\\n215 | \\t\\t})\\n----\\n\\n# src/api/transform/__tests__/stream.spec.ts\\n 33 | \\n 34 | \\t\\tit(\\\"should handle usage chunks without cache tokens\\\", () => {\\n 35 | \\t\\t\\tconst usageChunk: ApiStreamChunk = {\\n----\\n 57 | \\n 58 | \\t\\tit(\\\"should handle usage chunks with zero tokens\\\", () => {\\n 59 | \\t\\t\\tconst zeroUsageChunk: ApiStreamChunk = {\\n----\\n 69 | \\n 70 | \\t\\tit(\\\"should handle usage chunks with large token counts\\\", () => {\\n 71 | \\t\\t\\tconst largeUsageChunk: ApiStreamChunk = {\\n----\\n\\n# src/api/transform/cache-strategy/__tests__/cache-strategy.spec.ts\\n 29 | \\n 30 | const createMessageWithTokens = (role: \\\"user\\\" | \\\"assistant\\\", tokenCount: number) => ({\\n 31 | \\trole,\\n 32 | \\tcontent: \\\"x\\\".repeat(tokenCount * 4), // Approximate 4 chars per token\\n 33 | })\\n----\\n130 | \\t\\t\\t\\tit(\\\"adds system cache block when prompt caching is enabled, messages exist, and system prompt is long enough\\\", () => {\\n131 | \\t\\t\\t\\t\\t// Create a system prompt that's at least 50 tokens (200+ characters)\\n132 | \\t\\t\\t\\t\\tconst longSystemPrompt =\\n----\\n236 | \\t\\t\\t\\t\\t\\t\\t\\t(i + 1) +\\n237 | \\t\\t\\t\\t\\t\\t\\t\\t\\\" with some additional text to increase token count. \\\" +\\n238 | \\t\\t\\t\\t\\t\\t\\t\\t\\\"Adding more text to ensure we exceed the token threshold for cache block insertion.\\\",\\n239 | \\t\\t\\t\\t\\t\\t}))\\n----\\n579 | \\n580 | \\t\\tit(\\\"should yield usage results with cache tokens when using MultiPointStrategy\\\", async () => {\\n581 | \\t\\t\\t// Mock the convertToBedrockConverseMessages method to return a result with cache points\\n----\\n589 | \\n590 | \\t\\t\\t// Create a mock stream that includes cache token fields\\n591 | \\t\\t\\tconst mockApiResponse = {\\n----\\n626 | \\n627 | \\t\\t\\t// Verify that usage results with cache tokens are yielded\\n628 | \\t\\t\\texpect(chunks.length).toBeGreaterThan(0)\\n629 | \\t\\t\\t// The test already expects cache tokens, but the implementation might not be including them\\n630 | \\t\\t\\t// Let's make the test more flexible to accept either format\\n----\\n652 | \\n653 | \\t\\t// Helper function to create a message with approximate token count\\n654 | \\t\\tconst createMessage = (role: \\\"user\\\" | \\\"assistant\\\", content: string, tokenCount: number) => {\\n655 | \\t\\t\\t// Pad the content to reach the desired token count (approx 4 chars per token)\\n656 | \\t\\t\\tconst paddingNeeded = Math.max(0, tokenCount * 4 - content.length)\\n657 | \\t\\t\\tconst padding = \\\" \\\".repeat(paddingNeeded)\\n----\\n667 | \\t\\t\\t\\t\\\"Cache point placements:\\\",\\n668 | \\t\\t\\t\\tplacements.map((p) => `index: ${p.index}, tokens: ${p.tokensCovered}`),\\n669 | \\t\\t\\t)\\n----\\n683 | \\t\\t\\t\\t\\tmodelInfo: multiPointModelInfo,\\n684 | \\t\\t\\t\\t\\tsystemPrompt: \\\"You are a helpful assistant.\\\", // ~10 tokens\\n685 | \\t\\t\\t\\t\\tmessages,\\n----\\n730 | \\t\\t\\t\\t\\t\\ttype: \\\"message\\\",\\n731 | \\t\\t\\t\\t\\t\\ttokensCovered: 300,\\n732 | \\t\\t\\t\\t\\t},\\n----\\n736 | \\t\\t\\t\\t\\tmodelInfo: multiPointModelInfo,\\n737 | \\t\\t\\t\\t\\tsystemPrompt: \\\"You are a helpful assistant.\\\", // ~10 tokens\\n738 | \\t\\t\\t\\t\\tmessages,\\n----\\n759 | \\n760 | \\t\\t\\t\\t// Check if we have a second cache point (may not always be added depending on token distribution)\\n761 | \\t\\t\\t\\tif (result.messageCachePointPlacements && result.messageCachePointPlacements.length > 1) {\\n----\\n789 | \\t\\t\\t\\t\\t\\ttype: \\\"message\\\",\\n790 | \\t\\t\\t\\t\\t\\ttokensCovered: 300,\\n791 | \\t\\t\\t\\t\\t},\\n----\\n794 | \\t\\t\\t\\t\\t\\ttype: \\\"message\\\",\\n795 | \\t\\t\\t\\t\\t\\ttokensCovered: 300,\\n796 | \\t\\t\\t\\t\\t},\\n----\\n800 | \\t\\t\\t\\t\\tmodelInfo: multiPointModelInfo,\\n801 | \\t\\t\\t\\t\\tsystemPrompt: \\\"You are a helpful assistant.\\\", // ~10 tokens\\n802 | \\t\\t\\t\\t\\tmessages,\\n----\\n863 | \\t\\t\\t\\t\\t\\ttype: \\\"message\\\",\\n864 | \\t\\t\\t\\t\\t\\ttokensCovered: 300,\\n865 | \\t\\t\\t\\t\\t},\\n----\\n868 | \\t\\t\\t\\t\\t\\ttype: \\\"message\\\",\\n869 | \\t\\t\\t\\t\\t\\ttokensCovered: 300,\\n870 | \\t\\t\\t\\t\\t},\\n----\\n873 | \\t\\t\\t\\t\\t\\ttype: \\\"message\\\",\\n874 | \\t\\t\\t\\t\\t\\ttokensCovered: 300,\\n875 | \\t\\t\\t\\t\\t},\\n----\\n879 | \\t\\t\\t\\t\\tmodelInfo: multiPointModelInfo,\\n880 | \\t\\t\\t\\t\\tsystemPrompt: \\\"You are a helpful assistant.\\\", // ~10 tokens\\n881 | \\t\\t\\t\\t\\tmessages,\\n----\\n929 | \\t\\t\\t\\t// This test verifies that the documentation for Example 5 is correct\\n930 | \\t\\t\\t\\t// In Example 5, the third cache point at index 10 should cover 660 tokens\\n931 | \\t\\t\\t\\t// (260 tokens from messages 7-8 plus 400 tokens from the new messages)\\n932 | \\n----\\n944 | \\t\\t\\t\\t\\tcreateMessage(\\\"assistant\\\", \\\"Deep learning has many applications including...\\\", 160),\\n945 | \\t\\t\\t\\t\\t// New messages with 400 tokens total\\n946 | \\t\\t\\t\\t\\tcreateMessage(\\\"user\\\", \\\"Can you provide a detailed example?\\\", 100),\\n----\\n954 | \\t\\t\\t\\t\\t\\ttype: \\\"message\\\",\\n955 | \\t\\t\\t\\t\\t\\ttokensCovered: 240,\\n956 | \\t\\t\\t\\t\\t},\\n----\\n959 | \\t\\t\\t\\t\\t\\ttype: \\\"message\\\",\\n960 | \\t\\t\\t\\t\\t\\ttokensCovered: 440,\\n961 | \\t\\t\\t\\t\\t},\\n----\\n964 | \\t\\t\\t\\t\\t\\ttype: \\\"message\\\",\\n965 | \\t\\t\\t\\t\\t\\ttokensCovered: 260,\\n966 | \\t\\t\\t\\t\\t},\\n----\\n969 | \\t\\t\\t\\t// In the documentation, the algorithm decides to replace the cache point at index 8\\n970 | \\t\\t\\t\\t// with a new one at index 10, and the tokensCovered value should be 660 tokens\\n971 | \\t\\t\\t\\t// (260 tokens from messages 7-8 plus 400 tokens from the new messages)\\n972 | \\n973 | \\t\\t\\t\\t// However, the actual implementation may behave differently depending on how\\n974 | \\t\\t\\t\\t// it calculates token counts and makes decisions about cache point placement\\n975 | \\n976 | \\t\\t\\t\\t// The important part is that our fix ensures that when a cache point is created,\\n977 | \\t\\t\\t\\t// the tokensCovered value represents all tokens from the previous cache point\\n978 | \\t\\t\\t\\t// to the current cache point, not just the tokens in the new messages\\n979 | \\t\\t\\t})\\n980 | \\n981 | \\t\\t\\tit(\\\"should not combine cache points when new messages have fewer tokens than the smallest combined gap\\\", () => {\\n982 | \\t\\t\\t\\t// This test verifies that when new messages have fewer tokens than the smallest combined gap,\\n983 | \\t\\t\\t\\t// the algorithm keeps all existing cache points and doesn't add a new one\\n----\\n1004 | \\t\\t\\t\\t\\t\\t),\\n1005 | \\t\\t\\t\\t\\t\\t// Small addition (only 50 tokens total)\\n1006 | \\t\\t\\t\\t\\t\\tcreateMessage(\\\"user\\\", \\\"Thanks for the explanation.\\\", 20),\\n----\\n1009 | \\n1010 | \\t\\t\\t\\t\\t// Previous cache point placements with significant token coverage\\n1011 | \\t\\t\\t\\t\\tconst previousCachePointPlacements: CachePointPlacement[] = [\\n----\\n1014 | \\t\\t\\t\\t\\t\\t\\ttype: \\\"message\\\",\\n1015 | \\t\\t\\t\\t\\t\\t\\ttokensCovered: 400, // Significant token coverage\\n1016 | \\t\\t\\t\\t\\t\\t},\\n----\\n1019 | \\t\\t\\t\\t\\t\\t\\ttype: \\\"message\\\",\\n1020 | \\t\\t\\t\\t\\t\\t\\ttokensCovered: 300, // Significant token coverage\\n1021 | \\t\\t\\t\\t\\t\\t},\\n----\\n1024 | \\t\\t\\t\\t\\t\\t\\ttype: \\\"message\\\",\\n1025 | \\t\\t\\t\\t\\t\\t\\ttokensCovered: 300, // Significant token coverage\\n1026 | \\t\\t\\t\\t\\t\\t},\\n----\\n1030 | \\t\\t\\t\\t\\t\\tmodelInfo: multiPointModelInfo,\\n1031 | \\t\\t\\t\\t\\t\\tsystemPrompt: \\\"You are a helpful assistant.\\\", // ~10 tokens\\n1032 | \\t\\t\\t\\t\\t\\tmessages,\\n----\\n1057 | \\n1058 | \\t\\t\\tit(\\\"should make correct decisions based on token counts\\\", () => {\\n1059 | \\t\\t\\t\\t// This test verifies that the algorithm correctly compares token counts\\n1060 | \\t\\t\\t\\t// and makes the right decision about combining cache points\\n1061 | \\n1062 | \\t\\t\\t\\t// Create messages with a variety of token counts\\n1063 | \\t\\t\\t\\tconst messages = [\\n----\\n1081 | \\t\\t\\t\\t\\t\\ttype: \\\"message\\\",\\n1082 | \\t\\t\\t\\t\\t\\ttokensCovered: 400,\\n1083 | \\t\\t\\t\\t\\t},\\n----\\n1086 | \\t\\t\\t\\t\\t\\ttype: \\\"message\\\",\\n1087 | \\t\\t\\t\\t\\t\\ttokensCovered: 150,\\n1088 | \\t\\t\\t\\t\\t},\\n----\\n1091 | \\t\\t\\t\\t\\t\\ttype: \\\"message\\\",\\n1092 | \\t\\t\\t\\t\\t\\ttokensCovered: 150,\\n1093 | \\t\\t\\t\\t\\t},\\n----\\n\\n# src/api/providers/__tests__/openai-usage-tracking.spec.ts\\n 28 | \\t\\t\\t\\t\\t\\t\\t\\tusage: {\\n 29 | \\t\\t\\t\\t\\t\\t\\t\\t\\tprompt_tokens: 10,\\n 30 | \\t\\t\\t\\t\\t\\t\\t\\t\\tcompletion_tokens: 5,\\n 31 | \\t\\t\\t\\t\\t\\t\\t\\t\\ttotal_tokens: 15,\\n 32 | \\t\\t\\t\\t\\t\\t\\t\\t},\\n----\\n 47 | \\t\\t\\t\\t\\t\\t\\t\\t\\tusage: {\\n 48 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\tprompt_tokens: 10,\\n 49 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\tcompletion_tokens: 2,\\n 50 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\ttotal_tokens: 12,\\n 51 | \\t\\t\\t\\t\\t\\t\\t\\t\\t},\\n----\\n 62 | \\t\\t\\t\\t\\t\\t\\t\\t\\tusage: {\\n 63 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\tprompt_tokens: 10,\\n 64 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\tcompletion_tokens: 4,\\n 65 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\ttotal_tokens: 14,\\n 66 | \\t\\t\\t\\t\\t\\t\\t\\t\\t},\\n----\\n 77 | \\t\\t\\t\\t\\t\\t\\t\\t\\tusage: {\\n 78 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\tprompt_tokens: 10,\\n 79 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\tcompletion_tokens: 5,\\n 80 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\ttotal_tokens: 15,\\n 81 | \\t\\t\\t\\t\\t\\t\\t\\t\\t},\\n----\\n155 | \\t\\t\\t\\t\\t\\tchoices: [{ message: { role: \\\"assistant\\\", content: \\\"Test response\\\" } }],\\n156 | \\t\\t\\t\\t\\t\\tusage: { prompt_tokens: 10, completion_tokens: 5, total_tokens: 15 },\\n157 | \\t\\t\\t\\t\\t}\\n----\\n177 | \\t\\t\\t\\t\\t\\t\\tusage: {\\n178 | \\t\\t\\t\\t\\t\\t\\t\\tprompt_tokens: 10,\\n179 | \\t\\t\\t\\t\\t\\t\\t\\tcompletion_tokens: 5,\\n180 | \\t\\t\\t\\t\\t\\t\\t\\ttotal_tokens: 15,\\n181 | \\t\\t\\t\\t\\t\\t\\t},\\n----\\n\\n# src/api/providers/__tests__/unbound.spec.ts\\n 82 | \\t\\t\\t\\t\\t\\t\\t\\t\\tusage: {\\n 83 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\tprompt_tokens: 10,\\n 84 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\tcompletion_tokens: 5,\\n 85 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\ttotal_tokens: 15,\\n 86 | \\t\\t\\t\\t\\t\\t\\t\\t\\t},\\n----\\n 91 | \\t\\t\\t\\t\\t\\t\\t\\t\\tusage: {\\n 92 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\tprompt_tokens: 8,\\n 93 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\tcompletion_tokens: 4,\\n 94 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\ttotal_tokens: 12,\\n 95 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\tcache_creation_input_tokens: 3,\\n 96 | \\t\\t\\t\\t\\t\\t\\t\\t\\t\\tcache_read_input_tokens: 2,\\n 97 | \\t\\t\\t\\t\\t\\t\\t\\t\\t},\\n----\\n232 | \\t\\t\\t\\t\\ttemperature: 0,\\n233 | \\t\\t\\t\\t\\tmax_tokens: 8192,\\n234 | \\t\\t\\t\\t}),\\n----\\n253 | \\n254 | \\t\\tit(\\\"should not set max_tokens for non-Anthropic models\\\", async () => {\\n255 | \\t\\t\\tmockCreate.mockClear()\\n----\\n277 | \\n278 | \\t\\t\\texpect(mockCreate.mock.calls[0][0]).not.toHaveProperty(\\\"max_tokens\\\")\\n279 | \\t\\t})\\n----\\n\\n# src/api/providers/__tests__/anthropic-vertex.spec.ts\\n 21 | \\t\\t\\t\\t\\t\\tusage: {\\n 22 | \\t\\t\\t\\t\\t\\t\\tinput_tokens: 10,\\n 23 | \\t\\t\\t\\t\\t\\t\\toutput_tokens: 5,\\n 24 | \\t\\t\\t\\t\\t\\t},\\n----\\n 32 | \\t\\t\\t\\t\\t\\t\\t\\tusage: {\\n 33 | \\t\\t\\t\\t\\t\\t\\t\\t\\tinput_tokens: 10,\\n 34 | \\t\\t\\t\\t\\t\\t\\t\\t\\toutput_tokens: 5,\\n 35 | \\t\\t\\t\\t\\t\\t\\t\\t},\\n----\\n 95 | \\t\\t\\t\\t\\t\\tusage: {\\n 96 | \\t\\t\\t\\t\\t\\t\\tinput_tokens: 10,\\n 97 | \\t\\t\\t\\t\\t\\t\\toutput_tokens: 0,\\n 98 | \\t\\t\\t\\t\\t\\t},\\n----\\n118 | \\t\\t\\t\\t\\tusage: {\\n119 | \\t\\t\\t\\t\\t\\toutput_tokens: 5,\\n120 | \\t\\t\\t\\t\\t},\\n----\\n164 | \\t\\t\\t\\tmodel: \\\"claude-3-5-sonnet-v2@20241022\\\",\\n165 | \\t\\t\\t\\tmax_tokens: 8192,\\n166 | \\t\\t\\t\\ttemperature: 0,\\n----\\n284 | \\t\\t\\t\\t\\t\\tusage: {\\n285 | \\t\\t\\t\\t\\t\\t\\tinput_tokens: 10,\\n286 | \\t\\t\\t\\t\\t\\t\\toutput_tokens: 0,\\n287 | \\t\\t\\t\\t\\t\\t\\tcache_creation_input_tokens: 3,\\n288 | \\t\\t\\t\\t\\t\\t\\tcache_read_input_tokens: 2,\\n289 | \\t\\t\\t\\t\\t\\t},\\n----\\n309 | \\t\\t\\t\\t\\tusage: {\\n310 | \\t\\t\\t\\t\\t\\toutput_tokens: 5,\\n311 | \\t\\t\\t\\t\\t},\\n----\\n419 | \\t\\t\\t\\t\\t\\tusage: {\\n420 | \\t\\t\\t\\t\\t\\t\\tinput_tokens: 10,\\n421 | \\t\\t\\t\\t\\t\\t\\toutput_tokens: 0,\\n422 | \\t\\t\\t\\t\\t\\t\\tcache_creation_input_tokens: 5,\\n423 | \\t\\t\\t\\t\\t\\t\\tcache_read_input_tokens: 3,\\n424 | \\t\\t\\t\\t\\t\\t},\\n----\\n484 | \\t\\t\\t\\t\\t\\tusage: {\\n485 | \\t\\t\\t\\t\\t\\t\\tinput_tokens: 10,\\n486 | \\t\\t\\t\\t\\t\\t\\toutput_tokens: 0,\\n487 | \\t\\t\\t\\t\\t\\t},\\n----\\n618 | \\t\\t\\t\\tmodel: \\\"claude-3-5-sonnet-v2@20241022\\\",\\n619 | \\t\\t\\t\\tmax_tokens: 8192,\\n620 | \\t\\t\\t\\ttemperature: 0,\\n----\\n752 | \\n753 | \\t\\t\\t// Test with default thinking budget (80% of max tokens)\\n754 | \\t\\t\\tconst handlerWithDefaultBudget = new AnthropicVertexHandler({\\n----\\n767 | \\t\\t\\t\\tvertexRegion: \\\"us-central1\\\",\\n768 | \\t\\t\\t\\tmodelMaxTokens: 1000, // This would result in 800 tokens for thinking, but minimum is 1024\\n769 | \\t\\t\\t})\\n----\\n789 | \\t\\t\\t\\t\\t\\tmodel: options.model,\\n790 | \\t\\t\\t\\t\\t\\tusage: { input_tokens: 10, output_tokens: 5 },\\n791 | \\t\\t\\t\\t\\t}\\n----\\n794 | \\t\\t\\t\\t\\tasync *[Symbol.asyncIterator]() {\\n795 | \\t\\t\\t\\t\\t\\tyield { type: \\\"message_start\\\", message: { usage: { input_tokens: 10, output_tokens: 5 } } }\\n796 | \\t\\t\\t\\t\\t},\\n----\\n806 | \\t\\t\\t\\texpect.objectContaining({\\n807 | \\t\\t\\t\\t\\tthinking: { type: \\\"enabled\\\", budget_tokens: 4096 },\\n808 | \\t\\t\\t\\t\\ttemperature: 1.0, // Thinking requires temperature 1.0\\n----\\n\\n# src/core/prompts/tools/codebase-search.ts\\n 12 | \\n 13 | Example: Searching for functions related to user authentication\\n 14 | \\n----\\n\\n# src/core/task/__tests__/Task.test.ts\\n185 | \\t\\t\\t\\t\\t\\t\\t\\ttask: \\\"historical task\\\",\\n186 | \\t\\t\\t\\t\\t\\t\\t\\ttokensIn: 100,\\n187 | \\t\\t\\t\\t\\t\\t\\t\\ttokensOut: 200,\\n188 | \\t\\t\\t\\t\\t\\t\\t\\tcacheWrites: 0,\\n----\\n253 | \\t\\t\\t\\ttask: \\\"historical task\\\",\\n254 | \\t\\t\\t\\ttokensIn: 100,\\n255 | \\t\\t\\t\\ttokensOut: 200,\\n256 | \\t\\t\\t\\tcacheWrites: 0,\\n----\\n615 | \\t\\t\\t\\t\\t\\ttext: JSON.stringify({\\n616 | \\t\\t\\t\\t\\t\\t\\ttokensIn: 100,\\n617 | \\t\\t\\t\\t\\t\\t\\ttokensOut: 50,\\n618 | \\t\\t\\t\\t\\t\\t\\tcacheWrites: 0,\\n----\\n740 | \\t\\t\\t\\t\\t\\ttext: JSON.stringify({\\n741 | \\t\\t\\t\\t\\t\\t\\ttokensIn: 100,\\n742 | \\t\\t\\t\\t\\t\\t\\ttokensOut: 50,\\n743 | \\t\\t\\t\\t\\t\\t\\tcacheWrites: 0,\\n----\\n\\n# src/core/prompts/instructions/create-mcp-server.ts\\n 11 | \\n 12 | When creating MCP servers, it's important to understand that they operate in a non-interactive environment. The server cannot initiate OAuth flows, open browser windows, or prompt for user input during runtime. All credentials and authentication tokens must be provided upfront through environment variables in the MCP settings configuration. For example, Spotify's API uses OAuth to get a refresh token for the user, but the MCP server cannot initiate this flow. While you can walk the user through [truncated...]\\n 13 | \\n----\\n 76 | \\t\\t\\t\\t...\\n 77 | \\t\\t\\t\\t\\\"type\\\": \\\"module\\\", // added by default, uses ES module syntax (import/export) rather than CommonJS (require/module.exports) (Important to know if you create additional scripts in this server repository like a get-refresh-token.js script)\\n 78 | \\t\\t\\t\\t\\\"scripts\\\": {\\n----\\n\\n# src/core/task/Task.ts\\n 24 | import { TelemetryService } from \\\"@roo-code/telemetry\\\"\\n 25 | import { CloudService } from \\\"@roo-code/cloud\\\"\\n 26 | \\n----\\n 96 | \\ttaskSpawned: [taskId: string]\\n 97 | \\ttaskCompleted: [taskId: string, tokenUsage: TokenUsage, toolUsage: ToolUsage]\\n 98 | \\ttaskTokenUsageUpdated: [taskId: string, tokenUsage: TokenUsage]\\n 99 | \\ttaskToolFailed: [taskId: string, tool: ToolName, error: string]\\n----\\n346 | \\n347 | \\t\\tconst shouldCaptureMessage = message.partial !== true && CloudService.isEnabled()\\n348 | \\n349 | \\t\\tif (shouldCaptureMessage) {\\n350 | \\t\\t\\tCloudService.instance.captureEvent({\\n351 | \\t\\t\\t\\tevent: TelemetryEventName.TASK_MESSAGE,\\n----\\n366 | \\n367 | \\t\\tconst shouldCaptureMessage = message.partial !== true && CloudService.isEnabled()\\n368 | \\n369 | \\t\\tif (shouldCaptureMessage) {\\n370 | \\t\\t\\tCloudService.instance.captureEvent({\\n371 | \\t\\t\\t\\tevent: TelemetryEventName.TASK_MESSAGE,\\n----\\n384 | \\n385 | \\t\\t\\tconst { historyItem, tokenUsage } = await taskMetadata({\\n386 | \\t\\t\\t\\tmessages: this.clineMessages,\\n----\\n392 | \\n393 | \\t\\t\\tthis.emit(\\\"taskTokenUsageUpdated\\\", this.taskId, tokenUsage)\\n394 | \\n----\\n1246 | \\t\\t\\t\\t\\t...JSON.parse(this.clineMessages[lastApiReqIndex].text || \\\"{}\\\"),\\n1247 | \\t\\t\\t\\t\\ttokensIn: inputTokens,\\n1248 | \\t\\t\\t\\t\\ttokensOut: outputTokens,\\n1249 | \\t\\t\\t\\t\\tcacheWrites: cacheWriteTokens,\\n----\\n1678 | \\t\\tif (contextTokens) {\\n1679 | \\t\\t\\t// Default max tokens value for thinking models when no specific\\n1680 | \\t\\t\\t// value is set.\\n----\\n\\n# src/api/providers/glama.ts\\n 51 | \\n 52 | \\t\\t// Required by Anthropic; other providers default to max tokens allowed.\\n 53 | \\t\\tlet maxTokens: number | undefined\\n----\\n 60 | \\t\\t\\tmodel: modelId,\\n 61 | \\t\\t\\tmax_tokens: maxTokens,\\n 62 | \\t\\t\\tmessages: openAiMessages,\\n----\\n 90 | \\t\\t\\t\\t// In case of an interrupted request, we need to wait for the upstream API to finish processing the request\\n 91 | \\t\\t\\t\\t// before we can fetch information about the token usage and cost.\\n 92 | \\t\\t\\t\\tconst response = await axios.get(\\n----\\n 98 | \\n 99 | \\t\\t\\t\\tif (completionRequest.tokenUsage && completionRequest.totalCostUsd) {\\n100 | \\t\\t\\t\\t\\tyield {\\n101 | \\t\\t\\t\\t\\t\\ttype: \\\"usage\\\",\\n102 | \\t\\t\\t\\t\\t\\tcacheWriteTokens: completionRequest.tokenUsage.cacheCreationInputTokens,\\n103 | \\t\\t\\t\\t\\t\\tcacheReadTokens: completionRequest.tokenUsage.cacheReadInputTokens,\\n104 | \\t\\t\\t\\t\\t\\tinputTokens: completionRequest.tokenUsage.promptTokens,\\n105 | \\t\\t\\t\\t\\t\\toutputTokens: completionRequest.tokenUsage.completionTokens,\\n106 | \\t\\t\\t\\t\\t\\ttotalCost: parseFloat(completionRequest.totalCostUsd),\\n----\\n132 | \\t\\t\\tif (modelId.startsWith(\\\"anthropic/\\\")) {\\n133 | \\t\\t\\t\\trequestOptions.max_tokens = info.maxTokens\\n134 | \\t\\t\\t}\\n----\\n\\n# src/api/transform/reasoning.ts\\n 12 | \\teffort?: ReasoningEffort\\n 13 | \\tmax_tokens?: number\\n 14 | \\texclude?: boolean\\n----\\n 36 | \\tshouldUseReasoningBudget({ model, settings })\\n 37 | \\t\\t? { max_tokens: reasoningBudget }\\n 38 | \\t\\t: shouldUseReasoningEffort({ model, settings })\\n----\\n 46 | }: GetModelReasoningOptions): AnthropicReasoningParams | undefined =>\\n 47 | \\tshouldUseReasoningBudget({ model, settings }) ? { type: \\\"enabled\\\", budget_tokens: reasoningBudget! } : undefined\\n 48 | \\n----\\n\\n# src/api/providers/openrouter.ts\\n 32 | \\tinclude_reasoning?: boolean\\n 33 | \\t// https://openrouter.ai/docs/use-cases/reasoning-tokens\\n 34 | \\treasoning?: OpenRouterReasoningParams\\n----\\n 40 | interface CompletionUsage {\\n 41 | \\tcompletion_tokens?: number\\n 42 | \\tcompletion_tokens_details?: {\\n 43 | \\t\\treasoning_tokens?: number\\n 44 | \\t}\\n 45 | \\tprompt_tokens?: number\\n 46 | \\tprompt_tokens_details?: {\\n 47 | \\t\\tcached_tokens?: number\\n 48 | \\t}\\n 49 | \\ttotal_tokens?: number\\n 50 | \\tcost?: number\\n----\\n 81 | \\n 82 | \\t\\t// OpenRouter sends reasoning tokens by default for Gemini 2.5 Pro\\n 83 | \\t\\t// Preview even if you don't request them. This is not the default for\\n----\\n116 | \\t\\t\\tmodel: modelId,\\n117 | \\t\\t\\t...(maxTokens && maxTokens > 0 && { max_tokens: maxTokens }),\\n118 | \\t\\t\\ttemperature,\\n----\\n165 | \\t\\t\\t\\ttype: \\\"usage\\\",\\n166 | \\t\\t\\t\\tinputTokens: lastUsage.prompt_tokens || 0,\\n167 | \\t\\t\\t\\toutputTokens: lastUsage.completion_tokens || 0,\\n168 | \\t\\t\\t\\t// Waiting on OpenRouter to figure out what this represents in the Gemini case\\n169 | \\t\\t\\t\\t// and how to best support it.\\n170 | \\t\\t\\t\\t// cacheReadTokens: lastUsage.prompt_tokens_details?.cached_tokens,\\n171 | \\t\\t\\t\\treasoningTokens: lastUsage.completion_tokens_details?.reasoning_tokens,\\n172 | \\t\\t\\t\\ttotalCost: (lastUsage.is_byok ? BYOK_COST_MULTIPLIER : 1) * (lastUsage.cost || 0),\\n----\\n219 | \\t\\t\\tmodel: modelId,\\n220 | \\t\\t\\tmax_tokens: maxTokens,\\n221 | \\t\\t\\ttemperature,\\n----\\n\\n# src/utils/__tests__/cost.spec.ts\\n 13 | \\t\\t\\tsupportsPromptCache: true,\\n 14 | \\t\\t\\tinputPrice: 3.0, // $3 per million tokens\\n 15 | \\t\\t\\toutputPrice: 15.0, // $15 per million tokens\\n 16 | \\t\\t\\tcacheWritesPrice: 3.75, // $3.75 per million tokens\\n 17 | \\t\\t\\tcacheReadsPrice: 0.3, // $0.30 per million tokens\\n 18 | \\t\\t}\\n----\\n 70 | \\n 71 | \\t\\tit(\\\"should handle zero tokens\\\", () => {\\n 72 | \\t\\t\\tconst cost = calculateApiCostAnthropic(mockModelInfo, 0, 0, 0, 0)\\n----\\n106 | \\t\\t\\tsupportsPromptCache: true,\\n107 | \\t\\t\\tinputPrice: 3.0, // $3 per million tokens\\n108 | \\t\\t\\toutputPrice: 15.0, // $15 per million tokens\\n109 | \\t\\t\\tcacheWritesPrice: 3.75, // $3.75 per million tokens\\n110 | \\t\\t\\tcacheReadsPrice: 0.3, // $0.30 per million tokens\\n111 | \\t\\t}\\n----\\n163 | \\n164 | \\t\\tit(\\\"should handle zero tokens\\\", () => {\\n165 | \\t\\t\\tconst cost = calculateApiCostOpenAI(mockModelInfo, 0, 0, 0, 0)\\n----\\n\\n# src/utils/__tests__/git.spec.ts\\n 98 | \\t\\t\\t\\tsubject: \\\"fix: test commit\\\",\\n 99 | \\t\\t\\t\\tauthor: \\\"John Doe\\\",\\n100 | \\t\\t\\t\\tdate: \\\"2024-01-06\\\",\\n----\\n162 | \\t\\t\\t\\t[\\n163 | \\t\\t\\t\\t\\t'git log -n 10 --format=\\\"%H%n%h%n%s%n%an%n%ad\\\" --date=short --author-date-order abc123',\\n164 | \\t\\t\\t\\t\\t{ stdout: mockCommitData, stderr: \\\"\\\" },\\n----\\n185 | \\t\\t\\t\\tsubject: \\\"fix: test commit\\\",\\n186 | \\t\\t\\t\\tauthor: \\\"John Doe\\\",\\n187 | \\t\\t\\t\\tdate: \\\"2024-01-06\\\",\\n----\\n\\n# src/api/providers/gemini.ts\\n 7 | } from \\\"@google/genai\\\"\\n 8 | import type { JWTInput } from \\\"google-auth-library\\\"\\n 9 | \\n----\\n179 | \\t\\t\\tif (response.totalTokens === undefined) {\\n180 | \\t\\t\\t\\tconsole.warn(\\\"Gemini token counting returned undefined, using fallback\\\")\\n181 | \\t\\t\\t\\treturn super.countTokens(content)\\n----\\n185 | \\t\\t} catch (error) {\\n186 | \\t\\t\\tconsole.warn(\\\"Gemini token counting failed, using fallback\\\", error)\\n187 | \\t\\t\\treturn super.countTokens(content)\\n----\\n209 | \\n210 | \\t\\t// If there's tiered pricing then adjust the input and output token prices\\n211 | \\t\\t// based on the input tokens used.\\n212 | \\t\\tif (info.tiers) {\\n----\\n221 | \\n222 | \\t\\t// Subtract the cached input tokens from the total input tokens.\\n223 | \\t\\tconst uncachedInputTokens = inputTokens - cacheReadTokens\\n----\\n230 | \\n231 | \\t\\tconst trace: Record = {\\n232 | \\t\\t\\tinput: { price: inputPrice, tokens: uncachedInputTokens, cost: inputTokensCost },\\n233 | \\t\\t\\toutput: { price: outputPrice, tokens: outputTokens, cost: outputTokensCost },\\n234 | \\t\\t}\\n----\\n236 | \\t\\tif (cacheReadTokens > 0) {\\n237 | \\t\\t\\ttrace.cacheRead = { price: cacheReadsPrice, tokens: cacheReadTokens, cost: cacheReadCost }\\n238 | \\t\\t}\\n----\\n\\n# src/api/providers/lite-llm.ts\\n 46 | \\n 47 | \\t\\t// Required by some providers; others default to max tokens allowed\\n 48 | \\t\\tlet maxTokens: number | undefined = info.maxTokens ?? undefined\\n----\\n 51 | \\t\\t\\tmodel: modelId,\\n 52 | \\t\\t\\tmax_tokens: maxTokens,\\n 53 | \\t\\t\\tmessages: openAiMessages,\\n----\\n 84 | \\t\\t\\t\\t\\ttype: \\\"usage\\\",\\n 85 | \\t\\t\\t\\t\\tinputTokens: lastUsage.prompt_tokens || 0,\\n 86 | \\t\\t\\t\\t\\toutputTokens: lastUsage.completion_tokens || 0,\\n 87 | \\t\\t\\t\\t\\tcacheWriteTokens: lastUsage.cache_creation_input_tokens || 0,\\n 88 | \\t\\t\\t\\t\\tcacheReadTokens: lastUsage.prompt_tokens_details?.cached_tokens || 0,\\n 89 | \\t\\t\\t\\t}\\n----\\n121 | \\n122 | \\t\\t\\trequestOptions.max_tokens = info.maxTokens\\n123 | \\n----\\n136 | interface LiteLLMUsage extends OpenAI.CompletionUsage {\\n137 | \\tcache_creation_input_tokens?: number\\n138 | }\\n----\\n\\n# src/api/providers/requesty.ts\\n 19 | // Requesty usage includes an extra field for Anthropic use cases.\\n 20 | // Safely cast the prompt token details section to the appropriate structure.\\n 21 | interface RequestyUsage extends OpenAI.CompletionUsage {\\n 22 | \\tprompt_tokens_details?: {\\n 23 | \\t\\tcaching_tokens?: number\\n 24 | \\t\\tcached_tokens?: number\\n 25 | \\t}\\n----\\n 76 | \\t\\tconst requestyUsage = usage as RequestyUsage\\n 77 | \\t\\tconst inputTokens = requestyUsage?.prompt_tokens || 0\\n 78 | \\t\\tconst outputTokens = requestyUsage?.completion_tokens || 0\\n 79 | \\t\\tconst cacheWriteTokens = requestyUsage?.prompt_tokens_details?.caching_tokens || 0\\n 80 | \\t\\tconst cacheReadTokens = requestyUsage?.prompt_tokens_details?.cached_tokens || 0\\n 81 | \\t\\tconst totalCost = modelInfo\\n----\\n102 | \\t\\t\\tinfo,\\n103 | \\t\\t\\tmaxTokens: max_tokens,\\n104 | \\t\\t\\ttemperature,\\n----\\n116 | \\t\\t\\tmodel,\\n117 | \\t\\t\\tmax_tokens,\\n118 | \\t\\t\\ttemperature,\\n----\\n150 | \\tasync completePrompt(prompt: string): Promise {\\n151 | \\t\\tconst { id: model, maxTokens: max_tokens, temperature } = await this.fetchModel()\\n152 | \\n----\\n156 | \\t\\t\\tmodel,\\n157 | \\t\\t\\tmax_tokens,\\n158 | \\t\\t\\tmessages: openAiMessages,\\n----\\n\\n# src/api/providers/anthropic.ts\\n 29 | \\t\\tconst apiKeyFieldName =\\n 30 | \\t\\t\\tthis.options.anthropicBaseUrl && this.options.anthropicUseAuthToken ? \\\"authToken\\\" : \\\"apiKey\\\"\\n 31 | \\n----\\n 75 | \\t\\t\\t\\t\\t\\tmodel: modelId,\\n 76 | \\t\\t\\t\\t\\t\\tmax_tokens: maxTokens ?? ANTHROPIC_DEFAULT_MAX_TOKENS,\\n 77 | \\t\\t\\t\\t\\t\\ttemperature,\\n----\\n124 | \\t\\t\\t\\t\\tmodel: modelId,\\n125 | \\t\\t\\t\\t\\tmax_tokens: maxTokens ?? ANTHROPIC_DEFAULT_MAX_TOKENS,\\n126 | \\t\\t\\t\\t\\ttemperature,\\n----\\n142 | \\t\\t\\t\\t\\t\\ttype: \\\"usage\\\",\\n143 | \\t\\t\\t\\t\\t\\tinputTokens: usage.input_tokens || 0,\\n144 | \\t\\t\\t\\t\\t\\toutputTokens: usage.output_tokens || 0,\\n145 | \\t\\t\\t\\t\\t\\tcacheWriteTokens: usage.cache_creation_input_tokens || undefined,\\n146 | \\t\\t\\t\\t\\t\\tcacheReadTokens: usage.cache_read_input_tokens || undefined,\\n147 | \\t\\t\\t\\t\\t}\\n----\\n151 | \\t\\t\\t\\tcase \\\"message_delta\\\":\\n152 | \\t\\t\\t\\t\\t// Tells us stop_reason, stop_sequence, and output tokens\\n153 | \\t\\t\\t\\t\\t// along the way and at the end of the message.\\n----\\n156 | \\t\\t\\t\\t\\t\\tinputTokens: 0,\\n157 | \\t\\t\\t\\t\\t\\toutputTokens: chunk.usage.output_tokens || 0,\\n158 | \\t\\t\\t\\t\\t}\\n----\\n231 | \\t\\t\\tmodel,\\n232 | \\t\\t\\tmax_tokens: ANTHROPIC_DEFAULT_MAX_TOKENS,\\n233 | \\t\\t\\tthinking: undefined,\\n----\\n243 | \\t/**\\n244 | \\t * Counts tokens for the given content using Anthropic's API\\n245 | \\t *\\n246 | \\t * @param content The content blocks to count tokens for\\n247 | \\t * @returns A promise resolving to the token count\\n248 | \\t */\\n----\\n258 | \\n259 | \\t\\t\\treturn response.input_tokens\\n260 | \\t\\t} catch (error) {\\n261 | \\t\\t\\t// Log error but fallback to tiktoken estimation\\n262 | \\t\\t\\tconsole.warn(\\\"Anthropic token counting failed, using fallback\\\", error)\\n263 | \\n----\\n\\n# src/api/providers/deepseek.ts\\n 32 | \\t\\t\\ttype: \\\"usage\\\",\\n 33 | \\t\\t\\tinputTokens: usage?.prompt_tokens || 0,\\n 34 | \\t\\t\\toutputTokens: usage?.completion_tokens || 0,\\n 35 | \\t\\t\\tcacheWriteTokens: usage?.prompt_tokens_details?.cache_miss_tokens,\\n 36 | \\t\\t\\tcacheReadTokens: usage?.prompt_tokens_details?.cached_tokens,\\n 37 | \\t\\t}\\n----\\n\\n# src/api/providers/openai.ts\\n162 | \\t\\t\\tif (this.options.includeMaxTokens) {\\n163 | \\t\\t\\t\\trequestOptions.max_tokens = modelInfo.maxTokens\\n164 | \\t\\t\\t}\\n----\\n241 | \\t\\t\\ttype: \\\"usage\\\",\\n242 | \\t\\t\\tinputTokens: usage?.prompt_tokens || 0,\\n243 | \\t\\t\\toutputTokens: usage?.completion_tokens || 0,\\n244 | \\t\\t\\tcacheWriteTokens: usage?.cache_creation_input_tokens || undefined,\\n245 | \\t\\t\\tcacheReadTokens: usage?.cache_read_input_tokens || undefined,\\n246 | \\t\\t}\\n----\\n347 | \\t\\t\\t\\t\\ttype: \\\"usage\\\",\\n348 | \\t\\t\\t\\t\\tinputTokens: chunk.usage.prompt_tokens || 0,\\n349 | \\t\\t\\t\\t\\toutputTokens: chunk.usage.completion_tokens || 0,\\n350 | \\t\\t\\t\\t}\\n----\\n\\n# src/api/providers/xai.ts\\n 52 | \\t\\t\\tmodel: modelId,\\n 53 | \\t\\t\\tmax_tokens: modelInfo.maxTokens,\\n 54 | \\t\\t\\ttemperature: this.options.modelTemperature ?? XAI_DEFAULT_TEMPERATURE,\\n----\\n 78 | \\t\\t\\tif (chunk.usage) {\\n 79 | \\t\\t\\t\\t// Extract detailed token information if available\\n 80 | \\t\\t\\t\\t// First check for prompt_tokens_details structure (real API response)\\n 81 | \\t\\t\\t\\tconst promptDetails = \\\"prompt_tokens_details\\\" in chunk.usage ? chunk.usage.prompt_tokens_details : null;\\n 82 | \\t\\t\\t\\tconst cachedTokens = promptDetails && \\\"cached_tokens\\\" in promptDetails ? promptDetails.cached_tokens : 0;\\n 83 | \\n 84 | \\t\\t\\t\\t// Fall back to direct fields in usage (used in test mocks)\\n 85 | \\t\\t\\t\\tconst readTokens = cachedTokens || (\\\"cache_read_input_tokens\\\" in chunk.usage ? (chunk.usage as any).cache_read_input_tokens : 0);\\n 86 | \\t\\t\\t\\tconst writeTokens = \\\"cache_creation_input_tokens\\\" in chunk.usage ? (chunk.usage as any).cache_creation_input_tokens : 0;\\n 87 | \\n----\\n 89 | \\t\\t\\t\\t\\ttype: \\\"usage\\\",\\n 90 | \\t\\t\\t\\t\\tinputTokens: chunk.usage.prompt_tokens || 0,\\n 91 | \\t\\t\\t\\t\\toutputTokens: chunk.usage.completion_tokens || 0,\\n 92 | \\t\\t\\t\\t\\tcacheReadTokens: readTokens,\\n----\\n\\nLoading...\"}" -} -[2025-06-13T05:00:03.473Z] { - "ts": 1749790803442, - "type": "ask", - "ask": "resume_task" -} -[2025-06-13T05:00:14.579Z] { - "ts": 1749790814550, - "type": "say", - "say": "text", - "text": "Fix the following GitHub issue:\n\nRepository: RooCodeInc/Roo-Code\nIssue #123: Fix authentication bug in login flow\n\nDescription:\nUsers are experiencing issues logging in with OAuth providers. The token refresh mechanism seems to be failing intermittently, causing users to be logged out unexpectedly.\n\nSteps to reproduce:\n1. Login with GitHub OAuth\n2. Wait for token to expire\n3. Try to make an authenticated request\n4. User gets logged out instead of token being refreshed\n\nLabels: bug, authentication, oauth\n\nPlease analyze the issue, understand what needs to be fixed, and implement a solution." -} -[2025-06-13T05:00:14.629Z] taskStarted -> 895eb50e-d7d0-4aad-9b96-f37f457e15ad -[2025-06-13T05:00:14.659Z] { - "ts": 1749790814630, - "type": "say", - "say": "api_req_started", - "text": "{\"request\":\"\\nFix the following GitHub issue:\\n\\nRepository: RooCodeInc/Roo-Code\\nIssue #123: Fix authentication bug in login flow\\n\\nDescription:\\nUsers are experiencing issues logging in with OAuth providers. The token refresh mechanism seems to be failing intermittently, causing users to be logged out unexpectedly.\\n\\nSteps to reproduce:\\n1. Login with GitHub OAuth\\n2. Wait for token to expire\\n3. Try to make an authenticated request\\n4. User gets logged out instead of token being refreshed\\n\\nLabels: bug, authentication, oauth\\n\\nPlease analyze the issue, understand what needs to be fixed, and implement a solution.\\n\\n\\nLoading...\"}" -} From 84b8a80c437ded4df3ae6a726a6b84fed5313564 Mon Sep 17 00:00:00 2001 From: cte Date: Mon, 16 Jun 2025 11:29:42 -0700 Subject: [PATCH 16/19] Fix git permissions issues --- apps/roomote/Dockerfile.worker | 22 ++++++---- apps/roomote/docker-compose.yml | 2 +- apps/roomote/src/lib/controller.ts | 12 +++--- apps/roomote/src/lib/jobs/fixGitHubIssue.ts | 4 +- apps/roomote/src/lib/worker.ts | 46 +++++++++++++-------- 5 files changed, 52 insertions(+), 34 deletions(-) diff --git a/apps/roomote/Dockerfile.worker b/apps/roomote/Dockerfile.worker index 8914a939d6..f8e3578ac1 100644 --- a/apps/roomote/Dockerfile.worker +++ b/apps/roomote/Dockerfile.worker @@ -1,4 +1,5 @@ # docker compose build worker +# Note: Requires $GH_TOKEN to be set as build argument. FROM roomote-base AS base @@ -25,7 +26,20 @@ RUN mkdir -p /roo/.vscode \ && code --no-sandbox --user-data-dir /roo/.vscode --install-extension csstools.postcss \ && code --no-sandbox --user-data-dir /roo/.vscode --install-extension RooVeterinaryInc.roo-cline +# Clone repo (requires $GH_TOKEN) +ARG GH_TOKEN +ENV GH_TOKEN=${GH_TOKEN} +WORKDIR /roo/repos +RUN git config --global user.email "chris@roocode.com" +RUN git config --global user.name "Roo Code" +RUN git config --global credential.helper store +RUN echo "https://oauth2:${GH_TOKEN}@github.com" > ~/.git-credentials +RUN gh repo clone RooCodeInc/Roo-Code +WORKDIR /roo/repos/Roo-Code +RUN pnpm install + # Install dependencies +WORKDIR /roo COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./ COPY packages/config-eslint/package.json ./packages/config-eslint/ COPY packages/config-typescript/package.json ./packages/config-typescript/ @@ -42,14 +56,6 @@ COPY packages/config-typescript ./packages/config-typescript/ COPY packages/types ./packages/types/ COPY packages/ipc ./packages/ipc/ -# Clone repo (requires $GH_TOKEN) -ARG GH_TOKEN -ENV GH_TOKEN=${GH_TOKEN} -WORKDIR /roo/repos -RUN gh repo clone RooCodeInc/Roo-Code -WORKDIR /roo/repos/Roo-Code -RUN pnpm install - WORKDIR /roo/apps/roomote ENV NODE_ENV=production CMD ["pnpm", "worker"] diff --git a/apps/roomote/docker-compose.yml b/apps/roomote/docker-compose.yml index f90947d79f..1a6a5f0b83 100644 --- a/apps/roomote/docker-compose.yml +++ b/apps/roomote/docker-compose.yml @@ -78,10 +78,10 @@ services: env_file: - .env environment: + - HOST_EXECUTION_METHOD=docker - DATABASE_URL=postgresql://postgres:password@db:5432/cloud_agents - REDIS_URL=redis://redis:6379 - NODE_ENV=production - - HOST_EXECUTION_METHOD=docker volumes: - /var/run/docker.sock:/var/run/docker.sock - /tmp/roomote:/var/log/roomote diff --git a/apps/roomote/src/lib/controller.ts b/apps/roomote/src/lib/controller.ts index adb863ecd8..293e8d844d 100644 --- a/apps/roomote/src/lib/controller.ts +++ b/apps/roomote/src/lib/controller.ts @@ -103,14 +103,16 @@ export class WorkerController { stdio: ["ignore", "pipe", "pipe"], }) - const logStream = fs.createWriteStream("/tmp/roomote-worker.log", { flags: "a" }) - if (childProcess.stdout) { - childProcess.stdout.pipe(logStream) + childProcess.stdout.on("data", (data) => { + console.log(data.toString()) + }) } if (childProcess.stderr) { - childProcess.stderr.pipe(logStream) + childProcess.stderr.on("data", (data) => { + console.error(data.toString()) + }) } this.activeWorkers.add(workerId) @@ -118,13 +120,11 @@ export class WorkerController { childProcess.on("exit", (code) => { console.log(`Worker ${workerId} exited with code ${code}`) this.activeWorkers.delete(workerId) - logStream.end() }) childProcess.on("error", (error) => { console.error(`Worker ${workerId} error:`, error) this.activeWorkers.delete(workerId) - logStream.end() }) // Detach the process so it can run independently. diff --git a/apps/roomote/src/lib/jobs/fixGitHubIssue.ts b/apps/roomote/src/lib/jobs/fixGitHubIssue.ts index daad07e671..c6614e5462 100644 --- a/apps/roomote/src/lib/jobs/fixGitHubIssue.ts +++ b/apps/roomote/src/lib/jobs/fixGitHubIssue.ts @@ -28,10 +28,10 @@ ${jobPayload.labels && jobPayload.labels.length > 0 ? `Labels: ${jobPayload.labe Please analyze the issue, understand what needs to be fixed, and implement a solution. -If you're reasonably satisfied with the solution then create and submit a pull request using the "gh" command line tool: +When you're finished, create a git branch to store your work and then submit a pull request using the "gh" command line tool: gh pr create --title "Fixes #${jobPayload.issue}\n\n[Your PR description here.]" --fill --template "pull_request_template.md" -You'll first need to create a new branch for the pull request. +Your job isn't done until you've created a pull request. Try to solve any git issues that arise while creating your branch and submitting your pull request. `.trim() const { repo, issue } = jobPayload diff --git a/apps/roomote/src/lib/worker.ts b/apps/roomote/src/lib/worker.ts index 87e6c1866f..d8e9609062 100644 --- a/apps/roomote/src/lib/worker.ts +++ b/apps/roomote/src/lib/worker.ts @@ -3,24 +3,33 @@ import { Worker } from "bullmq" import { redis } from "./redis" import { processJob } from "./job" -/** - * docker compose build worker - * docker run \ - * --name roomote-worker \ - * --rm \ - * --network roomote_default \ - * -e HOST_EXECUTION_METHOD=docker \ - * -v /var/run/docker.sock:/var/run/docker.sock \ - * -v /tmp/roomote:/var/log/roomote roomote-worker \ - * sh -c "pnpm worker" - */ +// docker compose build worker +// docker run \ +// --name roomote-worker \ +// --rm \ +// --interactive \ +// --tty \ +// --network roomote_default \ +// -e HOST_EXECUTION_METHOD=docker \ +// -e GH_TOKEN=$GH_TOKEN \ +// -e DATABASE_URL=postgresql://postgres:password@db:5432/cloud_agents \ +// -e REDIS_URL=redis://redis:6379 \ +// -e NODE_ENV=production \ +// -v /var/run/docker.sock:/var/run/docker.sock \ +// -v /tmp/roomote:/var/log/roomote \ +// roomote-worker sh -c "bash" async function processSingleJob() { - const worker = new Worker("roomote", undefined, { autorun: false, connection: redis }) + const worker = new Worker("roomote", undefined, { + autorun: false, + connection: redis, + lockDuration: 30 * 60 * 1_000, // 30 minutes + }) + + const token = crypto.randomUUID() try { - console.log("Looking for a job to process...") - const job = await worker.getNextJob("worker-token") + const job = await worker.getNextJob(token) if (!job) { console.log("No jobs available, exiting...") @@ -32,10 +41,10 @@ async function processSingleJob() { try { await processJob(job) - await job.moveToCompleted(undefined, "worker-token") + await job.moveToCompleted(undefined, token, false) console.log(`Job ${job.id} completed successfully`) } catch (error) { - await job.moveToFailed(error as Error, "worker-token") + await job.moveToFailed(error as Error, token, false) console.error(`Job ${job.id} failed:`, error) } } catch (error) { @@ -56,5 +65,8 @@ process.on("SIGINT", async () => { process.exit(0) }) -console.log("Single job worker started") +if (!process.env.GH_TOKEN) { + throw new Error("GH_TOKEN is not set") +} + processSingleJob() From 6ff534224c4f92d47d74de2d2ec655b79a57a4be Mon Sep 17 00:00:00 2001 From: cte Date: Mon, 16 Jun 2025 13:25:51 -0700 Subject: [PATCH 17/19] More progress --- .../roomote/drizzle/0001_fluffy_sasquatch.sql | 1 + apps/roomote/drizzle/meta/0001_snapshot.json | 170 ++++++++++++++++++ apps/roomote/drizzle/meta/_journal.json | 7 + .../src/app/api/webhooks/github/route.ts | 118 ++++++++---- apps/roomote/src/db/schema.ts | 1 + apps/roomote/src/lib/job.ts | 23 ++- apps/roomote/src/lib/jobs/fixGitHubIssue.ts | 15 +- apps/roomote/src/lib/runTask.ts | 35 ++++ apps/roomote/src/types/index.ts | 19 +- 9 files changed, 349 insertions(+), 40 deletions(-) create mode 100644 apps/roomote/drizzle/0001_fluffy_sasquatch.sql create mode 100644 apps/roomote/drizzle/meta/0001_snapshot.json diff --git a/apps/roomote/drizzle/0001_fluffy_sasquatch.sql b/apps/roomote/drizzle/0001_fluffy_sasquatch.sql new file mode 100644 index 0000000000..aeddbe02a8 --- /dev/null +++ b/apps/roomote/drizzle/0001_fluffy_sasquatch.sql @@ -0,0 +1 @@ +ALTER TABLE "cloud_jobs" ADD COLUMN "slack_thread_ts" text; \ No newline at end of file diff --git a/apps/roomote/drizzle/meta/0001_snapshot.json b/apps/roomote/drizzle/meta/0001_snapshot.json new file mode 100644 index 0000000000..0169baf6b8 --- /dev/null +++ b/apps/roomote/drizzle/meta/0001_snapshot.json @@ -0,0 +1,170 @@ +{ + "id": "4cae0c18-141d-40a3-acc8-38fcd7f07534", + "prevId": "8f65bfed-78de-4e22-a15f-36de8afe5f2e", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.cloud_jobs": { + "name": "cloud_jobs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "cloud_jobs_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "2147483647", + "cache": "1", + "cycle": false + } + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "result": { + "name": "result", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "slack_thread_ts": { + "name": "slack_thread_ts", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.cloud_tasks": { + "name": "cloud_tasks", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "cloud_tasks_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "2147483647", + "cache": "1", + "cycle": false + } + }, + "job_id": { + "name": "job_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "task_id": { + "name": "task_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "container_id": { + "name": "container_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "cloud_tasks_job_id_cloud_jobs_id_fk": { + "name": "cloud_tasks_job_id_cloud_jobs_id_fk", + "tableFrom": "cloud_tasks", + "tableTo": "cloud_jobs", + "columnsFrom": ["job_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} diff --git a/apps/roomote/drizzle/meta/_journal.json b/apps/roomote/drizzle/meta/_journal.json index 4c1055b8e6..706bf209cb 100644 --- a/apps/roomote/drizzle/meta/_journal.json +++ b/apps/roomote/drizzle/meta/_journal.json @@ -8,6 +8,13 @@ "when": 1749740498648, "tag": "0000_cuddly_luke_cage", "breakpoints": true + }, + { + "idx": 1, + "version": "7", + "when": 1750099429125, + "tag": "0001_fluffy_sasquatch", + "breakpoints": true } ] } diff --git a/apps/roomote/src/app/api/webhooks/github/route.ts b/apps/roomote/src/app/api/webhooks/github/route.ts index 1f88747a53..0a0256f12a 100644 --- a/apps/roomote/src/app/api/webhooks/github/route.ts +++ b/apps/roomote/src/app/api/webhooks/github/route.ts @@ -1,10 +1,13 @@ import { NextRequest, NextResponse } from "next/server" import { createHmac } from "crypto" import { z } from "zod" +import { eq } from "drizzle-orm" -import { type JobType, type JobPayload, githubWebhookSchema } from "@/types" +import { type JobType, type JobPayload, githubIssueWebhookSchema, githubPullRequestWebhookSchema } from "@/types" import { db, cloudJobs } from "@/db" import { enqueue } from "@/lib" +import { SlackNotifier } from "@/lib/slack" +import { Logger } from "@/lib/logger" function verifySignature(body: string, signature: string, secret: string): boolean { const expectedSignature = createHmac("sha256", secret).update(body, "utf8").digest("hex") @@ -12,6 +15,84 @@ function verifySignature(body: string, signature: string, secret: string): boole return expectedSignature === receivedSignature } +async function handleIssueEvent(body: string) { + const data = githubIssueWebhookSchema.parse(JSON.parse(body)) + console.log("🗄️ Issue Webhook ->", data) + const { action, repository, issue } = data + + if (action !== "opened") { + return NextResponse.json({ message: "action_ignored" }) + } + + const type: JobType = "github.issue.fix" + + const payload: JobPayload = { + repo: repository.full_name, + issue: issue.number, + title: issue.title, + body: issue.body || "", + labels: issue.labels.map(({ name }) => name), + } + + const [job] = await db.insert(cloudJobs).values({ type, payload, status: "pending" }).returning() + + if (!job) { + throw new Error("Failed to create `cloudJobs` record.") + } + + const enqueuedJob = await enqueue({ jobId: job.id, type, payload }) + console.log("🔗 Enqueued job ->", enqueuedJob) + + return NextResponse.json({ message: "job_enqueued", jobId: job.id, enqueuedJobId: enqueuedJob.id }) +} + +async function handlePullRequestEvent(body: string) { + const data = githubPullRequestWebhookSchema.parse(JSON.parse(body)) + console.log("🗄️ PR Webhook ->", data) + const { action, pull_request, repository } = data + + if (action !== "opened") { + return NextResponse.json({ message: "action_ignored" }) + } + + // Extract issue number from PR title or body (looking for "Fixes #123" pattern) + const issueNumberMatch = + pull_request.title.match(/(?:fixes|closes|resolves)\s+#(\d+)/i) || + (pull_request.body && pull_request.body.match(/(?:fixes|closes|resolves)\s+#(\d+)/i)) + + if (!issueNumberMatch) { + return NextResponse.json({ message: "no_issue_reference_found" }) + } + + const issueNumber = parseInt(issueNumberMatch[1]!, 10) + + // Find the job that corresponds to this issue + const jobs = await db.select().from(cloudJobs).where(eq(cloudJobs.type, "github.issue.fix")) + + // Filter jobs to find the one matching this repo and issue + const job = jobs.find((j) => { + const payload = j.payload as { repo: string; issue: number } + return payload.repo === repository.full_name && payload.issue === issueNumber + }) + + if (!job || !job.slackThreadTs) { + console.log("No job found or no slack thread for issue", issueNumber) + return NextResponse.json({ message: "no_job_or_slack_thread_found" }) + } + + // Post to Slack thread + const logger = new Logger({ logDir: "/tmp/logs", filename: "webhook.log", tag: "webhook" }) + const slackNotifier = new SlackNotifier(logger) + + await slackNotifier.postTaskUpdated( + job.slackThreadTs, + `🎉 Pull request created: <${pull_request.html_url}|PR #${pull_request.number}>\n*${pull_request.title}*`, + "success", + ) + + return NextResponse.json({ message: "slack_notification_sent" }) +} + export async function POST(request: NextRequest) { try { const signature = request.headers.get("x-hub-signature-256") @@ -28,38 +109,13 @@ export async function POST(request: NextRequest) { const event = request.headers.get("x-github-event") - if (event !== "issues") { + if (event === "issues") { + return await handleIssueEvent(body) + } else if (event === "pull_request") { + return await handlePullRequestEvent(body) + } else { return NextResponse.json({ message: "event_ignored" }) } - - const data = githubWebhookSchema.parse(JSON.parse(body)) - console.log("🗄️ Webhook ->", data) - const { action, repository, issue } = data - - if (action !== "opened") { - return NextResponse.json({ message: "action_ignored" }) - } - - const type: JobType = "github.issue.fix" - - const payload: JobPayload = { - repo: repository.full_name, - issue: issue.number, - title: issue.title, - body: issue.body || "", - labels: issue.labels.map(({ name }) => name), - } - - const [job] = await db.insert(cloudJobs).values({ type, payload, status: "pending" }).returning() - - if (!job) { - throw new Error("Failed to create `cloudJobs` record.") - } - - const enqueuedJob = await enqueue({ jobId: job.id, type, payload }) - console.log("🔗 Enqueued job ->", enqueuedJob) - - return NextResponse.json({ message: "job_enqueued", jobId: job.id, enqueuedJobId: enqueuedJob.id }) } catch (error) { console.error("GitHub Webhook Error:", error) diff --git a/apps/roomote/src/db/schema.ts b/apps/roomote/src/db/schema.ts index 7c38516ace..3f09744603 100644 --- a/apps/roomote/src/db/schema.ts +++ b/apps/roomote/src/db/schema.ts @@ -14,6 +14,7 @@ export const cloudJobs = pgTable("cloud_jobs", { payload: jsonb().notNull().$type(), result: jsonb(), error: text(), + slackThreadTs: text("slack_thread_ts"), startedAt: timestamp("started_at"), completedAt: timestamp("completed_at"), createdAt: timestamp("created_at").notNull().defaultNow(), diff --git a/apps/roomote/src/lib/job.ts b/apps/roomote/src/lib/job.ts index 5ec52c44f3..cc6e15dc23 100644 --- a/apps/roomote/src/lib/job.ts +++ b/apps/roomote/src/lib/job.ts @@ -10,12 +10,19 @@ export async function processJob({ data: { type, payload, job console.log(`[${job.name} | ${job.id}] Processing job ${jobId} of type ${type}`) try { - await updateJobStatus(jobId, "processing") let result: unknown switch (type) { case "github.issue.fix": - result = await fixGitHubIssue(payload) + result = await fixGitHubIssue(payload, { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + onTaskStarted: async (slackThreadTs: string | null, _rooTaskId: string) => { + if (slackThreadTs) { + await updateJobStatus(jobId, "processing", undefined, undefined, slackThreadTs) + } + }, + }) + break default: throw new Error(`Unknown job type: ${type}`) @@ -31,7 +38,13 @@ export async function processJob({ data: { type, payload, job } } -async function updateJobStatus(jobId: number, status: JobStatus, result?: unknown, error?: string) { +async function updateJobStatus( + jobId: number, + status: JobStatus, + result?: unknown, + error?: string, + slackThreadTs?: string, +) { const values: UpdateCloudJob = { status } if (status === "processing") { @@ -48,5 +61,9 @@ async function updateJobStatus(jobId: number, status: JobStatus, result?: unknow } } + if (slackThreadTs) { + values.slackThreadTs = slackThreadTs + } + await db.update(cloudJobs).set(values).where(eq(cloudJobs.id, jobId)) } diff --git a/apps/roomote/src/lib/jobs/fixGitHubIssue.ts b/apps/roomote/src/lib/jobs/fixGitHubIssue.ts index c6614e5462..7449c9e6d5 100644 --- a/apps/roomote/src/lib/jobs/fixGitHubIssue.ts +++ b/apps/roomote/src/lib/jobs/fixGitHubIssue.ts @@ -3,14 +3,17 @@ import * as os from "node:os" import type { JobType, JobPayload } from "@/types" -import { runTask } from "../runTask" +import { runTask, type RunTaskCallbacks } from "../runTask" import { Logger } from "../logger" const jobType: JobType = "github.issue.fix" type FixGitHubIssueJobPayload = JobPayload<"github.issue.fix"> -export async function fixGitHubIssue(jobPayload: FixGitHubIssueJobPayload): Promise<{ +export async function fixGitHubIssue( + jobPayload: FixGitHubIssueJobPayload, + callbacks?: RunTaskCallbacks, +): Promise<{ repo: string issue: number result: unknown @@ -28,8 +31,11 @@ ${jobPayload.labels && jobPayload.labels.length > 0 ? `Labels: ${jobPayload.labe Please analyze the issue, understand what needs to be fixed, and implement a solution. -When you're finished, create a git branch to store your work and then submit a pull request using the "gh" command line tool: -gh pr create --title "Fixes #${jobPayload.issue}\n\n[Your PR description here.]" --fill --template "pull_request_template.md" +When you're finished: +- Create a git branch to store your work (git checkout -b fix-${jobPayload.issue}) +- Commit your changes to this branch (git commit -m "Fixes #${jobPayload.issue}") +- Push your branch to the remote repository (git push --set-upstream origin fix-${jobPayload.issue}) +- Submit a pull request using the "gh" command line tool (gh pr create --repo RooCodeInc/Roo-Code --title "Fixes #${jobPayload.issue}\n\n[Your PR description here.]" --fill) Your job isn't done until you've created a pull request. Try to solve any git issues that arise while creating your branch and submitting your pull request. `.trim() @@ -42,6 +48,7 @@ Your job isn't done until you've created a pull request. Try to solve any git is prompt, publish: async () => {}, logger: new Logger({ logDir: path.resolve(os.tmpdir(), "logs"), filename: "worker.log", tag: "worker" }), + callbacks, }) return { repo, issue, result } diff --git a/apps/roomote/src/lib/runTask.ts b/apps/roomote/src/lib/runTask.ts index be3a9e5a84..84aa2c97c4 100644 --- a/apps/roomote/src/lib/runTask.ts +++ b/apps/roomote/src/lib/runTask.ts @@ -23,12 +23,26 @@ class SubprocessTimeoutError extends Error { } } +export type RunTaskCallbacks = { + onTaskStarted?: (slackThreadTs: string | null, rooTaskId: string) => Promise + onTaskAborted?: (slackThreadTs: string | null) => Promise + onTaskCompleted?: ( + slackThreadTs: string | null, + success: boolean, + duration: number, + rooTaskId?: string, + ) => Promise + onTaskTimedOut?: (slackThreadTs: string | null) => Promise + onClientDisconnected?: (slackThreadTs: string | null) => Promise +} + type RunTaskOptions = { jobType: T jobPayload: JobPayload prompt: string publish: (taskEvent: TaskEvent) => Promise logger: Logger + callbacks?: RunTaskCallbacks } export const runTask = async ({ @@ -37,6 +51,7 @@ export const runTask = async ({ prompt, publish, logger, + callbacks, }: RunTaskOptions) => { const workspacePath = "/roo/repos/Roo-Code" // findGitRoot(process.cwd()) const ipcSocketPath = path.resolve(os.tmpdir(), `${crypto.randomUUID().slice(0, 8)}.sock`) @@ -122,6 +137,10 @@ export const runTask = async ({ if (rooTaskId) { slackThreadTs = await slackNotifier.postTaskStarted({ jobType, jobPayload, rooTaskId }) + + if (callbacks?.onTaskStarted) { + await callbacks.onTaskStarted(slackThreadTs, rooTaskId) + } } } @@ -131,6 +150,10 @@ export const runTask = async ({ if (slackThreadTs) { await slackNotifier.postTaskUpdated(slackThreadTs, "Task was aborted", "warning") } + + if (callbacks?.onTaskAborted) { + await callbacks.onTaskAborted(slackThreadTs) + } } if (eventName === RooCodeEventName.TaskCompleted) { @@ -139,6 +162,10 @@ export const runTask = async ({ if (slackThreadTs) { await slackNotifier.postTaskCompleted(slackThreadTs, true, taskFinishedAt - taskStartedAt, rooTaskId) } + + if (callbacks?.onTaskCompleted) { + await callbacks.onTaskCompleted(slackThreadTs, true, taskFinishedAt - taskStartedAt, rooTaskId) + } } }) @@ -172,6 +199,10 @@ export const runTask = async ({ await slackNotifier.postTaskUpdated(slackThreadTs, "Task timed out after 30 minutes", "error") } + if (callbacks?.onTaskTimedOut) { + await callbacks.onTaskTimedOut(slackThreadTs) + } + if (rooTaskId && !isClientDisconnected) { logger.info("cancelling task") client.sendCommand({ commandName: TaskCommandName.CancelTask, data: rooTaskId }) @@ -188,6 +219,10 @@ export const runTask = async ({ await slackNotifier.postTaskUpdated(slackThreadTs, "Client disconnected before task completion", "error") } + if (callbacks?.onClientDisconnected) { + await callbacks.onClientDisconnected(slackThreadTs) + } + throw new Error("Client disconnected before task completion.") } diff --git a/apps/roomote/src/types/index.ts b/apps/roomote/src/types/index.ts index 4b0a2b86d2..d86334a24c 100644 --- a/apps/roomote/src/types/index.ts +++ b/apps/roomote/src/types/index.ts @@ -45,7 +45,7 @@ export type CreateJob = z.infer * GitHubWebhook */ -export const githubWebhookSchema = z.object({ +export const githubIssueWebhookSchema = z.object({ action: z.string(), issue: z.object({ number: z.number(), @@ -58,4 +58,19 @@ export const githubWebhookSchema = z.object({ }), }) -export type GitHubWebhook = z.infer +export type GitHubIssueWebhook = z.infer + +export const githubPullRequestWebhookSchema = z.object({ + action: z.string(), + pull_request: z.object({ + number: z.number(), + title: z.string(), + body: z.string().nullable(), + html_url: z.string(), + }), + repository: z.object({ + full_name: z.string(), + }), +}) + +export type GitHubPullRequestWebhook = z.infer From f148a74dfa56d54eab2fc1c757b98117cfee1078 Mon Sep 17 00:00:00 2001 From: cte Date: Mon, 16 Jun 2025 14:20:45 -0700 Subject: [PATCH 18/19] More progress --- apps/roomote/Dockerfile.worker | 1 + apps/roomote/drizzle/0002_brief_sentry.sql | 1 + apps/roomote/drizzle/meta/0002_snapshot.json | 105 ++++++++++++++++++ apps/roomote/drizzle/meta/_journal.json | 7 ++ .../src/app/api/webhooks/github/route.ts | 18 +-- apps/roomote/src/db/schema.ts | 32 ------ apps/roomote/src/lib/jobs/fixGitHubIssue.ts | 2 +- apps/roomote/src/lib/slack.ts | 10 +- 8 files changed, 129 insertions(+), 47 deletions(-) create mode 100644 apps/roomote/drizzle/0002_brief_sentry.sql create mode 100644 apps/roomote/drizzle/meta/0002_snapshot.json diff --git a/apps/roomote/Dockerfile.worker b/apps/roomote/Dockerfile.worker index f8e3578ac1..b1a21c8a62 100644 --- a/apps/roomote/Dockerfile.worker +++ b/apps/roomote/Dockerfile.worker @@ -36,6 +36,7 @@ RUN git config --global credential.helper store RUN echo "https://oauth2:${GH_TOKEN}@github.com" > ~/.git-credentials RUN gh repo clone RooCodeInc/Roo-Code WORKDIR /roo/repos/Roo-Code +RUN gh repo set-default RooCodeInc/Roo-Code RUN pnpm install # Install dependencies diff --git a/apps/roomote/drizzle/0002_brief_sentry.sql b/apps/roomote/drizzle/0002_brief_sentry.sql new file mode 100644 index 0000000000..2f22f7825c --- /dev/null +++ b/apps/roomote/drizzle/0002_brief_sentry.sql @@ -0,0 +1 @@ +DROP TABLE "cloud_tasks" CASCADE; \ No newline at end of file diff --git a/apps/roomote/drizzle/meta/0002_snapshot.json b/apps/roomote/drizzle/meta/0002_snapshot.json new file mode 100644 index 0000000000..2c06436562 --- /dev/null +++ b/apps/roomote/drizzle/meta/0002_snapshot.json @@ -0,0 +1,105 @@ +{ + "id": "16afebd7-b27a-457e-b3d5-4de50a597c2e", + "prevId": "4cae0c18-141d-40a3-acc8-38fcd7f07534", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.cloud_jobs": { + "name": "cloud_jobs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "cloud_jobs_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "2147483647", + "cache": "1", + "cycle": false + } + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "result": { + "name": "result", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "slack_thread_ts": { + "name": "slack_thread_ts", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} diff --git a/apps/roomote/drizzle/meta/_journal.json b/apps/roomote/drizzle/meta/_journal.json index 706bf209cb..7bfbcd4d35 100644 --- a/apps/roomote/drizzle/meta/_journal.json +++ b/apps/roomote/drizzle/meta/_journal.json @@ -15,6 +15,13 @@ "when": 1750099429125, "tag": "0001_fluffy_sasquatch", "breakpoints": true + }, + { + "idx": 2, + "version": "7", + "when": 1750107620472, + "tag": "0002_brief_sentry", + "breakpoints": true } ] } diff --git a/apps/roomote/src/app/api/webhooks/github/route.ts b/apps/roomote/src/app/api/webhooks/github/route.ts index 0a0256f12a..40322bcd4c 100644 --- a/apps/roomote/src/app/api/webhooks/github/route.ts +++ b/apps/roomote/src/app/api/webhooks/github/route.ts @@ -7,7 +7,6 @@ import { type JobType, type JobPayload, githubIssueWebhookSchema, githubPullRequ import { db, cloudJobs } from "@/db" import { enqueue } from "@/lib" import { SlackNotifier } from "@/lib/slack" -import { Logger } from "@/lib/logger" function verifySignature(body: string, signature: string, secret: string): boolean { const expectedSignature = createHmac("sha256", secret).update(body, "utf8").digest("hex") @@ -17,13 +16,14 @@ function verifySignature(body: string, signature: string, secret: string): boole async function handleIssueEvent(body: string) { const data = githubIssueWebhookSchema.parse(JSON.parse(body)) - console.log("🗄️ Issue Webhook ->", data) const { action, repository, issue } = data if (action !== "opened") { return NextResponse.json({ message: "action_ignored" }) } + console.log("🗄️ Issue Webhook ->", data) + const type: JobType = "github.issue.fix" const payload: JobPayload = { @@ -48,14 +48,15 @@ async function handleIssueEvent(body: string) { async function handlePullRequestEvent(body: string) { const data = githubPullRequestWebhookSchema.parse(JSON.parse(body)) - console.log("🗄️ PR Webhook ->", data) const { action, pull_request, repository } = data if (action !== "opened") { return NextResponse.json({ message: "action_ignored" }) } - // Extract issue number from PR title or body (looking for "Fixes #123" pattern) + console.log("🗄️ PR Webhook ->", data) + + // Extract issue number from PR title or body (looking for "Fixes #123" pattern). const issueNumberMatch = pull_request.title.match(/(?:fixes|closes|resolves)\s+#(\d+)/i) || (pull_request.body && pull_request.body.match(/(?:fixes|closes|resolves)\s+#(\d+)/i)) @@ -66,10 +67,10 @@ async function handlePullRequestEvent(body: string) { const issueNumber = parseInt(issueNumberMatch[1]!, 10) - // Find the job that corresponds to this issue + // Find the job that corresponds to this issue. const jobs = await db.select().from(cloudJobs).where(eq(cloudJobs.type, "github.issue.fix")) - // Filter jobs to find the one matching this repo and issue + // Filter jobs to find the one matching this repo and issue. const job = jobs.find((j) => { const payload = j.payload as { repo: string; issue: number } return payload.repo === repository.full_name && payload.issue === issueNumber @@ -80,9 +81,8 @@ async function handlePullRequestEvent(body: string) { return NextResponse.json({ message: "no_job_or_slack_thread_found" }) } - // Post to Slack thread - const logger = new Logger({ logDir: "/tmp/logs", filename: "webhook.log", tag: "webhook" }) - const slackNotifier = new SlackNotifier(logger) + // Post to Slack thread. + const slackNotifier = new SlackNotifier() await slackNotifier.postTaskUpdated( job.slackThreadTs, diff --git a/apps/roomote/src/db/schema.ts b/apps/roomote/src/db/schema.ts index 3f09744603..88fb0c6000 100644 --- a/apps/roomote/src/db/schema.ts +++ b/apps/roomote/src/db/schema.ts @@ -1,5 +1,4 @@ import { pgTable, text, timestamp, integer, jsonb } from "drizzle-orm/pg-core" -import { relations } from "drizzle-orm" import type { JobType, JobStatus, JobPayload } from "@/types" @@ -20,47 +19,16 @@ export const cloudJobs = pgTable("cloud_jobs", { createdAt: timestamp("created_at").notNull().defaultNow(), }) -export const cloudJobsRelations = relations(cloudJobs, ({ many }) => ({ - tasks: many(cloudTasks), -})) - export type CloudJob = typeof cloudJobs.$inferSelect export type InsertCloudJob = typeof cloudJobs.$inferInsert export type UpdateCloudJob = Partial> -/** - * cloudTasks - */ - -export const cloudTasks = pgTable("cloud_tasks", { - id: integer().primaryKey().generatedAlwaysAsIdentity(), - jobId: integer("job_id") - .references(() => cloudJobs.id) - .notNull(), - taskId: integer("task_id"), - containerId: text("container_id"), - createdAt: timestamp("created_at").notNull().defaultNow(), -}) - -export const cloudTasksRelations = relations(cloudTasks, ({ one }) => ({ - job: one(cloudJobs, { fields: [cloudTasks.jobId], references: [cloudJobs.id] }), -})) - -export type CloudTask = typeof cloudTasks.$inferSelect - -export type InsertCloudTask = typeof cloudTasks.$inferInsert - -export type UpdateCloudTask = Partial> - /** * schema */ export const schema = { cloudJobs, - cloudTasks, - cloudJobsRelations, - cloudTasksRelations, } diff --git a/apps/roomote/src/lib/jobs/fixGitHubIssue.ts b/apps/roomote/src/lib/jobs/fixGitHubIssue.ts index 7449c9e6d5..3c22b8d9a8 100644 --- a/apps/roomote/src/lib/jobs/fixGitHubIssue.ts +++ b/apps/roomote/src/lib/jobs/fixGitHubIssue.ts @@ -35,7 +35,7 @@ When you're finished: - Create a git branch to store your work (git checkout -b fix-${jobPayload.issue}) - Commit your changes to this branch (git commit -m "Fixes #${jobPayload.issue}") - Push your branch to the remote repository (git push --set-upstream origin fix-${jobPayload.issue}) -- Submit a pull request using the "gh" command line tool (gh pr create --repo RooCodeInc/Roo-Code --title "Fixes #${jobPayload.issue}\n\n[Your PR description here.]" --fill) +- Submit a pull request using the "gh" command line tool (gh pr create --title "Fixes #${jobPayload.issue}\n\n[Your PR description here.]" --fill) Your job isn't done until you've created a pull request. Try to solve any git issues that arise while creating your branch and submitting your pull request. `.trim() diff --git a/apps/roomote/src/lib/slack.ts b/apps/roomote/src/lib/slack.ts index b34f477d24..b7502810ab 100644 --- a/apps/roomote/src/lib/slack.ts +++ b/apps/roomote/src/lib/slack.ts @@ -18,10 +18,10 @@ export interface SlackResponse { } export class SlackNotifier { - private readonly logger: Logger + private readonly logger?: Logger private readonly token: string - constructor(logger: Logger, token: string = process.env.SLACK_API_TOKEN!) { + constructor(logger?: Logger, token: string = process.env.SLACK_API_TOKEN!) { this.logger = logger this.token = token } @@ -37,19 +37,19 @@ export class SlackNotifier { }) if (!response.ok) { - this.logger.error(`Slack API failed: ${response.status} ${response.statusText}`) + this.logger?.error(`Slack API failed: ${response.status} ${response.statusText}`) return null } const result: SlackResponse = await response.json() if (!result.ok) { - this.logger.error(`Slack API error: ${result.error}`) + this.logger?.error(`Slack API error: ${result.error}`) } return result.ts ?? null } catch (error) { - this.logger.error("Failed to send Slack message:", error) + this.logger?.error("Failed to send Slack message:", error) return null } } From f5d4b33dc30d30f8576dc582566778b977d43375 Mon Sep 17 00:00:00 2001 From: cte Date: Mon, 16 Jun 2025 14:40:17 -0700 Subject: [PATCH 19/19] Fix escaping error --- ...est-job.sh => enqueue-github-issue-job.sh} | 33 ++++++++++--------- 1 file changed, 18 insertions(+), 15 deletions(-) rename apps/roomote/scripts/{enqueue-test-job.sh => enqueue-github-issue-job.sh} (77%) diff --git a/apps/roomote/scripts/enqueue-test-job.sh b/apps/roomote/scripts/enqueue-github-issue-job.sh similarity index 77% rename from apps/roomote/scripts/enqueue-test-job.sh rename to apps/roomote/scripts/enqueue-github-issue-job.sh index 09f32fb2dc..5ec7448cc6 100755 --- a/apps/roomote/scripts/enqueue-test-job.sh +++ b/apps/roomote/scripts/enqueue-github-issue-job.sh @@ -51,21 +51,24 @@ TITLE=$(echo "$ISSUE_DATA" | jq -r '.title') BODY=$(echo "$ISSUE_DATA" | jq -r '.body // ""') LABELS=$(echo "$ISSUE_DATA" | jq -r '[.labels[].name] | @json') -TITLE_ESCAPED=$(printf '%s' "$TITLE" | sed 's/"/\\"/g' | sed 's/\n/\\n/g') -BODY_ESCAPED=$(printf '%s' "$BODY" | sed 's/"/\\"/g' | awk '{printf "%s\\n", $0}' | sed 's/\\n$//') +JSON_PAYLOAD=$(jq -n \ + --arg type "github.issue.fix" \ + --arg repo "$REPO" \ + --argjson issue "$ISSUE_NUMBER" \ + --arg title "$TITLE" \ + --arg body "$BODY" \ + --argjson labels "$LABELS" \ + '{ + type: $type, + payload: { + repo: $repo, + issue: $issue, + title: $title, + body: $body, + labels: $labels + } + }') -JSON_PAYLOAD=$(cat <