Compare commits

...

4 commits

Author SHA1 Message Date
nsde 169f5469a9 Merge branch 'main' of https://github.com/Luna-OSS/nova-api 2023-10-09 19:09:02 +02:00
nsde d52aadd034 some stuff 2023-10-09 19:09:01 +02:00
nsde a3b94d45b7
Merge pull request #17 from monosans/patch-1
Refactor file operations
2023-10-09 15:08:45 +02:00
monosans ade7244cea
Refactor file operations 2023-10-09 09:02:09 +03:00
22 changed files with 96 additions and 79 deletions

View file

@ -167,7 +167,7 @@ You can also just add the *beginning* of an API address, like `12.123.` (without
### Core Keys ### Core Keys
`CORE_API_KEY` specifies the **very secret key** for which need to access the entire user database etc. `CORE_API_KEY` specifies the **very secret key** for which need to access the entire user database etc.
`TEST_NOVA_KEY` is the API key the which is used in tests. It should be one with tons of credits. `NOVA_KEY` is the API key the which is used in tests. It should be one with tons of credits.
### Webhooks ### Webhooks
`DISCORD_WEBHOOK__USER_CREATED` is the Discord webhook URL for when a user is created. `DISCORD_WEBHOOK__USER_CREATED` is the Discord webhook URL for when a user is created.

View file

@ -1,6 +1,8 @@
import os import os
import json import json
import asyncio import asyncio
import aiofiles
import aiofiles.os
from sys import argv from sys import argv
from bson import json_util from bson import json_util
@ -18,8 +20,7 @@ async def main(output_dir: str):
async def make_backup(output_dir: str): async def make_backup(output_dir: str):
output_dir = os.path.join(FILE_DIR, '..', 'backups', output_dir) output_dir = os.path.join(FILE_DIR, '..', 'backups', output_dir)
if not os.path.exists(output_dir): await aiofiles.os.makedirs(output_dir, exist_ok=True)
os.makedirs(output_dir)
client = AsyncIOMotorClient(MONGO_URI) client = AsyncIOMotorClient(MONGO_URI)
databases = await client.list_database_names() databases = await client.list_database_names()
@ -29,22 +30,22 @@ async def make_backup(output_dir: str):
if database == 'local': if database == 'local':
continue continue
if not os.path.exists(f'{output_dir}/{database}'): await aiofiles.os.makedirs(os.path.join(output_dir, database), exist_ok=True)
os.mkdir(f'{output_dir}/{database}')
for collection in databases[database]: for collection in databases[database]:
print(f'Initiated database backup for {database}/{collection}') print(f'Initiated database backup for {database}/{collection}')
await make_backup_for_collection(database, collection, output_dir) await make_backup_for_collection(database, collection, output_dir)
async def make_backup_for_collection(database, collection, output_dir): async def make_backup_for_collection(database, collection, output_dir):
path = f'{output_dir}/{database}/{collection}.json' path = os.path.join(output_dir, database, f'{collection}.json')
client = AsyncIOMotorClient(MONGO_URI) client = AsyncIOMotorClient(MONGO_URI)
collection = client[database][collection] collection = client[database][collection]
documents = await collection.find({}).to_list(length=None) documents = await collection.find({}).to_list(length=None)
with open(path, 'w') as f: async with aiofiles.open(path, 'w') as f:
json.dump(documents, f, default=json_util.default) for chunk in json.JSONEncoder(default=json_util.default).iterencode(documents):
await f.write(chunk)
if __name__ == '__main__': if __name__ == '__main__':
if len(argv) < 2 or len(argv) > 2: if len(argv) < 2 or len(argv) > 2:

View file

@ -13,6 +13,7 @@ import json
import hmac import hmac
import httpx import httpx
import fastapi import fastapi
import aiofiles
import functools import functools
from dhooks import Webhook, Embed from dhooks import Webhook, Embed
@ -130,7 +131,7 @@ async def run_checks(incoming_request: fastapi.Request):
checks.client.test_chat_non_stream_gpt4, checks.client.test_chat_non_stream_gpt4,
checks.client.test_chat_stream_gpt3, checks.client.test_chat_stream_gpt3,
checks.client.test_function_calling, checks.client.test_function_calling,
checks.client.test_image_generation, # checks.client.test_image_generation,
# checks.client.test_speech_to_text, # checks.client.test_speech_to_text,
checks.client.test_models checks.client.test_models
] ]
@ -148,11 +149,14 @@ async def run_checks(incoming_request: fastapi.Request):
async def get_crypto_price(cryptocurrency: str) -> float: async def get_crypto_price(cryptocurrency: str) -> float:
"""Gets the price of a cryptocurrency using coinbase's API.""" """Gets the price of a cryptocurrency using coinbase's API."""
if os.path.exists('cache/crypto_prices.json'): cache_path = os.path.join('cache', 'crypto_prices.json')
with open('cache/crypto_prices.json', 'r') as f: try:
cache = json.load(f) async with aiofiles.open(cache_path) as f:
else: content = await f.read()
except FileNotFoundError:
cache = {} cache = {}
else:
cache = json.loads(content)
is_old = time.time() - cache.get('_last_updated', 0) > 60 * 60 is_old = time.time() - cache.get('_last_updated', 0) > 60 * 60
@ -164,8 +168,9 @@ async def get_crypto_price(cryptocurrency: str) -> float:
cache[cryptocurrency] = usd_price cache[cryptocurrency] = usd_price
cache['_last_updated'] = time.time() cache['_last_updated'] = time.time()
with open('cache/crypto_prices.json', 'w') as f: async with aiofiles.open(cache_path, 'w') as f:
json.dump(cache, f) for chunk in json.JSONEncoder().iterencode(cache):
await f.write(chunk)
return cache[cryptocurrency] return cache[cryptocurrency]

View file

@ -3,6 +3,8 @@ import time
import random import random
import asyncio import asyncio
import aiofiles
import aiofiles.os
from aiocache import cached from aiocache import cached
from dotenv import load_dotenv from dotenv import load_dotenv
from cachetools import TTLCache from cachetools import TTLCache
@ -72,10 +74,10 @@ class KeyManager:
db = await self._get_collection('providerkeys') db = await self._get_collection('providerkeys')
num = 0 num = 0
for filename in os.listdir('api/secret'): for filename in await aiofiles.os.listdir(os.path.join('api', 'secret')):
if filename.endswith('.txt'): if filename.endswith('.txt'):
with open(f'api/secret/{filename}') as f: async with aiofiles.open(os.path.join('api', 'secret', filename)) as f:
for line in f.readlines(): async for line in f:
if not line.strip(): if not line.strip():
continue continue

View file

@ -14,7 +14,7 @@ except ImportError:
load_dotenv() load_dotenv()
with open(helpers.root + '/api/config/config.yml', encoding='utf8') as f: with open(os.path.join(helpers.root, 'api', 'config', 'config.yml'), encoding='utf8') as f:
credits_config = yaml.safe_load(f) credits_config = yaml.safe_load(f)
## MONGODB Setup ## MONGODB Setup

View file

@ -19,10 +19,11 @@ from helpers import tokens, errors, network
load_dotenv() load_dotenv()
users = UserManager() users = UserManager()
models_list = json.load(open('cache/models.json', encoding='utf8')) with open(os.path.join('cache', 'models.json'), encoding='utf8') as f:
models_list = json.load(f)
models = [model['id'] for model in models_list['data']] models = [model['id'] for model in models_list['data']]
with open('config/config.yml', encoding='utf8') as f: with open(os.path.join('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') moderation_debug_key_key = os.getenv('MODERATION_DEBUG_KEY')
@ -38,7 +39,10 @@ async def handle(incoming_request: fastapi.Request):
ip_address = await network.get_ip(incoming_request) ip_address = await network.get_ip(incoming_request)
if '/models' in path: if '/dashboard' in path:
return errors.error(404, 'You can\'t access /dashboard.', 'This is a private endpoint.')
if path.startswith('/v1/models'):
return fastapi.responses.JSONResponse(content=models_list) return fastapi.responses.JSONResponse(content=models_list)
try: try:
@ -94,7 +98,6 @@ async def handle(incoming_request: fastapi.Request):
if user['credits'] < cost: if user['credits'] < cost:
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 'DISABLE_VARS' not in key_tags: if 'DISABLE_VARS' not in key_tags:
payload_with_vars = json.dumps(payload) payload_with_vars = json.dumps(payload)

View file

@ -7,10 +7,10 @@ from rich import print
from dotenv import load_dotenv from dotenv import load_dotenv
from bson.objectid import ObjectId from bson.objectid import ObjectId
from fastapi.middleware.cors import CORSMiddleware
from slowapi.errors import RateLimitExceeded from slowapi.errors import RateLimitExceeded
from slowapi.middleware import SlowAPIMiddleware from slowapi.middleware import SlowAPIMiddleware
from fastapi.middleware.cors import CORSMiddleware
from slowapi.util import get_remote_address from slowapi.util import get_remote_address
from slowapi import Limiter, _rate_limit_exceeded_handler from slowapi import Limiter, _rate_limit_exceeded_handler

View file

@ -1,12 +1,2 @@
from . import \ from . import azure, webraft
azure \ MODULES = [azure, webraft]
# closed, \
# closed4
# closed432
MODULES = [
azure,
# closed,
# closed4,
# closed432,
]

View file

@ -3,14 +3,13 @@ import sys
import aiohttp import aiohttp
import asyncio import asyncio
import importlib import importlib
import aiofiles.os
from rich import print from rich import print
def remove_duplicate_keys(file): def remove_duplicate_keys(file):
with open(file, 'r', encoding='utf8') as f: with open(file, 'r', encoding='utf8') as f:
lines = f.readlines() unique_lines = set(f)
unique_lines = set(lines)
with open(file, 'w', encoding='utf8') as f: with open(file, 'w', encoding='utf8') as f:
f.writelines(unique_lines) f.writelines(unique_lines)
@ -22,7 +21,7 @@ async def main():
except IndexError: except IndexError:
print('List of available providers:') print('List of available providers:')
for file_name in os.listdir(os.path.dirname(__file__)): for file_name in await aiofiles.os.listdir(os.path.dirname(__file__)):
if file_name.endswith('.py') and not file_name.startswith('_'): if file_name.endswith('.py') and not file_name.startswith('_'):
print(file_name.split('.')[0]) print(file_name.split('.')[0])

View file

@ -2,7 +2,6 @@ from .helpers import utils
AUTH = True AUTH = True
ORGANIC = False ORGANIC = False
CONTEXT = True
STREAMING = True STREAMING = True
MODERATIONS = False MODERATIONS = False
ENDPOINT = 'https://nova-00001.openai.azure.com' ENDPOINT = 'https://nova-00001.openai.azure.com'
@ -12,7 +11,7 @@ MODELS = [
'gpt-4', 'gpt-4',
'gpt-4-32k' 'gpt-4-32k'
] ]
# MODELS = [f'{model}-azure' for model in MODELS] MODELS += [f'{model}-azure' for model in MODELS]
AZURE_API = '2023-08-01-preview' AZURE_API = '2023-08-01-preview'

View file

@ -2,7 +2,6 @@ from .helpers import utils
AUTH = True AUTH = True
ORGANIC = True ORGANIC = True
CONTEXT = True
STREAMING = True STREAMING = True
MODERATIONS = True MODERATIONS = True
ENDPOINT = 'https://api.openai.com' ENDPOINT = 'https://api.openai.com'

View file

@ -2,7 +2,6 @@ from .helpers import utils
AUTH = True AUTH = True
ORGANIC = False ORGANIC = False
CONTEXT = True
STREAMING = True STREAMING = True
MODERATIONS = True MODERATIONS = True
ENDPOINT = 'https://api.openai.com' ENDPOINT = 'https://api.openai.com'

View file

@ -2,7 +2,6 @@ from .helpers import utils
AUTH = True AUTH = True
ORGANIC = False ORGANIC = False
CONTEXT = True
STREAMING = True STREAMING = True
MODERATIONS = False MODERATIONS = False
ENDPOINT = 'https://api.openai.com' ENDPOINT = 'https://api.openai.com'

View file

@ -23,15 +23,5 @@ GPT_4_32K = GPT_4 + [
'gpt-4-32k-0613', 'gpt-4-32k-0613',
] ]
async def conversation_to_prompt(conversation: list) -> str:
text = ''
for message in conversation:
text += f'<|{message["role"]}|>: {message["content"]}\n'
text += '<|assistant|>:'
return text
async def random_secret_for(name: str) -> str: async def random_secret_for(name: str) -> str:
return await providerkeys.manager.get_key(name) return await providerkeys.manager.get_key(name)

View file

@ -2,7 +2,6 @@ from .helpers import utils
AUTH = True AUTH = True
ORGANIC = False ORGANIC = False
CONTEXT = True
STREAMING = True STREAMING = True
MODELS = ['llama-2-7b-chat'] MODELS = ['llama-2-7b-chat']
@ -12,7 +11,7 @@ async def chat_completion(**kwargs):
return { return {
'method': 'POST', 'method': 'POST',
'url': f'https://api.mandrillai.tech/v1/chat/completions', 'url': 'https://api.mandrillai.tech/v1/chat/completions',
'payload': payload, 'payload': payload,
'headers': { 'headers': {
'Authorization': f'Bearer {key}' 'Authorization': f'Bearer {key}'

25
api/providers/webraft.py Normal file
View file

@ -0,0 +1,25 @@
from .helpers import utils
AUTH = True
ORGANIC = False
STREAMING = True
MODELS = [
'gpt-3.5-turbo-0613',
'gpt-3.5-turbo-0301',
'gpt-3.5-turbo-16k-0613'
]
async def chat_completion(**kwargs):
payload = kwargs
key = await utils.random_secret_for('webraft')
return {
'method': 'POST',
'url': 'https://thirdparty.webraft.in/v1/chat/completions',
'payload': payload,
'headers': {
'Content-Type': 'application/json',
'Authorization': f'Bearer {key}'
},
'provider_auth': f'webraft>{key}'
}

View file

@ -96,7 +96,7 @@ proxies_in_files = []
for proxy_type in ['http', 'socks4', 'socks5']: for proxy_type in ['http', 'socks4', 'socks5']:
try: try:
with open(f'secret/proxies/{proxy_type}.txt') as f: with open(os.path.join('secret', 'proxies', f'{proxy_type}.txt')) as f:
for line in f: for line in f:
clean_line = line.split('#', 1)[0].strip() clean_line = line.split('#', 1)[0].strip()
if clean_line: if clean_line:

View file

@ -63,7 +63,14 @@ async def respond(
'Content-Type': 'application/json' 'Content-Type': 'application/json'
} }
for i in range(5): skipped_errors = {
'insufficient_quota': 0,
'billing_not_active': 0,
'critical_provider_error': 0,
'timeout': 0
}
for _ in range(5):
try: try:
if is_chat: if is_chat:
target_request = await load_balancing.balance_chat_request(payload) target_request = await load_balancing.balance_chat_request(payload)
@ -130,11 +137,13 @@ async def respond(
if error_code == 'insufficient_quota': if error_code == 'insufficient_quota':
print('[!] insufficient quota') print('[!] insufficient quota')
await keymanager.rate_limit_key(provider_name, provider_key, 86400) await keymanager.rate_limit_key(provider_name, provider_key, 86400)
skipped_errors['insufficient_quota'] += 1
continue continue
if error_code == 'billing_not_active': if error_code == 'billing_not_active':
print('[!] billing not active') print('[!] billing not active')
await keymanager.deactivate_key(provider_name, provider_key, 'billing_not_active') await keymanager.deactivate_key(provider_name, provider_key, 'billing_not_active')
skipped_errors['billing_not_active'] += 1
continue continue
critical_error = False critical_error = False
@ -144,23 +153,14 @@ async def respond(
critical_error = True critical_error = True
if critical_error: if critical_error:
print('[!] critical error') print('[!] critical provider error')
skipped_errors['critical_provider_error'] += 1
continue continue
if response.ok: if response.ok:
server_json_response = client_json_response server_json_response = client_json_response
else:
continue
if is_stream: if is_stream:
try:
response.raise_for_status()
except Exception as exc:
if 'Too Many Requests' in str(exc):
print('[!] too many requests')
continue
chunk_no = 0 chunk_no = 0
buffer = '' buffer = ''
@ -170,7 +170,7 @@ async def respond(
chunk = chunk.decode('utf8') chunk = chunk.decode('utf8')
if 'azure' in provider_name: if 'azure' in provider_name:
chunk = chunk.replace('data: ', '') chunk = chunk.replace('data: ', '', 1)
if not chunk or chunk_no == 1: if not chunk or chunk_no == 1:
continue continue
@ -178,19 +178,26 @@ async def respond(
subchunks = chunk.split('\n\n') subchunks = chunk.split('\n\n')
buffer += subchunks[0] buffer += subchunks[0]
yield buffer + '\n\n' for subchunk in [buffer] + subchunks[1:-1]:
buffer = subchunks[-1] if not subchunk.startswith('data: '):
subchunk = 'data: ' + subchunk
for subchunk in subchunks[1:-1]:
yield subchunk + '\n\n' yield subchunk + '\n\n'
buffer = subchunks[-1]
break break
except aiohttp.client_exceptions.ServerTimeoutError: except aiohttp.client_exceptions.ServerTimeoutError:
skipped_errors['timeout'] += 1
continue continue
else: else:
yield await errors.yield_error(500, 'Sorry, our API seems to have issues connecting to our provider(s).', 'This most likely isn\'t your fault. Please try again later.') skipped_errors = {k: v for k, v in skipped_errors.items() if v > 0}
skipped_errors = ujson.dumps(skipped_errors, indent=4)
yield await errors.yield_error(500,
'Sorry, our API seems to have issues connecting to our provider(s).',
f'Please send this info to support: {skipped_errors}'
)
return return
if (not is_stream) and server_json_response: if (not is_stream) and server_json_response:

View file

@ -100,7 +100,7 @@ async def test_chat_stream_gpt3() -> float:
async for chunk in response.aiter_text(): async for chunk in response.aiter_text():
for subchunk in chunk.split('\n\n'): for subchunk in chunk.split('\n\n'):
chunk = subchunk.replace('data: ', '').strip() chunk = subchunk.replace('data: ', '', 1).strip()
if chunk == '[DONE]': if chunk == '[DONE]':
break break

View file

@ -1,3 +1,4 @@
aiofiles==23.2.1
aiohttp==3.8.5 aiohttp==3.8.5
aiohttp_socks==0.8.0 aiohttp_socks==0.8.0
dhooks==1.1.4 dhooks==1.1.4

View file

@ -51,7 +51,7 @@ async def update_roles():
def launch(): def launch():
asyncio.run(main()) asyncio.run(main())
with open('rewards/last_update.txt', 'w', encoding='utf8') as f: with open(os.path.join('rewards', 'last_update.txt'), 'w', encoding='utf8') as f:
f.write(str(time.time())) f.write(str(time.time()))
if __name__ == '__main__': if __name__ == '__main__':

View file

@ -14,7 +14,6 @@ Runs for production on the speicified port.
import os import os
import sys import sys
import time
port = sys.argv[1] if len(sys.argv) > 1 else 2332 port = sys.argv[1] if len(sys.argv) > 1 else 2332
dev = True dev = True