Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added Support of reaching openai Without Proxy #1

Merged
merged 1 commit into from
Oct 5, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ os.environ["NUMEXA_API_KEY"] = "NUMEXA_API_KEY"
**Numexa Without proxy:**
```python
import os
os.environ["NUMEXA_PROXY"] = "false"
os.environ["NUMEXA_PROXY"] = "disable"
```
**Virtual Keys:** Navigate to the "API Keys" page on [Numexa](https://app.numexa.io/admin/keys) and hit the "Generate" button. Choose your AI provider and assign a unique name to your key. Your virtual key is ready!

Expand Down
2 changes: 1 addition & 1 deletion numexa/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
)

api_key = os.environ.get(NUMEXA_API_KEY)
if os.environ.get(NUMEXA_PROXY, "true").lower() == "true":
if not os.environ.get(NUMEXA_PROXY):
base_url = NUMEXA_PROXY_URL
else:
base_url = NUMEXA_DIRECT_URL
Expand Down
13 changes: 10 additions & 3 deletions numexa/api_resources/apis.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import os
from typing import Optional, Union, overload, Literal, List, Mapping, Any
from numexa.api_resources.base_client import APIClient
from .utils import (
Expand Down Expand Up @@ -215,9 +216,15 @@ def create(
top_p=top_p,
**kwargs,
)
# Proxy On
if not os.environ.get("NUMEXA_PROXY"):
url = "/chat/completions"
# proxy Off
else:
url = "/chat/completions/direct"
if config.mode == Modes.SINGLE.value:
return cls(_client)._post(
"/chat/completions",
url,
body=config.llms,
mode=Modes.SINGLE.value,
params=params,
Expand All @@ -227,7 +234,7 @@ def create(
)
if config.mode == Modes.FALLBACK.value:
return cls(_client)._post(
"/chat/completions",
url,
body=config.llms,
mode=Modes.FALLBACK,
params=params,
Expand All @@ -237,7 +244,7 @@ def create(
)
if config.mode == Modes.AB_TEST.value:
return cls(_client)._post(
"/v1/chatComplete",
url,
body=config.llms,
mode=Modes.AB_TEST,
params=params,
Expand Down
148 changes: 111 additions & 37 deletions numexa/api_resources/base_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,17 @@ def post(
stream=stream,
params=params,
)
elif path in NumexaApiPaths.CHAT_COMPLETION_DIRECT:
body = cast(List[Body], body)
opts = self._construct_direct(
method="post",
url=path.split("/direct")[0],
body=body,
mode=mode,
stream=stream,
params=params,
)

elif path.endswith("/generate"):
opts = self._construct_generate_options(
method="post",
Expand Down Expand Up @@ -205,6 +216,28 @@ def _construct(
opts.headers = None
return opts

def _construct_direct(
self,
*,
method: str,
url: str,
body: List[Body],
mode: str,
stream: bool,
params: Params,
) -> Options:
opts = Options.construct()
opts.method = method
opts.url = url
params_dict = {} if params is None else params.dict()
json_body = {
"model": self._config_direct(mode, body),
"messages": params_dict.get("messages", [{}])
}
opts.json_body = remove_empty_values(json_body)
opts.headers = None
return opts

def _config(self, mode: str, body: List[Body]) -> RequestConfig:
config = RequestConfig(mode=mode, options=[])
for i in body:
Expand All @@ -216,18 +249,32 @@ def _config(self, mode: str, body: List[Body]) -> RequestConfig:
config.options.append(options)
return config

def _config_direct(self, mode: str, body: List[Body]) -> List[str]:
config = []
for i in body:
config.append(i.model)
return config

@property
def _default_headers(self) -> Mapping[str, str]:
return {
"Content-Type": "application/json",
f"{NUMEXA_HEADER_PREFIX}Api-Key": self.api_key,
f"{NUMEXA_HEADER_PREFIX}package-version": f"numexa-{VERSION}",
f"{NUMEXA_HEADER_PREFIX}runtime": platform.python_implementation(),
f"{NUMEXA_HEADER_PREFIX}runtime-version": platform.python_version(),
f"{NUMEXA_HEADER_PREFIX}Cache": "true",
"Authorization": os.environ.get(OPEN_API_KEY),

}
# Proxy ON
if not os.environ.get("NUMEXA_PROXY"):
return {
"Content-Type": "application/json",
f"{NUMEXA_HEADER_PREFIX}Api-Key": self.api_key,
f"{NUMEXA_HEADER_PREFIX}package-version": f"numexa-{VERSION}",
f"{NUMEXA_HEADER_PREFIX}runtime": platform.python_implementation(),
f"{NUMEXA_HEADER_PREFIX}runtime-version": platform.python_version(),
f"{NUMEXA_HEADER_PREFIX}Cache": "true",
"Authorization": os.environ.get(OPEN_API_KEY),

}
# Proxy Off
else:
return {
"Content-Type": "application/json",
"Authorization": os.environ.get(OPEN_API_KEY)
}

def _build_headers(self, options: Options) -> httpx.Headers:
custom_headers = options.headers or {}
Expand Down Expand Up @@ -266,7 +313,7 @@ def __exit__(
) -> None:
self.close()

def _build_request(self, options: Options) -> httpx.Request:
def _build_request(self, options: Options) -> List[httpx.Request]:
headers = self._build_headers(options)
params = options.params
json_body = options.json_body
Expand All @@ -278,7 +325,28 @@ def _build_request(self, options: Options) -> httpx.Request:
json=json_body,
timeout=options.timeout,
)
return request
return [request]

def _build_request_direct(self, options: Options) -> List[httpx.Request]:
headers = self._build_headers(options)
new_payload = dict()
request_list = []
params = options.params
json_body = options.json_body
messages = json_body.get("messages", [{}])
models = json_body.get("model", [""])
new_payload["messages"] = messages
for model in models:
new_payload["model"] = model
request_list.append(self._client.build_request(
method=options.method,
url=options.url,
headers=headers,
params=params,
json=new_payload,
timeout=options.timeout,
))
return request_list

@overload
def _request(
Expand Down Expand Up @@ -321,32 +389,38 @@ def _request(
cast_to: Type[ResponseT],
stream_cls: Type[StreamT],
) -> Union[ResponseT, StreamT]:
request = self._build_request(options)
try:
res = self._client.send(request, auth=self.custom_auth, stream=stream)
res.raise_for_status()
except httpx.HTTPStatusError as err: # 4xx and 5xx errors
# If the response is streamed then we need to explicitly read the response
# to completion before attempting to access the response text.
err.response.read()
raise self._make_status_error_from_response(request, err.response) from None
except httpx.TimeoutException as err:
raise APITimeoutError(request=request) from err
except Exception as err:
raise APIConnectionError(request=request) from err
if stream or res.headers["content-type"] == "text/event-stream":
if stream_cls is None:
raise MissingStreamClassError()
stream_response = stream_cls(
response=res, cast_to=self._extract_stream_chunk_type(stream_cls)
# proxy on
if not os.environ.get("NUMEXA_PROXY"):
request_list = self._build_request(options)
# proxy off
else:
request_list = self._build_request_direct(options)
for request in request_list:
try:
res = self._client.send(request, auth=self.custom_auth, stream=stream)
res.raise_for_status()
except httpx.HTTPStatusError as err: # 4xx and 5xx errors
# If the response is streamed then we need to explicitly read the response
# to completion before attempting to access the response text.
print(err.response.read())
continue
# raise self._make_status_error_from_response(request, err.response) from None
except httpx.TimeoutException as err:
raise APITimeoutError(request=request) from err
except Exception as err:
raise APIConnectionError(request=request) from err
if stream or res.headers["content-type"] == "text/event-stream":
if stream_cls is None:
raise MissingStreamClassError()
stream_response = stream_cls(
response=res, cast_to=self._extract_stream_chunk_type(stream_cls)
)
return stream_response
response = cast(
ResponseT,
cast_to(**res.json()),
)
return stream_response

response = cast(
ResponseT,
cast_to(**res.json()),
)
return response
return response

def _extract_stream_chunk_type(self, stream_cls: Type) -> type:
args = get_args(stream_cls)
Expand Down
2 changes: 1 addition & 1 deletion numexa/api_resources/global_constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
VERSION = "0.1.0"
DEFAULT_TIMEOUT = 60
NUMEXA_HEADER_PREFIX = "X-Numexa-"
NUMEXA_DIRECT_URL = "https://app.numexa.io/proxy/v1/openapi"
NUMEXA_DIRECT_URL = "https://api.openai.com/v1"

NUMEXA_API_KEY = "NUMEXA_API_KEY"
NUMEXA_PROXY_URL = "https://app.numexa.io/proxy/v1/openai"
Expand Down
1 change: 1 addition & 0 deletions numexa/api_resources/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ class NumexaApiPaths(str, Enum, metaclass=MetaEnum):
CHAT_COMPLETION = "/chat/completions"
COMPLETION = "/complete"
GENERATION = "/v1/prompts/{prompt_id}/generate"
CHAT_COMPLETION_DIRECT = "/chat/completions/direct"


class Options(BaseModel):
Expand Down