mirror of
https://github.com/NovaOSS/nova-api.git
synced 2024-11-25 18:23:57 +01:00
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:
parent
008bf56fdf
commit
6bd5dc534c
|
@ -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
|
||||||
|
|
10
api/core.py
10
api/core.py
|
@ -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):
|
||||||
|
|
|
@ -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'
|
||||||
|
|
||||||
|
|
|
@ -27,11 +27,11 @@ 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
|
||||||
|
|
|
@ -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-...'))
|
||||||
|
|
|
@ -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())
|
||||||
|
|
12
setup.md
12
setup.md
|
@ -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`
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue