Skip to content
Open
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
8 changes: 4 additions & 4 deletions .fern/metadata.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"cliVersion": "5.55.0",
"generatorName": "fernapi/fern-python-sdk",
"generatorVersion": "5.22.1",
"generatorVersion": "5.29.3",
"generatorConfig": {
"tcp_keepalive": {
"enabled": true,
Expand Down Expand Up @@ -100,10 +100,10 @@
}
]
},
"originGitCommit": "5c526877987b8b4e11151461d0c251002eb24341",
"originGitCommit": "bbc2dda3546069d5bed7806a17ca9e2fdfb47df1",
"originGitCommitIsDirty": false,
"invokedBy": "ci",
"requestedVersion": "7.1.0",
"requestedVersion": "7.1.1",
"ciProvider": "github",
"sdkVersion": "7.1.0"
"sdkVersion": "7.1.1"
}
8 changes: 7 additions & 1 deletion .fern/replay.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

264 changes: 132 additions & 132 deletions poetry.lock

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ dynamic = ["version"]

[tool.poetry]
name = "cohere"
version = "7.1.0"
version = "7.1.1"
description = ""
readme = "README.md"
authors = []
Expand Down
4 changes: 2 additions & 2 deletions src/cohere/base_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -2010,7 +2010,7 @@ async def chat_stream(


async def main() -> None:
response = await client.chat_stream(
response = client.chat_stream(
model="command-a-03-2025",
message="hello!",
)
Expand Down Expand Up @@ -2484,7 +2484,7 @@ async def generate_stream(


async def main() -> None:
response = await client.generate_stream(
response = client.generate_stream(
prompt="Please explain to me how LLMs work",
)
async for chunk in response:
Expand Down
4 changes: 2 additions & 2 deletions src/cohere/core/client_wrapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,12 @@ def get_headers(self) -> typing.Dict[str, str]:
import platform

headers: typing.Dict[str, str] = {
"User-Agent": "cohere/7.1.0",
"User-Agent": "cohere/7.1.1",
"X-Fern-Language": "Python",
"X-Fern-Runtime": f"python/{platform.python_version()}",
"X-Fern-Platform": f"{platform.system().lower()}/{platform.release()}",
"X-Fern-SDK-Name": "cohere",
"X-Fern-SDK-Version": "7.1.0",
"X-Fern-SDK-Version": "7.1.1",
**(self.get_custom_headers() or {}),
}
if self._client_name is not None:
Expand Down
67 changes: 61 additions & 6 deletions src/cohere/core/http_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -272,7 +272,16 @@ def get_request_body(
data: typing.Optional[typing.Any],
request_options: typing.Optional[RequestOptions],
omit: typing.Optional[typing.Any],
optional_body: bool = False,
) -> typing.Tuple[typing.Optional[typing.Any], typing.Optional[typing.Any]]:
# A whole body left at the sentinel was never passed by the caller, so it is absent
# rather than empty: the request carries no content and no `Content-Type`.
if omit is not None:
if json is omit:
json = None
if data is omit:
data = None

json_body = None
data_body = None
if data is not None:
Expand All @@ -288,14 +297,36 @@ def get_request_body(
# Only collapse empty dict to None when the body was not explicitly provided
# and there are no additional body parameters. This preserves explicit empty
# bodies (e.g., when an endpoint has a request body type but all fields are optional).
if json_body == {} and json is None and not has_additional_body_parameters:
# `optional_body` marks an endpoint whose body the API does not require, where a body
# that ends up empty means the caller passed none of its properties, so the request is
# sent with no content and no `Content-Type`.
if json_body == {} and (json is None or optional_body) and not has_additional_body_parameters:
json_body = None
if data_body == {} and data is None and not has_additional_body_parameters:
if data_body == {} and (data is None or optional_body) and not has_additional_body_parameters:
data_body = None

return json_body, data_body


def drop_content_type_without_body(
headers: typing.Dict[str, typing.Any],
*,
json_body: typing.Optional[typing.Any],
data_body: typing.Optional[typing.Any],
optional_body: bool,
) -> typing.Dict[str, typing.Any]:
"""Strip ``Content-Type`` from a request that carries no body.

``get_request_body`` drops the body of an ``optional_body`` endpoint when the caller
supplied none of it, but the endpoint still passes the content type it would have used.
A request that sends nothing must not advertise a media type, so a server that branches
on the header sees a bodyless call for what it is.
"""
if not optional_body or json_body is not None or data_body is not None:
return headers
return {key: value for key, value in headers.items() if key.lower() != "content-type"}


class HttpClient:
def __init__(
self,
Expand Down Expand Up @@ -343,6 +374,7 @@ def request(
request_options: typing.Optional[RequestOptions] = None,
retries: int = 0,
omit: typing.Optional[typing.Any] = None,
optional_body: bool = False,
force_multipart: typing.Optional[bool] = None,
) -> httpx.Response:
base_url = self.get_base_url(base_url)
Expand All @@ -355,7 +387,9 @@ def request(
)
timeout = _timeout if _timeout is not None else httpx.USE_CLIENT_DEFAULT

json_body, data_body = get_request_body(json=json, data=data, request_options=request_options, omit=omit)
json_body, data_body = get_request_body(
json=json, data=data, request_options=request_options, omit=omit, optional_body=optional_body
)

request_files: typing.Optional[RequestFiles] = (
convert_file_dict_to_httpx_tuples(remove_omit_from_dict(remove_none_from_dict(files), omit))
Expand Down Expand Up @@ -398,6 +432,9 @@ def request(
}
)
)
_request_headers = drop_content_type_without_body(
_request_headers, json_body=json_body, data_body=data_body, optional_body=optional_body
)

if self.logger.is_debug():
self.logger.debug(
Expand Down Expand Up @@ -506,6 +543,7 @@ def stream(
request_options: typing.Optional[RequestOptions] = None,
retries: int = 0,
omit: typing.Optional[typing.Any] = None,
optional_body: bool = False,
force_multipart: typing.Optional[bool] = None,
) -> typing.Iterator[httpx.Response]:
base_url = self.get_base_url(base_url)
Expand All @@ -527,7 +565,9 @@ def stream(
if (request_files is None or len(request_files) == 0) and force_multipart:
request_files = FORCE_MULTIPART

json_body, data_body = get_request_body(json=json, data=data, request_options=request_options, omit=omit)
json_body, data_body = get_request_body(
json=json, data=data, request_options=request_options, omit=omit, optional_body=optional_body
)

data_body = _maybe_filter_none_from_multipart_data(data_body, request_files, force_multipart)

Expand Down Expand Up @@ -561,6 +601,9 @@ def stream(
}
)
)
_request_headers = drop_content_type_without_body(
_request_headers, json_body=json_body, data_body=data_body, optional_body=optional_body
)

if self.logger.is_debug():
self.logger.debug(
Expand Down Expand Up @@ -638,6 +681,7 @@ async def request(
request_options: typing.Optional[RequestOptions] = None,
retries: int = 0,
omit: typing.Optional[typing.Any] = None,
optional_body: bool = False,
force_multipart: typing.Optional[bool] = None,
) -> httpx.Response:
base_url = self.get_base_url(base_url)
Expand All @@ -659,7 +703,9 @@ async def request(
if (request_files is None or len(request_files) == 0) and force_multipart:
request_files = FORCE_MULTIPART

json_body, data_body = get_request_body(json=json, data=data, request_options=request_options, omit=omit)
json_body, data_body = get_request_body(
json=json, data=data, request_options=request_options, omit=omit, optional_body=optional_body
)

data_body = _maybe_filter_none_from_multipart_data(data_body, request_files, force_multipart)

Expand Down Expand Up @@ -696,6 +742,9 @@ async def request(
}
)
)
_request_headers = drop_content_type_without_body(
_request_headers, json_body=json_body, data_body=data_body, optional_body=optional_body
)

if self.logger.is_debug():
self.logger.debug(
Expand Down Expand Up @@ -804,6 +853,7 @@ async def stream(
request_options: typing.Optional[RequestOptions] = None,
retries: int = 0,
omit: typing.Optional[typing.Any] = None,
optional_body: bool = False,
force_multipart: typing.Optional[bool] = None,
) -> typing.AsyncIterator[httpx.Response]:
base_url = self.get_base_url(base_url)
Expand All @@ -825,7 +875,9 @@ async def stream(
if (request_files is None or len(request_files) == 0) and force_multipart:
request_files = FORCE_MULTIPART

json_body, data_body = get_request_body(json=json, data=data, request_options=request_options, omit=omit)
json_body, data_body = get_request_body(
json=json, data=data, request_options=request_options, omit=omit, optional_body=optional_body
)

data_body = _maybe_filter_none_from_multipart_data(data_body, request_files, force_multipart)

Expand Down Expand Up @@ -862,6 +914,9 @@ async def stream(
}
)
)
_request_headers = drop_content_type_without_body(
_request_headers, json_body=json_body, data_body=data_body, optional_body=optional_body
)

if self.logger.is_debug():
self.logger.debug(
Expand Down
4 changes: 4 additions & 0 deletions src/cohere/core/http_response.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ def headers(self) -> Dict[str, str]:
def status_code(self) -> int:
return self._response.status_code

@property
def response(self) -> httpx.Response:
return self._response


class HttpResponse(Generic[T], BaseHttpResponse):
"""HTTP response wrapper that exposes response headers and data."""
Expand Down
13 changes: 13 additions & 0 deletions src/cohere/core/jsonable_encoder.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from pathlib import PurePath
from types import GeneratorType
from typing import Any, Callable, Dict, List, Optional, Set, Union
from urllib.parse import quote

import pydantic
from .datetime_utils import serialize_datetime
Expand Down Expand Up @@ -118,3 +119,15 @@ def encode_path_param(obj: Any) -> str:
if isinstance(obj, bool):
return "true" if obj else "false"
return str(jsonable_encoder(obj))


def quote_path_param(obj: Any) -> str:
"""Encode a value for use in a URL path segment, percent-encoding it.

Same as encode_path_param, except the result is percent-encoded so
that a value containing "/" or ".." cannot change which endpoint
the request resolves to.
"""
if isinstance(obj, bool):
return "true" if obj else "false"
return quote(str(jsonable_encoder(obj)), safe="")
Loading
Loading