mirror of
https://github.com/NovaOSS/nova-api.git
synced 2024-11-25 16:23:57 +01:00
some stuff idfk
This commit is contained in:
parent
e76d675dc6
commit
72cea38d8d
|
@ -77,21 +77,14 @@ Create a `.env` file, make sure not to reveal it to anyone, and fill in the requ
|
||||||
- `PROXY_USER` (optional)
|
- `PROXY_USER` (optional)
|
||||||
- `PROXY_PASS` (optional)
|
- `PROXY_PASS` (optional)
|
||||||
|
|
||||||
### ClosedAI configuration
|
|
||||||
- `CLOSEDAI_KEY`: the API key used to access the ClosedAI API
|
|
||||||
- `CLOSEDAI_ENDPOINT` (defaults to `https://api.openai.com/v1`): the API endpoint which is used for the provider ClosedAI
|
|
||||||
|
|
||||||
### `ACTUAL_IPS` (optional)
|
### `ACTUAL_IPS` (optional)
|
||||||
This is a security measure to make sure a proxy, VPN, Tor or any other IP hiding service is used by the host when accessing "Closed"AI's API.
|
This is a security measure to make sure a proxy, VPN, Tor or any other IP hiding service is used by the host when accessing "Closed"AI's API.
|
||||||
It is a space separated list of IP addresses that are allowed to access the API.
|
It is a space separated list of IP addresses that are allowed to access the API.
|
||||||
You can also just add the *beginning* of an API address, like `12.123.` (without an asterisk!) to allow all IPs starting with `12.123.`.
|
You can also just add the *beginning* of an API address, like `12.123.` (without an asterisk!) to allow all IPs starting with `12.123.`.
|
||||||
> To disable the warning if you don't have this feature enabled, set `ACTUAL_IPS` to `None`.
|
> To disable the warning if you don't have this feature enabled, set `ACTUAL_IPS` to `None`.
|
||||||
|
|
||||||
### `DEMO_AUTH`
|
|
||||||
API key for demo purposes. You can give this to trusted team members. Never use it in production.
|
|
||||||
|
|
||||||
### `CORE_API_KEY`
|
### `CORE_API_KEY`
|
||||||
This will
|
This specifies the **very secret key** for accessing the entire user database etc.
|
||||||
|
|
||||||
## Run
|
## Run
|
||||||
> **Warning:** read the according section for production usage!
|
> **Warning:** read the according section for production usage!
|
||||||
|
|
|
@ -1,30 +0,0 @@
|
||||||
import random
|
|
||||||
import asyncio
|
|
||||||
|
|
||||||
import chat_providers
|
|
||||||
|
|
||||||
provider_modules = [
|
|
||||||
# chat_providers.twa,
|
|
||||||
chat_providers.quantum,
|
|
||||||
# chat_providers.churchless,
|
|
||||||
chat_providers.closed
|
|
||||||
]
|
|
||||||
|
|
||||||
async def balance(payload: dict) -> dict:
|
|
||||||
providers_available = []
|
|
||||||
|
|
||||||
for provider_module in provider_modules:
|
|
||||||
if payload['stream'] and not provider_module.STREAMING:
|
|
||||||
continue
|
|
||||||
|
|
||||||
if payload['model'] not in provider_module.MODELS:
|
|
||||||
continue
|
|
||||||
|
|
||||||
providers_available.append(provider_module)
|
|
||||||
|
|
||||||
provider = random.choice(providers_available)
|
|
||||||
return provider.chat_completion(**payload)
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
req = asyncio.run(balance(payload={'model': 'gpt-3.5-turbo', 'stream': True}))
|
|
||||||
print(req['url'])
|
|
23
api/config/credits.yml
Normal file
23
api/config/credits.yml
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
max-credits: 100001
|
||||||
|
start-credits: 1000
|
||||||
|
|
||||||
|
costs:
|
||||||
|
other: 50
|
||||||
|
|
||||||
|
chat-models:
|
||||||
|
gpt-3: 10
|
||||||
|
gpt-4: 75
|
||||||
|
gpt-4-32k: 100
|
||||||
|
|
||||||
|
# bonuses are multiplier for costs:
|
||||||
|
# final_cost = cost * bonus
|
||||||
|
bonuses:
|
||||||
|
owner: 0.1
|
||||||
|
admin: 0.3
|
||||||
|
helper: 0.4
|
||||||
|
booster: 0.5
|
||||||
|
|
||||||
|
# discord reward 0.99^lvl?
|
||||||
|
|
||||||
|
rewards:
|
||||||
|
day: 1000
|
28
api/core.py
28
api/core.py
|
@ -6,6 +6,7 @@ import fastapi
|
||||||
|
|
||||||
from db import users
|
from db import users
|
||||||
|
|
||||||
|
from dhooks import Webhook, Embed
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
load_dotenv()
|
load_dotenv()
|
||||||
|
@ -31,6 +32,20 @@ async def get_users(discord_id: int, incoming_request: fastapi.Request):
|
||||||
|
|
||||||
return user
|
return user
|
||||||
|
|
||||||
|
def new_user_webhook(user: dict) -> None:
|
||||||
|
dhook = Webhook(os.getenv('DISCORD_WEBHOOK__USER_CREATED'))
|
||||||
|
|
||||||
|
embed = Embed(
|
||||||
|
description='New User',
|
||||||
|
color=0x90ee90,
|
||||||
|
)
|
||||||
|
|
||||||
|
embed.add_field(name='ID', value=user['_id'], inline=False)
|
||||||
|
embed.add_field(name='Discord', value=user['auth']['discord'])
|
||||||
|
embed.add_field(name='Github', value=user['auth']['github'])
|
||||||
|
|
||||||
|
dhook.send(embed=embed)
|
||||||
|
|
||||||
@router.post('/users')
|
@router.post('/users')
|
||||||
async def create_user(incoming_request: fastapi.Request):
|
async def create_user(incoming_request: fastapi.Request):
|
||||||
auth_error = await check_core_auth(incoming_request)
|
auth_error = await check_core_auth(incoming_request)
|
||||||
|
@ -43,6 +58,17 @@ async def create_user(incoming_request: fastapi.Request):
|
||||||
discord_id = payload.get('discord_id')
|
discord_id = payload.get('discord_id')
|
||||||
except (json.decoder.JSONDecodeError, AttributeError):
|
except (json.decoder.JSONDecodeError, AttributeError):
|
||||||
return fastapi.Response(status_code=400, content='Invalid or no payload received.')
|
return fastapi.Response(status_code=400, content='Invalid or no payload received.')
|
||||||
|
|
||||||
user = await users.create(discord_id)
|
user = await users.create(discord_id)
|
||||||
|
new_user_webhook(user)
|
||||||
|
|
||||||
return user
|
return user
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
new_user_webhook({
|
||||||
|
'_id': 'JUST_A_TEST_IGNORE_ME',
|
||||||
|
'auth': {
|
||||||
|
'discord': 123,
|
||||||
|
'github': 'abc'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
|
@ -4,30 +4,30 @@ import time
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
from motor.motor_asyncio import AsyncIOMotorClient
|
from motor.motor_asyncio import AsyncIOMotorClient
|
||||||
|
|
||||||
|
from helpers import network
|
||||||
|
|
||||||
load_dotenv()
|
load_dotenv()
|
||||||
|
|
||||||
def _get_mongo(collection_name: str):
|
def _get_mongo(collection_name: str):
|
||||||
return AsyncIOMotorClient(os.getenv('MONGO_URI'))['nova-core'][collection_name]
|
return AsyncIOMotorClient(os.getenv('MONGO_URI'))['nova-core'][collection_name]
|
||||||
|
|
||||||
async def log_api_request(user, request, target_url):
|
async def log_api_request(user: dict, incoming_request, target_url: str):
|
||||||
payload = await request.json()
|
payload = await incoming_request.json()
|
||||||
|
|
||||||
last_prompt = None
|
last_prompt = None
|
||||||
if 'messages' in payload:
|
if 'messages' in payload:
|
||||||
last_prompt = payload['messages'][-1]['content']
|
last_prompt = payload['messages'][-1]['content']
|
||||||
|
|
||||||
model = None
|
model = payload.get('model')
|
||||||
if 'model' in payload:
|
|
||||||
model = payload['model']
|
|
||||||
|
|
||||||
new_log_item = {
|
new_log_item = {
|
||||||
'timestamp': time.time(),
|
'timestamp': time.time(),
|
||||||
'method': request.method,
|
'method': incoming_request.method,
|
||||||
'path': request.url.path,
|
'path': incoming_request.url.path,
|
||||||
'user_id': user['_id'],
|
'user_id': user['_id'],
|
||||||
'security': {
|
'security': {
|
||||||
'ip': request.client.host,
|
'ip': network.get_ip(incoming_request),
|
||||||
'useragent': request.headers.get('User-Agent')
|
'useragent': incoming_request.headers.get('User-Agent')
|
||||||
},
|
},
|
||||||
'details': {
|
'details': {
|
||||||
'model': model,
|
'model': model,
|
||||||
|
|
42
api/db/stats.py
Normal file
42
api/db/stats.py
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
import os
|
||||||
|
import pytz
|
||||||
|
import asyncio
|
||||||
|
import datetime
|
||||||
|
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
from motor.motor_asyncio import AsyncIOMotorClient
|
||||||
|
|
||||||
|
load_dotenv()
|
||||||
|
|
||||||
|
def _get_mongo(collection_name: str):
|
||||||
|
return AsyncIOMotorClient(os.getenv('MONGO_URI'))['nova-core'][collection_name]
|
||||||
|
|
||||||
|
async def add_date():
|
||||||
|
date = datetime.datetime.now(pytz.timezone('GMT')).strftime('%Y.%m.%d')
|
||||||
|
year, month, day = date.split('.')
|
||||||
|
|
||||||
|
await _get_mongo('stats').update_one({}, {'$inc': {f'dates.{year}.{month}.{day}': 1}}, upsert=True)
|
||||||
|
|
||||||
|
async def add_ip_address(ip_address: str):
|
||||||
|
ip_address = ip_address.replace('.', '_')
|
||||||
|
await _get_mongo('stats').update_one({}, {'$inc': {f'ips.{ip_address}': 1}}, upsert=True)
|
||||||
|
|
||||||
|
async def add_target(url: str):
|
||||||
|
await _get_mongo('stats').update_one({}, {'$inc': {f'targets.{url}': 1}}, upsert=True)
|
||||||
|
|
||||||
|
async def add_tokens(tokens: int, model: str):
|
||||||
|
await _get_mongo('stats').update_one({}, {'$inc': {f'tokens.{model}': tokens}}, upsert=True)
|
||||||
|
|
||||||
|
async def add_model(model: str):
|
||||||
|
await _get_mongo('stats').update_one({}, {'$inc': {f'models.{model}': 1}}, upsert=True)
|
||||||
|
|
||||||
|
async def add_path(path: str):
|
||||||
|
path = path.replace('/', '_')
|
||||||
|
await _get_mongo('stats').update_one({}, {'$inc': {f'paths.{path}': 1}}, upsert=True)
|
||||||
|
|
||||||
|
async def get_value(obj_filter):
|
||||||
|
return await _get_mongo('stats').find_one({obj_filter})
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
asyncio.run(add_date())
|
||||||
|
asyncio.run(add_path('/__demo/test'))
|
|
@ -1,4 +1,5 @@
|
||||||
import os
|
import os
|
||||||
|
import yaml
|
||||||
import random
|
import random
|
||||||
import string
|
import string
|
||||||
import asyncio
|
import asyncio
|
||||||
|
@ -8,11 +9,15 @@ from motor.motor_asyncio import AsyncIOMotorClient
|
||||||
|
|
||||||
load_dotenv()
|
load_dotenv()
|
||||||
|
|
||||||
|
with open('config/credits.yml', encoding='utf8') as f:
|
||||||
|
credits_config = yaml.safe_load(f)
|
||||||
|
|
||||||
def _get_mongo(collection_name: str):
|
def _get_mongo(collection_name: str):
|
||||||
return AsyncIOMotorClient(os.getenv('MONGO_URI'))['nova-core'][collection_name]
|
return AsyncIOMotorClient(os.getenv('MONGO_URI'))['nova-core'][collection_name]
|
||||||
|
|
||||||
async def create(discord_id: int=0) -> dict:
|
async def create(discord_id: int=0) -> dict:
|
||||||
"""Adds a new user to the MongoDB collection."""
|
"""Adds a new user to the MongoDB collection."""
|
||||||
|
|
||||||
chars = string.ascii_letters + string.digits
|
chars = string.ascii_letters + string.digits
|
||||||
|
|
||||||
infix = os.getenv('KEYGEN_INFIX')
|
infix = os.getenv('KEYGEN_INFIX')
|
||||||
|
@ -23,7 +28,7 @@ async def create(discord_id: int=0) -> dict:
|
||||||
|
|
||||||
new_user = {
|
new_user = {
|
||||||
'api_key': new_api_key,
|
'api_key': new_api_key,
|
||||||
'credits': 1000,
|
'credits': credits_config['start-credits'],
|
||||||
'role': '',
|
'role': '',
|
||||||
'status': {
|
'status': {
|
||||||
'active': True,
|
'active': True,
|
||||||
|
|
2
api/helpers/network.py
Normal file
2
api/helpers/network.py
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
async def get_ip(request) -> str:
|
||||||
|
return request.client.host
|
42
api/load_balancing.py
Normal file
42
api/load_balancing.py
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
import random
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
import chat_providers
|
||||||
|
|
||||||
|
provider_modules = [
|
||||||
|
chat_providers.twa,
|
||||||
|
chat_providers.quantum,
|
||||||
|
chat_providers.churchless,
|
||||||
|
chat_providers.closed,
|
||||||
|
chat_providers.closed4
|
||||||
|
]
|
||||||
|
|
||||||
|
async def balance_chat_request(payload: dict) -> dict:
|
||||||
|
providers_available = []
|
||||||
|
|
||||||
|
for provider_module in provider_modules:
|
||||||
|
if payload['stream'] and not provider_module.STREAMING:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if payload['model'] not in provider_module.MODELS:
|
||||||
|
continue
|
||||||
|
|
||||||
|
providers_available.append(provider_module)
|
||||||
|
|
||||||
|
provider = random.choice(providers_available)
|
||||||
|
return provider.chat_completion(**payload)
|
||||||
|
|
||||||
|
async def balance_organic_request(request: dict) -> dict:
|
||||||
|
providers_available = []
|
||||||
|
|
||||||
|
for provider_module in provider_modules:
|
||||||
|
if provider_module.ORGANIC:
|
||||||
|
providers_available.append(provider_module)
|
||||||
|
|
||||||
|
provider = random.choice(providers_available)
|
||||||
|
|
||||||
|
return provider.organify(request)
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
req = asyncio.run(balance_chat_request(payload={'model': 'gpt-3.5-turbo', 'stream': True}))
|
||||||
|
print(req['url'])
|
|
@ -9,8 +9,6 @@ from dotenv import load_dotenv
|
||||||
import core
|
import core
|
||||||
import transfer
|
import transfer
|
||||||
|
|
||||||
from db import users
|
|
||||||
|
|
||||||
load_dotenv()
|
load_dotenv()
|
||||||
|
|
||||||
app = fastapi.FastAPI()
|
app = fastapi.FastAPI()
|
||||||
|
|
18
api/moderation.py
Normal file
18
api/moderation.py
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
import os
|
||||||
|
import asyncio
|
||||||
|
import openai as closedai
|
||||||
|
|
||||||
|
from typing import Union
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
|
load_dotenv()
|
||||||
|
|
||||||
|
closedai.api_key = os.getenv('LEGIT_CLOSEDAI_KEY')
|
||||||
|
|
||||||
|
async def is_safe(text: Union[str, list]) -> bool:
|
||||||
|
return closedai.Moderation.create(
|
||||||
|
input=text,
|
||||||
|
)['results'][0]['flagged']
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
asyncio.run(is_safe('Hello'))
|
|
@ -1,61 +0,0 @@
|
||||||
import os
|
|
||||||
import aiohttp
|
|
||||||
import asyncio
|
|
||||||
import aiohttp_socks
|
|
||||||
|
|
||||||
from dotenv import load_dotenv
|
|
||||||
|
|
||||||
import proxies
|
|
||||||
|
|
||||||
from helpers import exceptions
|
|
||||||
|
|
||||||
load_dotenv()
|
|
||||||
|
|
||||||
async def stream(request: dict, demo_mode: bool=False):
|
|
||||||
headers = {
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
}
|
|
||||||
|
|
||||||
for k, v in request.get('headers', {}).items():
|
|
||||||
headers[k] = v
|
|
||||||
|
|
||||||
for _ in range(3):
|
|
||||||
async with aiohttp.ClientSession(connector=proxies.default_proxy.connector) as session:
|
|
||||||
async with session.get(
|
|
||||||
# 'GET',
|
|
||||||
'https://checkip.amazonaws.com/'
|
|
||||||
) as response:
|
|
||||||
print(response.content)
|
|
||||||
print(type(response.content))
|
|
||||||
|
|
||||||
# html = await response.text()
|
|
||||||
# print(html)
|
|
||||||
|
|
||||||
# async with session.get(
|
|
||||||
# method='GET',
|
|
||||||
# url='https://checkip.amazonaws.com',
|
|
||||||
# method=request.get('method', 'POST'),
|
|
||||||
# url=request['url'],
|
|
||||||
# json=request.get('payload', {}),
|
|
||||||
# headers=headers,
|
|
||||||
# timeout=aiohttp.ClientTimeout(total=float(os.getenv('TRANSFER_TIMEOUT', '120'))),
|
|
||||||
# ) as response:
|
|
||||||
# try:
|
|
||||||
# await response.raise_for_status()
|
|
||||||
# except Exception as exc:
|
|
||||||
# if 'Too Many Requests' in str(exc):
|
|
||||||
# continue
|
|
||||||
# else:
|
|
||||||
# break
|
|
||||||
|
|
||||||
async for chunk in response.content.iter_chunks():
|
|
||||||
# chunk = f'{chunk.decode("utf8")}\n\n'
|
|
||||||
|
|
||||||
if demo_mode:
|
|
||||||
print(chunk)
|
|
||||||
|
|
||||||
yield chunk
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
asyncio.run(stream({'method': 'GET', 'url': 'https://checkip.amazonaws.com'}, True))
|
|
||||||
|
|
107
api/streaming.py
Normal file
107
api/streaming.py
Normal file
|
@ -0,0 +1,107 @@
|
||||||
|
import os
|
||||||
|
import yaml
|
||||||
|
import asyncio
|
||||||
|
import aiohttp
|
||||||
|
import starlette
|
||||||
|
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
|
import proxies
|
||||||
|
import load_balancing
|
||||||
|
|
||||||
|
from db import logs, users, stats
|
||||||
|
from rich import print
|
||||||
|
from helpers import network
|
||||||
|
|
||||||
|
load_dotenv()
|
||||||
|
|
||||||
|
DEMO_PAYLOAD = {
|
||||||
|
'model': 'gpt-3.5-turbo',
|
||||||
|
'messages': [
|
||||||
|
{
|
||||||
|
'role': 'user',
|
||||||
|
'content': '1+1='
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
with open('config/credits.yml', encoding='utf8') as f:
|
||||||
|
max_credits = yaml.safe_load(f)['max-credits']
|
||||||
|
|
||||||
|
async def stream(
|
||||||
|
path: str='/v1/chat/completions',
|
||||||
|
user: dict=None,
|
||||||
|
payload: dict=None,
|
||||||
|
credits_cost: int=0,
|
||||||
|
demo_mode: bool=False,
|
||||||
|
input_tokens: int=0,
|
||||||
|
incoming_request: starlette.requests.Request=None,
|
||||||
|
):
|
||||||
|
payload = payload or DEMO_PAYLOAD
|
||||||
|
|
||||||
|
if 'chat/completions' in path: # is a chat endpoint
|
||||||
|
target_request = await load_balancing.balance_chat_request(payload)
|
||||||
|
else:
|
||||||
|
target_request = await load_balancing.balance_organic_request(payload)
|
||||||
|
|
||||||
|
headers = {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
|
||||||
|
for k, v in target_request.get('headers', {}).items():
|
||||||
|
headers[k] = v
|
||||||
|
|
||||||
|
for _ in range(5):
|
||||||
|
async with aiohttp.ClientSession(connector=proxies.default_proxy.connector) as session:
|
||||||
|
async with session.request(
|
||||||
|
method=target_request.get('method', 'POST'),
|
||||||
|
url=target_request['url'],
|
||||||
|
|
||||||
|
data=target_request.get('data'),
|
||||||
|
json=target_request.get('payload'),
|
||||||
|
|
||||||
|
headers=headers,
|
||||||
|
cookies=target_request.get('cookies'),
|
||||||
|
|
||||||
|
ssl=False,
|
||||||
|
|
||||||
|
timeout=aiohttp.ClientTimeout(total=float(os.getenv('TRANSFER_TIMEOUT', '120'))),
|
||||||
|
) as response:
|
||||||
|
try:
|
||||||
|
await response.raise_for_status()
|
||||||
|
except Exception as exc:
|
||||||
|
if 'Too Many Requests' in str(exc):
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
|
||||||
|
if user and incoming_request:
|
||||||
|
await logs.log_api_request(
|
||||||
|
user=user,
|
||||||
|
incoming_request=incoming_request,
|
||||||
|
target_url=target_request['url']
|
||||||
|
)
|
||||||
|
|
||||||
|
if credits_cost and user:
|
||||||
|
await users.update_by_id(user['_id'], {
|
||||||
|
'$inc': {'credits': -credits_cost}
|
||||||
|
})
|
||||||
|
|
||||||
|
if not demo_mode:
|
||||||
|
await stats.add_date()
|
||||||
|
await stats.add_ip_address(network.get_ip(incoming_request))
|
||||||
|
await stats.add_model(payload.get('model', '_non-chat'))
|
||||||
|
await stats.add_path(path)
|
||||||
|
await stats.add_target(target_request['url'])
|
||||||
|
await stats.add_tokens(input_tokens)
|
||||||
|
|
||||||
|
async for chunk in response.content.iter_chunks():
|
||||||
|
# chunk = f'{chunk.decode("utf8")}\n\n'
|
||||||
|
|
||||||
|
if demo_mode:
|
||||||
|
print(chunk)
|
||||||
|
|
||||||
|
yield chunk
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
asyncio.run(stream())
|
|
@ -2,13 +2,13 @@
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import json
|
import json
|
||||||
|
import yaml
|
||||||
import logging
|
import logging
|
||||||
import starlette
|
import starlette
|
||||||
|
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
import netclient
|
import streaming
|
||||||
import chat_balancing
|
|
||||||
|
|
||||||
from db import logs, users
|
from db import logs, users
|
||||||
from helpers import tokens, errors, exceptions
|
from helpers import tokens, errors, exceptions
|
||||||
|
@ -24,35 +24,41 @@ logging.basicConfig(
|
||||||
|
|
||||||
logging.info('API started')
|
logging.info('API started')
|
||||||
|
|
||||||
|
with open('config/credits.yml', encoding='utf8') as f:
|
||||||
|
credits_config = yaml.safe_load(f)
|
||||||
|
|
||||||
async def handle(incoming_request):
|
async def handle(incoming_request):
|
||||||
"""Transfer a streaming response from the incoming request to the target endpoint"""
|
"""Transfer a streaming response from the incoming request to the target endpoint"""
|
||||||
|
|
||||||
path = incoming_request.url.path
|
path = incoming_request.url.path
|
||||||
|
|
||||||
|
# METHOD
|
||||||
if incoming_request.method not in ['GET', 'POST', 'PUT', 'DELETE', 'PATCH']:
|
if incoming_request.method not in ['GET', 'POST', 'PUT', 'DELETE', 'PATCH']:
|
||||||
return errors.error(405, f'Method "{incoming_request.method}" is not allowed.', 'Change the request method to the correct one.')
|
return errors.error(405, f'Method "{incoming_request.method}" is not allowed.', 'Change the request method to the correct one.')
|
||||||
|
|
||||||
|
# PAYLOAD
|
||||||
try:
|
try:
|
||||||
payload = await incoming_request.json()
|
payload = await incoming_request.json()
|
||||||
except json.decoder.JSONDecodeError:
|
except json.decoder.JSONDecodeError:
|
||||||
payload = {}
|
payload = {}
|
||||||
|
|
||||||
|
# TOKENS
|
||||||
try:
|
try:
|
||||||
input_tokens = tokens.count_for_messages(payload['messages'])
|
input_tokens = tokens.count_for_messages(payload['messages'])
|
||||||
except (KeyError, TypeError):
|
except (KeyError, TypeError):
|
||||||
input_tokens = 0
|
input_tokens = 0
|
||||||
|
|
||||||
auth_header = incoming_request.headers.get('Authorization')
|
# AUTH
|
||||||
|
received_key = incoming_request.headers.get('Authorization')
|
||||||
|
|
||||||
if not auth_header:
|
if not received_key:
|
||||||
return errors.error(401, 'No NovaAI API key given!', 'Add "Authorization: Bearer nv-..." to your request headers.')
|
return errors.error(401, 'No NovaAI API key given!', 'Add "Authorization: Bearer nv-..." to your request headers.')
|
||||||
|
|
||||||
received_key = auth_header
|
if received_key.startswith('Bearer '):
|
||||||
|
received_key = received_key.split('Bearer ')[1]
|
||||||
|
|
||||||
if auth_header.startswith('Bearer '):
|
# USER
|
||||||
received_key = auth_header.split('Bearer ')[1]
|
user = await users.by_api_key(received_key.strip())
|
||||||
|
|
||||||
user = await users.by_api_key(received_key)
|
|
||||||
|
|
||||||
if not user:
|
if not user:
|
||||||
return errors.error(401, 'Invalid NovaAI API key!', 'Create a new NovaOSS API key.')
|
return errors.error(401, 'Invalid NovaAI API key!', 'Create a new NovaOSS API key.')
|
||||||
|
@ -64,31 +70,36 @@ async def handle(incoming_request):
|
||||||
if not user['status']['active']:
|
if not user['status']['active']:
|
||||||
return errors.error(418, 'Your NovaAI account is not active (paused).', 'Simply re-activate your account using a Discord command or the web panel.')
|
return errors.error(418, 'Your NovaAI account is not active (paused).', 'Simply re-activate your account using a Discord command or the web panel.')
|
||||||
|
|
||||||
|
# COST
|
||||||
|
costs = credits_config['costs']
|
||||||
|
cost = costs['other']
|
||||||
|
|
||||||
|
if 'chat/completions' in path:
|
||||||
|
for model_name, model_cost in costs['chat-models'].items():
|
||||||
|
if model_name in payload['model']:
|
||||||
|
cost = model_cost
|
||||||
|
|
||||||
|
role_cost_multiplier = credits_config['bonuses'].get(user['role'], 1)
|
||||||
|
cost = round(cost * role_cost_multiplier)
|
||||||
|
|
||||||
|
if user['credits'] < cost:
|
||||||
|
return errors.error(429, 'Not enough credits.', 'Wait or earn more credits. Learn more on our website or Discord server.')
|
||||||
|
|
||||||
|
# READY
|
||||||
|
|
||||||
payload['user'] = str(user['_id'])
|
payload['user'] = str(user['_id'])
|
||||||
|
|
||||||
cost = 1
|
|
||||||
|
|
||||||
if '/chat/completions' in path:
|
|
||||||
cost = 5
|
|
||||||
|
|
||||||
if 'gpt-4' in payload['model']:
|
|
||||||
cost = 10
|
|
||||||
|
|
||||||
else:
|
|
||||||
return errors.error(404, f'Sorry, we don\'t support "{path}" yet. We\'re working on it.', 'Contact our team.')
|
|
||||||
|
|
||||||
if not payload.get('stream') is True:
|
if not payload.get('stream') is True:
|
||||||
payload['stream'] = False
|
payload['stream'] = False
|
||||||
|
|
||||||
if user['credits'] < cost:
|
return starlette.responses.StreamingResponse(
|
||||||
return errors.error(429, 'Not enough credits.', 'You do not have enough credits to complete this request.')
|
content=streaming.stream(
|
||||||
|
user=user,
|
||||||
await users.update_by_id(user['_id'], {'$inc': {'credits': -cost}})
|
path=path,
|
||||||
|
payload=payload,
|
||||||
target_request = await chat_balancing.balance(payload)
|
credits_cost=cost,
|
||||||
|
input_tokens=input_tokens,
|
||||||
print(target_request['url'])
|
incoming_request=incoming_request,
|
||||||
|
),
|
||||||
return errors.error(500, 'Sorry, the API is currenly under maintainance.', 'Please try again later.')
|
media_type='text/event-stream'
|
||||||
|
)
|
||||||
return starlette.responses.StreamingResponse(netclient.stream(target_request))
|
|
||||||
|
|
|
@ -52,7 +52,7 @@ def test_api(model: str=MODEL, messages: List[dict]=None) -> dict:
|
||||||
}
|
}
|
||||||
|
|
||||||
response = httpx.post(
|
response = httpx.post(
|
||||||
url=f'{api_endpoint}/chat/completions',
|
url=f'{api_endpoint}/v1/chat/completions',
|
||||||
headers=headers,
|
headers=headers,
|
||||||
json=json_data,
|
json=json_data,
|
||||||
timeout=20
|
timeout=20
|
||||||
|
@ -87,6 +87,7 @@ def test_all():
|
||||||
print(test_library())
|
print(test_library())
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
api_endpoint = 'https://api.nova-oss.com'
|
# api_endpoint = 'https://api.nova-oss.com'
|
||||||
|
api_endpoint = 'http://localhost:2332'
|
||||||
api_key = os.getenv('TEST_NOVA_KEY')
|
api_key = os.getenv('TEST_NOVA_KEY')
|
||||||
test_all()
|
test_all()
|
||||||
|
|
Loading…
Reference in a new issue