├── .dockerignore ├── .gitattributes ├── .gitignore ├── Dockerfile ├── LICENSE ├── README.md ├── assets ├── ProxyGPTbanner.png └── graphics_module.png ├── entrypoint.sh ├── example.env ├── main.py ├── modules ├── __init__.py ├── graphics.py └── logging.py ├── requirements.txt ├── settings.py ├── templates └── dashboard.html └── utils.py /.dockerignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | *.pyc 3 | *.pyo 4 | *.pyd 5 | .Python 6 | env 7 | *.env 8 | venv 9 | proxygpt.db -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Databases 2 | *.db 3 | 4 | # Byte-compiled / optimized / DLL files 5 | __pycache__/ 6 | *.py[cod] 7 | *$py.class 8 | 9 | # C extensions 10 | *.so 11 | 12 | # Distribution / packaging 13 | .Python 14 | build/ 15 | develop-eggs/ 16 | dist/ 17 | downloads/ 18 | eggs/ 19 | .eggs/ 20 | lib/ 21 | lib64/ 22 | parts/ 23 | sdist/ 24 | var/ 25 | wheels/ 26 | share/python-wheels/ 27 | *.egg-info/ 28 | .installed.cfg 29 | *.egg 30 | MANIFEST 31 | 32 | # PyInstaller 33 | # Usually these files are written by a python script from a template 34 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 35 | *.manifest 36 | *.spec 37 | 38 | # Installer logs 39 | pip-log.txt 40 | pip-delete-this-directory.txt 41 | 42 | # Unit test / coverage reports 43 | htmlcov/ 44 | .tox/ 45 | .nox/ 46 | .coverage 47 | .coverage.* 48 | .cache 49 | nosetests.xml 50 | coverage.xml 51 | *.cover 52 | *.py,cover 53 | .hypothesis/ 54 | .pytest_cache/ 55 | cover/ 56 | 57 | # Translations 58 | *.mo 59 | *.pot 60 | 61 | # Django stuff: 62 | *.log 63 | local_settings.py 64 | db.sqlite3 65 | db.sqlite3-journal 66 | 67 | # Flask stuff: 68 | instance/ 69 | .webassets-cache 70 | 71 | # Scrapy stuff: 72 | .scrapy 73 | 74 | # Sphinx documentation 75 | docs/_build/ 76 | 77 | # PyBuilder 78 | .pybuilder/ 79 | target/ 80 | 81 | # Jupyter Notebook 82 | .ipynb_checkpoints 83 | 84 | # IPython 85 | profile_default/ 86 | ipython_config.py 87 | 88 | # pyenv 89 | # For a library or package, you might want to ignore these files since the code is 90 | # intended to run in multiple environments; otherwise, check them in: 91 | # .python-version 92 | 93 | # pipenv 94 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 95 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 96 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 97 | # install all needed dependencies. 98 | #Pipfile.lock 99 | 100 | # poetry 101 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 102 | # This is especially recommended for binary packages to ensure reproducibility, and is more 103 | # commonly ignored for libraries. 104 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 105 | #poetry.lock 106 | 107 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 108 | __pypackages__/ 109 | 110 | # Celery stuff 111 | celerybeat-schedule 112 | celerybeat.pid 113 | 114 | # SageMath parsed files 115 | *.sage.py 116 | 117 | # Environments 118 | .env 119 | .venv 120 | env/ 121 | venv/ 122 | ENV/ 123 | env.bak/ 124 | venv.bak/ 125 | 126 | # Spyder project settings 127 | .spyderproject 128 | .spyproject 129 | 130 | # Rope project settings 131 | .ropeproject 132 | 133 | # mkdocs documentation 134 | /site 135 | 136 | # mypy 137 | .mypy_cache/ 138 | .dmypy.json 139 | dmypy.json 140 | 141 | # Pyre type checker 142 | .pyre/ 143 | 144 | # pytype static type analyzer 145 | .pytype/ 146 | 147 | # Cython debug symbols 148 | cython_debug/ 149 | 150 | # PyCharm 151 | # JetBrains specific template is maintainted in a separate JetBrains.gitignore that can 152 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 153 | # and can be added to the global gitignore or merged into this file. For a more nuclear 154 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 155 | #.idea/ 156 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Use the lightweight Python 3.8-slim base image 2 | FROM python:3.8-slim 3 | 4 | # Install system dependencies and clean up in a single RUN command 5 | RUN apt-get update && \ 6 | rm -rf /var/lib/apt/lists/* && \ 7 | # Set up the Python virtual environment 8 | python -m venv /opt/venv && \ 9 | chmod +x /opt/venv/bin/activate 10 | 11 | ENV PATH="/opt/venv/bin:$PATH" 12 | 13 | # Copy the requirements.txt file and install Python packages 14 | COPY requirements.txt . 15 | RUN pip install --no-cache-dir -r requirements.txt 16 | 17 | # Copy the application files 18 | COPY . . 19 | 20 | # Copy the entrypoint script 21 | COPY entrypoint.sh . 22 | 23 | # Give the execution permissions to the entrypoint script 24 | RUN chmod +x ./entrypoint.sh 25 | 26 | # Run the entrypoint script when the container starts 27 | CMD ["./entrypoint.sh"] 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Benjamin Klieger 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 | # 🌎 ProxyGPT 2 | [](https://railway.app/template/r1n-d9?referralCode=XB7cD1) 3 | 4 |  5 | 6 | ## Overview & Features 7 | 8 | ProxyGPT is a dockerized lightweight OpenAI wrapper using FastAPI. This solution allows you to add custom hourly and daily rate limits to your OpenAI API usage, and share OpenAI access with your team without providing your secret key. The app comes with optional modules for logging and graphing API call content and results. You can confine OpenAI API usage through ProxyGPT with hourly and daily rate limits, in addition to only exposing specific endpoints of the OpenAI API. ProxyGPT also allows you the ability to reset or remove your team's access to the OpenAI API for only the services or people using a specific instance of ProxyGPT, rather than needing to reset the original OpenAI API key which could impact other projects if multiple services are using the same key (which is not recommended in production). 9 | 10 | ## Modules 11 | 12 | ProxyGPT comes with several modules, which are additional features beyond a simple proxy. These include logging and graphics, which allows full logging of the request and response for the API calls, and produces a dashboard for analyzing the results of the API calls. 13 | 14 |  15 | 16 | See [Installation](#installation) to get started. 17 | 18 | ## Installation 19 | 20 | 1. First, download the repository. 21 | 2. Next, configure the settings in settings.py. 22 | 23 | USE_HOURLY_RATE_LIMIT (bool) 24 | 25 | USE_DAILY_RATE_LIMIT (bool) 26 | 27 | INSECURE_DEBUG (bool) 28 | 29 | Note that both rate limits can be active and enforced simultaneously. There is also an INSTALLED_MODULES array which can be modified to remove modules that power unwanted features. 30 | 31 | 4. Set the environment variables in .env. See [Environment Variables](#environment-variables). 32 | 5. Run with or without Docker. See [Running with Docker](#running-with-docker) and [Running without Docker](#running-without-docker). 33 | 34 | ## Customization 35 | 36 | If you wish to use ProxyGPT out of the box for only the gpt-3.5-turbo chat completion model, you can skip this section. 37 | 38 | In order to customize ProxyGPT with new endpoints, simply add them in main.py based upon the implementation of get_openai_gpt3_completion. Ensure you handle errors and log API usage as is done with get_openai_gpt3_completion. 39 | 40 | ## Details 41 | 42 | This project was developed with the goal of creating a simple and lightweight OpenAI wrapper, with basic yet powerful logging, rate limiting, and graphing tools. Strong documentation, easy customizability, and comprehensive initialization checks were integrated throughout the codebase. As part of the project's simple design, the service employs a local SQLite database, forgoing the use of long-term storage solutions like Docker volumes. This can be customized as you desire. 43 | 44 | It's important to understand that the rate limits currently apply for any calls to OpenAI, meaning all calls will increase the rate count, irrespective of whether or not they were successful. This is simple to change if you wish, and just requires different placement of the log function to after validation of the response from the OpenAI API. 45 | 46 | Rate limits, hourly and daily, are not tied to calendar hours or days. Instead, they operate on rolling windows of time, specifically the last 3600 seconds for hourly limits, and 86400 seconds for daily limits. Thus, usage counts do not reset at the beginning of a new day or hour, but are only no longer counted once they are greater than one hour or one day from the current time. 47 | 48 | In addition, you can use both hourly and daily rate limits together, just one of the two, or none. They are seperate checks, and if either are active and the usage exceeds them, the call to ProxyGPT will be returned with status code 429 (Too Many Requests). 49 | 50 | You can view the enabled rate limits and current usage from the /ratelimit endpoint. Use /docs or /redoc to explore all the endpoints by ProxyGPT. 51 | 52 | Finally, it should be noted that any errors that arise in the code may be passed directly to the API client for easy debugging. However, this increases the risk of leaking any secret keys stored on the server side. You can turn this off by changing INSECURE_DEBUG to False in settings.py. 53 | 54 | ## Environment variables 55 | 56 | Required: 57 | 58 | OPENAI_API_KEY = str: Your secure OpenAI API Key 59 | 60 | PROXYGPT_API_KEY = str: Your strong custom API key for the proxy 61 | 62 | 63 | Optional: 64 | 65 | If using hourly rate limit (from settings): 66 | 67 | PROXYGPT_HOURLY_RATE_LIMIT = int: max amount of calls to OpenAI through proxy allowed within a rolling one hour window 68 | 69 | If using daily rate limit (from settings): 70 | 71 | PROXYGPT_DAILY_RATE_LIMIT = int: max amount of calls to OpenAI through proxy allowed within a rolling one day window 72 | 73 | ## Running with Docker 74 | 75 | ### To build the docker image 76 | ~~~ 77 | docker build -f Dockerfile -t proxygpt:latest . 78 | ~~~ 79 | 80 | ### Run docker image in same directory as env file 81 | ~~~ 82 | docker run --env-file .env -p 8000:8000 proxygpt:latest 83 | ~~~ 84 | Set environment variables in .env first. Run with -d for detached. 85 | 86 | ProxyGPT is now online, and can be accessed at http://127.0.0.1:8000. Visit http://127.0.0.1:8000/docs to explore the auto-generated documentation. 87 | 88 | ## Running without Docker 89 | 90 | ### To create virtual env 91 | ~~~ 92 | python -m venv venv 93 | ~~~ 94 | 95 | ### To activate virtual env 96 | ~~~ 97 | source venv/bin/activate 98 | ~~~ 99 | 100 | ### To install libraries 101 | ~~~ 102 | pip install -r requirements.txt 103 | ~~~ 104 | 105 | ### Set environment variables 106 | ~~~ 107 | touch .env 108 | ~~~ 109 | Add the variables to the .env file. See example.env and [Environment Variables](#environment-variables). 110 | ~~~ 111 | export $(cat .env | xargs) 112 | ~~~ 113 | 114 | ### Run with uvicorn 115 | 116 | ~~~ 117 | uvicorn main:app --reload 118 | ~~~ 119 | 120 | ### Run with gunicorn (alternative for production environment) 121 | 122 | ~~~ 123 | gunicorn -w 4 -k uvicorn.workers.UvicornWorker main:app -b 0.0.0.0:8000 124 | ~~~ 125 | 126 | ProxyGPT is now online, and can be accessed at http://127.0.0.1:8000. Visit http://127.0.0.1:8000/docs to explore the auto-generated documentation. 127 | 128 | ## Changelog 129 | 130 | v0.1.0-beta: 131 | - Initialized Project for Release 132 | 133 | v0.1.0: 134 | - Application Tested in Beta, README.md updated 135 | 136 | v0.1.1: 137 | - Allow passing in multiple API keys for PROXYGPT_API_KEY 138 | 139 | v0.2.0-beta: 140 | - Create modules, starting with logging and graphics 141 | 142 | ## Future Features 143 | 144 | Add unit tests to entire codebase 145 | 146 | Allow multiple API keys with different rate limits in PROXYGPT_API_KEY 147 | 148 | Allow use of production database 149 | 150 | Add CORS origins restriction option through middleware 151 | 152 | Convert endpoints to async so logging and returning data can be executed concurrently 153 | 154 | Add Docker Volume integration for long term database 155 | 156 | Add further abstraction for increased customizability (such as adding database name to settings.py) 157 | -------------------------------------------------------------------------------- /assets/ProxyGPTbanner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bklieger/ProxyGPT/bd962e1aa43d551466f5fba2dbd34628f74a0649/assets/ProxyGPTbanner.png -------------------------------------------------------------------------------- /assets/graphics_module.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bklieger/ProxyGPT/bd962e1aa43d551466f5fba2dbd34628f74a0649/assets/graphics_module.png -------------------------------------------------------------------------------- /entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | gunicorn -w 4 -k uvicorn.workers.UvicornWorker main:app -------------------------------------------------------------------------------- /example.env: -------------------------------------------------------------------------------- 1 | # EXAMPLE ONLY. Do not use, unless you replace the values and add to your .gitignore. 2 | 3 | OPENAI_API_KEY=sk-xxxxxxxxxxxxxxxxx 4 | PROXYGPT_HOURLY_RATE_LIMIT=100 5 | PROXYGPT_DAILY_RATE_LIMIT=500 6 | 7 | # PROXYGPT_API_KEYS keys cannot contain commas. PROXYGPT_API_KEY will be used if present. 8 | # Choose one: 9 | 10 | PROXYGPT_API_KEY=insecure-api-key-example 11 | PROXYGPT_API_KEYS="insecure-api-key-example-1","insecure-api-key-example-2" -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | """ 2 | Main.py file for ProxyGPT. This file contains the main code for the API. 3 | 4 | Author: Benjamin Klieger 5 | Version: 0.2.0-beta 6 | Date: 2024-01-05 7 | License: MIT 8 | """ 9 | 10 | # ------------- [Import Libraries] ------------- 11 | 12 | # Required libraries from FastAPI for API functionality 13 | from fastapi import FastAPI, HTTPException, Depends, Security 14 | from fastapi.security.api_key import APIKeyHeader, APIKey 15 | from fastapi.security import HTTPBearer 16 | from fastapi.responses import JSONResponse 17 | 18 | # Required libraries from Pydantic for API functionality 19 | from pydantic import BaseModel 20 | from typing import List 21 | 22 | # Required for environment variables 23 | import os 24 | 25 | # Required for inspecting code 26 | import inspect 27 | 28 | # Import requests for making API calls 29 | import requests 30 | 31 | # Required for rate limiting with database and timestamps 32 | import sqlite3 33 | import time 34 | 35 | # Required for printing styled log messages 36 | from utils import * 37 | 38 | 39 | # ------------- [Settings] ------------- 40 | 41 | # Import settings from settings.py 42 | from settings import * 43 | 44 | # Check if settings are properly imported and set, raise exception if not 45 | if USE_HOURLY_RATE_LIMIT==None or USE_DAILY_RATE_LIMIT==None or INSECURE_DEBUG==None or INSTALLED_MODULES==None: 46 | raise Exception("One or more of the settings are not set or have been removed. They are required for operation of ProxyGPT, unless the code has been modified.") 47 | 48 | # Import the modules 49 | if "graphics" in INSTALLED_MODULES: 50 | # Import graphics module 51 | from modules.graphics import * 52 | 53 | from fastapi.templating import Jinja2Templates 54 | from fastapi.responses import HTMLResponse 55 | from fastapi import Request 56 | 57 | templates = Jinja2Templates(directory="templates") 58 | 59 | graphics_installed_bool = True 60 | else: 61 | graphics_installed_bool = False 62 | 63 | 64 | if "logging" in INSTALLED_MODULES: 65 | # Import logging module 66 | from modules.logging import * 67 | 68 | # Import time which is needed 69 | import time 70 | 71 | logging_installed_bool = True 72 | else: 73 | logging_installed_bool = False 74 | 75 | 76 | # ------------- [Initialization: App] ------------- 77 | 78 | # Create FastAPI app 79 | app = FastAPI( 80 | title="ProxyGPT", 81 | description="Lightweight wrapper for OpenAI python library. Add custom hourly and daily rate limits to API usage, and share OpenAI access with your development team without providing your secret key.", 82 | version="v0.2.0-beta", 83 | ) 84 | 85 | # ------------- [Initialization: Env] ------------- 86 | 87 | # Set minimum length for secure key 88 | MIN_LENGTH_FOR_SECURE_KEY = 5 89 | 90 | # Set ProxyGPT API key securely from environment variable, either singular key or multiple keys 91 | proxygpt_api_key = os.getenv("PROXYGPT_API_KEY") 92 | proxygpt_api_keys = os.getenv("PROXYGPT_API_KEYS") 93 | 94 | if proxygpt_api_keys is not None: 95 | # Get list of keys with comma seperated keys 96 | proxygpt_api_keys = proxygpt_api_keys.split(",") 97 | 98 | # Initialization check 99 | initialization_transcript = "" 100 | critical_exist = False 101 | 102 | # Check if the singular key is set 103 | if proxygpt_api_key is None and proxygpt_api_keys is None: 104 | initialization_transcript += red_critical(f'[Critical] PROXYGPT_API_KEY environment variable is not set. (Line {inspect.currentframe().f_lineno} in {os.path.basename(__file__)})\n') 105 | critical_exist = True 106 | 107 | # If the singular key is set, check if it is strong 108 | elif proxygpt_api_key is not None and len(proxygpt_api_key) < MIN_LENGTH_FOR_SECURE_KEY: 109 | initialization_transcript+= yellow_warning(f'[Warning] PROXYGPT_API_KEY environment variable is too short to be secure. (Line {inspect.currentframe().f_lineno} in {os.path.basename(__file__)})\n') 110 | 111 | # Check if multiple keys are set, and if so, check if there exist more than 0 keys 112 | elif proxygpt_api_keys is not None and len(proxygpt_api_keys) == 0: 113 | initialization_transcript += red_critical(f'[Critical] PROXYGPT_API_KEYS environment variable is set, but no keys were parsed. (Line {inspect.currentframe().f_lineno} in {os.path.basename(__file__)})\n') 114 | critical_exist = True 115 | 116 | # Check if multiple keys are set, and if so, check if they are strong 117 | elif proxygpt_api_keys is not None: 118 | for key in proxygpt_api_keys: 119 | if len(key) < MIN_LENGTH_FOR_SECURE_KEY: 120 | initialization_transcript += yellow_warning(f'[Warning] PROXYGPT_API_KEYS environment variable contains a key that is too short to be secure. (Line {inspect.currentframe().f_lineno} in {os.path.basename(__file__)})\n') 121 | 122 | # Set OpenAI API key securely from environment variable 123 | openai_api_key = os.getenv("OPENAI_API_KEY") 124 | 125 | if USE_HOURLY_RATE_LIMIT: 126 | hourly_rate_limit = (os.getenv("PROXYGPT_HOURLY_RATE_LIMIT")) 127 | if USE_DAILY_RATE_LIMIT: 128 | daily_rate_limit = (os.getenv("PROXYGPT_DAILY_RATE_LIMIT")) 129 | 130 | # Check if the key is set 131 | if openai_api_key is None: 132 | initialization_transcript += red_critical(f'[Critical] OPENAI_API_KEY environment variable is not set. (Line {inspect.currentframe().f_lineno} in {os.path.basename(__file__)})\n') 133 | critical_exist = True 134 | 135 | # If the key is set, check if it is valid 136 | elif len(openai_api_key) <5: 137 | initialization_transcript += red_critical(f'[Critical] OPENAI_API_KEY environment variable is too short to be a working key. (Line {inspect.currentframe().f_lineno} in {os.path.basename(__file__)})\n') 138 | critical_exist = True 139 | elif openai_api_key.startswith("sk-")==False: 140 | initialization_transcript += red_critical(f'[Critical] OPENAI_API_KEY environment variable is not a valid secret key. (Line {inspect.currentframe().f_lineno} in {os.path.basename(__file__)})\n') 141 | critical_exist = True 142 | 143 | # Check if the rate limit(s) are set correctly 144 | if USE_HOURLY_RATE_LIMIT: 145 | if hourly_rate_limit == None: 146 | initialization_transcript += red_critical(f'[Critical] PROXYGPT_HOURLY_RATE_LIMIT environment variable is not set. Please change the settings on line 16 of main.py if you do not wish to use an hourly rate limit. (Line {inspect.currentframe().f_lineno} in {os.path.basename(__file__)})\n') 147 | critical_exist = True 148 | elif hourly_rate_limit.isdigit() == False: # Will return False for floating point numbers 149 | initialization_transcript += red_critical(f'[Critical] PROXYGPT_HOURLY_RATE_LIMIT environment variable is not a valid integer. (Line {inspect.currentframe().f_lineno} in {os.path.basename(__file__)})\n') 150 | critical_exist = True 151 | else: 152 | hourly_rate_limit = int(hourly_rate_limit) 153 | 154 | if USE_DAILY_RATE_LIMIT: 155 | if daily_rate_limit == None: 156 | initialization_transcript += red_critical(f'[Critical] PROXYGPT_DAILY_RATE_LIMIT environment variable is not set. Please change the settings on line 16 of main.py if you do not wish to use an daily rate limit. (Line {inspect.currentframe().f_lineno} in {os.path.basename(__file__)})\n') 157 | critical_exist = True 158 | elif daily_rate_limit.isdigit() == False: # Will return False for floating point numbers 159 | initialization_transcript += red_critical(f'[Critical] PROXYGPT_DAILY_RATE_LIMIT environment variable is not a valid integer. (Line {inspect.currentframe().f_lineno} in {os.path.basename(__file__)})\n') 160 | critical_exist = True 161 | else: 162 | daily_rate_limit = int(daily_rate_limit) 163 | 164 | # Print results of initialization check 165 | print("Initialization check:") 166 | print(initialization_transcript) 167 | if critical_exist: 168 | print(red_critical("Critical errors found in initialization check. Please fix them before deploying. If you are building the Docker image and have not yet set the environment variables, you may ignore this message.")) 169 | else: 170 | print(green_success("No critical errors found in initialization check.")) 171 | 172 | 173 | # ------------- [Initialization: DB] ------------- 174 | 175 | # Check if database is needed for rate limiting 176 | if USE_HOURLY_RATE_LIMIT or USE_DAILY_RATE_LIMIT: 177 | # Use SQLITE database to store API usage 178 | # Create a table for API usage if it does not exist 179 | conn = sqlite3.connect('proxygpt.db') 180 | c = conn.cursor() 181 | c.execute('''CREATE TABLE IF NOT EXISTS api_usage 182 | (api_timestamp integer)''') 183 | conn.commit() 184 | conn.close() 185 | 186 | 187 | # ------------- [Helper Functions] ------------- 188 | 189 | # Make function for adding API usage 190 | def log_api_usage() -> None: 191 | """ 192 | This function logs an instance of API usage to the SQLite database. 193 | It only logs the instance if the rate limit is enabled. 194 | """ 195 | if USE_HOURLY_RATE_LIMIT or USE_DAILY_RATE_LIMIT: 196 | with sqlite3.connect('proxygpt.db') as conn: 197 | c = conn.cursor() 198 | c.execute("INSERT INTO api_usage VALUES (?)", (int(time.time()),)) 199 | conn.commit() 200 | 201 | # Make function for getting API usage (hourly) 202 | def get_api_usage_from_last_hour() -> int: 203 | """ 204 | This function returns the number of API calls to OpenAI in the last hour. 205 | """ 206 | with sqlite3.connect('proxygpt.db') as conn: 207 | c = conn.cursor() 208 | c.execute("SELECT COUNT(*) FROM api_usage WHERE api_timestamp > ?", (int(time.time())-3600,)) 209 | return c.fetchone()[0] 210 | 211 | # Make function for getting API usage (daily) 212 | def get_api_usage_from_last_day() -> int: 213 | """ 214 | This function returns the number of API calls to OpenAI in the last day. 215 | """ 216 | with sqlite3.connect('proxygpt.db') as conn: 217 | c = conn.cursor() 218 | c.execute("SELECT COUNT(*) FROM api_usage WHERE api_timestamp > ?", (int(time.time())-86400,)) 219 | return c.fetchone()[0] 220 | 221 | # Make function for checking rate limit 222 | def check_rate_limit() -> bool: 223 | """ 224 | This function checks if the rate limit has been reached. 225 | 226 | Note that both hourly and daily rate limits can simultaneously be 227 | in effect. 228 | 229 | Returns: 230 | bool: True if rate limit has not been reached, False otherwise. 231 | """ 232 | if USE_HOURLY_RATE_LIMIT and get_api_usage_from_last_hour() >= hourly_rate_limit: 233 | return False 234 | if USE_DAILY_RATE_LIMIT and get_api_usage_from_last_day() >= daily_rate_limit: 235 | return False 236 | else: 237 | return True 238 | 239 | 240 | # ------------- [Classes and Other] ------------- 241 | 242 | # Define a model of ChatMessage 243 | class ChatMessage(BaseModel): 244 | role: str 245 | content: str 246 | 247 | # Define a security scheme for API key 248 | bearer_scheme = HTTPBearer() 249 | 250 | # Define validation function for API key 251 | def valid_api_key(api_key_header: APIKey = Depends(bearer_scheme)): 252 | 253 | if proxygpt_api_key: 254 | # Check if API key is valid 255 | if api_key_header.credentials != proxygpt_api_key: 256 | raise HTTPException( 257 | status_code=400, detail="Invalid API key" 258 | ) 259 | return api_key_header.credentials 260 | else: 261 | # Check if API key is valid 262 | if api_key_header.credentials not in proxygpt_api_keys: 263 | raise HTTPException( 264 | status_code=400, detail="Invalid API key" 265 | ) 266 | return api_key_header.credentials 267 | 268 | # Define validation function for API key with rate limit 269 | def valid_api_key_rate_limit(api_key_header: APIKey = Depends(bearer_scheme)): 270 | # Check if rate limit has been reached 271 | if check_rate_limit() == False: 272 | raise HTTPException(status_code=429, detail="Rate limit reached. Try again later. See /ratelimit to view status and settings.") 273 | 274 | if proxygpt_api_key: 275 | # Check if API key is valid 276 | if api_key_header.credentials != proxygpt_api_key: 277 | raise HTTPException( 278 | status_code=400, detail="Invalid API key" 279 | ) 280 | return api_key_header.credentials 281 | else: 282 | # Check if API key is valid 283 | if api_key_header.credentials not in proxygpt_api_keys: 284 | raise HTTPException( 285 | status_code=400, detail="Invalid API key" 286 | ) 287 | return api_key_header.credentials 288 | 289 | # ------------- [Routes and Endpoints] ------------- 290 | 291 | @app.post('/api/openai/completions/gpt3') 292 | async def get_openai_gpt3_completion(message: List[ChatMessage], temperature: float, api_key: str = Depends(valid_api_key_rate_limit)): 293 | """ 294 | This endpoint allows you to interact with OpenAI's GPT-3 model for Chat Completion. 295 | 296 | - **message**: A list of message objects. Each object should have a "role" (which can be "system", "user", or "assistant") and a "content" (which is the actual content of the message). 297 | - **temperature**: The temperature to use for the model's response. 298 | 299 | The endpoint will return a string containing the model's response. 300 | """ 301 | 302 | try: 303 | # Log API usage. Note, you could move this to the end of the endpoint and check the response content if you want to log only successful requests. 304 | log_api_usage() 305 | 306 | # Log time if logging installed. 307 | if logging_installed_bool: 308 | start_time = time.time() 309 | 310 | # Send request to OpenAI 311 | url = "https://api.openai.com/v1/chat/completions" 312 | payload = { "model": "gpt-3.5-turbo", "messages": [{"role": msg.role, "content": msg.content} for msg in message], "temperature": temperature } 313 | 314 | headers = { 315 | "content-type": "application/json", 316 | "Authorization": "Bearer " + str(openai_api_key) 317 | } 318 | 319 | # Send the request 320 | response = requests.post(url, json=payload, headers=headers) 321 | 322 | # Log API results if logging installed. 323 | if logging_installed_bool: 324 | insert_api_log(response_time=round((time.time()-start_time)*1000),response_code=response.status_code,endpoint=url,request=str(payload),response_str=response.text) 325 | 326 | return JSONResponse(status_code=200, content={"message": response.json()}) 327 | except Exception as e: 328 | if INSECURE_DEBUG: 329 | return JSONResponse(status_code=500, content={"error": str(e)}) 330 | else: 331 | print(e) 332 | return JSONResponse(status_code=500, content={"error": "Internal server error. Set INSECURE_DEBUG to True to view error details from client side."}) 333 | 334 | 335 | # Define a route for the GET of /ratelimit 336 | @app.get('/ratelimit') 337 | async def get_ratelimit(api_key: str = Depends(valid_api_key)): 338 | """ 339 | This endpoint allows you to view the current rate limit status and settings. 340 | """ 341 | 342 | # Return rate limit status and settings if rate limits are enabled 343 | json_to_return = {} 344 | if USE_DAILY_RATE_LIMIT: 345 | json_to_return["daily_rate_limit"] = daily_rate_limit 346 | json_to_return["daily_api_usage"] = get_api_usage_from_last_day() 347 | if USE_HOURLY_RATE_LIMIT: 348 | json_to_return["hourly_rate_limit"] = hourly_rate_limit 349 | json_to_return["hourly_api_usage"] = get_api_usage_from_last_hour() 350 | if len(json_to_return) == 0: 351 | json_to_return = {"error": "Rate limit is not enabled."} 352 | 353 | return JSONResponse(status_code=200, content=json_to_return) 354 | 355 | 356 | if graphics_installed_bool: 357 | @app.get("/dashboard", response_class=HTMLResponse) 358 | async def get_dashboard(request: Request): 359 | """ 360 | This endpoint allows you to view the dashboard of ProxyGPT. 361 | """ 362 | 363 | return templates.TemplateResponse( 364 | "dashboard.html", 365 | {"request": request}) 366 | 367 | @app.get("/dashboard-data") 368 | async def get_dashboard_data(api_key: str = Depends(valid_api_key)): 369 | """ 370 | This endpoint allows you to view the dashboard data of ProxyGPT. 371 | """ 372 | #TODO: clean logs to remove injection vulnerabilities 373 | 374 | log_results = transform_api_logs(get_api_logs()) 375 | 376 | # Reverse 377 | log_results.reverse() 378 | 379 | return JSONResponse(status_code=200, content=log_results) -------------------------------------------------------------------------------- /modules/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bklieger/ProxyGPT/bd962e1aa43d551466f5fba2dbd34628f74a0649/modules/__init__.py -------------------------------------------------------------------------------- /modules/graphics.py: -------------------------------------------------------------------------------- 1 | """ 2 | Graphics.py file for ProxyGPT. This file contains the graphics module code for the API. 3 | 4 | Author: Benjamin Klieger 5 | Version: 0.2.0-beta 6 | Date: 2024-01-05 7 | License: MIT 8 | """ 9 | -------------------------------------------------------------------------------- /modules/logging.py: -------------------------------------------------------------------------------- 1 | """ 2 | Logging.py file for ProxyGPT. This file contains the logging module code for the API. 3 | 4 | Author: Benjamin Klieger 5 | Version: 0.2.0-beta 6 | Date: 2024-01-05 7 | License: MIT 8 | """ 9 | 10 | # ------------- [Import Libraries] ------------- 11 | 12 | # Required libraries from Pydantic for API functionality 13 | from pydantic import BaseModel 14 | from typing import List 15 | 16 | # Required for inspecting code 17 | import inspect 18 | 19 | # Required for rate limiting with database and timestamps 20 | import sqlite3 21 | import time 22 | 23 | # Required for printing styled log messages 24 | from utils import * 25 | 26 | 27 | # ------------- [Initialization: DB] ------------- 28 | 29 | # Use SQLITE database to store API logs 30 | # Create a table for API usage if it does not exist 31 | conn = sqlite3.connect('proxygpt.db') 32 | c = conn.cursor() 33 | 34 | # Create the api_logs table with the new schema 35 | c.execute(''' 36 | CREATE TABLE IF NOT EXISTS api_logs ( 37 | api_timestamp INTEGER, 38 | response_time FLOAT, 39 | response_code INTEGER, 40 | endpoint TEXT, 41 | request TEXT, 42 | response_str TEXT 43 | ) 44 | ''') 45 | 46 | # Commit the changes and close the connection 47 | conn.commit() 48 | conn.close() 49 | 50 | 51 | # ------------- [Helper Functions] ------------- 52 | 53 | def get_last_n(list_to_cut: List[BaseModel], n: int) -> List[BaseModel]: 54 | """ 55 | This function returns the last n number of items from a list. 56 | If n is None, the function returns the entire list. 57 | 58 | Args: 59 | list_to_cut (List[BaseModel]): The list to cut. 60 | n (int): The number of items to return. 61 | 62 | Returns: 63 | List[BaseModel]: The last n number of items from the list. 64 | """ 65 | 66 | # If n is None, return the entire list 67 | if n is None: 68 | return list_to_cut 69 | # Otherwise, return the last n number of items from the list 70 | else: 71 | return list_to_cut[-n:] 72 | 73 | 74 | # ------------- [Functions] ------------- 75 | 76 | # Function for inserting API log 77 | def insert_api_log(response_time: float, response_code: int, endpoint: str, request: str, response_str: str) -> None: 78 | """ 79 | This function inserts an instance of API usage into the SQLite database. 80 | 81 | Args: 82 | response_time (float): The response time of the API call. 83 | response_code (int): The response code of the API call. 84 | endpoint (str): The endpoint url of the API call. 85 | request (str) (optional): The request data of the API call. 86 | response_str (str) (optional): The response string of the API call. 87 | """ 88 | 89 | conn = sqlite3.connect('proxygpt.db') 90 | c = conn.cursor() 91 | 92 | # Using parameterized query for safe insertion 93 | c.execute(''' 94 | INSERT INTO api_logs (api_timestamp, response_time, response_code, endpoint, request, response_str) 95 | VALUES (?, ?, ?, ?, ?, ?) 96 | ''', (int(time.time()), response_time, response_code, endpoint, request, response_str)) 97 | 98 | conn.commit() 99 | conn.close() 100 | 101 | 102 | # Function to returning the API logs 103 | def get_api_logs(start_time: int = None, end_time: int = None, last_n: int = None) -> List[BaseModel]: 104 | """ 105 | This function returns a list of API logs from the SQLite database. 106 | 107 | Args: 108 | start_time (int) (optional): The start time of the API logs to return. 109 | end_time (int) (optional): The end time of the API logs to return. 110 | last_n (int) (optional): The last n number of API logs to return (will 111 | select from filtered list if start_time and/or end_time are provided). 112 | 113 | Returns: 114 | List[BaseModel]: A list of API logs. 115 | """ 116 | 117 | conn = sqlite3.connect('proxygpt.db') 118 | c = conn.cursor() 119 | 120 | # If start_time and end_time are provided, filter the logs by time 121 | if start_time and end_time: 122 | c.execute(''' 123 | SELECT * FROM api_logs WHERE api_timestamp BETWEEN ? AND ? 124 | ''', (start_time, end_time)) 125 | # If only start_time is provided, filter the logs by time 126 | elif start_time: 127 | c.execute(''' 128 | SELECT * FROM api_logs WHERE api_timestamp >= ? 129 | ''', (start_time,)) 130 | # If only end_time is provided, filter the logs by time 131 | elif end_time: 132 | c.execute(''' 133 | SELECT * FROM api_logs WHERE api_timestamp <= ? 134 | ''', (end_time,)) 135 | # If no time parameters are provided, select all logs 136 | else: 137 | c.execute(''' 138 | SELECT * FROM api_logs 139 | ''') 140 | 141 | # Get the results from the query 142 | results = c.fetchall() 143 | 144 | # Filter by n, which will make no modification if n is None 145 | results = get_last_n(results, last_n) 146 | 147 | # Close the connection 148 | conn.close() 149 | 150 | # Return the results 151 | return results 152 | 153 | # Function for transforming list of API logs into list of dictionaries 154 | def transform_api_logs(logs: List[BaseModel]) -> List[BaseModel]: 155 | """ 156 | This function transforms a list of API logs into a list of dictionaries. 157 | 158 | Args: 159 | logs (List[BaseModel]): A list of API logs. 160 | 161 | Returns: 162 | List[BaseModel]: A list of API logs. 163 | """ 164 | 165 | transformed_logs = [] 166 | for log in logs: 167 | transformed_log = { 168 | "timestamp": log[0], 169 | "response_time": log[1], 170 | "response_code": log[2], 171 | "endpoint": log[3], 172 | "request": log[4], 173 | "response": log[5] 174 | } 175 | transformed_logs.append(transformed_log) 176 | 177 | return transformed_logs -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | aiohttp==3.8.5 2 | aiosignal==1.3.1 3 | annotated-types==0.5.0 4 | anyio==3.7.1 5 | async-timeout==4.0.2 6 | asynctest==0.13.0 7 | attrs==23.1.0 8 | certifi==2023.7.22 9 | charset-normalizer==3.2.0 10 | click==8.1.6 11 | exceptiongroup==1.1.2 12 | fastapi==0.100.1 13 | frozenlist==1.3.3 14 | gunicorn==21.2.0 15 | h11==0.14.0 16 | idna==3.4 17 | importlib-metadata==6.7.0 18 | multidict==6.0.4 19 | packaging==23.1 20 | pydantic==2.1.1 21 | pydantic-core==2.4.0 22 | requests==2.31.0 23 | sniffio==1.3.0 24 | starlette==0.27.0 25 | tqdm==4.65.0 26 | typing-extensions==4.7.1 27 | urllib3==2.0.4 28 | uvicorn==0.22.0 29 | yarl==1.9.2 30 | zipp==3.15.0 31 | -------------------------------------------------------------------------------- /settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Settings.py file for ProxyGPT. This file contains the settings for the API. 3 | 4 | Author: Benjamin Klieger 5 | Version: 0.2.0-beta 6 | Date: 2024-01-05 7 | License: MIT 8 | """ 9 | 10 | # ------------- [Settings] ------------- 11 | 12 | # Settings (Note: Both can be true and simultaneously active and enforced) 13 | USE_HOURLY_RATE_LIMIT = True # Set to False to disable hourly rate limit 14 | USE_DAILY_RATE_LIMIT = True # Set to False to disable daily rate limit 15 | 16 | """ 17 | Set INSECURE_DEBUG to False to disable debug mode. When debug mode is off, 18 | server errors will no longer be passed through to the client, and instead 19 | present a generic error message to the API client, limiting the risk of 20 | exposing any secret variables stored on the server side to client. 21 | """ 22 | INSECURE_DEBUG = True 23 | 24 | """ 25 | Set the installed and used modules here. Removing a module from this list 26 | will skip the import in main.py. Note some modules are dependent on others. 27 | """ 28 | INSTALLED_MODULES = ["graphics","logging"] 29 | 30 | 31 | # ------------- [Checks] ------------- 32 | 33 | # Check the dependencies of the installed modules 34 | dependencies = {"graphics":["logging"],"logging":[]} 35 | 36 | # Add the dependencies 37 | for module in INSTALLED_MODULES: 38 | for dependency in dependencies[module]: 39 | if dependency not in INSTALLED_MODULES: 40 | INSTALLED_MODULES.append(dependency) -------------------------------------------------------------------------------- /templates/dashboard.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 | 6 |