from __future__ import annotations import json import platform from abc import ABC, abstractmethod from typing import Generic, TypeVar, Union, Any, Optional, Dict, BinaryIO import httpx from ai21.errors import ( BadRequest, Unauthorized, UnprocessableEntity, TooManyRequestsError, AI21ServerError, ServiceUnavailable, AI21APIError, MissingApiKeyError, ) from ai21.models.request_options import RequestOptions from ai21.stream.async_stream import AsyncStream from ai21.stream.stream import Stream from ai21.version import VERSION _HttpxClientT = TypeVar("_HttpxClientT", bound=Union[httpx.Client, httpx.AsyncClient]) _DefaultStreamT = TypeVar("_DefaultStreamT", bound=Union[Stream[Any], AsyncStream[Any]]) DEFAULT_TIMEOUT_SEC = 300 DEFAULT_NUM_RETRIES = 0 RETRY_BACK_OFF_FACTOR = 0.5 TIME_BETWEEN_RETRIES = 1 RETRY_ERROR_CODES = (408, 429, 500, 503) RETRY_METHOD_WHITELIST = ["GET", "POST", "PUT"] def handle_non_success_response(status_code: int, response_text: str): if status_code == 400: raise BadRequest(details=response_text) if status_code == 401: raise Unauthorized(details=response_text) if status_code == 422: raise UnprocessableEntity(details=response_text) if status_code == 429: raise TooManyRequestsError(details=response_text) if status_code == 500: raise AI21ServerError(details=response_text) if status_code == 503: raise ServiceUnavailable(details=response_text) raise AI21APIError(status_code, details=response_text) class BaseHttpClient(ABC, Generic[_HttpxClientT, _DefaultStreamT]): _client: Optional[_HttpxClientT] = None _request: Any def __init__( self, base_url: str, api_key: Optional[str] = None, requires_api_key: bool = True, timeout_sec: int = None, num_retries: int = None, headers: Dict = None, via: Optional[str] = None, ): if requires_api_key and api_key is None: raise MissingApiKeyError() self._base_url = base_url self._api_key = api_key self._timeout_sec = timeout_sec or DEFAULT_TIMEOUT_SEC self._num_retries = num_retries or DEFAULT_NUM_RETRIES self._headers = headers or {} self._apply_retry_policy = self._num_retries > 0 self._via = via def _should_retry(self, response: httpx.Response) -> bool: return response.status_code in RETRY_ERROR_CODES and response.request.method in RETRY_METHOD_WHITELIST def _get_request_data( self, files: Optional[Dict[str, BinaryIO]], method: str, body=Optional[Dict], ) -> Dict[str, Any] | bytes: if files is not None: if method != "POST": raise ValueError( f"execute_http_request supports only POST for files upload, but {method} was supplied instead" ) return body else: return json.dumps(body).encode() if body else None def _get_request_headers( self, files: Optional[Dict[str, BinaryIO]], headers: Optional[Dict[str, Any]] = None ) -> Dict[str, Any]: if files is not None and "Content-Type" in self._headers: return {key: value for key, value in self._headers.items() if key != "Content-Type"} if headers is not None: return {**self._headers, **headers} return self._headers def add_headers(self, headers: Dict[str, Any]) -> None: self._headers.update(headers) @abstractmethod def execute_http_request( self, method: str, path: str, params: Optional[Dict] = None, stream: bool = False, files: Optional[Dict[str, BinaryIO]] = None, extra_headers: Optional[Dict] = None, ) -> httpx.Response: pass @abstractmethod def _run_request( self, options: RequestOptions, ) -> httpx.Response: pass @abstractmethod def _init_client(self, client: Optional[_HttpxClientT]) -> _HttpxClientT: pass def _build_headers(self, passed_headers: Optional[Dict[str, Any]]) -> Dict[str, Any]: headers = { "Content-Type": "application/json", "User-Agent": self._build_user_agent(), } if self._api_key: headers["Authorization"] = f"Bearer {self._api_key}" if passed_headers is not None: headers.update(passed_headers) return headers def _build_user_agent(self) -> str: user_agent = ( f"AI21 studio SDK {VERSION} Python {platform.python_version()} Operating System {platform.platform()}" ) if self._via is not None: user_agent = f"{user_agent} via: {self._via}" return user_agent def _build_request(self, options: RequestOptions) -> httpx.Request: data = self._get_request_data(files=options.files, method=options.method, body=options.body) headers = self._get_request_headers(files=options.files, headers=options.headers) return self._client.build_request( method=options.method, url=self._prepare_url(options), headers=headers, timeout=options.timeout, params=options.params, data=data, files=options.files, ) def _prepare_url(self, options: RequestOptions) -> str: if options.path: return f"{options.url}{options.path}" return options.url def _extract_streaming_error_details(self, response: httpx.Response) -> str: try: return response.read().decode("utf-8") except Exception: return "could not extract streaming error details"