mirror of
https://github.com/NovaOSS/nova-api.git
synced 2024-11-25 21:43:57 +01:00
Compare commits
2 commits
01aa41b6b1
...
21331874db
Author | SHA1 | Date | |
---|---|---|---|
21331874db | |||
6ef8441681 |
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -22,9 +22,6 @@ last_update.txt
|
||||||
.log
|
.log
|
||||||
*.log.*
|
*.log.*
|
||||||
|
|
||||||
providers/*
|
|
||||||
providers/
|
|
||||||
|
|
||||||
secret/*
|
secret/*
|
||||||
secret/
|
secret/
|
||||||
/secret
|
/secret
|
||||||
|
|
|
@ -12,7 +12,7 @@ class KeyManager:
|
||||||
self.conn = AsyncIOMotorClient(os.environ['MONGO_URI'])
|
self.conn = AsyncIOMotorClient(os.environ['MONGO_URI'])
|
||||||
|
|
||||||
async def _get_collection(self, collection_name: str):
|
async def _get_collection(self, collection_name: str):
|
||||||
return self.conn[os.getenv('MONGO_NAME', 'nova-test')][collection_name]
|
return self.conn['nova-core'][collection_name]
|
||||||
|
|
||||||
async def add_key(self, provider: str, key: str, source: str='?'):
|
async def add_key(self, provider: str, key: str, source: str='?'):
|
||||||
db = await self._get_collection('providerkeys')
|
db = await self._get_collection('providerkeys')
|
||||||
|
@ -36,7 +36,7 @@ class KeyManager:
|
||||||
})
|
})
|
||||||
|
|
||||||
if key is None:
|
if key is None:
|
||||||
return ValueError('No keys available for this provider!')
|
return '--NO_KEY--'
|
||||||
|
|
||||||
return key['key']
|
return key['key']
|
||||||
|
|
||||||
|
@ -87,4 +87,4 @@ class KeyManager:
|
||||||
manager = KeyManager()
|
manager = KeyManager()
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
asyncio.run(manager.delete_empty_keys())
|
asyncio.run(manager.import_all())
|
||||||
|
|
|
@ -8,8 +8,7 @@ async def error(code: int, message: str, tip: str) -> starlette.responses.Respon
|
||||||
'code': code,
|
'code': code,
|
||||||
'message': message,
|
'message': message,
|
||||||
'tip': tip,
|
'tip': tip,
|
||||||
'website': 'https://nova-oss.com',
|
'powered_by': 'nova-api'
|
||||||
'by': 'NovaOSS/Nova-API'
|
|
||||||
}}
|
}}
|
||||||
|
|
||||||
return starlette.responses.Response(status_code=code, content=json.dumps(info))
|
return starlette.responses.Response(status_code=code, content=json.dumps(info))
|
||||||
|
@ -20,5 +19,6 @@ async def yield_error(code: int, message: str, tip: str) -> str:
|
||||||
return json.dumps({
|
return json.dumps({
|
||||||
'code': code,
|
'code': code,
|
||||||
'message': message,
|
'message': message,
|
||||||
'tip': tip
|
'tip': tip,
|
||||||
|
'powered_by': 'nova-api'
|
||||||
})
|
})
|
||||||
|
|
|
@ -35,9 +35,9 @@ limiter = Limiter(
|
||||||
swallow_errors=True,
|
swallow_errors=True,
|
||||||
key_func=get_remote_address,
|
key_func=get_remote_address,
|
||||||
default_limits=[
|
default_limits=[
|
||||||
'1/second',
|
'2/second',
|
||||||
'20/minute',
|
'30/minute',
|
||||||
'300/hour'
|
'400/hour'
|
||||||
])
|
])
|
||||||
|
|
||||||
app.state.limiter = limiter
|
app.state.limiter = limiter
|
||||||
|
|
10
api/providers/__init__.py
Normal file
10
api/providers/__init__.py
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
from . import \
|
||||||
|
closed, \
|
||||||
|
closed4
|
||||||
|
# closed432
|
||||||
|
|
||||||
|
MODULES = [
|
||||||
|
closed,
|
||||||
|
closed4,
|
||||||
|
# closed432,
|
||||||
|
]
|
52
api/providers/__main__.py
Normal file
52
api/providers/__main__.py
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from rich import print
|
||||||
|
|
||||||
|
def remove_duplicate_keys(file):
|
||||||
|
with open(file, 'r', encoding='utf8') as f:
|
||||||
|
lines = f.readlines()
|
||||||
|
|
||||||
|
unique_lines = set(lines)
|
||||||
|
|
||||||
|
with open(file, 'w', encoding='utf8') as f:
|
||||||
|
f.writelines(unique_lines)
|
||||||
|
|
||||||
|
try:
|
||||||
|
provider_name = sys.argv[1]
|
||||||
|
|
||||||
|
if provider_name == '--clear':
|
||||||
|
for file in os.listdir('secret/'):
|
||||||
|
if file.endswith('.txt'):
|
||||||
|
remove_duplicate_keys(f'secret/{file}')
|
||||||
|
|
||||||
|
exit()
|
||||||
|
|
||||||
|
except IndexError:
|
||||||
|
print('List of available providers:')
|
||||||
|
|
||||||
|
for file_name in os.listdir(os.path.dirname(__file__)):
|
||||||
|
if file_name.endswith('.py') and not file_name.startswith('_'):
|
||||||
|
print(file_name.split('.')[0])
|
||||||
|
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
try:
|
||||||
|
provider = __import__(provider_name)
|
||||||
|
except ModuleNotFoundError as exc:
|
||||||
|
print(f'Provider "{provider_name}" not found.')
|
||||||
|
print('Available providers:')
|
||||||
|
for file_name in os.listdir(os.path.dirname(__file__)):
|
||||||
|
if file_name.endswith('.py') and not file_name.startswith('_'):
|
||||||
|
print(file_name.split('.')[0])
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
if len(sys.argv) > 2:
|
||||||
|
model = sys.argv[2]
|
||||||
|
else:
|
||||||
|
model = provider.MODELS[-1]
|
||||||
|
|
||||||
|
|
||||||
|
print(f'{provider_name} @ {model}')
|
||||||
|
comp = provider.chat_completion(model=model)
|
||||||
|
print(comp)
|
35
api/providers/closed.py
Normal file
35
api/providers/closed.py
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
from .helpers import utils
|
||||||
|
|
||||||
|
AUTH = True
|
||||||
|
ORGANIC = True
|
||||||
|
CONTEXT = True
|
||||||
|
STREAMING = True
|
||||||
|
MODERATIONS = True
|
||||||
|
ENDPOINT = 'https://api.openai.com'
|
||||||
|
MODELS = utils.GPT_3
|
||||||
|
|
||||||
|
async def get_key() -> str:
|
||||||
|
return await utils.random_secret_for('closed')
|
||||||
|
|
||||||
|
async def chat_completion(**kwargs):
|
||||||
|
payload = kwargs
|
||||||
|
key = await get_key()
|
||||||
|
|
||||||
|
return {
|
||||||
|
'method': 'POST',
|
||||||
|
'url': f'{ENDPOINT}/v1/chat/completions',
|
||||||
|
'payload': payload,
|
||||||
|
'headers': {
|
||||||
|
'Authorization': f'Bearer {key}'
|
||||||
|
},
|
||||||
|
'provider_auth': f'closed>{key}'
|
||||||
|
}
|
||||||
|
|
||||||
|
async def organify(request: dict) -> dict:
|
||||||
|
key = await get_key()
|
||||||
|
|
||||||
|
request['url'] = ENDPOINT + request['path']
|
||||||
|
request['headers']['Authorization'] = f'Bearer {key}'
|
||||||
|
request['provider_auth'] = f'closed>{key}'
|
||||||
|
|
||||||
|
return request
|
35
api/providers/closed4.py
Normal file
35
api/providers/closed4.py
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
from .helpers import utils
|
||||||
|
|
||||||
|
AUTH = True
|
||||||
|
ORGANIC = False
|
||||||
|
CONTEXT = True
|
||||||
|
STREAMING = True
|
||||||
|
MODERATIONS = True
|
||||||
|
ENDPOINT = 'https://api.openai.com'
|
||||||
|
MODELS = utils.GPT_4
|
||||||
|
|
||||||
|
async def get_key() -> str:
|
||||||
|
return await utils.random_secret_for('closed4')
|
||||||
|
|
||||||
|
async def chat_completion(**kwargs):
|
||||||
|
payload = kwargs
|
||||||
|
key = await get_key()
|
||||||
|
|
||||||
|
return {
|
||||||
|
'method': 'POST',
|
||||||
|
'url': f'{ENDPOINT}/v1/chat/completions',
|
||||||
|
'payload': payload,
|
||||||
|
'headers': {
|
||||||
|
'Authorization': f'Bearer {key}'
|
||||||
|
},
|
||||||
|
'provider_auth': f'closed4>{key}'
|
||||||
|
}
|
||||||
|
|
||||||
|
async def organify(request: dict) -> dict:
|
||||||
|
key = await get_key()
|
||||||
|
|
||||||
|
request['url'] = ENDPOINT + request['path']
|
||||||
|
request['headers']['Authorization'] = f'Bearer {key}'
|
||||||
|
request['provider_auth'] = f'closed4>{key}'
|
||||||
|
|
||||||
|
return request
|
35
api/providers/closed432.py
Normal file
35
api/providers/closed432.py
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
from .helpers import utils
|
||||||
|
|
||||||
|
AUTH = True
|
||||||
|
ORGANIC = False
|
||||||
|
CONTEXT = True
|
||||||
|
STREAMING = True
|
||||||
|
MODERATIONS = False
|
||||||
|
ENDPOINT = 'https://api.openai.com'
|
||||||
|
MODELS = utils.GPT_4_32K
|
||||||
|
|
||||||
|
async def get_key() -> str:
|
||||||
|
return await utils.random_secret_for('closed432')
|
||||||
|
|
||||||
|
async def chat_completion(**kwargs):
|
||||||
|
payload = kwargs
|
||||||
|
key = await get_key()
|
||||||
|
|
||||||
|
return {
|
||||||
|
'method': 'POST',
|
||||||
|
'url': f'{ENDPOINT}/v1/chat/completions',
|
||||||
|
'payload': payload,
|
||||||
|
'headers': {
|
||||||
|
'Authorization': f'Bearer {key}'
|
||||||
|
},
|
||||||
|
'provider_auth': f'closed432>{key}'
|
||||||
|
}
|
||||||
|
|
||||||
|
async def organify(request: dict) -> dict:
|
||||||
|
key = await get_key()
|
||||||
|
|
||||||
|
request['url'] = ENDPOINT + request['path']
|
||||||
|
request['headers']['Authorization'] = f'Bearer {key}'
|
||||||
|
request['provider_auth'] = f'closed432>{key}'
|
||||||
|
|
||||||
|
return request
|
37
api/providers/helpers/utils.py
Normal file
37
api/providers/helpers/utils.py
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
from db import providerkeys
|
||||||
|
|
||||||
|
GPT_3 = [
|
||||||
|
'gpt-3.5-turbo',
|
||||||
|
'gpt-3.5-turbo-16k',
|
||||||
|
'gpt-3.5-turbo-0613',
|
||||||
|
'gpt-3.5-turbo-0301',
|
||||||
|
'gpt-3.5-turbo-16k-0613',
|
||||||
|
]
|
||||||
|
|
||||||
|
GPT_4 = GPT_3 + [
|
||||||
|
'gpt-4',
|
||||||
|
'gpt-4-0314',
|
||||||
|
'gpt-4-0613',
|
||||||
|
]
|
||||||
|
|
||||||
|
GPT_4_32K = GPT_4 + [
|
||||||
|
'gpt-4-32k',
|
||||||
|
'gpt-4-32k-0314',
|
||||||
|
'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:
|
||||||
|
try:
|
||||||
|
return await providerkeys.manager.get_key(name)
|
||||||
|
except ValueError:
|
||||||
|
raise ValueError(f'Keys missing for "{name}" <no_keys>')
|
|
@ -38,7 +38,6 @@ async def respond(
|
||||||
is_chat = False
|
is_chat = False
|
||||||
|
|
||||||
model = None
|
model = None
|
||||||
is_stream = False
|
|
||||||
|
|
||||||
if 'chat/completions' in path:
|
if 'chat/completions' in path:
|
||||||
is_chat = True
|
is_chat = True
|
||||||
|
@ -73,6 +72,13 @@ async def respond(
|
||||||
provider_name = provider_auth.split('>')[0]
|
provider_name = provider_auth.split('>')[0]
|
||||||
provider_key = provider_auth.split('>')[1]
|
provider_key = provider_auth.split('>')[1]
|
||||||
|
|
||||||
|
if provider_key == '--NO_KEY--':
|
||||||
|
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.'
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
target_request['headers'].update(target_request.get('headers', {}))
|
target_request['headers'].update(target_request.get('headers', {}))
|
||||||
|
|
||||||
if target_request['method'] == 'GET' and not payload:
|
if target_request['method'] == 'GET' and not payload:
|
||||||
|
@ -91,12 +97,13 @@ async def respond(
|
||||||
timeout=aiohttp.ClientTimeout(
|
timeout=aiohttp.ClientTimeout(
|
||||||
connect=1.0,
|
connect=1.0,
|
||||||
total=float(os.getenv('TRANSFER_TIMEOUT', '500'))
|
total=float(os.getenv('TRANSFER_TIMEOUT', '500'))
|
||||||
),
|
)
|
||||||
) as response:
|
) as response:
|
||||||
is_stream = response.content_type == 'text/event-stream'
|
is_stream = response.content_type == 'text/event-stream'
|
||||||
|
|
||||||
if response.status == 429:
|
if response.status == 429:
|
||||||
await keymanager.rate_limit_key(provider_name, provider_key)
|
print('[!] rate limit')
|
||||||
|
# await keymanager.rate_limit_key(provider_name, provider_key)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if response.content_type == 'application/json':
|
if response.content_type == 'application/json':
|
||||||
|
@ -112,12 +119,14 @@ async def respond(
|
||||||
critical_error = True
|
critical_error = True
|
||||||
|
|
||||||
if critical_error:
|
if critical_error:
|
||||||
|
print('[!] critical error')
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if response.ok:
|
if response.ok:
|
||||||
server_json_response = client_json_response
|
server_json_response = client_json_response
|
||||||
|
|
||||||
else:
|
else:
|
||||||
|
print('[!] non-ok response', client_json_response)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if is_stream:
|
if is_stream:
|
||||||
|
@ -136,10 +145,6 @@ async def respond(
|
||||||
|
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
print('[!] exception', exc)
|
print('[!] exception', exc)
|
||||||
if 'too many requests' in str(exc):
|
|
||||||
#!TODO
|
|
||||||
pass
|
|
||||||
|
|
||||||
continue
|
continue
|
||||||
|
|
||||||
else:
|
else:
|
||||||
|
|
|
@ -26,6 +26,12 @@ MESSAGES = [
|
||||||
|
|
||||||
api_endpoint = os.getenv('CHECKS_ENDPOINT', 'http://localhost:2332/v1')
|
api_endpoint = os.getenv('CHECKS_ENDPOINT', 'http://localhost:2332/v1')
|
||||||
|
|
||||||
|
async def _response_base_check(response: httpx.Response) -> None:
|
||||||
|
try:
|
||||||
|
response.raise_for_status()
|
||||||
|
except httpx.HTTPStatusError as exc:
|
||||||
|
raise ConnectionError(f'API returned an error: {response.json()}') from exc
|
||||||
|
|
||||||
async def test_server():
|
async def test_server():
|
||||||
"""Tests if the API server is running."""
|
"""Tests if the API server is running."""
|
||||||
|
|
||||||
|
@ -36,7 +42,7 @@ async def test_server():
|
||||||
url=f'{api_endpoint.replace("/v1", "")}',
|
url=f'{api_endpoint.replace("/v1", "")}',
|
||||||
timeout=3
|
timeout=3
|
||||||
)
|
)
|
||||||
response.raise_for_status()
|
await _response_base_check(response)
|
||||||
|
|
||||||
assert response.json()['ping'] == 'pong', 'The API did not return a correct response.'
|
assert response.json()['ping'] == 'pong', 'The API did not return a correct response.'
|
||||||
except httpx.ConnectError as exc:
|
except httpx.ConnectError as exc:
|
||||||
|
@ -63,7 +69,7 @@ async def test_chat_non_stream_gpt4() -> float:
|
||||||
json=json_data,
|
json=json_data,
|
||||||
timeout=10,
|
timeout=10,
|
||||||
)
|
)
|
||||||
response.raise_for_status()
|
await _response_base_check(response)
|
||||||
|
|
||||||
assert '1337' in response.json()['choices'][0]['message']['content'], 'The API did not return a correct response.'
|
assert '1337' in response.json()['choices'][0]['message']['content'], 'The API did not return a correct response.'
|
||||||
return time.perf_counter() - request_start
|
return time.perf_counter() - request_start
|
||||||
|
@ -86,7 +92,7 @@ async def test_chat_stream_gpt3() -> float:
|
||||||
json=json_data,
|
json=json_data,
|
||||||
timeout=10,
|
timeout=10,
|
||||||
)
|
)
|
||||||
response.raise_for_status()
|
await _response_base_check(response)
|
||||||
|
|
||||||
chunks = []
|
chunks = []
|
||||||
resulting_text = ''
|
resulting_text = ''
|
||||||
|
@ -128,7 +134,7 @@ async def test_image_generation() -> float:
|
||||||
json=json_data,
|
json=json_data,
|
||||||
timeout=10,
|
timeout=10,
|
||||||
)
|
)
|
||||||
response.raise_for_status()
|
await _response_base_check(response)
|
||||||
|
|
||||||
assert '://' in response.json()['data'][0]['url']
|
assert '://' in response.json()['data'][0]['url']
|
||||||
return time.perf_counter() - request_start
|
return time.perf_counter() - request_start
|
||||||
|
@ -166,7 +172,7 @@ async def test_function_calling():
|
||||||
json=json_data,
|
json=json_data,
|
||||||
timeout=15,
|
timeout=15,
|
||||||
)
|
)
|
||||||
response.raise_for_status()
|
await _response_base_check(response)
|
||||||
|
|
||||||
res = response.json()
|
res = response.json()
|
||||||
output = json.loads(res['choices'][0]['message']['function_call']['arguments'])
|
output = json.loads(res['choices'][0]['message']['function_call']['arguments'])
|
||||||
|
@ -185,7 +191,7 @@ async def test_models():
|
||||||
headers=HEADERS,
|
headers=HEADERS,
|
||||||
timeout=3
|
timeout=3
|
||||||
)
|
)
|
||||||
response.raise_for_status()
|
await _response_base_check(response)
|
||||||
res = response.json()
|
res = response.json()
|
||||||
|
|
||||||
all_models = [model['id'] for model in res['data']]
|
all_models = [model['id'] for model in res['data']]
|
||||||
|
@ -208,20 +214,10 @@ async def demo():
|
||||||
else:
|
else:
|
||||||
raise ConnectionError('API Server is not running.')
|
raise ConnectionError('API Server is not running.')
|
||||||
|
|
||||||
# print('[lightblue]Checking if function calling works...')
|
for func in [test_chat_non_stream_gpt4, test_chat_stream_gpt3]:
|
||||||
# print(await test_function_calling())
|
print(f'[*] {func.__name__}')
|
||||||
|
result = await func()
|
||||||
print('Checking non-streamed chat completions...')
|
print(f'[+] {func.__name__} - {result}')
|
||||||
print(await test_chat_non_stream_gpt4())
|
|
||||||
|
|
||||||
print('Checking streamed chat completions...')
|
|
||||||
print(await test_chat_stream_gpt3())
|
|
||||||
|
|
||||||
# print('[lightblue]Checking if image generation works...')
|
|
||||||
# print(await test_image_generation())
|
|
||||||
|
|
||||||
# print('Checking the models endpoint...')
|
|
||||||
# print(await test_models())
|
|
||||||
|
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
print('[red]Error: ' + str(exc))
|
print('[red]Error: ' + str(exc))
|
||||||
|
|
Loading…
Reference in a new issue