From 6326e66144aa6e6723ca4cc8fd5c4521e51bbc4f Mon Sep 17 00:00:00 2001 From: Johnson Lai Date: Fri, 19 Jul 2024 16:55:56 +0800 Subject: [PATCH] Initialize --- .dockerignore | 25 +++ .env.sample | 29 +++ .eslintignore | 31 +++ .eslintrc.json | 37 ++++ .github/workflows/pre-merge-checks.yml | 135 ++++++++++++ .gitignore | 21 ++ .husky/pre-commit | 3 + .lintstagedrc.json | 9 + .prettierignore | 31 +++ .prettierrc.json | 26 +++ Dockerfile | 24 +++ LICENSE.txt | 21 ++ README.md | 38 ++++ bun.lockb | Bin 0 -> 251028 bytes docker-compose.yml | 13 ++ docs/CONTRIBUTING.md | 31 +++ docs/DEVELOP.md | 164 +++++++++++++++ docs/OPTIMZATION.md | 25 +++ docs/SETUP.md | 123 +++++++++++ docs/UPDATE.md | 31 +++ jest.config.ts | 198 ++++++++++++++++++ package.json | 62 ++++++ script/openaiCompatibleTest.ts | 52 +++++ script/publish_docker_hub.sh | 27 +++ script/randomSamplingTest.ts | 45 ++++ script/test_groq_query.sh | 23 ++ src/config.ts | 96 +++++++++ src/integration/OpenAIBase.ts | 67 ++++++ src/integration/groq.ts | 22 ++ src/integration/ollama.ts | 20 ++ src/integration/openai.ts | 20 ++ src/integration/openrouter.ts | 25 +++ src/middleware/apiKeyMiddleware.ts | 30 +++ src/server/express.ts | 64 ++++++ src/server/handshake.ts | 84 ++++++++ src/server/webhook.ts | 87 ++++++++ src/utils/apiKey.ts | 13 ++ src/utils/llm.ts | 70 +++++++ src/utils/logger.ts | 47 +++++ src/utils/version.ts | 5 + tests/integration/Groq.test.ts | 46 ++++ tests/integration/OpenRouter.test.ts | 46 ++++ tests/setup.ts | 18 ++ .../middleware/APIKeyAuthMiddleware.test.ts | 104 +++++++++ tests/unit/server/Webhook.test.ts | 102 +++++++++ tests/unit/utils/APIKey.test.ts | 23 ++ tests/unit/utils/LLM.test.ts | 55 +++++ tsconfig.json | 109 ++++++++++ 48 files changed, 2377 insertions(+) create mode 100644 .dockerignore create mode 100644 .env.sample create mode 100644 .eslintignore create mode 100644 .eslintrc.json create mode 100644 .github/workflows/pre-merge-checks.yml create mode 100644 .gitignore create mode 100644 .husky/pre-commit create mode 100644 .lintstagedrc.json create mode 100644 .prettierignore create mode 100644 .prettierrc.json create mode 100644 Dockerfile create mode 100644 LICENSE.txt create mode 100644 README.md create mode 100755 bun.lockb create mode 100644 docker-compose.yml create mode 100644 docs/CONTRIBUTING.md create mode 100644 docs/DEVELOP.md create mode 100644 docs/OPTIMZATION.md create mode 100644 docs/SETUP.md create mode 100644 docs/UPDATE.md create mode 100644 jest.config.ts create mode 100644 package.json create mode 100644 script/openaiCompatibleTest.ts create mode 100755 script/publish_docker_hub.sh create mode 100644 script/randomSamplingTest.ts create mode 100755 script/test_groq_query.sh create mode 100644 src/config.ts create mode 100644 src/integration/OpenAIBase.ts create mode 100644 src/integration/groq.ts create mode 100644 src/integration/ollama.ts create mode 100644 src/integration/openai.ts create mode 100644 src/integration/openrouter.ts create mode 100644 src/middleware/apiKeyMiddleware.ts create mode 100644 src/server/express.ts create mode 100644 src/server/handshake.ts create mode 100644 src/server/webhook.ts create mode 100644 src/utils/apiKey.ts create mode 100644 src/utils/llm.ts create mode 100644 src/utils/logger.ts create mode 100644 src/utils/version.ts create mode 100644 tests/integration/Groq.test.ts create mode 100644 tests/integration/OpenRouter.test.ts create mode 100644 tests/setup.ts create mode 100644 tests/unit/middleware/APIKeyAuthMiddleware.test.ts create mode 100644 tests/unit/server/Webhook.test.ts create mode 100644 tests/unit/utils/APIKey.test.ts create mode 100644 tests/unit/utils/LLM.test.ts create mode 100644 tsconfig.json diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..c9ab69c --- /dev/null +++ b/.dockerignore @@ -0,0 +1,25 @@ +# IDE +.idea +.vscode + +# Build +node_modules/ +dist/ +coverage + +# Environment Files +.env + +# Configuration Files +Dockerfile +.dockerignore +.git +.gitignore + +# Logs +npm-debug.log + +# Documentation +README.md + + diff --git a/.env.sample b/.env.sample new file mode 100644 index 0000000..4c5c834 --- /dev/null +++ b/.env.sample @@ -0,0 +1,29 @@ +PORT=3001 +LOGGER_LEVEL=debug + +# Chasm +# Orchestrator URL +ORCHESTRATOR_URL= +SCOUT_NAME= +# Scout UID +SCOUT_UID= +# Scout API Key +WEBHOOK_API_KEY= +# Scout Webhook Url, update based on your server's IP and Port, e.g. http://123.123.123.123:3001/ +WEBHOOK_URL= + +# Chosen Provider (groq, openrouter) +# Seperated by comma (,) +# The scout will prioritize the first provider and fallback to the next one +PROVIDERS=groq,openrouter +MODEL=gemma-7b-it + +# Provider +GROQ_API_KEY= + +# Optional +OPENAI_API_KEY= +OPENROUTER_API_KEY= + +#local or production +NODE_ENV=local diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000..2adaf02 --- /dev/null +++ b/.eslintignore @@ -0,0 +1,31 @@ +# IDE +.idea/ +.vscode/ + +# Dependencies +node_modules/ +dist/ +coverage +package.json + +# Docker Configs +docker-compose.yml + +# System Files +.DS_Store +Thumbs.db + +# Environment Files +.env + +# Configuration Files +tsconfig.json +.eslintrc.json +.prettierrc.json +.lintstagedrc.json + +# GitHub Actions +.github/ + +# Logs +*.log diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..2d21641 --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,37 @@ +{ + "env": { + "es6": true, + "node": true + }, + "extends": [ + "eslint:recommended", + "plugin:@typescript-eslint/recommended", + "prettier" + ], + "parser": "@typescript-eslint/parser", + "parserOptions": { + "ecmaVersion": 6, + "sourceType": "module" + }, + "plugins": [ + "@typescript-eslint", + "unused-imports" + ], + "rules": { + "prefer-const": "warn", + "no-self-assign": "warn", + "@typescript-eslint/no-explicit-any": "warn", + "@typescript-eslint/no-var-requires": "warn", + "@typescript-eslint/no-unused-vars": "off", //"no-unused-vars": "off", + "unused-imports/no-unused-imports": "warn", + "unused-imports/no-unused-vars": [ + "warn", + { + "vars": "all", + "varsIgnorePattern": "^_", + "args": "after-used", + "argsIgnorePattern": "^_" + } + ] + } +} diff --git a/.github/workflows/pre-merge-checks.yml b/.github/workflows/pre-merge-checks.yml new file mode 100644 index 0000000..b76ce84 --- /dev/null +++ b/.github/workflows/pre-merge-checks.yml @@ -0,0 +1,135 @@ +name: Pre-merge Checks + +on: + pull_request: + branches: [ main, master ] + +jobs: + checkout-install: + name: Checkout and Install Dependencies + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Bun + uses: oven-sh/setup-bun@v1 + with: + bun-version: latest + + - name: Cache Bun packages + id: cache-bun + uses: actions/cache@v3 + with: + path: | + ~/.bun/bun + ~/.bun/install + ~/.bun/bin + key: ${{ runner.os }}-bun-${{ hashFiles('**/bun.lockb') }} + restore-keys: | + ${{ runner.os }}-bun- + + - name: Install dependencies + run: bun install + + - name: Upload Bun cache + if: steps.cache-bun.outputs.cache-hit != 'true' + uses: actions/cache@v3 + with: + path: | + ~/.bun/bun + ~/.bun/install + ~/.bun/bin + key: ${{ runner.os }}-bun-${{ hashFiles('**/bun.lockb') }} + + formatting-linting: + name: Formatting and Linting Checks + runs-on: ubuntu-latest + needs: checkout-install + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Restore Bun cache + uses: actions/cache@v3 + with: + path: | + ~/.bun/bun + ~/.bun/install + ~/.bun/bin + key: ${{ runner.os }}-bun-${{ hashFiles('**/bun.lockb') }} + + - name: Setup Bun + uses: oven-sh/setup-bun@v1 + with: + bun-version: latest + + - name: Install dependencies + run: bun install + + - name: Run formatting checks + run: bun run prettier + + - name: Run linting checks + run: bun run lint + + jest-tests: + name: Jest Tests + runs-on: ubuntu-latest + needs: checkout-install + environment: CI + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Restore Bun cache + uses: actions/cache@v3 + with: + path: | + ~/.bun/bun + ~/.bun/install + ~/.bun/bin + key: ${{ runner.os }}-bun-${{ hashFiles('**/bun.lockb') }} + + - name: Setup Bun + uses: oven-sh/setup-bun@v1 + with: + bun-version: latest + + - name: Install dependencies + run: bun install + + - name: Run Jest unit & integration tests + env: + OPENAI_API_KEY: ${{ vars.OPENAI_API_KEY }} + GROQ_API_KEY: ${{ vars.GROQ_API_KEY }} + OPENROUTER_API_KEY: ${{ vars.OPENROUTER_API_KEY }} + run: bun run test --verbose + + type-check: + name: Type Checks + runs-on: ubuntu-latest + needs: [formatting-linting, jest-tests] + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Restore Bun cache + uses: actions/cache@v3 + with: + path: | + ~/.bun/bun + ~/.bun/install + ~/.bun/bin + key: ${{ runner.os }}-bun-${{ hashFiles('**/bun.lockb') }} + + - name: Setup Bun + uses: oven-sh/setup-bun@v1 + with: + bun-version: latest + + - name: Install dependencies + run: bun install + + - name: Run type checks + run: bun run build diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c9df7fa --- /dev/null +++ b/.gitignore @@ -0,0 +1,21 @@ +# IDE +.idea +.vscode + +# Build +node_modules/ +dist/ +coverage + +# System Files +.DS_Store +Thumbs.db + +# Environment Files +.env +.env.prod +.env.test +.env.test + +# Logs +*.log diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100644 index 0000000..468432c --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1,3 @@ +bunx lint-staged +bun run build +# bun run test diff --git a/.lintstagedrc.json b/.lintstagedrc.json new file mode 100644 index 0000000..6fbd223 --- /dev/null +++ b/.lintstagedrc.json @@ -0,0 +1,9 @@ +{ + "*.ts": [ + "bun run prettier:fix", + "bun run lint" + ], + "*.md": [ + "bun run prettier:fix" + ] +} diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..2adaf02 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,31 @@ +# IDE +.idea/ +.vscode/ + +# Dependencies +node_modules/ +dist/ +coverage +package.json + +# Docker Configs +docker-compose.yml + +# System Files +.DS_Store +Thumbs.db + +# Environment Files +.env + +# Configuration Files +tsconfig.json +.eslintrc.json +.prettierrc.json +.lintstagedrc.json + +# GitHub Actions +.github/ + +# Logs +*.log diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 0000000..3430576 --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1,26 @@ +{ + "printWidth": 80, + "tabWidth": 2, + "useTabs": false, + "semi": true, + "quoteProps": "consistent", + "trailingComma": "all", + "bracketSpacing": true, + "arrowParens": "always", + "proseWrap": "preserve", + "importOrder": [ + "^dotenv(.*)$", + "", + "", + "^[./]" + ], + "importOrderParserPlugins": [ + "typescript", + "jsx", + "decorators-legacy" + ], + "importOrderTypeScriptVersion": "5.0.0", + "plugins": [ + "@ianvs/prettier-plugin-sort-imports" + ] +} diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..f8dd224 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,24 @@ +FROM oven/bun:alpine AS builder + +ENV PATH="/root/.bun/bin:${PATH}" +WORKDIR /usr/src/app +COPY package.json tsconfig.json bun.lockb* ./ +RUN bun install +COPY src ./src +RUN bun run build + + +FROM oven/bun:alpine + +ENV PATH="/root/.bun/bin:${PATH}" +WORKDIR /usr/src/app +COPY --from=builder /usr/src/app/dist ./dist +COPY --from=builder /usr/src/app/package.json ./package.json +RUN bun install --production --ignore-scripts +EXPOSE 3001 + +RUN addgroup -S scout && adduser -S appuser -G scout +RUN chown -R appuser:scout /usr/src/app/ +USER appuser + +CMD ["bun", "run", "dist/src/server/express.js"] diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..17a7a73 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Chasm Pte Ltd + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..ee2f8ca --- /dev/null +++ b/README.md @@ -0,0 +1,38 @@ +# Chasm Scout + +## What is Scout + +In the Chasm decentralised AI ecosystem, a scout is essentially a node that connected to an orchestrator to run LLM jobs. + +## Installation + +Please see the [Setup](./docs/SETUP.md) to run the scout yourself + +## Architecture + +Read more about the architecture design [here](https://superoo7.com/posts/chasm-scout-architecture-design/) + +## Supported LLM + +Supported Platform: + +- Groq +- OpenRouter +- ollama +- OpenAI ChatGPT + +## Optimization + +Please read the [Optimization Guide](./docs/OPTIMZATION.md) + +## Development + +Please see the [Development Guide](./docs/DEVELOPMENT.md) for more details. + +## Contributing + +We welcome contributions! Please see our [Contributing Guidelines](./docs/CONTRIBUTING.md) for more details. + +## License + +This project is licensed under the MIT License. See the [LICENSE](./LICENSE) file for details. diff --git a/bun.lockb b/bun.lockb new file mode 100755 index 0000000000000000000000000000000000000000..c7bc778355843a6a170562fab36ef32209dffd43 GIT binary patch literal 251028 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 Wc@)vuoj#kqgIe62UGo3w-+ux%&)_cr literal 0 HcmV?d00001 diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..f8a956e --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,13 @@ +version: '3.8' +services: + scout: + container_name: chasm_scout + build: + context: . + dockerfile: Dockerfile + restart: always + ports: + - "${PORT}:${PORT}" + env_file: + - ./.env + diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md new file mode 100644 index 0000000..a3646d3 --- /dev/null +++ b/docs/CONTRIBUTING.md @@ -0,0 +1,31 @@ +# Contributing to Chasm Scout + +Thank you for considering contributing to Chasm Scout! +We welcome contributions from the community to help improve the project. +Please take a moment to review this document before submitting your contributions. + +## How to Contribute + +### Reporting Issues + +If you encounter a bug or have a feature request, please open an issue +in the [GitHub Issue Tracker](https://github.com/ChasmNetwork/chasm-scout/issues). +When reporting an issue, please include: + +- A clear and descriptive title. +- A detailed description of the problem or the enhancement you are requesting. +- Steps to reproduce the issue (if applicable). +- Any relevant logs, screenshots, or other information that might help us understand the issue. + +### Submitting Pull Requests + +We accept pull requests for bug fixes, improvements, and new features. To submit a pull request: + +1. **Fork the Repository**: Click the "Fork" button on the top right of the repository page to create your own fork of the project. + +2. **Clone the Fork**: Clone your fork to your local machine. + + ```sh + git clone https://github.com/ChasmNetwork/chasm-scout.git + cd chasm-scout + ``` diff --git a/docs/DEVELOP.md b/docs/DEVELOP.md new file mode 100644 index 0000000..3903016 --- /dev/null +++ b/docs/DEVELOP.md @@ -0,0 +1,164 @@ +# Developing Chasm Scout + +## Guideline + +To start developing on Chasm Scout, follow these steps: + +1. Fork the Repository: Click the "Fork" button on the top right of the repository page to create your own fork of the project. + +2. Clone the Fork: Clone your fork to your local machine. + +```sh +git clone https://github.com/your-username/chasm-scout.git +cd chasm-scout +``` + +3. Install dependencies: + + ```sh + bun install + ``` + +4. Set up the environment variables: + + ```sh + cp .env.example .env + ``` + +5. Start the development server: + + ```sh + bun dev + ``` + +6. Run/Add Tests: Ensure that all tests pass before making changes. + + ```sh + bun run test + ``` + +7. Make Your Changes: Develop new features, fix bugs, or improve documentation. + +8. Commit Your Changes: Once you are satisfied with your changes, commit them to your fork. + + ```sh + git add . + git commit -m "Your commit message" + git push origin main + ``` + +9. Create a Pull Request: Go to the original repository and click on the "New Pull Request" button to create a new pull request from your fork. + +## Docker Setup + +To run the project using Docker, follow these steps: + +1. Build the Docker image: + + ```sh + docker build -t chasm-scout . + ``` + +2. Run the Docker container: + + ```sh + docker run -p 3000:3000 chasm-scout + ``` + +3. Access the application at `http://localhost:3000`. + +4. To stop the container, run: + + ```sh + docker stop $(docker ps -a -q --filter ancestor=chasm-scout --format="{{.ID}}") + ``` + +## Docker Compose Setup + +To run the project using Docker Compose, follow these steps: + +1. Build the Docker image: + + ```sh + docker-compose build + ``` + +2. Run the Docker container: + + ```sh + docker-compose up + ``` + +3. Access the application at `http://localhost:3000`. + +4. To stop the container, run: + + ```sh + docker-compose down + ``` + +## Formatting & Linting + +This project follows strict formatting and linting standards to ensure clean and consistent code across the codebase. + +### Setting up VSCode + +Step 1: +Install the [ESLint](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint) extension from Microsoft. + +Step 2: +Open up `settings.json` + +Step 3: +Add the following lines + +```json +{ + "prettier.configPath": ".prettierrc.json", + "editor.formatOnSave": true, + "[javascript]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "editor.codeActionsOnSave": { + "source.fixAll.eslint": true + }, + "eslint.validate": ["typescript"], + "eslint.codeActionsOnSave.mode": "problems" +} +``` + +### Prettier Setup + +We utilize [Prettier](https://prettier.io/) for code formatting. Additionally, we have integrated the `@ianvs/prettier-plugin-sort-imports` plugin to sort import statements automatically. + +To customize the import order, you can update the `importOrder` key in `.prettierrc.json` file as shown below: + +```json +{ + "importOrder": ["", "", "^[./]"] +} +``` + +### Linting + +For linting, we use ESLint, enforcing default recommended rules along with TypeScript-specific rules. ESLint is configured to only check for syntax issues and problems. + +To configure additional rules, you can either use third-party plugins like Airbnb or define your own rules. Below are examples: + +```javascript +// .eslintrc.json +{ + // Other configuration here + rules: { + // Custom rules here + } +} +``` + +### Husky integration with lint-staged + +We have set up Lint-Staged and Husky to run Prettier and ESLint on commit. This ensures that any formatting or linting issues are fixed automatically. If there are failures, the commit will be prevented. + +### Github Workflows + +GitHub Actions workflows have been configured to check formatting and linting on pull requests. Merging to the test, staging, and production environments will be blocked if there are any issues detected. diff --git a/docs/OPTIMZATION.md b/docs/OPTIMZATION.md new file mode 100644 index 0000000..60a3369 --- /dev/null +++ b/docs/OPTIMZATION.md @@ -0,0 +1,25 @@ +# Scout Optimization + +Scout nodes are essential elements within Chasm's CoDEX, responsible for delivering LLM results with precision and speed. Effective optimization of these nodes is crucial for maintaining peak system functionality. + +## How to optimize your scout + +The primary goal is to enhance the consistency, uptime, and response time of the Scout nodes to ensure they operate at peak efficiency. + +Here are a few strategies for optimizing the scouts: + +1. Infrastructure Management + +Ensure Scout nodes are hosted on robust servers equipped to manage anticipated loads, optimizing for performance and stability: + +- [Docker](https://www.docker.com/): Utilize Docker for scalable and efficient management of containerized applications. This allows for agile adjustments to resources based on demand. +- [PM2](https://pm2.keymetrics.io/): Implement PM2 for improved security and uptime. This tool automates process management, helping to streamline deployments and maintain operations through continuous monitoring and automatic restarts. + +2. Redundancy and Reliability + To minimize downtime and maintain continuous service availability: + +- Failover Mechanisms: Implement robust failover systems that automatically switch to backup operations in case the primary systems fail. +- External Resources: Integrate third-party cloud services or deploy self-hosted GPU clusters to augment computing power and provide additional redundancy. + +3. Update The latest code + Scout node code is updated periodically. Ensure you are running the latest version of the Scout nodes to achieve the best performance. diff --git a/docs/SETUP.md b/docs/SETUP.md new file mode 100644 index 0000000..7a3b1be --- /dev/null +++ b/docs/SETUP.md @@ -0,0 +1,123 @@ +# Setup Guide + +## Prerequisite + +- Server Requirement + + - Operating System: Linux (linux/amd64, linux/arm64) + - Min Requirement: 1 vCPU, 1GB RAM / 20GB Disk, Static IP + - Suggested Requirement: 2 vCPU, 4GB RAM / 50GB SSD, Static IP + +- Docker installation: https://docs.docker.com/engine/install/ubuntu/ + +# Quick run + +0. Getting API Key from API Provider + +- [Groq](https://console.groq.com/keys) + +1. Prepare a .env file and copy paste it from [.env.sample](../.env.sample) and edit to your own need + +```sh +# Use your fav text editor, vim/nano +nano .env +``` + +2. Pull the docker images from https://hub.docker.com/r/johnsonchasm/chasm-scout + +```sh +docker pull johnsonchasm/chasm-scout:latest +``` + +3. Run the file + +```sh +docker run -d --restart=always --env-file ./.env -p 3001:3001 --name scout johnsonchasm/chasm-scout +``` + +# Run docker from codebase + +## 1. Download Codebase + +Download the codebase + +```sh +# Download Via SSH +git clone git@github.com:ChasmNetwork/chasm-scout.git +# Download Via HTTPS +git clone https://github.com/ChasmNetwork/chasm-scout.git +``` + +Navigate to the project directory + +``` +cd chasm-scout +``` + +## 2. Getting API Key and UID from Chasm + +- Follow the steps provided by Chasm Network to obtain your API key and UID. + +## 3. Getting API Key from API Provider + +- [OpenAI ChatGPT API](https://help.openai.com/en/articles/4936850-where-do-i-find-my-openai-api-key) +- [Groq](https://console.groq.com/keys) + +## 4. Setup .env + +To the run command below replacing the path to your `.env` file + +```bash +# Copy dot env +cp .env.sample .env +``` + +Update the `.env` file + +## 5. Running the server with Docker + +> Scouts need to establish an internet connection with the orchestrator. This requires ensuring that the `PORT` specified in .env is reachable on the Docker Container via the internet. This involves either opening the port on the firewall or configuring port forwarding. + +```bash +# Build Docker Image +docker build --tag 'chasm_scout' . +# Run docker +# Can consider adding --restart=always flag to always restart for production +# Note: Remember to change the port +docker run --env-file ./.env -p 3001:3001 --name scout chasm_scout +``` + +Alternative: Docker Compose + +``` +PORT=3001 docker compose up -d +``` + +## 6. Verifying the Setup + +Access the running application at http://:3001 to verify the setup. + +Check the Docker container logs for any issues: + +```bash +docker logs scout +``` + +# Run via Nodejs + +Make sure your node.js version are >21 and have bun installed + +```sh +bun i +bun run build +bun run dist/server/express.js +``` + +## Troubleshooting + +- **Environment Variables**: Double-check that all required environment variables are set correctly in the `.env` file. + +## Additional Resources + +- **Optimization Guide**: Follow the [optimization guide](/docs/OPTIMZATION.md) for performance improvements. +- **Update Guide**: Follow the [update guide](/docs/UPDATE.md) for updating your scout. diff --git a/docs/UPDATE.md b/docs/UPDATE.md new file mode 100644 index 0000000..5e21f50 --- /dev/null +++ b/docs/UPDATE.md @@ -0,0 +1,31 @@ +# Updating Scout + +## Pull the latest code + +```sh +# Pull the latest code +git fetch +git pull origin main +``` + +## Docker container + +### For Docker Run + +```sh +# Restart docker +docker stop scout +docker rm scout + +# Rebuild docker +docker build --tag 'chasm_scout' . +# Can consider adding --restart=always flag to always restart for production +docker run --env-file ./.env -p 3001:3001 --name scout chasm_scout +``` + +### For Docker Compose + +```sh +docker compose down +PORT=3001 docker-compose up --build -d +``` diff --git a/jest.config.ts b/jest.config.ts new file mode 100644 index 0000000..6e53365 --- /dev/null +++ b/jest.config.ts @@ -0,0 +1,198 @@ +/** + * For a detailed explanation regarding each configuration property, visit: + * https://jestjs.io/docs/configuration + */ + +import type { Config } from "jest"; + +const config: Config = { + // All imported modules in your tests should be mocked automatically + // automock: false, + + // Stop running tests after `n` failures + // bail: 0, + + // The directory where Jest should store its cached dependency information + // cacheDirectory: "/private/var/folders/59/nw7wq89d40zd49732yrjp78c0000gn/T/jest_dx", + + // Automatically clear mock calls, instances, contexts and results before every test + clearMocks: true, + + // Indicates whether the coverage information should be collected while executing the test + collectCoverage: true, + + // An array of glob patterns indicating a set of files for which coverage information should be collected + // collectCoverageFrom: undefined, + + // The directory where Jest should output its coverage files + coverageDirectory: "coverage", + + // An array of regexp pattern strings used to skip coverage collection + // coveragePathIgnorePatterns: [ + // "/node_modules/" + // ], + + // Indicates which provider should be used to instrument code for coverage + coverageProvider: "v8", + + // A list of reporter names that Jest uses when writing coverage reports + // coverageReporters: [ + // "json", + // "text", + // "lcov", + // "clover" + // ], + + // An object that configures minimum threshold enforcement for coverage results + // coverageThreshold: undefined, + + // A path to a custom dependency extractor + // dependencyExtractor: undefined, + + // Make calling deprecated APIs throw helpful error messages + // errorOnDeprecated: false, + + // The default configuration for fake timers + // fakeTimers: { + // "enableGlobally": false + // }, + + // Force coverage collection from ignored files using an array of glob patterns + // forceCoverageMatch: [], + + // A path to a module which exports an async function that is triggered once before all test suites + // globalSetup: undefined, + + // A path to a module which exports an async function that is triggered once after all test suites + // globalTeardown: undefined, + + // A set of global variables that need to be available in all test environments + // globals: {}, + + // The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers. + // maxWorkers: "50%", + + // An array of directory names to be searched recursively up from the requiring module's location + // moduleDirectories: [ + // "node_modules" + // ], + + // An array of file extensions your modules use + // moduleFileExtensions: [ + // "js", + // "mjs", + // "cjs", + // "jsx", + // "ts", + // "tsx", + // "json", + // "node" + // ], + + // A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module + // moduleNameMapper: {}, + + // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader + // modulePathIgnorePatterns: [], + + // Activates notifications for test results + // notify: false, + + // An enum that specifies notification mode. Requires { notify: true } + // notifyMode: "failure-change", + + // A preset that is used as a base for Jest's configuration + preset: "ts-jest", + + // Run tests from one or more projects + // projects: undefined, + + // Use this configuration option to add custom reporters to Jest + // reporters: undefined, + + // Automatically reset mock state before every test + // resetMocks: false, + + // Reset the module registry before running each individual test + // resetModules: false, + + // A path to a custom resolver + // resolver: undefined, + + // Automatically restore mock state and implementation before every test + // restoreMocks: false, + + // The root directory that Jest should scan for tests and modules within + // rootDir: undefined, + + // A list of paths to directories that Jest should use to search for files in + // roots: [ + // "" + // ], + + // Allows you to use a custom runner instead of Jest's default test runner + // runner: "jest-runner", + + // The paths to modules that run some code to configure or set up the testing environment before each test + setupFiles: [], + + // A list of paths to modules that run some code to configure or set up the testing framework before each test + setupFilesAfterEnv: ["./tests/setup.ts"], + + // The number of seconds after which a test is considered as slow and reported as such in the results. + // slowTestThreshold: 5, + + // A list of paths to snapshot serializer modules Jest should use for snapshot testing + // snapshotSerializers: [], + + // The test environment that will be used for testing + testEnvironment: "node", + + // Options that will be passed to the testEnvironment + // testEnvironmentOptions: {}, + + // Adds a location field to test results + // testLocationInResults: false, + + // The glob patterns Jest uses to detect test files + testMatch: ["**/__tests__/**/*.+(ts|tsx)", "**/?(*.)+(spec|test).+(ts|tsx)"], + + // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped + // testPathIgnorePatterns: [ + // "/node_modules/" + // ], + + // The regexp pattern or array of patterns that Jest uses to detect test files + // testRegex: [], + + // This option allows the use of a custom results processor + // testResultsProcessor: undefined, + + // This option allows use of a custom test runner + // testRunner: "jest-circus/runner", + + // A map from regular expressions to paths to transformers + transform: { + "^.+\\.(ts|tsx)$": "ts-jest", + }, + + // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation + // transformIgnorePatterns: [ + // "/node_modules/", + // "\\.pnp\\.[^\\/]+$" + // ], + + // An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them + // unmockedModulePathPatterns: undefined, + + // Indicates whether each individual test should be reported during the run + // verbose: undefined, + + // An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode + // watchPathIgnorePatterns: [], + + // Whether to use watchman for file crawling + // watchman: true, +}; + +export default config; diff --git a/package.json b/package.json new file mode 100644 index 0000000..5c65675 --- /dev/null +++ b/package.json @@ -0,0 +1,62 @@ +{ + "name": "scout", + "version": "0.0.3", + "description": "", + "main": "index.js", + "scripts": { + "dev": "nodemon src/server/express.ts", + "build": "tsc", + "prepare": "husky", + "prettier": "prettier . --check", + "prettier:fix": "prettier . --write", + "lint": "eslint .", + "lint:fix": "eslint . --fix", + "test": "jest", + "test:unit": "jest --testPathPattern=tests/unit", + "test:integration": "jest --testPathPattern=tests/integration" + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "axios": "^1.6.7", + "chalk": "^4", + "dotenv": "^16.4.5", + "envalid": "^8.0.0", + "express": "^4.18.3", + "openai": "^4.28.4", + "uuid": "^9.0.1", + "viem": "^2.7.19", + "winston": "^3.13.0", + "ws": "^8.17.0" + }, + "devDependencies": { + "@ianvs/prettier-plugin-sort-imports": "^4.2.1", + "@types/axios": "^0.14.0", + "@types/bun": "^1.1.5", + "@types/chance": "^1.1.6", + "@types/dotenv": "^8.2.0", + "@types/express": "^4.17.21", + "@types/jest": "^29.5.12", + "@types/node": "^20.11.24", + "@types/supertest": "^6.0.2", + "@types/uuid": "^9.0.8", + "@types/ws": "^8.5.10", + "@typescript-eslint/eslint-plugin": "^7.13.0", + "@typescript-eslint/parser": "^7.13.0", + "chance": "^1.1.11", + "eslint": "8.57.0", + "eslint-config-prettier": "^9.1.0", + "eslint-plugin-unused-imports": "^3.2.0", + "husky": "^9.0.11", + "jest": "^29.7.0", + "jest-ai": "^2.0.1", + "lint-staged": "^15.2.7", + "nodemon": "^3.1.0", + "prettier": "^3.3.2", + "supertest": "^6.3.4", + "ts-jest": "^29.1.5", + "ts-node": "^10.9.2", + "typescript": "^5.3.3" + } +} diff --git a/script/openaiCompatibleTest.ts b/script/openaiCompatibleTest.ts new file mode 100644 index 0000000..f56e5dc --- /dev/null +++ b/script/openaiCompatibleTest.ts @@ -0,0 +1,52 @@ +import dotenv from "dotenv"; +import OpenAI from "openai"; + +dotenv.config(); + +const baseURL = new URL(process.env.WEBHOOK_URL!).toString() + "v1"; +console.log(baseURL); + +const openai = new OpenAI({ + baseURL, + apiKey: process.env.WEBHOOK_API_KEY, +}); + +(async () => { + console.log("Calling Stream\n"); + const result1 = await openai.chat.completions.create({ + stream: true, + model: "gemma-7b-it", + messages: [ + { + role: "system", + content: "You are a helpful assistant.", + }, + { + role: "user", + content: "What is the meaning of life?", + }, + ], + }); + for await (const chunk of result1) { + process.stdout.write(chunk.choices[0].delta.content || ""); + } + console.log("\n---------------"); + console.log("\n\nCalling Non-Stream"); + const result2 = await openai.chat.completions.create({ + stream: false, + model: "gemma2-9b-it", + messages: [ + { + role: "system", + content: "You are a helpful assistant.", + }, + { + role: "user", + content: "What is the meaning of life?", + }, + ], + }); + console.log(result2.choices[0].message.content); + console.log("\n---------------"); + console.log((result2 as any).scout); +})(); diff --git a/script/publish_docker_hub.sh b/script/publish_docker_hub.sh new file mode 100755 index 0000000..1670379 --- /dev/null +++ b/script/publish_docker_hub.sh @@ -0,0 +1,27 @@ +#!/bin/sh +# Get the version from package.json +VERSION=$(jq -r '.version' package.json) + +# Check if the -y flag is provided +CONFIRM=true +for arg in "$@" +do + if [ "$arg" = "-y" ]; then + CONFIRM=false + break + fi +done + +if $CONFIRM; then + echo "You are about to build and push the Docker image with version: $VERSION" + read -p "Do you want to proceed? (y/n) " -n 1 -r + echo + if [[ ! $REPLY =~ ^[Yy]$ ]]; then + echo "Aborting Docker build and push." + exit 1 + fi +fi + + +# Confirm +docker buildx build --platform linux/amd64,linux/arm64 -t johnsonchasm/chasm-scout:$VERSION -t johnsonchasm/chasm-scout:latest --push . diff --git a/script/randomSamplingTest.ts b/script/randomSamplingTest.ts new file mode 100644 index 0000000..32d24e8 --- /dev/null +++ b/script/randomSamplingTest.ts @@ -0,0 +1,45 @@ +import { groqQuery } from "../src/integration/groq"; +import { ollamaQuery } from "../src/integration/ollama"; +import { openRouterQuery } from "../src/integration/openrouter"; +import { getModelName, LLMProvider } from "../src/utils/llm"; + +const model = "gemma-7b-it"; +const query: any = { + messages: [ + { + role: "system", + content: + "You are a helpful assistant. You are given a question and answer choices, please only pick one of the answer choice without explanation.", + }, + { + role: "user", + content: `Question: Find the characteristic of the ring Z x Z. +Answer Choices: [ "0", "3", "12", "30" ] +`, + }, + ], + seed: Math.floor(Math.random() * 10000), + temperature: 0.1, + model, +}; + +async function main() { + const res = await Promise.all([ + ollamaQuery({ + ...query, + model: getModelName(LLMProvider.OLLAMA, model), + }), + openRouterQuery({ + ...query, + model: getModelName(LLMProvider.OPENROUTER, model), + }), + groqQuery({ + ...query, + model: getModelName(LLMProvider.GROQ, model), + }), + ]); + + console.log(res); +} + +main(); diff --git a/script/test_groq_query.sh b/script/test_groq_query.sh new file mode 100755 index 0000000..3446cd1 --- /dev/null +++ b/script/test_groq_query.sh @@ -0,0 +1,23 @@ +#!/bin/sh + +# Load the .env file +export $(grep -v '^#' .env | xargs) + +# Check if WEBHOOK_API_KEY is set +if [ -z "$WEBHOOK_API_KEY" ]; then + echo "WEBHOOK_API_KEY is not set in the .env file" + exit 1 +fi + +# Check if WEBHOOK_URL is set +if [ -z "$WEBHOOK_URL" ]; then + echo "WEBHOOK_URL is not set in the .env file" + exit 1 +fi + +# Make a POST request using curl to 3001 with authorization +curl -X POST \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer $WEBHOOK_API_KEY" \ + -d '{"body":"{\"model\":\"gemma-7b-it\",\"messages\":[{\"role\":\"system\",\"content\":\"You are a helpful assistant.\"}]}"}' \ + $WEBHOOK_URL diff --git a/src/config.ts b/src/config.ts new file mode 100644 index 0000000..e2c519e --- /dev/null +++ b/src/config.ts @@ -0,0 +1,96 @@ +import "dotenv/config"; + +import { cleanEnv, json, makeValidator, num, port, str, url } from "envalid"; + +import { LLMProvider } from "./utils/llm"; + +process.env.OPENAI_API_KEY = process.env.OPENAI_API_KEY || ""; +process.env.OPENROUTER_API_KEY = process.env.OPENROUTER_API_KEY || ""; + +// Validator for LLM providers +const providersValidator = makeValidator((x) => { + const providers = x.split(",") as LLMProvider[]; + if (providers.length === 0) { + throw new Error("No PROVIDERS provided"); + } + providers.forEach((provider) => { + if (!Object.values(LLMProvider).includes(provider)) { + throw new Error(`Invalid PROVIDER: ${provider}`); + } + }); + + return providers; +}); + +// Validator for API keys based on provider type +const apiKeyValidator = (type: string, providers: LLMProvider[]) => + makeValidator((apiKey) => { + // If the provider is not in the list, we don't need to validate the API key + if (!providers.includes(type as LLMProvider)) { + return apiKey; + } + + switch (type) { + case "groq": { + if (!apiKey.startsWith("gsk_")) { + throw new Error(`Invalid ${type} API key: ${apiKey}`); + } + break; + } + case "openai": + case "openrouter": { + if (!apiKey.startsWith("sk")) { + throw new Error(`Invalid ${type} API key: ${apiKey}`); + } + break; + } + case "chasm": { + if (apiKey.length !== 44) { + throw new Error(`Invalid ${type} API key: ${apiKey}`); + } + break; + } + default: { + throw new Error(`Invalid API key type: ${type}`); + } + } + + return apiKey.trim(); + }); + +const PROVIDERS = process.env.PROVIDERS?.split(",") as LLMProvider[]; +const handshakeProtocol = process.env.NODE_ENV !== "production" ? "ws" : "wss"; + +// Validate and clean environment variables +export const env = cleanEnv(process.env, { + PORT: port({ + default: 3001, + }), + LOGGER_LEVEL: str({ choices: ["debug", "info", "warn", "error", "fatal"] }), + ORCHESTRATOR_URL: url(), + SCOUT_NAME: str(), + SCOUT_UID: num(), + WEBHOOK_API_KEY: str(), + WEBHOOK_URL: url(), + PROVIDERS: providersValidator(), + GROQ_API_KEY: apiKeyValidator("groq", PROVIDERS)(), + OPENAI_API_KEY: apiKeyValidator("openai", PROVIDERS)(), + OPENROUTER_API_KEY: apiKeyValidator("openrouter", PROVIDERS)(), + MODEL: str(), + OPENROUTER_CUSTOM_CONFIG: json({ + default: { + temperature: 0, + provider: { + allow_fallbacks: false, + order: ["Fireworks"], + }, + }, + }), + + // Optional + HANDSHAKE_PROTOCOL: str({ + choices: ["ws", "wss"], + default: handshakeProtocol, + }), + IP: str({ default: "127.0.0.1" }), +}); diff --git a/src/integration/OpenAIBase.ts b/src/integration/OpenAIBase.ts new file mode 100644 index 0000000..f26ec49 --- /dev/null +++ b/src/integration/OpenAIBase.ts @@ -0,0 +1,67 @@ +import OpenAI from "openai"; +import { Stream } from "openai/streaming"; + +import { logger } from "../utils/logger"; + +export abstract class OpenAIBase { + protected openai: OpenAI; + + constructor(apiKey: string, baseURL?: string) { + this.openai = new OpenAI({ + apiKey: apiKey, + baseURL: baseURL, + }); + } + + async query( + body: OpenAI.Chat.Completions.ChatCompletionCreateParamsNonStreaming, + ): Promise< + | OpenAI.Chat.Completions.ChatCompletion + | Stream + > { + const isStreaming = body.stream ?? false; + try { + const stream = await this.openai.chat.completions.create({ + ...body, + stream: true, + }); + if (isStreaming) { + return stream; + } else { + let accumulatedData = ""; + let completion: OpenAI.Chat.Completions.ChatCompletion | null = null; + const usage = { + prompt_tokens: 0, + completion_tokens: 0, + total_tokens: 0, + }; + + for await (const chunk of stream) { + accumulatedData += chunk.choices[0].delta.content || ""; + completion = chunk as any; + usage.completion_tokens += 1; + } + usage.total_tokens = usage.prompt_tokens + usage.completion_tokens; + + return { + ...completion!, + usage, + choices: [ + { + index: 0, + message: { + role: "assistant", + content: accumulatedData, + }, + finish_reason: "stop", + logprobs: null, + }, + ], + }; + } + } catch (error) { + logger.debug("[base] Error: %o", error); + throw error; + } + } +} diff --git a/src/integration/groq.ts b/src/integration/groq.ts new file mode 100644 index 0000000..a4f1c47 --- /dev/null +++ b/src/integration/groq.ts @@ -0,0 +1,22 @@ +import OpenAI from "openai"; + +import { env } from "../config"; +import { logger } from "../utils/logger"; +import { OpenAIBase } from "./OpenAIBase"; + +class GroqLLM extends OpenAIBase { + constructor(apiKey: string) { + // Groq OpenAI Compatibility: https://console.groq.com/docs/openai + super(apiKey, "https://api.groq.com/openai/v1/"); + } +} + +export const groqQuery = async ( + body: OpenAI.Chat.Completions.ChatCompletionCreateParamsNonStreaming, +) => { + logger.debug("[groq] Req: %o", body); + const groq = new GroqLLM(env.GROQ_API_KEY); + const response = await groq.query(body); + logger.debug("[groq] Res: %o", response); + return response; +}; diff --git a/src/integration/ollama.ts b/src/integration/ollama.ts new file mode 100644 index 0000000..5f0e00e --- /dev/null +++ b/src/integration/ollama.ts @@ -0,0 +1,20 @@ +import OpenAI from "openai"; + +import { logger } from "../utils/logger"; +import { OpenAIBase } from "./OpenAIBase"; + +class OllamaLLM extends OpenAIBase { + constructor() { + super("ollama", "http://localhost:11434/v1"); + } +} + +export const ollamaQuery = async ( + body: OpenAI.Chat.Completions.ChatCompletionCreateParamsNonStreaming, +) => { + logger.debug("[ollama] Req: %o", body); + const ollama = new OllamaLLM(); + const response = await ollama.query(body); + logger.debug("[ollama] Res: %o", response); + return response; +}; diff --git a/src/integration/openai.ts b/src/integration/openai.ts new file mode 100644 index 0000000..a4eb2e3 --- /dev/null +++ b/src/integration/openai.ts @@ -0,0 +1,20 @@ +import OpenAI from "openai"; + +import { env } from "../config"; +import { logger } from "../utils/logger"; + +class OpenAILLM extends OpenAI { + constructor(apiKey: string) { + super({ apiKey }); + } +} + +export const openAiQuery = async ( + body: OpenAI.Chat.Completions.ChatCompletionCreateParamsNonStreaming, +) => { + logger.debug("[openai] Req: %o", body); + const openai = new OpenAILLM(env.OPENAI_API_KEY); + const response = await openai.chat.completions.create(body); + logger.debug("[openai] Res: %o", response); + return response; +}; diff --git a/src/integration/openrouter.ts b/src/integration/openrouter.ts new file mode 100644 index 0000000..91eb56d --- /dev/null +++ b/src/integration/openrouter.ts @@ -0,0 +1,25 @@ +import OpenAI from "openai"; + +import { env } from "../config"; +import { logger } from "../utils/logger"; +import { OpenAIBase } from "./OpenAIBase"; + +class OpenRouterLLM extends OpenAIBase { + constructor(apiKey: string) { + super(apiKey, "https://openrouter.ai/api/v1"); + } +} + +export const openRouterQuery = async ( + body: OpenAI.Chat.Completions.ChatCompletionCreateParamsNonStreaming, +) => { + const newBody = { + ...body, + ...env.OPENROUTER_CUSTOM_CONFIG, + }; + logger.debug("[openrouter] Req: %o", newBody); + const or = new OpenRouterLLM(env.OPENROUTER_API_KEY); + const response = await or.query(newBody); + logger.debug("[openrouter] Res: %o", response); + return response; +}; diff --git a/src/middleware/apiKeyMiddleware.ts b/src/middleware/apiKeyMiddleware.ts new file mode 100644 index 0000000..4f56f70 --- /dev/null +++ b/src/middleware/apiKeyMiddleware.ts @@ -0,0 +1,30 @@ +import { NextFunction, Request, Response } from "express"; + +import { env } from "../config"; +import { verifyApiKey } from "../utils/apiKey"; + +export const apikeyAuthMiddleware = async ( + req: Request, + res: Response, + next: NextFunction, +) => { + const authorization = req.headers.authorization; + const apiKey = authorization ? authorization.split(" ")[1] : null; + + if (!apiKey) { + res.status(401).json({ message: "Unauthorized" }); + return; + } + + try { + const valid = verifyApiKey(apiKey, env.WEBHOOK_API_KEY); + + if (valid) { + next(); + } else { + res.status(401).json({ message: "Unauthorized" }); + } + } catch (error: any) { + res.status(400).json({ message: error.message }); + } +}; diff --git a/src/server/express.ts b/src/server/express.ts new file mode 100644 index 0000000..a0a9254 --- /dev/null +++ b/src/server/express.ts @@ -0,0 +1,64 @@ +import chalk from "chalk"; +import express from "express"; + +import { env } from "../config"; +import { apikeyAuthMiddleware } from "../middleware/apiKeyMiddleware"; +import { logger } from "../utils/logger"; +import { handshake } from "./handshake"; +import { webhook } from "./webhook"; + +const { + GROQ_API_KEY, + OPENAI_API_KEY, + OPENROUTER_API_KEY, + ORCHESTRATOR_URL, + PORT, + PROVIDERS, + SCOUT_NAME, + SCOUT_UID, +} = env; + +function getProviders() { + const providers = []; + if (OPENAI_API_KEY) { + providers.push("OpenAi"); + } + if (GROQ_API_KEY) { + providers.push("Groq"); + } + if (OPENROUTER_API_KEY) { + providers.push("OpenRouter"); + } + return providers.join(", "); +} + +logger.info(`Starting Scout ⚜️`); +logger.info("--------------------------------------------"); +logger.info(`Scout UID: ${chalk.bold(SCOUT_UID)}`); +logger.info(`Scout Name: ${chalk.bold(SCOUT_NAME)}`); +logger.info(`Port: ${chalk.bold(PORT)}`); +logger.info(`Providers: ${chalk.bold(PROVIDERS.join(", "))}`); +logger.info(`Provider API Key set for: ${chalk.bold(getProviders())}`); +logger.info(`Orchestrator URL: ${chalk.bold(ORCHESTRATOR_URL)}`); +logger.info("--------------------------------------------"); + +const app = express(); + +app.use(express.json()); + +// app.post("/", webhook); +app.post("/", apikeyAuthMiddleware, webhook()); +app.post("/v1/chat/completions", apikeyAuthMiddleware, webhook(true)); +app.get("/", (req, res) => { + return res.status(200).send("OK"); +}); + +app.listen(PORT, async () => { + logger.info(`Server running on port ${PORT}`); + await handshake().catch((err) => { + logger.error(err.message); + process.exit(1); + }); +}); + +export default app; diff --git a/src/server/handshake.ts b/src/server/handshake.ts new file mode 100644 index 0000000..ca6fee3 --- /dev/null +++ b/src/server/handshake.ts @@ -0,0 +1,84 @@ +import chalk from "chalk"; +import WebSocket from "ws"; + +import { env } from "../config"; +import { logger } from "../utils/logger"; +import { getVersion } from "../utils/version"; + +const { + IP, + HANDSHAKE_PROTOCOL, + ORCHESTRATOR_URL, + SCOUT_NAME, + SCOUT_UID, + WEBHOOK_API_KEY, + WEBHOOK_URL, + PROVIDERS, + MODEL, +} = env; + +export async function handshake() { + if (!ORCHESTRATOR_URL) { + throw new Error("Please set the ORCHESTRATOR_URL environment variable"); + } + + logger.debug("Connecting to orchestrator at " + ORCHESTRATOR_URL); + + const ws = new WebSocket( + `${HANDSHAKE_PROTOCOL}://${ORCHESTRATOR_URL.split("//")[1]}?uid=${SCOUT_UID}`, + { + headers: { + Authorization: `Bearer ${WEBHOOK_API_KEY}`, + }, + }, + ); + + ws.on("open", function open() { + logger.debug("Initiated WS handshake"); + + logger.info(`Setting up webhook at: ${WEBHOOK_URL}`); + ws.send( + JSON.stringify({ + type: "webhook", + webhookUrl: WEBHOOK_URL, + ip: IP, + version: getVersion(), + name: SCOUT_NAME, + providers: PROVIDERS, + model: MODEL, + }), + ); + }); + + ws.on("error", (error) => { + throw new Error( + `Handshake failed: ${error.message}\n${JSON.stringify(error, null, 2)}`, + ); + }); + + ws.on("close", (code, reason) => { + if (code !== 1000) { + throw new Error( + `❌ Handshake with orchestrator at ${ORCHESTRATOR_URL} failed with error:\n${reason}`, + ); + } + logger.debug(`WS connection closed by the server with ${code}`); + }); + + ws.on("message", function incoming(data) { + const { success } = JSON.parse(data.toString()); + if (success) { + logger.info( + chalk.green.bold( + `✅ Handshake with orchestrator at ${ORCHESTRATOR_URL} complete`, + ), + ); + ws.close(); + return; + } else { + throw new Error( + "❌ Handshake with orchestrator failed. No response from server.", + ); + } + }); +} diff --git a/src/server/webhook.ts b/src/server/webhook.ts new file mode 100644 index 0000000..d104847 --- /dev/null +++ b/src/server/webhook.ts @@ -0,0 +1,87 @@ +import { Request, Response } from "express"; +import OpenAI from "openai"; +import { Stream } from "openai/streaming"; + +import { env } from "../config"; +import { getLlmQuery, getModelName } from "../utils/llm"; +import { logger } from "../utils/logger"; + +export const webhook = + (openAiCompatible: boolean = false) => + async (req: Request, res: Response) => { + const { body } = req.body; + let requestBody; + + if (openAiCompatible) { + requestBody = req.body; + } else { + requestBody = JSON.parse(body); + } + const model = requestBody.model; + + let result: + | OpenAI.Chat.Completions.ChatCompletion + | Stream + | null = null; + let usedProvider = ""; + let usedModel = ""; + + try { + for (let i = 0; i < env.PROVIDERS.length; i++) { + const provider = env.PROVIDERS[i]; + + try { + const llmQuery = getLlmQuery(provider); + const modelName = getModelName(provider, model); + result = await llmQuery({ + ...requestBody, + model: modelName, + }); + usedProvider = provider; + usedModel = modelName; + if (result instanceof Stream) { + res.setHeader("Content-Type", "text/event-stream"); + res.setHeader("Cache-Control", "no-cache"); + res.setHeader("Connection", "keep-alive"); + try { + for await (const chunk of result) { + const chunkData = JSON.stringify({ + ...chunk, + scout: { + provider: usedProvider, + model: usedModel, + }, + }); + res.write(`data: ${chunkData}\n\n`); + } + res.write(`data: [DONE]\n\n`); + } catch (streamErr) { + logger.error("Error while streaming", streamErr); + } finally { + res.end(); // Ensure response is properly ended if an error occurs during streaming + } + return; + } + break; + } catch (err) { + logger.error("Provider failed", provider); + logger.error(err); + } + } + + if (!result) { + return res.status(500).send({ error: "All providers failed" }); + } + + return res.status(200).send({ + ...result, + scout: { + provider: usedProvider, + model: usedModel, + }, + }); + } catch (err) { + logger.error(err); + return res.status(500).send({ error: "Internal Server Error" }); + } + }; diff --git a/src/utils/apiKey.ts b/src/utils/apiKey.ts new file mode 100644 index 0000000..fa5987d --- /dev/null +++ b/src/utils/apiKey.ts @@ -0,0 +1,13 @@ +export function verifyApiKey( + providedApiKey: string, + storedApiKey: string, +): boolean { + const decodedProvidedApiKey = Buffer.from(providedApiKey, "base64").toString( + "utf8", + ); + const decodedStoredApiKey = Buffer.from(storedApiKey, "base64").toString( + "utf8", + ); + + return decodedProvidedApiKey === decodedStoredApiKey; +} diff --git a/src/utils/llm.ts b/src/utils/llm.ts new file mode 100644 index 0000000..1af76d4 --- /dev/null +++ b/src/utils/llm.ts @@ -0,0 +1,70 @@ +import { groqQuery } from "../integration/groq"; +import { ollamaQuery } from "../integration/ollama"; +import { openAiQuery } from "../integration/openai"; +import { openRouterQuery } from "../integration/openrouter"; +import { logger } from "./logger"; + +export enum LLMProvider { + GROQ = "groq", + OPENAI = "openai", + OPENROUTER = "openrouter", + OLLAMA = "ollama", +} + +export type QueryFunction = + | typeof groqQuery + | typeof openAiQuery + | typeof openRouterQuery; + +export const getLlmQuery = (provider: LLMProvider): QueryFunction => { + const queryFunctions: Record = { + [LLMProvider.GROQ]: groqQuery, + [LLMProvider.OPENAI]: openAiQuery, + [LLMProvider.OPENROUTER]: openRouterQuery, + [LLMProvider.OLLAMA]: ollamaQuery, + }; + + return queryFunctions[provider]; +}; + +export const getModelName = (provider: LLMProvider, model: string): string => { + const modelMap: Record>> = { + "gemma2-9b-it": { + [LLMProvider.GROQ]: "gemma2-9b-it", + [LLMProvider.OPENROUTER]: "google/gemma-2-9b-it", + [LLMProvider.OLLAMA]: "gemma2:9b", + }, + "gemma-7b-it": { + [LLMProvider.GROQ]: "gemma-7b-it", + [LLMProvider.OPENROUTER]: "google/gemma-7b-it", + [LLMProvider.OLLAMA]: "gemma:7b", + }, + "gpt-3.5-turbo-0125": { + [LLMProvider.OPENAI]: "gpt-3.5-turbo-0125", + }, + "gpt-3.5-turbo": { + [LLMProvider.OPENAI]: "gpt-3.5-turbo-0125", + }, + "gpt-4-turbo": { + [LLMProvider.OPENAI]: "gpt-4-turbo-2024-04-09", + }, + "gpt-4-turbo-2024-04-09": { + [LLMProvider.OPENAI]: "gpt-4-turbo-2024-04-09", + }, + "gpt-4": { + [LLMProvider.OPENAI]: "gpt-4-0613", + }, + "gpt-4-0613": { + [LLMProvider.OPENAI]: "gpt-4-0613", + }, + }; + + const modelForProvider = modelMap[model]; + if (!modelForProvider || !modelForProvider[provider]) { + const errorMessage = `Model not found for provider ${provider} and model ${model}`; + logger.error(errorMessage); + throw new Error(errorMessage); + } + + return modelForProvider[provider]!; +}; diff --git a/src/utils/logger.ts b/src/utils/logger.ts new file mode 100644 index 0000000..5b1168a --- /dev/null +++ b/src/utils/logger.ts @@ -0,0 +1,47 @@ +import "dotenv/config"; + +import chalk from "chalk"; +import { createLogger, format, transports } from "winston"; + +const { combine, timestamp, label, printf, simple, splat } = format; + +const consoleFormat = printf(({ level, message, label, timestamp }) => { + const levelUpper = level.toUpperCase(); + switch (levelUpper) { + case "DEBUG": + message = chalk.magenta(message); + level = chalk.white.bgMagentaBright.bold(level); + break; + + case "INFO": + message = message; + level = chalk.bgGreenBright.bold(level); + break; + + case "WARN": + message = chalk.yellow(message); + level = chalk.black.bgYellowBright.bold(level); + break; + + case "ERROR": + message = chalk.red(message); + level = chalk.black.bgRedBright.bold(level); + break; + + default: + break; + } + return `${level} ${message}`; +}); + +const LOGGER_LEVEL = process.env.LOGGER_LEVEL || "info"; +export const logger = createLogger({ + level: LOGGER_LEVEL, + format: combine(format.splat(), consoleFormat), + transports: [ + new transports.Console(), + new transports.File({ filename: "combined.log" }), + ], +}); + +logger.info(`Logger level set to ${chalk.bold(LOGGER_LEVEL)}\n`); diff --git a/src/utils/version.ts b/src/utils/version.ts new file mode 100644 index 0000000..e68b077 --- /dev/null +++ b/src/utils/version.ts @@ -0,0 +1,5 @@ +import { version } from "../../package.json"; + +export function getVersion() { + return version; +} diff --git a/tests/integration/Groq.test.ts b/tests/integration/Groq.test.ts new file mode 100644 index 0000000..6b767d1 --- /dev/null +++ b/tests/integration/Groq.test.ts @@ -0,0 +1,46 @@ +import OpenAI from "openai"; + +import { env } from "../../src/config"; +import { groqQuery } from "../../src/integration/groq"; +import { getModelName, LLMProvider } from "../../src/utils/llm"; + +// Adding this cause sometimes the LLM takes time and the test will fail CI +// Default jest timeout is 5000 ms for a test +jest.setTimeout(60000); + +describe("Groq Tests", (): void => { + const provider: LLMProvider = env.PROVIDERS.filter( + (provider: LLMProvider): boolean => provider === LLMProvider.GROQ, + )[0]; + + const model: string = getModelName(provider, env.MODEL); + + const payload = { + temperature: 0.7, + messages: [ + { + role: "system", + content: "You are a helpful assistant.", + }, + { + role: "user", + content: "What is the sum of 1 and 2?", + }, + ], + } as unknown as Request; + + it("should return an answer that matches the query", async (): Promise => { + const response = await groqQuery({ + ...payload, + model, + stream: false, + } as unknown as OpenAI.Chat.Completions.ChatCompletionCreateParamsNonStreaming); + + const content = (response as OpenAI.Chat.Completions.ChatCompletion) + .choices[0].message.content as string; + + await expect(content).toSatisfyStatement( + "It contains an answer to the sum of 1 and 2.", + ); + }); +}); diff --git a/tests/integration/OpenRouter.test.ts b/tests/integration/OpenRouter.test.ts new file mode 100644 index 0000000..c6ddc99 --- /dev/null +++ b/tests/integration/OpenRouter.test.ts @@ -0,0 +1,46 @@ +import OpenAI from "openai"; + +import { env } from "../../src/config"; +import { openRouterQuery } from "../../src/integration/openrouter"; +import { getModelName, LLMProvider } from "../../src/utils/llm"; + +// Adding this cause sometimes the LLM takes time and the test will fail CI +// Default jest timeout is 5000 ms for a test +jest.setTimeout(60000); + +describe("OpenRouter Tests", (): void => { + const provider: LLMProvider = env.PROVIDERS.filter( + (provider: LLMProvider): boolean => provider === LLMProvider.OPENROUTER, + )[0]; + + const model: string = getModelName(provider, env.MODEL); + + const payload = { + temperature: 0.7, + messages: [ + { + role: "system", + content: "You are a helpful assistant.", + }, + { + role: "user", + content: "What is the sum of 1 and 2?", + }, + ], + } as unknown as Request; + + it("should return an answer that matches the query", async (): Promise => { + const response = await openRouterQuery({ + ...payload, + model, + stream: false, + } as unknown as OpenAI.Chat.Completions.ChatCompletionCreateParamsNonStreaming); + + const content = (response as OpenAI.Chat.Completions.ChatCompletion) + .choices[0].message.content as string; + + await expect(content).toSatisfyStatement( + "It contains an answer to the sum of 1 and 2.", + ); + }); +}); diff --git a/tests/setup.ts b/tests/setup.ts new file mode 100644 index 0000000..9f8c584 --- /dev/null +++ b/tests/setup.ts @@ -0,0 +1,18 @@ +import Chance from "chance"; + +import "jest-ai"; +import "dotenv/config"; + +// Initialize Chance for generating random data +export const chance: Chance.Chance = new Chance(); + +// Mock dotenv/config globally +process.env.PORT = "3001"; +process.env.LOGGER_LEVEL = "debug"; +process.env.ORCHESTRATOR_URL = "http://example.com"; +process.env.SCOUT_NAME = "test"; +process.env.SCOUT_UID = "123"; +process.env.WEBHOOK_API_KEY = "test_api_key"; +process.env.WEBHOOK_URL = "http://webhook.example.com"; +process.env.PROVIDERS = "groq,openrouter"; +process.env.MODEL = "gemma2-9b-it"; diff --git a/tests/unit/middleware/APIKeyAuthMiddleware.test.ts b/tests/unit/middleware/APIKeyAuthMiddleware.test.ts new file mode 100644 index 0000000..e587d9c --- /dev/null +++ b/tests/unit/middleware/APIKeyAuthMiddleware.test.ts @@ -0,0 +1,104 @@ +import { NextFunction, Request, Response } from "express"; + +import { env } from "../../../src/config"; +import { apikeyAuthMiddleware } from "../../../src/middleware/apiKeyMiddleware"; +import * as apiKey from "../../../src/utils/apiKey"; + +// Mock Express Request and Response objects +const mockRequest = { + body: { + model: "gemma-7b-it", + temperature: 0.7, + messages: [ + { + role: "system", + content: "You are a helpful assistant.", + }, + { + role: "user", + content: "What is the meaning of life?", + }, + ], + }, +} as Request; +const mockResponse = { + status: jest.fn().mockReturnThis(), + json: jest.fn(), +} as unknown as Response; +const mockNext = jest.fn() as NextFunction; + +describe("API Key Auth Middleware Tests", (): void => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it("should return 401 Unauthorized if no API key is provided", async (): Promise => { + // Mock request headers + mockRequest.headers = {}; + + // Act + await apikeyAuthMiddleware(mockRequest, mockResponse, mockNext); + + // Assert + expect(mockResponse.status).toHaveBeenCalledWith(401); + expect(mockResponse.json).toHaveBeenCalledWith({ message: "Unauthorized" }); + expect(mockNext).not.toHaveBeenCalled(); + }); + + it("should return 401 Unauthorized if API key is invalid", async (): Promise => { + // Arrange + const invalidApiKey: string = "invalid-api-key"; + const middleware = apikeyAuthMiddleware; + + // Mock request headers + mockRequest.headers = { + authorization: `Bearer ${invalidApiKey}`, + }; + + // Act + await middleware(mockRequest, mockResponse, mockNext); + + // Assert + expect(mockResponse.status).toHaveBeenCalledWith(401); + expect(mockResponse.json).toHaveBeenCalledWith({ message: "Unauthorized" }); + expect(mockNext).not.toHaveBeenCalled(); + }); + + it("should return 400 Bad Request if an error occurs during verification", async (): Promise => { + // Arrange + const errorMessage: string = "Verification failed"; + + // Mock request headers + mockRequest.headers = { + authorization: `Bearer invalid-api-key`, + }; + + // Override the actual verification to simulate hitting error + jest.spyOn(apiKey, "verifyApiKey").mockImplementationOnce(() => { + throw new Error(errorMessage); + }); + + // Act + await apikeyAuthMiddleware(mockRequest, mockResponse, mockNext); + + // Assert + expect(mockResponse.status).toHaveBeenCalledWith(400); + expect(mockResponse.json).toHaveBeenCalledWith({ message: errorMessage }); + expect(mockNext).not.toHaveBeenCalled(); + }); + + it("should call next() if API key is valid", async (): Promise => { + // Mock request headers + mockRequest.headers = { + authorization: `Bearer ${env.WEBHOOK_API_KEY}`, + }; + + // Act + await apikeyAuthMiddleware(mockRequest, mockResponse, mockNext); + + // Assert + expect(mockResponse.status).not.toHaveBeenCalled(); + expect(mockResponse.json).not.toHaveBeenCalled(); + expect(mockNext).toHaveBeenCalled(); + }); +}); diff --git a/tests/unit/server/Webhook.test.ts b/tests/unit/server/Webhook.test.ts new file mode 100644 index 0000000..b756b80 --- /dev/null +++ b/tests/unit/server/Webhook.test.ts @@ -0,0 +1,102 @@ +import { Request, Response } from "express"; + +import { env } from "../../../src/config"; +import { webhook } from "../../../src/server/webhook"; +import { logger } from "../../../src/utils/logger"; + +// Mock the logger +jest.mock("../../../src/utils/logger", () => ({ + logger: { + error: jest.fn(), + }, +})); + +describe("Webhook Tests", (): void => { + let req: Partial; + let res: Partial; + + const setupMockConfig = (overrides: Record): void => { + jest.mock("../../../src/config", () => { + const originalModule = jest.requireActual("../../../src/config"); + return { + ...originalModule, + env: { + ...originalModule.env, + ...overrides, + }, + }; + }); + }; + + const executeWebhook = async ( + req: Partial, + res: Partial, + openAiCompatible: boolean = true, + ) => { + const controller = webhook(openAiCompatible); + await controller(req as Request, res as Response); + }; + + beforeEach((): void => { + req = { + body: { + model: "gemma-7b-it", + temperature: 0.7, + messages: [ + { + role: "system", + content: "You are a helpful assistant.", + }, + { + role: "user", + content: "What is the meaning of life?", + }, + ], + }, + }; + + res = { + status: jest.fn().mockReturnThis(), + send: jest.fn(), + setHeader: jest.fn(), + write: jest.fn(), + end: jest.fn(), + } as Partial; + }); + + afterEach((): void => { + jest.clearAllMocks(); + }); + + it("should handle error when getModelName encounters invalid model", async (): Promise => { + setupMockConfig({ MODEL: "mocked-model" }); + + await executeWebhook(req, res); + + // Assert logging of "Provider failed" and the error message + expect(logger.error).toHaveBeenCalledWith( + "Provider failed", + env.PROVIDERS[0], + ); + + // Assert response status and message + expect(res.status).toHaveBeenCalledWith(500); + expect(res.send).toHaveBeenCalledWith({ error: "All providers failed" }); + }); + + it("should handle error when getModelName encounters invalid provider", async (): Promise => { + setupMockConfig({ PROVIDERS: "mocked-provider" }); + + await executeWebhook(req, res); + + // Assert logging of "Provider failed" and the error message + expect(logger.error).toHaveBeenCalledWith( + "Provider failed", + env.PROVIDERS[0], + ); + + // Assert response status and message + expect(res.status).toHaveBeenCalledWith(500); + expect(res.send).toHaveBeenCalledWith({ error: "All providers failed" }); + }); +}); diff --git a/tests/unit/utils/APIKey.test.ts b/tests/unit/utils/APIKey.test.ts new file mode 100644 index 0000000..673f211 --- /dev/null +++ b/tests/unit/utils/APIKey.test.ts @@ -0,0 +1,23 @@ +import { env } from "../../../src/config"; +import { verifyApiKey } from "../../../src/utils/apiKey"; +import { chance } from "../../setup"; + +describe("Utility: API Key Tests", (): void => { + const storedAPIKey: string = env.WEBHOOK_API_KEY; + + it("should return true for matching keys", (): void => { + const providedApiKey: string = env.WEBHOOK_API_KEY; + const result: boolean = verifyApiKey(providedApiKey, storedAPIKey); + + expect(result).toBe(true); + }); + + it("should return false for mismatched keys", (): void => { + const invalidProvidedApiKey: string = chance.string({ + length: storedAPIKey.length, + }); // Invalid provided API key in base64 + const result: boolean = verifyApiKey(invalidProvidedApiKey, storedAPIKey); + + expect(result).toBe(false); + }); +}); diff --git a/tests/unit/utils/LLM.test.ts b/tests/unit/utils/LLM.test.ts new file mode 100644 index 0000000..8aac3e8 --- /dev/null +++ b/tests/unit/utils/LLM.test.ts @@ -0,0 +1,55 @@ +import { groqQuery } from "../../../src/integration/groq"; +import { ollamaQuery } from "../../../src/integration/ollama"; +import { openAiQuery } from "../../../src/integration/openai"; +import { openRouterQuery } from "../../../src/integration/openrouter"; +import { getLlmQuery, getModelName, LLMProvider } from "../../../src/utils/llm"; + +describe("Utility: LLM Tests", (): void => { + it("should throw error for unknown provider", (): void => { + const unknownProvider: LLMProvider = "unknown_provider" as LLMProvider; + + expect(getLlmQuery(unknownProvider)).toBeUndefined(); + }); + + it("should return correct query function for each provider", (): void => { + expect(getLlmQuery(LLMProvider.GROQ)).toBe(groqQuery); + expect(getLlmQuery(LLMProvider.OPENAI)).toBe(openAiQuery); + expect(getLlmQuery(LLMProvider.OPENROUTER)).toBe(openRouterQuery); + expect(getLlmQuery(LLMProvider.OLLAMA)).toBe(ollamaQuery); + }); + + it("should throw error for unknown provider", (): void => { + const unknownProvider: LLMProvider = "unknown_provider" as LLMProvider; + const knownModel: string = "gemma-7b-it"; + const expectedErrorMessage: string = `Model not found for provider ${unknownProvider} and model ${knownModel}`; + + expect(() => getModelName(unknownProvider, knownModel)).toThrow( + expectedErrorMessage, + ); + }); + + it("should throw error for unknown model for a provider", (): void => { + const knownProvider: LLMProvider.GROQ = LLMProvider.GROQ; + const unknownModel: string = "unknown_model"; + const expectedErrorMessage: string = `Model not found for provider ${knownProvider} and model ${unknownModel}`; + + expect(() => getModelName(knownProvider, unknownModel)).toThrow( + expectedErrorMessage, + ); + }); + + it("should return correct model name for known provider and model", (): void => { + expect(getModelName(LLMProvider.GROQ, "gemma-7b-it")).toBe("gemma-7b-it"); + expect(getModelName(LLMProvider.OPENROUTER, "gemma-7b-it")).toBe( + "google/gemma-7b-it", + ); + expect(getModelName(LLMProvider.OLLAMA, "gemma-7b-it")).toBe("gemma:7b"); + expect(getModelName(LLMProvider.OPENAI, "gpt-3.5-turbo")).toBe( + "gpt-3.5-turbo-0125", + ); + expect(getModelName(LLMProvider.OPENAI, "gpt-4-turbo")).toBe( + "gpt-4-turbo-2024-04-09", + ); + expect(getModelName(LLMProvider.OPENAI, "gpt-4")).toBe("gpt-4-0613"); + }); +}); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..c142185 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,109 @@ +{ + "compilerOptions": { + /* Visit https://aka.ms/tsconfig to read more about this file */ + + /* Projects */ + // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ + // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ + // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ + // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ + // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ + // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ + + /* Language and Environment */ + "target": "es2016", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ + // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ + // "jsx": "preserve", /* Specify what JSX code is generated. */ + // "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */ + // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ + // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ + // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ + // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ + // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ + // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ + // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ + // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ + + /* Modules */ + "module": "commonjs", /* Specify what module code is generated. */ + // "rootDir": "./", /* Specify the root folder within your source files. */ + // "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */ + // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ + // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ + // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ + // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ + "types": ["node", "jest", "jest-ai"], /* Specify type package names to be included without being referenced in a source file. */ + // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ + // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ + // "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */ + // "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */ + // "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */ + // "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */ + "resolveJsonModule": true, /* Enable importing .json files. */ + // "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */ + // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ + + /* JavaScript Support */ + // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ + // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ + // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ + + /* Emit */ + // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ + // "declarationMap": true, /* Create sourcemaps for d.ts files. */ + // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ + // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ + // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ + // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ + "outDir": "./dist", /* Specify an output folder for all emitted files. */ + // "removeComments": true, /* Disable emitting comments. */ + // "noEmit": true, /* Disable emitting files from a compilation. */ + // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ + // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */ + // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ + // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ + // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ + // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ + // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ + // "newLine": "crlf", /* Set the newline character for emitting files. */ + // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ + // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ + // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ + // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ + // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ + // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ + + /* Interop Constraints */ + // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ + // "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */ + // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ + "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ + // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ + "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ + + /* Type Checking */ + "strict": true, /* Enable all strict type-checking options. */ + // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ + // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ + // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ + // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ + // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ + // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ + // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ + // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ + // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ + // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ + // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ + // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ + // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ + // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ + // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ + // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ + // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ + // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ + + /* Completeness */ + // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ + "skipLibCheck": true /* Skip type checking all .d.ts files. */ + } +}