├── .eslintignore ├── .eslintrc ├── .github └── workflows │ ├── ci.yml │ └── release.yml ├── .gitignore ├── .husky ├── commit-msg └── pre-commit ├── .prettierrc ├── .releaserc ├── .vscode └── settings.json ├── CHANGELOG.md ├── Dockerfile ├── LICENSE ├── README.md ├── commitlint.config.cjs ├── example.env ├── jest.config.ts ├── nest-cli.json ├── package.json ├── src ├── app.module.ts ├── chat │ ├── chat.controller.spec.ts │ ├── chat.controller.ts │ ├── chat.dto.ts │ ├── chat.module.ts │ ├── chat.service.spec.ts │ └── chat.service.ts ├── completion │ ├── completion.controller.spec.ts │ ├── completion.controller.ts │ ├── completion.dto.ts │ ├── completion.module.ts │ ├── completion.service.spec.ts │ └── completion.service.ts ├── global │ ├── clients │ │ └── chatgpt.client.ts │ ├── global.module.ts │ └── middlewares │ │ └── request-logger.ts └── main.ts ├── test ├── app.e2e-spec.ts ├── jest-e2e.json └── tsconfig.json ├── tsconfig.build.json ├── tsconfig.json └── yarn.lock /.eslintignore: -------------------------------------------------------------------------------- 1 | local_tests 2 | node_modules 3 | dist -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["@rpidanny/eslint-config-typescript"], 3 | "rules": { 4 | "simple-import-sort/imports": "error", 5 | "simple-import-sort/exports": "error", 6 | "arrow-parens": "off" 7 | }, 8 | "parserOptions": { 9 | "ecmaFeatures": { 10 | "jsx": true, 11 | "tsx": true 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches-ignore: 6 | - 'main' 7 | workflow_dispatch: 8 | 9 | jobs: 10 | build-and-test: 11 | name: Build and Test 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v3 16 | 17 | - uses: actions/setup-node@v3 18 | with: 19 | node-version: 18 20 | cache: 'npm' 21 | 22 | - name: Install 23 | run: yarn install --frozen-lockfile 24 | 25 | - name: Lint 26 | run: yarn lint 27 | 28 | - name: Build 29 | run: yarn build 30 | 31 | # - name: Test 32 | # run: yarn test 33 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: 'Release' 2 | 3 | on: 4 | push: 5 | branches: 6 | - 'main' 7 | 8 | env: 9 | NODE_VERSION: 18 10 | 11 | jobs: 12 | build-test: 13 | name: Build and Test 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: Clone Repo 17 | uses: actions/checkout@v3 18 | 19 | - uses: actions/setup-node@v3 20 | with: 21 | node-version: ${{env.NODE_VERSION}} 22 | cache: 'npm' 23 | 24 | - name: Install 25 | run: yarn install --frozen-lockfile 26 | 27 | - name: Lint 28 | run: yarn lint 29 | 30 | - name: Build 31 | run: yarn build 32 | 33 | # - name: Test 34 | # run: yarn test 35 | 36 | newRelease: 37 | name: Create New Release 38 | needs: [build-test] 39 | runs-on: ubuntu-latest 40 | permissions: 41 | contents: write 42 | issues: write 43 | pull-requests: write 44 | steps: 45 | - uses: actions/checkout@v3 46 | 47 | - uses: actions/setup-node@v3 48 | with: 49 | node-version: ${{env.NODE_VERSION}} 50 | cache: 'npm' 51 | 52 | - name: Install 53 | run: yarn install --frozen-lockfile 54 | 55 | - name: Lint 56 | run: yarn lint 57 | 58 | - name: Build 59 | run: yarn build 60 | 61 | - name: Release 62 | run: yarn release 63 | env: 64 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 65 | NPM_TOKEN: ${{secrets.GITHUB_TOKEN}} 66 | CI: true 67 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # compiled output 2 | /dist 3 | /node_modules 4 | 5 | # Logs 6 | logs 7 | *.log 8 | npm-debug.log* 9 | pnpm-debug.log* 10 | yarn-debug.log* 11 | yarn-error.log* 12 | lerna-debug.log* 13 | 14 | # OS 15 | .DS_Store 16 | 17 | # Tests 18 | /coverage 19 | /.nyc_output 20 | 21 | # IDEs and editors 22 | /.idea 23 | .project 24 | .classpath 25 | .c9/ 26 | *.launch 27 | .settings/ 28 | *.sublime-workspace 29 | 30 | # IDE - VSCode 31 | .vscode/* 32 | !.vscode/settings.json 33 | !.vscode/tasks.json 34 | !.vscode/launch.json 35 | !.vscode/extensions.json 36 | 37 | .env 38 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | npx --no-install commitlint --edit "$1" 5 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | npx lint-staged 5 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "es5", 4 | "requirePragma": false, 5 | "arrowParens": "always" 6 | } 7 | -------------------------------------------------------------------------------- /.releaserc: -------------------------------------------------------------------------------- 1 | { 2 | "branches": [ 3 | "main", 4 | { 5 | "name": "beta", 6 | "prerelease": true 7 | }, 8 | { 9 | "name": "alpha", 10 | "prerelease": true 11 | } 12 | ], 13 | "plugins": [ 14 | [ 15 | "@semantic-release/commit-analyzer", 16 | { 17 | "preset": "conventionalcommits", 18 | "releaseRules": [ 19 | { 20 | "type": "docs", 21 | "scope": "README", 22 | "release": "patch" 23 | }, 24 | { 25 | "type": "refactor", 26 | "release": "patch" 27 | }, 28 | { 29 | "type": "style", 30 | "release": "patch" 31 | } 32 | ], 33 | "parserOpts": { 34 | "noteKeywords": [ 35 | "BREAKING CHANGE", 36 | "BREAKING CHANGES" 37 | ] 38 | } 39 | } 40 | ], 41 | [ 42 | "@semantic-release/release-notes-generator", 43 | { 44 | "preset": "conventionalcommits" 45 | } 46 | ], 47 | "@semantic-release/changelog", 48 | [ 49 | "@semantic-release/npm", 50 | { 51 | "npmPublish": false 52 | } 53 | ], 54 | [ 55 | "@semantic-release/github", 56 | { 57 | "assets": [] 58 | } 59 | ], 60 | [ 61 | "@semantic-release/git", 62 | { 63 | "assets": [ 64 | "docs", 65 | "CHANGELOG", 66 | "CHANGELOG.md", 67 | "package.json", 68 | "yarn.lock", 69 | "README.md" 70 | ], 71 | "message": "chore(release): ${nextRelease.version} \n\n${nextRelease.notes}" 72 | } 73 | ] 74 | ] 75 | } 76 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib" 3 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 1.0.0 (2023-05-20) 2 | 3 | 4 | ### Features 5 | 6 | * add auto release ([a52f603](https://github.com/rpidanny/chatgpt-browser-api-proxy/commit/a52f60379c669e6fc9fb22927e5e74e82ce09ff9)) 7 | * initial commit ([012d4b6](https://github.com/rpidanny/chatgpt-browser-api-proxy/commit/012d4b63acfac91cd6305e743829b49a0c1af221)) 8 | * initial implementation 🚀 ([5746931](https://github.com/rpidanny/chatgpt-browser-api-proxy/commit/5746931b85b32f36c022be0eb16f98162bbdb45b)) 9 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # 2 | # Base image 3 | # 4 | 5 | FROM mcr.microsoft.com/playwright:v1.32.0-jammy as base 6 | 7 | RUN apt update -y && apt upgrade -y && \ 8 | apt install -y bash && \ 9 | mkdir /opt/app 10 | 11 | WORKDIR /opt/app 12 | 13 | # 14 | # Build image 15 | # 16 | 17 | FROM base as build 18 | 19 | RUN apt update -y && apt upgrade -y && \ 20 | apt install -y make gcc g++ 21 | 22 | COPY package.json yarn.lock ./ 23 | 24 | RUN yarn 25 | 26 | COPY . . 27 | 28 | RUN yarn build 29 | 30 | # 31 | # Runtime image 32 | # 33 | 34 | FROM base 35 | 36 | ENV NODE_ENV production 37 | 38 | COPY --from=build /opt/app . 39 | 40 | CMD bash -c 'node dist/main' 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Abhishek Maharjan 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ChatGPT Browser API Proxy 2 | 3 | The **ChatGPT Browser API Proxy** is a project that enables the use of OpenAI APIs by leveraging the ChatGPT unofficial browser API while bypassing Cloudflare anti-bot detection. This proxy allows you to make API requests to OpenAI's services directly from your local machine. 4 | 5 | ## Prerequisites 6 | 7 | Before using this API proxy, ensure that you have the following: 8 | 9 | - Node.js installed on your machine 10 | - Yarn package manager installed 11 | 12 | ## Getting Started 13 | 14 | To set up and use the ChatGPT Browser API Proxy, follow these steps: 15 | 16 | 1. Clone the repository to your local machine: 17 | 18 | ```shell 19 | git clone https://github.com/rpidanny/chatgpt-browser-api-proxy.git 20 | ``` 21 | 22 | 2. Navigate to the project directory: 23 | 24 | ```shell 25 | cd chatgpt-browser-api-proxy 26 | ``` 27 | 28 | 3. Install the project dependencies: 29 | 30 | ```shell 31 | yarn install 32 | ``` 33 | 34 | 4. Copy the example environment file and rename it to `.env`: 35 | 36 | ```shell 37 | cp example.env .env 38 | ``` 39 | 40 | 5. Open the `.env` file and add your OpenAI Access Token obtained from the OpenAI platform. Replace `` with your actual token. 41 | 42 | 6. Start the proxy server in development mode: 43 | 44 | ```shell 45 | yarn start:dev 46 | ``` 47 | 48 | *Note:* 49 | 50 | You can get an Access Token by logging in to the ChatGPT webapp and then opening `https://chat.openai.com/api/auth/session`, which will return a JSON object containing your Access Token string. 51 | 52 | Access tokens last for few days. 53 | 54 | ## Configuring for LangChain 55 | 56 | If you intend to use the proxy with _LangChain_, you need to set the `OPENAI_API_BASE` environment variable to specify the API base URL. 57 | 58 | ```shell 59 | export OPENAI_API_BASE=http://localhost:3000/v1 60 | ``` 61 | 62 | ## Making API Requests 63 | 64 | Once the proxy server is running, you can make API requests to OpenAI's services using the provided routes and endpoints. The proxy will handle the communication with the ChatGPT unofficial browser API and forward the responses to your local machine. 65 | 66 | ## Notes 67 | 68 | - This project is an unofficial implementation and may not provide the same level of reliability or stability as official OpenAI APIs. 69 | - Usage of this project may be subject to OpenAI's terms of service. Please ensure compliance with their guidelines and policies. 70 | 71 | ## Disclaimer 72 | 73 | This project is provided as-is, without any warranty or guarantee of its functionality. The developers and contributors are not responsible for any damages or issues arising from the use of this project. 74 | 75 | ## License 76 | 77 | This project is licensed under the [MIT License](LICENSE). 78 | -------------------------------------------------------------------------------- /commitlint.config.cjs: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { extends: ['@commitlint/config-conventional'] }; 4 | -------------------------------------------------------------------------------- /example.env: -------------------------------------------------------------------------------- 1 | OPENAI_API_TOKEN= -------------------------------------------------------------------------------- /jest.config.ts: -------------------------------------------------------------------------------- 1 | // import type { Config } from 'jest'; 2 | import { defaults } from 'jest-config'; 3 | import type { JestConfigWithTsJest } from 'ts-jest'; 4 | 5 | const config: JestConfigWithTsJest = { 6 | preset: 'ts-jest/presets/default-esm', 7 | extensionsToTreatAsEsm: ['.ts'], 8 | moduleNameMapper: { 9 | '^(\\.{1,2}/.*)\\.js$': '$1', 10 | }, 11 | moduleFileExtensions: ['js', 'json', 'ts'], 12 | rootDir: '.', 13 | testRegex: defaults.testRegex, 14 | transform: { 15 | '^.+\\.(t|j)s$': [ 16 | 'ts-jest', 17 | { 18 | useESM: true, 19 | tsconfig: '/test/tsconfig.json', 20 | }, 21 | ], 22 | }, 23 | testEnvironment: 'jest-environment-node', 24 | testPathIgnorePatterns: [ 25 | '/src/client', 26 | '/dist/', 27 | '/tmp/', 28 | '/node_modules/', 29 | '/local_tests/', 30 | ], 31 | testTimeout: 20_000, 32 | // setupFiles: ['/test/helpers/init.js'], 33 | // collectCoverageFrom: ['**/*.(t|j)s'], 34 | collectCoverageFrom: ['/src/**/*.(t|j)s'], 35 | coverageDirectory: '/coverage', 36 | coverageReporters: [['lcov', { projectRoot: './' }], 'text'], 37 | coveragePathIgnorePatterns: [ 38 | '/node_modules/', 39 | '/test/', 40 | '/dist/', 41 | '/tmp/', 42 | '/local_tests/', 43 | '/coverage/', 44 | '/src/utils/ui/', 45 | '/src/client/', 46 | ], 47 | coverageThreshold: { 48 | global: { 49 | statements: 82, 50 | branches: 62, 51 | functions: 77, 52 | lines: 82, 53 | }, 54 | }, 55 | }; 56 | 57 | export default config; 58 | -------------------------------------------------------------------------------- /nest-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/nest-cli", 3 | "collection": "@nestjs/schematics", 4 | "sourceRoot": "src", 5 | "compilerOptions": { 6 | "deleteOutDir": true 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chatgpt-browser-api-proxy", 3 | "version": "1.0.0", 4 | "description": "A powerful API proxy that seamlessly integrates ChatGPT unofficial browser API with OpenAI, bypassing CloudFlare's anti-bot detection for uninterrupted access.", 5 | "author": "abhishek <@rpidanny>", 6 | "private": true, 7 | "license": "MIT", 8 | "type": "module", 9 | "lint-staged": { 10 | "*.ts": [ 11 | "yarn lint" 12 | ] 13 | }, 14 | "scripts": { 15 | "build": "nest build", 16 | "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", 17 | "start": "nest start", 18 | "start:dev": "nest start --watch", 19 | "start:debug": "nest start --debug --watch", 20 | "start:prod": "node dist/main", 21 | "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", 22 | "test": "jest", 23 | "test:watch": "jest --watch", 24 | "test:cov": "jest --coverage", 25 | "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", 26 | "test:e2e": "jest --config ./test/jest-e2e.json", 27 | "release": "semantic-release" 28 | }, 29 | "dependencies": { 30 | "@nestjs/common": "^9.0.0", 31 | "@nestjs/config": "^2.3.2", 32 | "@nestjs/core": "^9.0.0", 33 | "@nestjs/mapped-types": "*", 34 | "@nestjs/platform-express": "^9.0.0", 35 | "class-transformer": "^0.5.1", 36 | "class-validator": "^0.14.0", 37 | "playwright": "^1.33.0", 38 | "reflect-metadata": "^0.1.13", 39 | "rxjs": "^7.2.0" 40 | }, 41 | "devDependencies": { 42 | "@commitlint/cli": "^17.6.3", 43 | "@commitlint/config-conventional": "^17.6.3", 44 | "@nestjs/cli": "^9.0.0", 45 | "@nestjs/schematics": "^9.0.0", 46 | "@nestjs/testing": "^9.0.0", 47 | "@rpidanny/eslint-config-typescript": "^1.1.0", 48 | "@semantic-release/changelog": "^6.0.3", 49 | "@semantic-release/commit-analyzer": "^9.0.2", 50 | "@semantic-release/git": "^10.0.1", 51 | "@semantic-release/github": "^8.0.7", 52 | "@semantic-release/npm": "^10.0.3", 53 | "@semantic-release/release-notes-generator": "^11.0.1", 54 | "@types/express": "^4.17.13", 55 | "@types/jest": "29.5.0", 56 | "@types/node": "18.15.11", 57 | "@types/supertest": "^2.0.11", 58 | "@typescript-eslint/eslint-plugin": "^5.0.0", 59 | "@typescript-eslint/parser": "^5.0.0", 60 | "commitlint": "^17.6.3", 61 | "eslint": "^8.0.1", 62 | "eslint-config-prettier": "^8.3.0", 63 | "eslint-plugin-prettier": "^4.0.0", 64 | "eslint-plugin-simple-import-sort": "^10.0.0", 65 | "husky": "^8.0.3", 66 | "jest": "29.5.0", 67 | "lint-staged": "^13.2.2", 68 | "prettier": "^2.3.2", 69 | "semantic-release": "^21.0.2", 70 | "source-map-support": "^0.5.20", 71 | "supertest": "^6.1.3", 72 | "ts-jest": "29.0.5", 73 | "ts-loader": "^9.2.3", 74 | "ts-node": "^10.0.0", 75 | "tsconfig-paths": "4.2.0", 76 | "typescript": "^4.7.4" 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/app.module.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Logger, 3 | MiddlewareConsumer, 4 | Module, 5 | RequestMethod, 6 | } from '@nestjs/common'; 7 | import { ConfigModule } from '@nestjs/config'; 8 | 9 | import { ChatModule } from './chat/chat.module.js'; 10 | import { CompletionModule } from './completion/completion.module.js'; 11 | import { GlobalModule } from './global/global.module.js'; 12 | import { RequestLoggerMiddleware } from './global/middlewares/request-logger.js'; 13 | 14 | @Module({ 15 | imports: [ 16 | ConfigModule.forRoot(), 17 | GlobalModule.forRootAsync(), 18 | CompletionModule, 19 | ChatModule, 20 | ], 21 | controllers: [], 22 | providers: [Logger], 23 | }) 24 | export class AppModule { 25 | configure(consumer: MiddlewareConsumer): any { 26 | consumer.apply(RequestLoggerMiddleware).forRoutes({ 27 | path: '*', 28 | method: RequestMethod.ALL, 29 | }); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/chat/chat.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | 3 | import { ChatController } from './chat.controller.js'; 4 | import { ChatService } from './chat.service.js'; 5 | 6 | describe('ChatController', () => { 7 | let controller: ChatController; 8 | 9 | beforeEach(async () => { 10 | const module: TestingModule = await Test.createTestingModule({ 11 | controllers: [ChatController], 12 | providers: [ChatService], 13 | }).compile(); 14 | 15 | controller = module.get(ChatController); 16 | }); 17 | 18 | it('should be defined', () => { 19 | expect(controller).toBeDefined(); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /src/chat/chat.controller.ts: -------------------------------------------------------------------------------- 1 | import { Body, Controller, Post } from '@nestjs/common'; 2 | 3 | import { 4 | ChatCompletionRequestV1DTO, 5 | ChatCompletionResponseV1DTO, 6 | } from './chat.dto.js'; 7 | import { ChatService } from './chat.service.js'; 8 | 9 | @Controller('v1/chat') 10 | export class ChatController { 11 | constructor(private readonly chatService: ChatService) {} 12 | 13 | @Post('completions') 14 | async complete( 15 | @Body() ChatDto: ChatCompletionRequestV1DTO 16 | ): Promise { 17 | return await this.chatService.complete(ChatDto.messages); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/chat/chat.dto.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable camelcase */ 2 | import { Expose } from 'class-transformer'; 3 | import { 4 | IsArray, 5 | IsBoolean, 6 | IsNumber, 7 | IsObject, 8 | IsOptional, 9 | IsString, 10 | } from 'class-validator'; 11 | 12 | @Expose() 13 | export class LogprobsV1DTO { 14 | tokens: string[]; 15 | token_logprobs: number[]; 16 | top_logprobs: Record; 17 | text_offset: number[]; 18 | text: string; 19 | finish_reason: string; 20 | } 21 | 22 | export class UsageV1DTO { 23 | prompt_tokens: number; 24 | completion_tokens: number; 25 | total_tokens: number; 26 | } 27 | 28 | export class MessageV1DTO { 29 | role: string; 30 | content: string; 31 | name?: string; 32 | } 33 | 34 | export class ChatCompletionRequestV1DTO { 35 | @IsString() 36 | model!: string; 37 | 38 | @IsArray() 39 | messages!: MessageV1DTO[]; 40 | 41 | @IsNumber() 42 | @IsOptional() 43 | temperature?: number; 44 | 45 | @IsNumber() 46 | @IsOptional() 47 | top_p?: number; 48 | 49 | @IsNumber() 50 | @IsOptional() 51 | n?: number; 52 | 53 | @IsBoolean() 54 | @IsOptional() 55 | stream?: number; 56 | 57 | @IsOptional() 58 | stop?: string | string[]; 59 | 60 | @IsNumber() 61 | @IsOptional() 62 | max_tokens?: number; 63 | 64 | @IsNumber() 65 | @IsOptional() 66 | presence_penalty?: number; 67 | 68 | @IsNumber() 69 | @IsOptional() 70 | frequency_penalty?: number; 71 | 72 | @IsObject() 73 | @IsOptional() 74 | logit_bias?: Record; 75 | 76 | @IsString() 77 | @IsOptional() 78 | user?: string; 79 | } 80 | 81 | export class ChatCompletionResponseV1DTO { 82 | @Expose() 83 | id: string; 84 | 85 | @Expose() 86 | object: string; 87 | 88 | @Expose() 89 | created: number; 90 | 91 | @Expose() 92 | choices: ChoiceV1DTO[]; 93 | 94 | @Expose() 95 | usage: UsageV1DTO; 96 | } 97 | 98 | @Expose() 99 | export class ChoiceV1DTO { 100 | index: number; 101 | message: MessageV1DTO; 102 | finish_reason: string; 103 | } 104 | -------------------------------------------------------------------------------- /src/chat/chat.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | 3 | import { ChatController } from './chat.controller.js'; 4 | import { ChatService } from './chat.service.js'; 5 | 6 | @Module({ 7 | controllers: [ChatController], 8 | providers: [ChatService], 9 | }) 10 | export class ChatModule {} 11 | -------------------------------------------------------------------------------- /src/chat/chat.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | 3 | import { ChatService } from './chat.service.js'; 4 | 5 | describe('ChatService', () => { 6 | let service: ChatService; 7 | 8 | beforeEach(async () => { 9 | const module: TestingModule = await Test.createTestingModule({ 10 | providers: [ChatService], 11 | }).compile(); 12 | 13 | service = module.get(ChatService); 14 | }); 15 | 16 | it('should be defined', () => { 17 | expect(service).toBeDefined(); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /src/chat/chat.service.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable camelcase */ 2 | import { Injectable } from '@nestjs/common'; 3 | import { randomUUID } from 'crypto'; 4 | 5 | import { ChatGPTClient } from '../global/clients/chatgpt.client.js'; 6 | import { ChatCompletionResponseV1DTO, MessageV1DTO } from './chat.dto.js'; 7 | 8 | @Injectable() 9 | export class ChatService { 10 | constructor(private readonly chatGptClient: ChatGPTClient) {} 11 | 12 | async complete(messages: MessageV1DTO[]) { 13 | const response = await this.chatGptClient.conversation(messages[0].content); 14 | 15 | return this.generateChatCompletionResponse(response); 16 | } 17 | 18 | private generateChatCompletionResponse( 19 | response: string 20 | ): ChatCompletionResponseV1DTO { 21 | return { 22 | id: randomUUID(), 23 | object: 'chat.completion', 24 | created: new Date().getTime(), 25 | choices: [ 26 | { 27 | index: 0, 28 | message: { 29 | role: 'assistant', 30 | content: response, 31 | }, 32 | finish_reason: 'stop', 33 | }, 34 | ], 35 | usage: { 36 | prompt_tokens: 1, 37 | completion_tokens: 1, 38 | total_tokens: 2, 39 | }, 40 | }; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/completion/completion.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | 3 | import { CompletionController } from './completion.controller'; 4 | import { CompletionService } from './completion.service'; 5 | 6 | describe('CompletionController', () => { 7 | let controller: CompletionController; 8 | 9 | beforeEach(async () => { 10 | const module: TestingModule = await Test.createTestingModule({ 11 | controllers: [CompletionController], 12 | providers: [CompletionService], 13 | }).compile(); 14 | 15 | controller = module.get(CompletionController); 16 | }); 17 | 18 | it('should be defined', () => { 19 | expect(controller).toBeDefined(); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /src/completion/completion.controller.ts: -------------------------------------------------------------------------------- 1 | import { Body, Controller, Post } from '@nestjs/common'; 2 | 3 | import { CompletionRequestV1DTO } from './completion.dto.js'; 4 | import { CompletionService } from './completion.service.js'; 5 | 6 | @Controller('v1/completion') 7 | export class CompletionController { 8 | constructor(private readonly completionService: CompletionService) {} 9 | 10 | @Post() 11 | async complete(@Body() completionDto: CompletionRequestV1DTO) { 12 | return await this.completionService.complete(completionDto.prompt); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/completion/completion.dto.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable camelcase */ 2 | import { Expose } from 'class-transformer'; 3 | import { 4 | IsBoolean, 5 | IsNumber, 6 | IsObject, 7 | IsOptional, 8 | IsString, 9 | } from 'class-validator'; 10 | 11 | @Expose() 12 | export class LogprobsV1DTO { 13 | tokens: string[]; 14 | token_logprobs: number[]; 15 | top_logprobs: Record; 16 | text_offset: number[]; 17 | text: string; 18 | finish_reason: string; 19 | } 20 | 21 | export class UsageV1DTO { 22 | prompt_tokens: number; 23 | completion_tokens: number; 24 | total_tokens: number; 25 | } 26 | 27 | export class CompletionRequestV1DTO { 28 | @IsString() 29 | model!: string; 30 | 31 | @IsOptional() 32 | prompt!: string[] | string; 33 | 34 | @IsOptional() 35 | @IsString() 36 | suffix?: string; 37 | 38 | @IsNumber() 39 | @IsOptional() 40 | max_tokens?: number; 41 | 42 | @IsNumber() 43 | @IsOptional() 44 | temperature?: number; 45 | 46 | @IsNumber() 47 | @IsOptional() 48 | top_p?: number; 49 | 50 | @IsNumber() 51 | @IsOptional() 52 | n?: number; 53 | 54 | @IsBoolean() 55 | @IsOptional() 56 | stream?: number; 57 | 58 | @IsNumber() 59 | @IsOptional() 60 | logprobs?: number; 61 | 62 | @IsBoolean() 63 | @IsOptional() 64 | echo?: boolean; 65 | 66 | @IsOptional() 67 | stop?: string | string[]; 68 | 69 | @IsNumber() 70 | @IsOptional() 71 | presence_penalty?: number; 72 | 73 | @IsNumber() 74 | @IsOptional() 75 | frequency_penalty?: number; 76 | 77 | @IsNumber() 78 | @IsOptional() 79 | best_of?: number; 80 | 81 | @IsObject() 82 | @IsOptional() 83 | logit_bias?: Record; 84 | 85 | @IsString() 86 | @IsOptional() 87 | user?: string; 88 | } 89 | 90 | export class CompletionResponseV1DTO { 91 | @Expose() 92 | id: string; 93 | 94 | @Expose() 95 | object: string; 96 | 97 | @Expose() 98 | created: number; 99 | 100 | @Expose() 101 | model: string; 102 | 103 | @Expose() 104 | choices: ChoiceV1DTO[]; 105 | 106 | @Expose() 107 | usage: UsageV1DTO; 108 | } 109 | 110 | @Expose() 111 | export class ChoiceV1DTO { 112 | text: string; 113 | index: number; 114 | logprobs: LogprobsV1DTO | null; 115 | finish_reason: string; 116 | } 117 | -------------------------------------------------------------------------------- /src/completion/completion.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | 3 | import { CompletionController } from './completion.controller.js'; 4 | import { CompletionService } from './completion.service.js'; 5 | 6 | @Module({ 7 | controllers: [CompletionController], 8 | providers: [CompletionService], 9 | }) 10 | export class CompletionModule {} 11 | -------------------------------------------------------------------------------- /src/completion/completion.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | 3 | import { CompletionService } from './completion.service.js'; 4 | 5 | describe('CompletionService', () => { 6 | let service: CompletionService; 7 | 8 | beforeEach(async () => { 9 | const module: TestingModule = await Test.createTestingModule({ 10 | providers: [CompletionService], 11 | }).compile(); 12 | 13 | service = module.get(CompletionService); 14 | }); 15 | 16 | it('should be defined', () => { 17 | expect(service).toBeDefined(); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /src/completion/completion.service.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable camelcase */ 2 | import { Injectable } from '@nestjs/common'; 3 | import { randomUUID } from 'crypto'; 4 | 5 | import { ChatGPTClient } from '../global/clients/chatgpt.client.js'; 6 | import { CompletionResponseV1DTO } from './completion.dto.js'; 7 | 8 | @Injectable() 9 | export class CompletionService { 10 | constructor(private readonly chatGptClient: ChatGPTClient) {} 11 | 12 | async complete(prompt: string | string[]) { 13 | if (typeof prompt === 'string') { 14 | const response = await this.chatGptClient.conversation(prompt); 15 | 16 | return this.generateCompletionResponse(response); 17 | } else { 18 | throw new Error('Not implemented'); 19 | } 20 | } 21 | 22 | private generateCompletionResponse( 23 | response: string 24 | ): CompletionResponseV1DTO { 25 | return { 26 | id: randomUUID(), 27 | object: 'text_completion', 28 | created: new Date().getTime(), 29 | model: 'text-davinci-002-render-sha', 30 | choices: [ 31 | { 32 | text: response, 33 | index: 0, 34 | logprobs: null, 35 | finish_reason: 'stop', 36 | }, 37 | ], 38 | usage: { 39 | prompt_tokens: 1, 40 | completion_tokens: 1, 41 | total_tokens: 2, 42 | }, 43 | }; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/global/clients/chatgpt.client.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable camelcase */ 2 | import { Inject, Injectable, Logger } from '@nestjs/common'; 3 | import { randomUUID } from 'crypto'; 4 | import { Browser, chromium } from 'playwright'; 5 | 6 | @Injectable() 7 | export class ChatGPTClient { 8 | baseUrl = 'https://chat.openai.com'; 9 | parentMessageId = randomUUID(); 10 | browser: Browser; 11 | 12 | constructor(@Inject(Logger) private readonly logger: Logger) {} 13 | 14 | async init() { 15 | this.logger.log('Initializing ChatGPTClient'); 16 | this.browser = await chromium.launch({ headless: false }); 17 | this.logger.log('ChatGPTClient initialized'); 18 | } 19 | 20 | async conversation(prompt: string): Promise { 21 | const url = `${this.baseUrl}/backend-api/conversation`; 22 | 23 | const headers = this.getHeaders(); 24 | const payload = this.generateConversationPayload(prompt); 25 | 26 | const { answer } = await this.call('POST', url, headers, payload); 27 | 28 | return answer; 29 | } 30 | 31 | private getHeaders(): Record { 32 | return { 33 | 'Content-Type': 'application/json', 34 | Authorization: `Bearer ${process.env.OPENAI_API_TOKEN}`, 35 | }; 36 | } 37 | 38 | private generateConversationPayload(prompt: string) { 39 | return { 40 | action: 'next', 41 | messages: [ 42 | { 43 | id: randomUUID(), 44 | author: { 45 | role: 'user', 46 | }, 47 | content: { 48 | content_type: 'text', 49 | parts: [prompt], 50 | }, 51 | }, 52 | ], 53 | parent_message_id: this.parentMessageId, 54 | model: 'text-davinci-002-render-sha', 55 | timezone_offset_min: -120, 56 | history_and_training_disabled: false, 57 | supports_modapi: true, 58 | }; 59 | } 60 | 61 | private async call( 62 | method: string, 63 | url: string, 64 | headers?: Record, 65 | body?: Record 66 | ): Promise<{ answer: string; conversationId: string }> { 67 | const context = await this.browser.newContext(); 68 | const page = await context.newPage(); 69 | await page.goto(this.baseUrl); 70 | 71 | const { answer, conversationId } = await page.evaluate( 72 | async ({ method, url, headers, body }) => { 73 | const textDecoder = new TextDecoder(); 74 | 75 | const resp = await fetch(url, { 76 | method, 77 | headers, 78 | body: JSON.stringify(body), 79 | }); 80 | 81 | if (resp.status !== 200) 82 | throw new Error('Error while calling ChatGPT API'); 83 | 84 | const reader = resp.body?.getReader(); 85 | if (!reader) throw new Error('No reader found'); 86 | 87 | let chunk: ReadableStreamReadResult; 88 | let answer = ''; 89 | let conversationId = ''; 90 | 91 | while ((chunk = await reader.read()).done === false) { 92 | textDecoder 93 | .decode(chunk.value) 94 | .split('data: ') 95 | .map((e) => e.trim().replace(/^\n+/, '').replace(/\n+$/, '')) 96 | .filter((e) => e.length > 0 || e !== '[DONE]') 97 | .map((e) => { 98 | console.log(e); 99 | try { 100 | const parsedEvent = JSON.parse(e); 101 | 102 | if (parsedEvent.message.author.role === 'assistant') { 103 | answer = parsedEvent.message.content.parts.join(' '); 104 | conversationId = parsedEvent.message.conversation_id; 105 | return answer; 106 | } 107 | return ''; 108 | } catch (error) {} 109 | }); 110 | } 111 | 112 | return { answer, conversationId }; 113 | }, 114 | { method, url, headers, body } 115 | ); 116 | 117 | await context.close(); 118 | 119 | return { answer, conversationId }; 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /src/global/global.module.ts: -------------------------------------------------------------------------------- 1 | import { DynamicModule, Global, Logger, Module } from '@nestjs/common'; 2 | 3 | import { ChatGPTClient } from './clients/chatgpt.client.js'; 4 | 5 | @Global() 6 | @Module({}) 7 | export class GlobalModule { 8 | static forRootAsync(): DynamicModule { 9 | return { 10 | module: GlobalModule, 11 | imports: [], 12 | providers: [ 13 | Logger, 14 | { 15 | provide: ChatGPTClient, 16 | useFactory: async (logger: Logger) => { 17 | const client = new ChatGPTClient(logger); 18 | await client.init(); 19 | 20 | return client; 21 | }, 22 | inject: [Logger], 23 | }, 24 | ], 25 | exports: [ChatGPTClient], 26 | }; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/global/middlewares/request-logger.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable camelcase */ 2 | import { Inject, Injectable, Logger, NestMiddleware } from '@nestjs/common'; 3 | import { NextFunction, Request, Response } from 'express'; 4 | 5 | const hrToMillis = ([s, ns]: [number, number]): number => 6 | Math.round(s * 1e3 + ns / 1e6); 7 | 8 | const isError = (statusCode: number): boolean => statusCode >= 500; 9 | 10 | const isWarn = (statusCode: number): boolean => 11 | statusCode >= 400 && !isError(statusCode); 12 | 13 | @Injectable() 14 | export class RequestLoggerMiddleware implements NestMiddleware { 15 | constructor(@Inject(Logger) private readonly logger: Logger) {} 16 | 17 | use(req: Request, res: Response, next: NextFunction) { 18 | const startAt = process.hrtime(); 19 | const requestUrl = req.originalUrl || req.url; 20 | 21 | this.logger.log(`Incoming request ${req.method} ${requestUrl}`); 22 | 23 | res.on('finish', (): void => { 24 | const requestDuration = hrToMillis(process.hrtime(startAt)); 25 | 26 | if (isError(res.statusCode)) { 27 | this.logger.error( 28 | this.formatResponseLog(res.statusCode, requestDuration) 29 | ); 30 | } else if (isWarn(res.statusCode)) { 31 | this.logger.warn( 32 | this.formatResponseLog(res.statusCode, requestDuration) 33 | ); 34 | } else { 35 | this.logger.log( 36 | this.formatResponseLog(res.statusCode, requestDuration) 37 | ); 38 | } 39 | }); 40 | 41 | next(); 42 | } 43 | 44 | private formatResponseLog(statusCode: number, duration: number): string { 45 | return `Responding with status ${statusCode} in ${duration}ms`; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { ValidationPipe } from '@nestjs/common'; 2 | import { NestFactory } from '@nestjs/core'; 3 | 4 | import { AppModule } from './app.module.js'; 5 | 6 | async function bootstrap() { 7 | const app = await NestFactory.create(AppModule); 8 | app.useGlobalPipes(new ValidationPipe()); 9 | await app.listen(3000); 10 | } 11 | bootstrap(); 12 | -------------------------------------------------------------------------------- /test/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { INestApplication } from '@nestjs/common'; 2 | import { Test, TestingModule } from '@nestjs/testing'; 3 | import * as request from 'supertest'; 4 | 5 | import { AppModule } from './../src/app.module'; 6 | 7 | describe('AppController (e2e)', () => { 8 | let app: INestApplication; 9 | 10 | beforeEach(async () => { 11 | const moduleFixture: TestingModule = await Test.createTestingModule({ 12 | imports: [AppModule], 13 | }).compile(); 14 | 15 | app = moduleFixture.createNestApplication(); 16 | await app.init(); 17 | }); 18 | 19 | it('/ (GET)', () => { 20 | return request(app.getHttpServer()) 21 | .get('/') 22 | .expect(200) 23 | .expect('Hello World!'); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /test/jest-e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "moduleFileExtensions": ["js", "json", "ts"], 3 | "rootDir": ".", 4 | "testEnvironment": "node", 5 | "testRegex": ".e2e-spec.ts$", 6 | "transform": { 7 | "^.+\\.(t|j)s$": "ts-jest" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /test/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "noEmit": true, 5 | "composite": true, 6 | "rootDirs": ["src", "test"] 7 | }, 8 | // "references": [{ "path": "." }], 9 | "include": ["src/**/*", "test/**/*.spec.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "NodeNext", 4 | "declaration": true, 5 | "removeComments": true, 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "allowSyntheticDefaultImports": true, 9 | "target": "ES2021", 10 | "sourceMap": true, 11 | "outDir": "./dist", 12 | "baseUrl": "./", 13 | "incremental": true, 14 | "skipLibCheck": true, 15 | "strictNullChecks": true, 16 | "noImplicitAny": true, 17 | "strictBindCallApply": true, 18 | "forceConsistentCasingInFileNames": true, 19 | "noFallthroughCasesInSwitch": true, 20 | "allowJs": true, 21 | "resolveJsonModule": true, 22 | "esModuleInterop": true, 23 | }, 24 | "ts-node": { 25 | "esm": true, 26 | "transpileOnly": true 27 | }, 28 | "include": ["src/**/*"], 29 | "exclude": ["node_modules", "dist", "local_tests"] 30 | } 31 | --------------------------------------------------------------------------------