Special: halved GPT-3 credits cost (thanks for 1.000 members!)

If no model is given, the API now defaults to gpt-3.5-turbo
We now also support gpt-3.5-turbo-0301!
Made provider code asynchronous
New dependency requirement: aiofiles
Staff now gets a notification when a provider key is invalid
Internal improvements with log webhooks for staff
Removed image model check
This commit is contained in:
nsde 2023-09-02 21:15:55 +02:00
parent 008bf56fdf
commit 6bd5dc534c
7 changed files with 70 additions and 30 deletions

View file

@ -8,23 +8,21 @@ costs:
chat-models: chat-models:
gpt-4-32k: 100 gpt-4-32k: 100
gpt-4: 30 gpt-4: 30
gpt-3: 10 gpt-3: 5
## Roles Explanation ## Roles Explanation
# Bonuses: They are a multiplier for costs # Bonuses: They are a multiplier for costs
# They work like: final_cost = cost * bonus # They work like: final_cost = cost * bonus
# Rate limits: Limit the requests of the user
# Seconds to wait between requests
roles: roles:
owner: owner:
bonus: 0.1 bonus: 0
admin: admin:
bonus: 0.3 bonus: 0.2
helper: helper:
bonus: 0.4 bonus: 0.4
booster: booster:
bonus: 0.5 bonus: 0.6
default: default:
bonus: 1.0 bonus: 1.0

View file

@ -3,14 +3,11 @@
import os import os
import sys import sys
from helpers import errors
project_root = os.path.abspath(os.path.join(os.path.dirname(__file__), '..')) project_root = os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))
sys.path.append(project_root) sys.path.append(project_root)
# the code above is to allow importing from the root folder # the code above is to allow importing from the root folder
import os
import json import json
import hmac import hmac
import fastapi import fastapi
@ -20,6 +17,7 @@ from dotenv import load_dotenv
import checks.client import checks.client
from helpers import errors
from db.users import UserManager from db.users import UserManager
load_dotenv() load_dotenv()
@ -64,11 +62,13 @@ async def new_user_webhook(user: dict) -> None:
color=0x90ee90, color=0x90ee90,
) )
dc = user['auth']['discord']
embed.add_field(name='ID', value=str(user['_id']), inline=False) embed.add_field(name='ID', value=str(user['_id']), inline=False)
embed.add_field(name='Discord', value=user['auth']['discord'] or '-') embed.add_field(name='Discord', value=dc or '-')
embed.add_field(name='Github', value=user['auth']['github'] or '-') embed.add_field(name='Github', value=user['auth']['github'] or '-')
dhook.send(embed=embed) dhook.send(content=f'<@{dc}>', embed=embed)
@router.post('/users') @router.post('/users')
async def create_user(incoming_request: fastapi.Request): async def create_user(incoming_request: fastapi.Request):

View file

@ -1,5 +1,6 @@
"""Does quite a few checks and prepares the incoming request for the target endpoint, so it can be streamed""" """Does quite a few checks and prepares the incoming request for the target endpoint, so it can be streamed"""
import os
import json import json
import yaml import yaml
import time import time
@ -23,6 +24,8 @@ models_list = json.load(open('models.json', encoding='utf8'))
with open('config/config.yml', encoding='utf8') as f: with open('config/config.yml', encoding='utf8') as f:
config = yaml.safe_load(f) config = yaml.safe_load(f)
moderation_debug_key_key = os.getenv('MODERATION_DEBUG_KEY')
async def handle(incoming_request: fastapi.Request): async def handle(incoming_request: fastapi.Request):
""" """
### Transfer a streaming response ### Transfer a streaming response
@ -47,7 +50,7 @@ async def handle(incoming_request: fastapi.Request):
received_key = incoming_request.headers.get('Authorization') received_key = incoming_request.headers.get('Authorization')
if not received_key or not received_key.startswith('Bearer '): if not received_key or not received_key.startswith('Bearer '):
return await errors.error(403, 'No NovaAI API key given!', 'Add \'Authorization: Bearer nv-...\' to your request headers.') return await errors.error(401, 'No NovaAI API key given!', 'Add \'Authorization: Bearer nv-...\' to your request headers.')
key_tags = '' key_tags = ''
@ -58,7 +61,7 @@ async def handle(incoming_request: fastapi.Request):
user = await users.user_by_api_key(received_key.split('Bearer ')[1].strip()) user = await users.user_by_api_key(received_key.split('Bearer ')[1].strip())
if not user or not user['status']['active']: if not user or not user['status']['active']:
return await errors.error(403, 'Invalid or inactive NovaAI API key!', 'Create a new NovaOSS API key or reactivate your account.') return await errors.error(418, 'Invalid or inactive NovaAI API key!', 'Create a new NovaOSS API key or reactivate your account.')
if user.get('auth', {}).get('discord'): if user.get('auth', {}).get('discord'):
print(f'[bold green]>Discord[/bold green] {user["auth"]["discord"]}') print(f'[bold green]>Discord[/bold green] {user["auth"]["discord"]}')
@ -86,7 +89,7 @@ async def handle(incoming_request: fastapi.Request):
return await errors.error(429, 'Not enough credits.', 'Wait or earn more credits. Learn more on our website or Discord server.') return await errors.error(429, 'Not enough credits.', 'Wait or earn more credits. Learn more on our website or Discord server.')
if not 'DISABLE_VARS' in key_tags: if 'DISABLE_VARS' not in key_tags:
payload_with_vars = json.dumps(payload) payload_with_vars = json.dumps(payload)
replace_dict = { replace_dict = {
@ -112,26 +115,30 @@ async def handle(incoming_request: fastapi.Request):
payload = json.loads(payload_with_vars) payload = json.loads(payload_with_vars)
policy_violation = False policy_violation = False
if '/moderations' not in path:
inp = ''
if 'input' in payload or 'prompt' in payload: if not (moderation_debug_key_key and moderation_debug_key_key in key_tags and 'gpt-3' in payload.get('model', '')):
inp = payload.get('input', payload.get('prompt', '')) if '/moderations' not in path:
inp = ''
if isinstance(payload.get('messages'), list): if 'input' in payload or 'prompt' in payload:
inp = '\n'.join([message['content'] for message in payload['messages']]) inp = payload.get('input', payload.get('prompt', ''))
if inp and len(inp) > 2 and not inp.isnumeric(): if isinstance(payload.get('messages'), list):
policy_violation = await moderation.is_policy_violated(inp) inp = '\n'.join([message['content'] for message in payload['messages']])
if inp and len(inp) > 2 and not inp.isnumeric():
policy_violation = await moderation.is_policy_violated(inp)
if policy_violation: if policy_violation:
return await errors.error( return await errors.error(
400, f'The request contains content which violates this model\'s policies for "{policy_violation}".', 400, f'The request contains content which violates this model\'s policies for <{policy_violation}>.',
'We currently don\'t support any NSFW models.' 'We currently don\'t support any NSFW models.'
) )
if 'chat/completions' in path and not payload.get('stream', False): if 'chat/completions' in path and not payload.get('stream', False):
payload['stream'] = False payload['stream'] = False
if 'chat/completions' in path and not payload.get('model'):
payload['model'] = 'gpt-3.5-turbo'
media_type = 'text/event-stream' if payload.get('stream', False) else 'application/json' media_type = 'text/event-stream' if payload.get('stream', False) else 'application/json'

View file

@ -27,10 +27,10 @@ async def balance_chat_request(payload: dict) -> dict:
providers_available.append(provider_module) providers_available.append(provider_module)
if not providers_available: if not providers_available:
raise NotImplementedError(f'The model "{payload["model"]}" is not available. MODEl_UNAVAILABLE') raise ValueError(f'The model "{payload["model"]}" is not available. MODEL_UNAVAILABLE')
provider = random.choice(providers_available) provider = random.choice(providers_available)
target = provider.chat_completion(**payload) target = await provider.chat_completion(**payload)
module_name = await _get_module_name(provider) module_name = await _get_module_name(provider)
target['module'] = module_name target['module'] = module_name
@ -61,7 +61,7 @@ async def balance_organic_request(request: dict) -> dict:
providers_available.append(provider_module) providers_available.append(provider_module)
provider = random.choice(providers_available) provider = random.choice(providers_available)
target = provider.organify(request) target = await provider.organify(request)
module_name = await _get_module_name(provider) module_name = await _get_module_name(provider)
target['module'] = module_name target['module'] = module_name

View file

@ -1,7 +1,28 @@
"""This module contains functions for authenticating with providers.""" """This module contains functions for authenticating with providers."""
import os
import asyncio import asyncio
from dotenv import load_dotenv
from dhooks import Webhook, Embed
load_dotenv()
async def invalidation_webhook(provider_and_key: str) -> None:
"""Runs when a new user is created."""
dhook = Webhook(os.environ['DISCORD_WEBHOOK__API_ISSUE'])
embed = Embed(
description='Key Invalidated',
color=0xffee90,
)
embed.add_field(name='Provider', value=provider_and_key.split('>')[0])
embed.add_field(name='Key (censored)', value=f'||{provider_and_key.split(">")[1][:10]}...||', inline=False)
dhook.send(embed=embed)
async def invalidate_key(provider_and_key: str) -> None: async def invalidate_key(provider_and_key: str) -> None:
""" """
@ -28,5 +49,7 @@ async def invalidate_key(provider_and_key: str) -> None:
with open(f'secret/{provider}.invalid.txt', 'a', encoding='utf8') as f: with open(f'secret/{provider}.invalid.txt', 'a', encoding='utf8') as f:
f.write(key + '\n') f.write(key + '\n')
await invalidation_webhook(provider_and_key)
if __name__ == '__main__': if __name__ == '__main__':
asyncio.run(invalidate_key('closed>cd...')) asyncio.run(invalidate_key('closed>demo-...'))

View file

@ -140,8 +140,8 @@ async def demo():
print('[lightblue]Checking if the API works...') print('[lightblue]Checking if the API works...')
print(await test_chat()) print(await test_chat())
print('[lightblue]Checking if SDXL image generation works...') # print('[lightblue]Checking if SDXL image generation works...')
print(await test_sdxl()) # print(await test_sdxl())
print('[lightblue]Checking if the moderation endpoint works...') print('[lightblue]Checking if the moderation endpoint works...')
print(await test_api_moderation()) print(await test_api_moderation())

View file

@ -1,3 +1,13 @@
# Setup
## Requirements
- Python 3.9+
- pip
- MongoDB database
## Recommended
- `git` (for updates)
- `screen` (for production)
- Cloudflare (for security, anti-DDoS, etc.) - we fully support Cloudflare
## Install ## Install
Assuming you have a new version of Python 3.9+ and pip installed: Assuming you have a new version of Python 3.9+ and pip installed:
@ -110,6 +120,8 @@ You can also specify a port, e.g.:
python run 1337 python run 1337
``` ```
## Adding a provider
## Test if it works ## Test if it works
`python checks` `python checks`