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
1,005 changes: 996 additions & 9 deletions poetry.lock

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@ Repository = 'https://github.com/cohere-ai/cohere-python'
[tool.poetry.dependencies]
python = "^3.9"
fastavro = "^1.9.4"
httpx = ">=0.21.2"
httpx = ">=0.27.0"
httpx-aiohttp = "^0.1.12"
pydantic = ">= 1.9.2"
pydantic-core = ">=2.18.2"
requests = "^2.0.0"
Expand Down
3 changes: 2 additions & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
fastavro==1.9.4
httpx>=0.21.2
httpx>=0.27.0
httpx-aiohttp>=0.1.12
pydantic>= 1.9.2
pydantic-core>=2.18.2
requests==2.0.0
Expand Down
62 changes: 62 additions & 0 deletions scripts/demo_async_client_httpx.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
"""
Run chat twice: once with the SDK default async HTTP stack (HttpxAiohttpClient),
once with an explicit httpx.AsyncClient.

Requires CO_API_KEY in the environment. Optional: CO_MODEL (defaults to command-r-plus).
"""

from __future__ import annotations

import asyncio
import os
import sys

import httpx

from cohere import AsyncClient


def _api_key() -> str:
key = os.getenv("CO_API_KEY")
if not key:
print("Set CO_API_KEY to your Cohere API key.", file=sys.stderr)
sys.exit(1)
return key


def _model() -> str:
return os.getenv("CO_MODEL", "command-a-03-2025")


async def chat_default_httpx_aiohttp() -> None:
"""Uses the SDK default (aiohttp-backed HttpxAiohttpClient)."""
async with AsyncClient(api_key=_api_key()) as client:
inner = client._client_wrapper.httpx_client.httpx_client
print(f"default inner client: {type(inner).__module__}.{type(inner).__name__}")
response = await client.chat(message="Hello", model=_model())
print("default — reply:", (response.text or "")[:200])


async def chat_custom_httpx() -> None:
"""Uses a caller-provided httpx.AsyncClient."""
custom = httpx.AsyncClient(timeout=120.0)
try:
async with AsyncClient(api_key=_api_key(), httpx_client=custom) as client:
inner = client._client_wrapper.httpx_client.httpx_client
print(f"custom inner client: {type(inner).__module__}.{type(inner).__name__}")
assert inner is custom
response = await client.chat(message="Hello", model=_model())
print("custom — reply:", (response.text or "")[:200])
finally:
# Context manager already closed `custom` when using httpx_client=; this is a no-op if closed.
if not custom.is_closed:
await custom.aclose()


async def main() -> None:
await chat_default_httpx_aiohttp()
await chat_custom_httpx()


if __name__ == "__main__":
asyncio.run(main())
7 changes: 4 additions & 3 deletions src/cohere/base_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
from .types.tokenize_response import TokenizeResponse
from .types.tool import Tool
from .types.tool_result import ToolResult
from httpx_aiohttp import HttpxAiohttpClient
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Top-level import forces aiohttp for sync-only users

Medium Severity

The from httpx_aiohttp import HttpxAiohttpClient import is at the module top level, unconditionally executed when base_client.py is loaded. Since client.py imports BaseCohere and AsyncBaseCohere from this module, and cohere.__init__ imports from client.py, any import cohere — even for purely sync usage via cohere.Client — triggers loading of httpx_aiohttp and transitively all of aiohttp (including its C extensions). HttpxAiohttpClient is only used inside AsyncBaseCohere.__init__, so the import could be deferred there or placed behind typing.TYPE_CHECKING with a lazy import at the call site.

Fix in Cursor Fix in Web


if typing.TYPE_CHECKING:
from .audio.client import AsyncAudioClient, AudioClient
Expand Down Expand Up @@ -1611,7 +1612,7 @@ class AsyncBaseCohere:
Whether the default httpx client follows redirects or not, this is irrelevant if a custom httpx client is passed in.

httpx_client : typing.Optional[httpx.AsyncClient]
The httpx client to use for making requests, a preconfigured client is used by default, however this is useful should you want to pass in any custom httpx configuration.
The httpx-compatible async client to use for making requests. By default an aiohttp-backed client (httpx_aiohttp.HttpxAiohttpClient) is used for better concurrency under high parallelism; pass a custom httpx.AsyncClient if you need a different transport or configuration.

logging : typing.Optional[typing.Union[LogConfig, Logger]]
Configure logging for the SDK. Accepts a LogConfig dict with 'level' (debug/info/warn/error), 'logger' (custom logger implementation), and 'silent' (boolean, defaults to True) fields. You can also pass a pre-configured Logger instance.
Expand Down Expand Up @@ -1651,9 +1652,9 @@ def __init__(
headers=headers,
httpx_client=httpx_client
if httpx_client is not None
else httpx.AsyncClient(timeout=_defaulted_timeout, follow_redirects=follow_redirects)
else HttpxAiohttpClient(timeout=_defaulted_timeout, follow_redirects=follow_redirects)
if follow_redirects is not None
else httpx.AsyncClient(timeout=_defaulted_timeout),
else HttpxAiohttpClient(timeout=_defaulted_timeout),
timeout=_defaulted_timeout,
logging=logging,
)
Expand Down
28 changes: 26 additions & 2 deletions tests/test_async_client.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,16 @@
import os
import unittest

import httpx

import cohere
from cohere import ChatConnector, ClassifyExample, CreateConnectorServiceAuth, Tool, \
ToolParameterDefinitionsValue, ToolResult, UserMessage, ChatbotMessage
from cohere import (
ChatbotMessage,
Tool,
ToolParameterDefinitionsValue,
ToolResult,
UserMessage,
)

package_dir = os.path.dirname(os.path.abspath(__file__))
embed_job = os.path.join(package_dir, 'embed_job.jsonl')
Expand All @@ -23,6 +30,23 @@ async def test_context_manager(self) -> None:
async with cohere.AsyncClient(api_key="xxx") as client:
self.assertIsNotNone(client)

async def test_custom_httpx_async_client_is_used_verbatim(self) -> None:
"""A plain httpx.AsyncClient must be passed through, not replaced by HttpxAiohttpClient."""
custom = httpx.AsyncClient(timeout=30.0)
try:
client = cohere.AsyncClient(api_key="xxx", httpx_client=custom)
self.assertIs(client._client_wrapper.httpx_client.httpx_client, custom)
finally:
await custom.aclose()

async def test_async_client_v2_custom_httpx_async_client(self) -> None:
custom = httpx.AsyncClient(timeout=30.0)
try:
client = cohere.AsyncClientV2(api_key="xxx", httpx_client=custom)
self.assertIs(client._client_wrapper.httpx_client.httpx_client, custom)
finally:
await custom.aclose()

async def test_chat(self) -> None:
chat = await self.co.chat(
model="command-a-03-2025",
Expand Down
Loading