BaseClient
def _enforce_trailing_slash(self, url: URL) -> URL:
if url.raw_path.endswith(b"/"):
return url
return url.copy_with(raw_path=url.raw_path + b"/")
确保URL的路径以/结尾
def _make_status_error_from_response(
self,
response: httpx.Response,
) -> APIStatusError:
if response.is_closed and not response.is_stream_consumed:
# We can't read the response body as it has been closed
# before it was read. This can happen if an event hook
# raises a status error.
body = None
err_msg = f"Error code: {response.status_code}"
else:
err_text = response.text.strip()
body = err_text
try:
body = json.loads(err_text)
err_msg = f"Error code: {response.status_code} - {body}"
except Exception:
err_msg = err_text or f"Error code: {response.status_code}"
return self._make_status_error(err_msg, body=body, response=response)
def _make_status_error(
self,
err_msg: str,
*,
body: object,
response: httpx.Response,
) -> _exceptions.APIStatusError:
raise NotImplementedError()
"""
class APIStatusError(APIError):
# Raised when an API response has a status code of 4xx or 5xx.
response: httpx.Response
status_code: int
def __init__(self, message: str, *, response: httpx.Response, body: object | None) -> None:
super().__init__(message, response.request, body=body)
self.response = response
self.status_code = response.status_code
"""
处理API响应的状态码,4xx 或 5xx 时被抛出
当响应体没有被完全读取,将 body 设置为 None,并将错误消息设置为响应状态码。
如果响应体未被关闭,我们尝试从中提取错误文本
提供一个统一的错误处理方式,无论响应体是文本、JSON 还是其他格式,都能够正确地创建一个 APIStatusError 实例,以便于在应用程序中进行错误处理
def _remaining_retries(
self,
remaining_retries: Optional[int],
options: FinalRequestOptions,
) -> int:
return remaining_retries if remaining_retries is not None else options.get_max_retries(self.max_retries)
"""
class FinalRequestOptions(pydantic.BaseModel):
method: str
url: str
params: Query = {}
headers: Union[Headers, NotGiven] = NotGiven()
max_retries: Union[int, NotGiven] = NotGiven()
timeout: Union[float, Timeout, None, NotGiven] = NotGiven()
files: Union[HttpxRequestFiles, None] = None
idempotency_key: Union[str, None] = None
post_parser: Union[Callable[[Any], Any], NotGiven] = NotGiven()
# It should be noted that we cannot use `json` here as that would override
# a BaseModel method in an incompatible fashion.
json_data: Union[Body, None] = None
extra_json: Union[AnyMapping, None] = None
if PYDANTIC_V2:
model_config: ClassVar[ConfigDict] = ConfigDict(arbitrary_types_allowed=True)
else:
class Config(pydantic.BaseConfig): # pyright: ignore[reportDeprecated]
arbitrary_types_allowed: bool = True
def get_max_retries(self, max_retries: int) -> int:
if isinstance(self.max_retries, NotGiven):
return max_retries
return self.max_retries
def _strip_raw_response_header(self) -> None:
if not is_given(self.headers):
return
if self.headers.get(RAW_RESPONSE_HEADER):
self.headers = {**self.headers}
self.headers.pop(RAW_RESPONSE_HEADER)
# override the `construct` method so that we can run custom transformations.
# this is necessary as we don't want to do any actual runtime type checking
# (which means we can't use validators) but we do want to ensure that `NotGiven`
# values are not present
#
# type ignore required because we're adding explicit types to `**values`
@classmethod
def construct( # type: ignore
cls,
_fields_set: set[str] | None = None,
**values: Unpack[FinalRequestOptionsInput],
) -> FinalRequestOptions:
kwargs: dict[str, Any] = {
# we unconditionally call `strip_not_given` on any value
# as it will just ignore any non-mapping types
key: strip_not_given(value)
for key, value in values.items()
}
if PYDANTIC_V2:
return super().model_construct(_fields_set, **kwargs)
return cast(FinalRequestOptions, super().construct(_fields_set, **kwargs)) # pyright: ignore[reportDeprecated]
if not TYPE_CHECKING:
# type checkers incorrectly complain about this assignment
model_construct = construct
"""
计算HTTP请求剩余的重试次数
FinalRequestOptions 提供一个灵活的HTTP请求配置类,使用pydantic来解析和验证数据,同时允许自定义处理逻辑,如删除请求头中的特定字段等
def _build_headers(self, options: FinalRequestOptions) -> httpx.Headers:
custom_headers = options.headers or {}
headers_dict = _merge_mappings(self.default_headers, custom_headers)
self._validate_headers(headers_dict, custom_headers)
# headers are case-insensitive while dictionaries are not.
headers = httpx.Headers(headers_dict)
idempotency_header = self._idempotency_header
if idempotency_header and options.method.lower() != "get" and idempotency_header not in headers:
headers[idempotency_header] = options.idempotency_key or self._idempotency_key()
return headers
将默认头部信息和自定义头部信息合并,然后添加幂等性头部字段(如果需要),最后返回一个httpx.Headers对象
def _prepare_url(self, url: str) -> URL:
"""
Merge a URL argument together with any 'base_url' on the client,
to create the URL used for the outgoing request.
"""
# Copied from httpx's `_merge_url` method.
merge_url = URL(url)
if merge_url.is_relative_url:
merge_raw_path = self.base_url.raw_path + merge_url.raw_path.lstrip(b"/")
return self.base_url.copy_with(raw_path=merge_raw_path)
return merge_url
确保发出的HTTP请求使用正确的URL。如果传入的URL是相对的,它会与客户端的base_url相对应地合并
def _build_request(
self,
options: FinalRequestOptions,
) -> httpx.Request:
if log.isEnabledFor(logging.DEBUG):
log.debug("Request options: %s", model_dump(options, exclude_unset=True))
kwargs: dict[str, Any] = {}
json_data = options.json_data
if options.extra_json is not None:
if json_data is None:
json_data = cast(Body, options.extra_json)
elif is_mapping(json_data):
json_data = _merge_mappings(json_data, options.extra_json)
else:
raise RuntimeError(f"Unexpected JSON data type, {type(json_data)}, cannot merge with `extra_body`")
headers = self._build_headers(options)
params = _merge_mappings(self._custom_query, options.params)
# If the given Content-Type header is multipart/form-data then it
# has to be removed so that httpx can generate the header with
# additional information for us as it has to be in this form
# for the server to be able to correctly parse the request:
# multipart/form-data; boundary=---abc--
if headers.get("Content-Type") == "multipart/form-data":
headers.pop("Content-Type")
# As we are now sending multipart/form-data instead of application/json
# we need to tell httpx to use it, https://www.python-httpx.org/advanced/#multipart-file-encoding
if json_data:
if not is_dict(json_data):
raise TypeError(
f"Expected query input to be a dictionary for multipart requests but got {type(json_data)} instead."
)
kwargs["data"] = self._serialize_multipartform(json_data)
# TODO: report this error to httpx
return self._client.build_request( # pyright: ignore[reportUnknownMemberType]
headers=headers,
timeout=self.timeout if isinstance(options.timeout, NotGiven) else options.timeout,
method=options.method,
url=self._prepare_url(options.url),
# the `Query` type that we use is incompatible with qs'
# `Params` type as it needs to be typed as `Mapping[str, object]`
# so that passing a `TypedDict` doesn't cause an error.
# https://github.com/microsoft/pyright/issues/3526#event-6715453066
params=self.qs.stringify(cast(Mapping[str, Any], params)) if params else None,
json=json_data,
files=options.files,
**kwargs,
)
"""
def cast(typ, val):
return val
def isEnabledFor(self, level):
# Is this logger enabled for level 'level'?
if self.disabled:
return False
try:
return self._cache[level]
except KeyError:
_acquireLock()
try:
if self.manager.disable >= level:
is_enabled = self._cache[level] = False
else:
is_enabled = self._cache[level] = (
level >= self.getEffectiveLevel()
)
finally:
_releaseLock()
return is_enabled
def _merge_mappings(
obj1: Mapping[_T_co, Union[_T, Omit]],
obj2: Mapping[_T_co, Union[_T, Omit]],
) -> Dict[_T_co, _T]:
merged = {**obj1, **obj2}
return {key: value for key, value in merged.items() if not isinstance(value, Omit)}
"""
创建并返回一个httpx.Request对象,该对象包含了所有必要的请求信息。
如果请求头中包含multipart/form-data,则需要httpx处理表单数据,因此需要移除这个内容类型头
如果请求体是JSON数据,并且Content-Type是multipart/form-data,则需要将JSON数据转换为适合multipart格式的数据
cast 告诉类型检查器一个变量应该具有的类型,而不需要在运行时进行实际的类型转换
isEnabledFor 用于确定日志记录器是否应该处理和发出给定严重级别的日志消息
_merge_mappings 用于合并两个映射对象,合并时会优先考虑第二个映射对象中的值,并且会移除所有值为Omit的键值对
def _serialize_multipartform(self, data: Mapping[object, object]) -> dict[str, object]:
items = self.qs.stringify_items(
# TODO: type ignore is required as stringify_items is well typed but we can't be
# well typed without heavy validation.
data, # type: ignore
array_format="brackets",
)
serialized: dict[str, object] = {}
for key, value in items:
if key in serialized:
raise ValueError(f"Duplicate key encountered: {key}; This behaviour is not supported")
serialized[key] = value
return serialized
"""
def stringify_items(
self,
params: Params,
*,
array_format: NotGivenOr[ArrayFormat] = NOT_GIVEN,
nested_format: NotGivenOr[NestedFormat] = NOT_GIVEN,
) -> list[tuple[str, str]]:
opts = Options(
qs=self,
array_format=array_format,
nested_format=nested_format,
)
return flatten([self._stringify_item(key, value, opts) for key, value in params.items()])
"""
将一个字典(data)中的键值对序列化为适用于multipart/form-data请求格式的字典
stringify_items 将一个Params对象中的键值对序列化为字符串,以便在URL查询字符串中使用
def _process_response(
self,
*,
cast_to: Type[ResponseT],
options: FinalRequestOptions,
response: httpx.Response,
stream: bool,
stream_cls: type[Stream[Any]] | type[AsyncStream[Any]] | None,
) -> ResponseT:
api_response = APIResponse(
raw=response,
client=self,
cast_to=cast_to,
stream=stream,
stream_cls=stream_cls,
options=options,
)
if response.request.headers.get(RAW_RESPONSE_HEADER) == "true":
return cast(ResponseT, api_response)
return api_response.parse()
"""
def parse(self) -> R:
if self._parsed is not None:
return self._parsed
parsed = self._parse()
if is_given(self._options.post_parser):
parsed = self._options.post_parser(parsed)
self._parsed = parsed
return parsed
"""
处理HTTP响应,并根据需要将响应转换为特定的类型
parse 如果响应数据已经被解析,直接返回已解析的数据,否则使用_parse方法解析响应数据。如果指定了post_parser选项,则允许用户在响应解析后应用自定义的处理逻辑。将解析后的数据存储在APIResponse对象中返回解析后的数据
def _process_response_data(
self,
*,
data: object,
cast_to: type[ResponseT],
response: httpx.Response,
) -> ResponseT:
if data is None:
return cast(ResponseT, None)
if cast_to is UnknownResponse:
return cast(ResponseT, data)
try:
if inspect.isclass(cast_to) and issubclass(cast_to, ModelBuilderProtocol):
return cast(ResponseT, cast_to.build(response=response, data=data))
if self._strict_response_validation:
return cast(ResponseT, validate_type(type_=cast_to, value=data))
return cast(ResponseT, construct_type(type_=cast_to, value=data))
except pydantic.ValidationError as err:
raise APIResponseValidationError(response=response, body=data) from err
def _should_stream_response_body(self, *, request: httpx.Request) -> bool:
if request.headers.get(STREAMED_RAW_RESPONSE_HEADER) == "true":
return True
return False
_process_response_data:处理响应数据,并根据指定的类型cast_to将数据转换成相应的类型
如果cast_to是一个类,并且是ModelBuilderProtocol的子类,则调用build方法来构建一个模型实例
如果设置了self._strict_response_validation,则使用validate_type函数来验证数据是否符合cast_to类型的期望
如果cast_to是一个普通类,而不是模型类,则使用construct_type函数来构造一个实例
_should_stream_response_body:判断响应体是否应该以流式方式处理
# 定义一个属性,返回一个Querystring类的实例
@property
def qs(self) -> Querystring:
return Querystring()
# 定义一个属性,返回一个httpx.Auth对象或None
@property
def custom_auth(self) -> httpx.Auth | None:
return None
# 定义一个属性,返回一个包含认证相关HTTP头部的字典
@property
def auth_headers(self) -> dict[str, str]:
return {}
# 定义一个属性,返回一个包含默认HTTP头部的字典
@property
def default_headers(self) -> dict[str, str | Omit]:
return {
"Accept": "application/json",
"Content-Type": "application/json",
"User-Agent": self.user_agent,
**self.platform_headers(),
**self.auth_headers,
**self._custom_headers,
}
# 定义一个方法,用于验证传递给请求的默认头部和自定义头部
def _validate_headers(
self,
headers: Headers, # noqa: ARG002
custom_headers: Headers, # noqa: ARG002
) -> None:
"""Validate the given default headers and custom headers.
Does nothing by default.
"""
return
# 定义一个属性,返回一个字符串,包含用户代理信息
@property
def user_agent(self) -> str:
return f"{self.__class__.__name__}/Python {self._version}"
# 定义一个属性,用于获取或设置请求的基本URL
@property
def base_url(self) -> URL:
return self._base_url
# 定义一个设置器方法,用于设置请求的基本URL,并确保URL总是带有尾随的斜杠
@base_url.setter
def base_url(self, url: URL | str) -> None:
self._base_url = self._enforce_trailing_slash(url if isinstance(url, URL) else URL(url))
# 定义一个方法,返回与平台相关的HTTP头部信息
def platform_headers(self) -> Dict[str, str]:
return platform_headers(self._version)
处理HTTP请求的头部信息,用户代理,以及请求的基本URL
def _calculate_retry_timeout(
self,
remaining_retries: int,
options: FinalRequestOptions,
response_headers: Optional[httpx.Headers] = None,
) -> float:
max_retries = options.get_max_retries(self.max_retries)
try:
# About the Retry-After header: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After
#
# <http-date>". See https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After#syntax for
# details.
if response_headers is not None:
retry_header = response_headers.get("retry-after")
try:
retry_after = float(retry_header)
except Exception:
retry_date_tuple = email.utils.parsedate_tz(retry_header)
if retry_date_tuple is None:
retry_after = -1
else:
retry_date = email.utils.mktime_tz(retry_date_tuple)
retry_after = int(retry_date - time.time())
else:
retry_after = -1
except Exception:
retry_after = -1
# If the API asks us to wait a certain amount of time (and it's a reasonable amount), just do what it says.
if 0 < retry_after <= 60:
return retry_after
initial_retry_delay = 0.5
max_retry_delay = 8.0
nb_retries = max_retries - remaining_retries
# Apply exponential backoff, but not more than the max.
sleep_seconds = min(initial_retry_delay * pow(2.0, nb_retries), max_retry_delay)
# Apply some jitter, plus-or-minus half a second.
jitter = 1 - 0.25 * random()
timeout = sleep_seconds * jitter
return timeout if timeout >= 0 else 0
根据HTTP响应头部中的Retry-After信息或预定义的指数退避策略来计算在重试HTTP请求之前应该等待的时间
指数退避策略:将初始延迟乘以2的幂来实现的,幂次与剩余重试次数成正比。然后,它将这个值与最大延迟进行比较,取较小者
def _should_retry(self, response: httpx.Response) -> bool:
# Note: this is not a standard header
should_retry_header = response.headers.get("x-should-retry")
# If the server explicitly says whether or not to retry, obey.
if should_retry_header == "true":
return True
if should_retry_header == "false":
return False
# Retry on request timeouts.
if response.status_code == 408:
return True
# Retry on lock timeouts.
if response.status_code == 409:
return True
# Retry on rate limits.
if response.status_code == 429:
return True
# Retry internal errors.
if response.status_code >= 500:
return True
return False
def _idempotency_key(self) -> str:
return f"stainless-python-retry-{uuid.uuid4()}"
_should_retry 决定是否重试
_idempotency_key 生成一个唯一标识符来保证重试请求的幂等性
SyncAPIClient
HTTP客户端的实现,提供了对HTTP请求的封装和处理,以及一些辅助方法来简化和增强HTTP请求的功能
is_closed(self) -> bool
:检查底层的 HTTPX 客户端是否已关闭。
close(self) -> None
:关闭底层的 HTTPX 客户端。在关闭后,客户端将不再可用。
__enter__(self: _T) -> _T
和 __exit__(self, exc_type: type[BaseException] | None, exc: BaseException | None, exc_tb: TracebackType | None) -> None
:这两个方法实现了 Python 上下文管理器协议,允许在 with
语句中使用实例。在 __exit__
中调用了 close()
方法来确保资源的正确释放。
_prepare_options(self, options: FinalRequestOptions)
和 _prepare_request(self, request: httpx.Request)
:这两个方法用作钩子函数,在请求发送前可以对请求选项和请求对象进行修改。例如,你可以在 _prepare_request
方法中添加一些请求头。
request(self, ...)
和 _request(self, ...)
:这两个方法是进行实际 HTTP 请求的主要方法。request
方法是对外暴露的接口,根据传入的参数调用 _request
方法,处理 HTTP 请求和响应。在 _request
方法中,首先调用了 _prepare_options
和 _build_request
方法来准备请求选项和构建请求对象,然后尝试发送请求,处理超时异常和其他可能的异常情况,并根据情况重试请求。最后,处理响应状态码,并根据需要重试请求。
_retry_request(self, ...)
:这个方法用于在发生请求重试时执行。它计算了重试的超时时间,然后在超时时间之后重新发起请求。首先,减少了剩余重试次数,然后根据当前的重试次数和响应头信息计算出超时时间,使用 time.sleep()
函数来进行等待,最后调用 _request
方法来重新发起请求。
_request_api_list(self, ...)
:这个方法用于向 API 发送一个列表请求,并返回一个同步的页面对象。它接受一个模型类型 model
、一个同步页面类型 page
和最终请求选项 options
。在内部,它定义了一个名为 _parser
的函数,用于解析页面响应,并将私有属性设置为客户端、模型和选项。然后,将这个解析函数赋值给选项的 post_parser
字段,并调用 request
方法发起请求。
get(self, ...)
:这个方法是对 HTTP GET 请求的封装。根据传入的参数,构造了最终的请求选项 opts
,然后调用 request
方法来发起请求,并根据 stream
参数来决定返回响应还是流对象。最终返回的是请求的响应对象或流对象。
post(self, ...)
:这个方法用于发送 HTTP POST 请求。它根据传入的参数构造了最终的请求选项 opts
,包括请求的方法、URL、请求体数据、文件等信息。然后调用 request
方法来发起请求,并根据 stream
参数来决定返回响应还是流对象。
patch(self, ...)
、put(self, ...)
和 delete(self, ...)
:这些方法分别对应于 HTTP PATCH、PUT 和 DELETE 请求。它们的实现方式与 post
方法类似,只是在构造请求选项 opts
时,指定了不同的请求方法。
get_api_list(self, ...)
:这个方法用于获取 API 列表。它接受一个 URL 路径 path
、一个模型类型 model
、一个页面类型 page
、可选的请求体数据 body
和其他请求选项 options
。在内部,它构造了最终的请求选项 opts
,然后调用了 _request_api_list
方法来发送请求,并返回一个同步页面对象。
OpenAI()
__init__():接受了一系列参数,包括 api_key
、organization
、base_url
等,用于配置客户端的行为。其中,api_key
和 organization
可以从环境变量中自动获取,如果没有提供则会从环境变量中获取。然后调用了父类的初始化方法,并将传入的参数传递给父类的初始化方法进行初始化。还实例化了一系列资源对象,如 resources.Completions
、resources.Chat
等。这些资源对象用于封装具体的 API 调用。设置了一个默认的流类型 Stream
,用于处理 API 响应的流式数据。
qs
、auth_headers、
default_headers:三个属性装饰器。
这些装饰器通过 @property
装饰器将方法转换为只读属性,用于配置客户端的查询字符串、身份验证头和默认头部信息
copy
:用于创建一个新的客户端实例,其选项与当前客户端相同,但可以选择性地覆盖某些选项。方法接受一系列参数,包括新的 API 密钥、组织、基础 URL、超时时间、HTTP 客户端等。如果未提供新的值,则默认使用当前客户端的相应选项。该方法将当前客户端的选项复制到新的客户端实例,并根据提供的参数进行必要的覆盖。
with_options
: copy
方法的别名,用于在内联使用时更加方便。例如,可以通过 client.with_options(timeout=10).foo.create(...)
的方式来创建一个具有特定选项的客户端实例。
_make_status_error
:该方法是一个重写方法,用于根据 HTTP 响应状态码和响应体创建相应的 API 错误实例。根据不同的状态码,会抛出不同类型的异常,如 400、401、403 等。对于非预期的状态码,会抛出 APIStatusError
异常。
completions create
参数
-
messages
: 聊天对话中的消息列表,用于构建模型响应。消息格式需要符合特定规范,详情可参考文档中提供的链接。 -
model
: 要使用的模型的标识符。可选的模型包括各种预训练模型,如 "gpt-3.5-turbo"、"gpt-4" 等。 -
frequency_penalty
: 用于惩罚模型生成重复文本的惩罚系数,范围在 -2.0 到 2.0 之间。 -
function_call
: 控制模型是否调用函数以及调用哪个函数。可选值包括 "none"、"auto" 或指定函数名称。 -
functions
: 指定模型可能调用的函数列表。 -
logit_bias
: 修改模型生成文本中指定标记出现的可能性。 -
logprobs
: 是否返回输出标记的对数概率。 -
max_tokens
: 聊天完成中允许生成的最大标记数。 -
n
: 每个输入消息生成的聊天完成选择数量。 -
presence_penalty
: 用于惩罚模型生成与先前文本不相关的新文本的惩罚系数。 -
response_format
: 指定模型输出的格式,可选项包括{ "type": "json_object" }
等。 -
seed
: 控制模型的随机性。 -
stop
: 指定模型停止生成文本的标记。 -
stream
: 是否以流的方式返回部分消息。 -
temperature
: 控制采样温度,介于 0 和 2 之间。 -
tool_choice
: 控制模型是否调用函数以及调用哪个函数。 -
tools
: 指定模型可能调用的工具列表。 -
top_logprobs
: 指定返回每个标记位置的最有可能的标记数量。 -
top_p
: 用于控制 nucleus 采样的参数。 -
user
: 表示最终用户的唯一标识符。 -
extra_headers
: 发送额外的请求头。 -
extra_query
: 添加额外的查询参数到请求中。 -
extra_body
: 添加额外的 JSON 属性到请求中。 -
timeout
: 覆盖客户端级别的默认超时时间。
create
方法
-
"/chat/completions"
:指定了要发送请求的端点路径。 -
body
:请求体参数,包含了聊天完成所需的各种参数,如消息列表、模型、惩罚系数等。 -
maybe_transform
:用于将请求体参数转换为特定类型,这里使用了completion_create_params.CompletionCreateParams
类型。 -
options
:请求选项,包含了额外的请求头、查询参数、请求体参数和超时时间等。 -
cast_to
:指定了返回结果的类型,这里是ChatCompletion
类型。 -
stream
:是否以流的形式返回结果。 -
stream_cls
:指定了流对象的类型,这里是Stream[ChatCompletionChunk]
。