Files
wehub-resource-sync 97e91a83f3
Ruff / Ruff (push) Waiting to run
Test / Core Tests (push) Waiting to run
Test / Offline Coverage Tests (Python 3.10) (push) Waiting to run
Test / Offline Coverage Tests (Python 3.11) (push) Waiting to run
Test / Offline Coverage Tests (Python 3.12) (push) Waiting to run
Test / Offline Coverage Tests (Python 3.13) (push) Waiting to run
Test / Offline Coverage Tests (Python 3.9) (push) Waiting to run
Test / Full Coverage (Python 3.11) (push) Waiting to run
Test / Core Provider Tests (OpenAI) (push) Blocked by required conditions
Test / Core Provider Tests (Anthropic) (push) Blocked by required conditions
Test / Core Provider Tests (Google) (push) Blocked by required conditions
Test / Core Provider Tests (Other) (push) Blocked by required conditions
Test / Anthropic Tests (push) Blocked by required conditions
Test / Gemini Tests (push) Blocked by required conditions
Test / Google GenAI Tests (push) Blocked by required conditions
Test / Vertex AI Tests (push) Blocked by required conditions
Test / OpenAI Tests (push) Blocked by required conditions
Test / Writer Tests (push) Blocked by required conditions
Test / Auto Client Tests (push) Blocked by required conditions
ty / type-check (push) Waiting to run
chore: import upstream snapshot with attribution
2026-07-13 13:36:38 +08:00

478 lines
15 KiB
Python

"""Focused coverage for the v2 Cohere clients and mode handlers."""
from __future__ import annotations
import builtins
import importlib
import runpy
from collections.abc import AsyncIterator, Iterator
from pathlib import Path
from types import SimpleNamespace
from typing import Any, cast
from unittest.mock import AsyncMock, Mock
import cohere
import httpx
import pytest
import pytest_asyncio
from pydantic import BaseModel
from instructor.v2.core.client import AsyncInstructor, Instructor
from instructor.v2.core.errors import (
ClientError,
ConfigurationError,
ModeError,
ResponseParsingError,
)
from instructor.v2.core.mode import Mode, reset_deprecated_mode_warnings
from instructor.v2.core.providers import Provider
from instructor.v2.dsl.iterable import IterableModel
from instructor.v2.providers.cohere.client import from_cohere
from instructor.v2.providers.cohere.handlers import (
CohereJSONSchemaHandler,
CohereMDJSONHandler,
CohereToolsHandler,
_convert_messages_to_cohere_v1,
_extract_text_from_response,
_extract_text_from_stream_chunk,
)
from instructor.v2.providers.cohere.templating import process_message
from tests.coverage._streams import async_items
class Answer(BaseModel):
answer: int
@pytest.fixture
def http_client() -> Iterator[httpx.Client]:
with httpx.Client(trust_env=False) as client:
yield client
@pytest_asyncio.fixture
async def async_http_client() -> AsyncIterator[httpx.AsyncClient]:
async with httpx.AsyncClient(trust_env=False) as client:
yield client
def _v1_response(text: str) -> cohere.NonStreamedChatResponse:
return cohere.NonStreamedChatResponse(text=text, finish_reason="COMPLETE")
def _v2_response(text: str) -> cohere.ChatResponse:
return cohere.ChatResponse(
id="response-1",
finish_reason="COMPLETE",
message=cohere.AssistantMessageResponse(
content=[cohere.TextAssistantMessageResponseContentItem(text=text)]
),
)
def _v2_delta(text: str | None) -> cohere.ContentDeltaV2ChatStreamResponse:
return cohere.ContentDeltaV2ChatStreamResponse(
delta=cohere.ChatContentDeltaEventDelta(
message=cohere.ChatContentDeltaEventDeltaMessage(
content=cohere.ChatContentDeltaEventDeltaMessageContent(text=text)
)
)
)
def _stream_events(answer: int) -> list[Any]:
return [
_v2_delta('{"tasks":['),
cohere.ContentEndV2ChatStreamResponse(),
_v2_delta(f'{{"answer":{answer}}}]}}'),
]
@pytest.mark.parametrize(
("client_type", "response_factory", "version"),
[
(cohere.Client, _v1_response, "v1"),
(cohere.ClientV2, _v2_response, "v2"),
],
)
def test_sync_cohere_clients_retry_and_stream(
client_type: type[Any],
response_factory: Any,
version: str,
http_client: httpx.Client,
) -> None:
raw = client_type(
api_key="test-key",
base_url="http://127.0.0.1:9",
httpx_client=http_client,
)
raw.chat = Mock(
side_effect=[
response_factory('{"answer":"wrong"}'),
response_factory('{"answer":7}'),
]
)
raw.chat_stream = Mock(return_value=iter(_stream_events(8)))
wrapped = from_cohere(raw, mode=Mode.TOOLS)
assert isinstance(wrapped, Instructor)
assert wrapped.provider is Provider.COHERE
assert wrapped.mode is Mode.TOOLS
assert wrapped.kwargs["_cohere_client_version"] == version
result = wrapped.create(
response_model=Answer,
messages=[{"role": "user", "content": "Give the answer"}],
model_name="command-r",
max_retries=1,
)
streamed = list(
wrapped.create_iterable(
response_model=Answer,
messages=[{"role": "user", "content": "Stream the answer"}],
model_name="command-r",
max_retries=0,
)
)
assert result.model_dump() == {"answer": 7}
assert streamed == [Answer(answer=8)]
assert raw.chat.call_count == 2
assert raw.chat_stream.call_count == 1
retry_kwargs = raw.chat.call_args_list[1].kwargs
stream_kwargs = raw.chat_stream.call_args.kwargs
assert retry_kwargs["model"] == stream_kwargs["model"] == "command-r"
assert "model_name" not in retry_kwargs
assert "stream" not in stream_kwargs
if version == "v1":
assert "messages" not in retry_kwargs
assert "Correct the following JSON response" in retry_kwargs["message"]
assert retry_kwargs["chat_history"][-1]["message"] == "Give the answer"
assert stream_kwargs["message"] == "Stream the answer"
else:
assert (
"Correct the following JSON response"
in retry_kwargs["messages"][-1]["content"]
)
assert stream_kwargs["messages"][-1]["content"] == "Stream the answer"
def test_cohere_legacy_tools_mode_warns_and_normalizes(
http_client: httpx.Client,
) -> None:
raw = cohere.ClientV2(
api_key="test-key",
base_url="http://127.0.0.1:9",
httpx_client=http_client,
)
reset_deprecated_mode_warnings()
with pytest.warns(DeprecationWarning, match=r"Mode\.COHERE_TOOLS.*Mode\.TOOLS"):
wrapped = from_cohere(raw, mode=Mode.COHERE_TOOLS)
assert wrapped.mode is Mode.TOOLS
reset_deprecated_mode_warnings()
@pytest.mark.parametrize(
"message",
[
{"role": "user", "content": "{{ name }}"},
{"role": "user", "message": [{"type": "text", "text": "{{ name }}"}]},
{"role": "user", "message": None},
],
)
def test_cohere_templating_leaves_non_string_messages_unchanged(
message: dict[str, Any],
) -> None:
calls: list[tuple[str, dict[str, Any]]] = []
def apply_template(value: str, context: dict[str, Any]) -> str:
calls.append((value, context))
return value
result = process_message(message, {"name": "Ada"}, apply_template)
assert result is message
assert calls == []
def test_cohere_templating_applies_context_to_a_string_message() -> None:
message = {"role": "user", "message": "Hello, {{ name }}"}
result = process_message(
message,
{"name": "Ada"},
lambda value, context: value.replace("{{ name }}", context["name"]),
)
assert result == {"role": "user", "message": "Hello, Ada"}
@pytest.mark.asyncio
@pytest.mark.parametrize(
("client_type", "response_factory", "version"),
[
(cohere.AsyncClient, _v1_response, "v1"),
(cohere.AsyncClientV2, _v2_response, "v2"),
],
)
async def test_async_cohere_clients_retry_and_stream(
client_type: type[Any],
response_factory: Any,
version: str,
async_http_client: httpx.AsyncClient,
) -> None:
raw = client_type(
api_key="test-key",
base_url="http://127.0.0.1:9",
httpx_client=async_http_client,
)
responses = [
response_factory('{"answer":"wrong"}'),
response_factory('{"answer":9}'),
]
raw.chat = AsyncMock(side_effect=responses)
raw.chat_stream = Mock(return_value=async_items(_stream_events(10)))
wrapped = from_cohere(raw, mode=Mode.TOOLS)
assert isinstance(wrapped, AsyncInstructor)
assert wrapped.provider is Provider.COHERE
assert wrapped.mode is Mode.TOOLS
assert wrapped.kwargs["_cohere_client_version"] == version
result = await wrapped.create(
response_model=Answer,
messages=[{"role": "user", "content": "Give the answer"}],
model_name="command-r",
max_retries=1,
)
streamed = [
item
async for item in wrapped.create_iterable(
response_model=Answer,
messages=[{"role": "user", "content": "Stream the answer"}],
model_name="command-r",
max_retries=0,
)
]
assert result.model_dump() == {"answer": 9}
assert streamed == [Answer(answer=10)]
assert raw.chat.await_count == 2
assert raw.chat_stream.call_count == 1
retry_kwargs = raw.chat.call_args_list[1].kwargs
stream_kwargs = raw.chat_stream.call_args.kwargs
assert retry_kwargs["model"] == stream_kwargs["model"] == "command-r"
assert "stream" not in stream_kwargs
if version == "v1":
assert "Correct the following JSON response" in retry_kwargs["message"]
assert stream_kwargs["message"] == "Stream the answer"
else:
assert (
"Correct the following JSON response"
in retry_kwargs["messages"][-1]["content"]
)
assert stream_kwargs["messages"][-1]["content"] == "Stream the answer"
@pytest.mark.asyncio
async def test_async_cohere_client_accepts_a_non_awaitable_chat_fallback(
monkeypatch: pytest.MonkeyPatch,
async_http_client: httpx.AsyncClient,
) -> None:
raw = cohere.AsyncClientV2(
api_key="test-key",
base_url="http://127.0.0.1:9",
httpx_client=async_http_client,
)
chat = Mock(return_value=_v2_response('{"answer":13}'))
monkeypatch.setattr(raw, "chat", chat)
wrapped = from_cohere(raw, mode=Mode.TOOLS)
result = await wrapped.create(
response_model=Answer,
messages=[{"role": "user", "content": "Give the answer"}],
model_name="command-r",
max_retries=0,
)
assert result.model_dump() == {"answer": 13}
chat.assert_called_once()
def test_cohere_factory_rejects_bad_mode_and_client(
http_client: httpx.Client,
) -> None:
raw = cohere.ClientV2(
api_key="test-key",
base_url="http://127.0.0.1:9",
httpx_client=http_client,
)
with pytest.raises(ModeError, match="Invalid mode 'json_mode'") as mode_error:
from_cohere(raw, mode=Mode.JSON)
assert mode_error.value.provider == "cohere"
assert "tool_call" in mode_error.value.valid_modes
with pytest.raises(ClientError, match="Got: object"):
from_cohere(cast(cohere.ClientV2, object()), mode=Mode.TOOLS)
def test_cohere_imports_fail_cleanly_without_optional_sdk(
monkeypatch: pytest.MonkeyPatch,
) -> None:
real_import = builtins.__import__
client_path = Path(
importlib.import_module("instructor.v2.providers.cohere.client").__file__
)
package_path = client_path.with_name("__init__.py")
def block_cohere(name: str, *args: Any, **kwargs: Any) -> Any:
if name == "cohere":
raise ImportError("cohere is unavailable")
return real_import(name, *args, **kwargs)
with monkeypatch.context() as context:
context.setattr(builtins, "__import__", block_cohere)
client_globals = runpy.run_path(str(client_path))
assert client_globals["cohere"] is None
with pytest.raises(ClientError, match="cohere is not installed"):
client_globals["from_cohere"](object())
def block_client(name: str, *args: Any, **kwargs: Any) -> Any:
if name == "instructor.v2.providers.cohere.client":
raise ImportError("cohere client is unavailable")
return real_import(name, *args, **kwargs)
with monkeypatch.context() as context:
context.setattr(builtins, "__import__", block_client)
package_globals = runpy.run_path(str(package_path))
assert package_globals["from_cohere"] is None
def test_cohere_v1_conversion_and_invalid_response_shapes() -> None:
converted = _convert_messages_to_cohere_v1(
{
"_cohere_client_version": "v1",
"messages": [
{"role": "user", "content": "First"},
{"role": "assistant", "content": "Second"},
{"role": "user", "content": "Third"},
],
"model_name": "command-r",
"strict": True,
}
)
assert converted == {
"message": "Third",
"chat_history": [
{"role": "user", "message": "First"},
{"role": "assistant", "message": "Second"},
],
"model": "command-r",
}
with pytest.raises(ResponseParsingError, match="Could not extract text"):
_extract_text_from_response(object())
with pytest.raises(ResponseParsingError, match="Could not extract text"):
_extract_text_from_response(
SimpleNamespace(
message=SimpleNamespace(
content=[
SimpleNamespace(type="thinking", text="ignored"),
SimpleNamespace(type="image"),
]
)
)
)
def test_cohere_stream_chunk_shapes_and_sync_extractor() -> None:
v1_text = cohere.TextGenerationStreamedChatResponse(text="v1 text")
v2_text = _v2_delta("v2 text")
empty_delta = _v2_delta(None)
content_end = cohere.ContentEndV2ChatStreamResponse()
assert _extract_text_from_stream_chunk(v1_text) == "v1 text"
assert _extract_text_from_stream_chunk(v2_text) == "v2 text"
assert _extract_text_from_stream_chunk(empty_delta) is None
assert _extract_text_from_stream_chunk(content_end) is None
assert list(
CohereToolsHandler().extract_streaming_json(
[content_end, v1_text, empty_delta, v2_text]
)
) == ["v1 text", "v2 text"]
@pytest.mark.asyncio
async def test_cohere_tools_stream_parser_forwards_context_and_strictness() -> None:
handler = CohereToolsHandler()
iterable_answer = IterableModel(Answer)
sync_values = list(
handler.parse_response(
_stream_events(11),
iterable_answer,
validation_context=None,
strict=None,
stream=True,
is_async=False,
)
)
async_values = [
item
async for item in handler.parse_response(
async_items(_stream_events(12)),
iterable_answer,
validation_context={"request_id": "req-1"},
strict=True,
stream=True,
is_async=True,
)
]
assert sync_values == [Answer(answer=11)]
assert async_values == [Answer(answer=12)]
with pytest.raises(ConfigurationError, match="Iterable or Partial"):
handler.parse_response(iter(_stream_events(13)), Answer, stream=True)
def test_cohere_tools_falls_back_when_tool_call_has_no_parameters() -> None:
response = SimpleNamespace(
text='{"answer":14}', tool_calls=[SimpleNamespace(name="answer")]
)
assert CohereToolsHandler().parse_response(response, Answer) == Answer(answer=14)
@pytest.mark.parametrize("handler", [CohereJSONSchemaHandler(), CohereMDJSONHandler()])
def test_cohere_json_handlers_reask_v1_and_reject_streaming(handler: Any) -> None:
without_history = handler.handle_reask(
{"message": "Original question"}, object(), ValueError("invalid field")
)
with_history = handler.handle_reask(
{
"message": "Original question",
"chat_history": [{"role": "assistant", "message": "Earlier"}],
},
_v1_response('{"answer":"wrong"}'),
ValueError("invalid field"),
)
assert without_history["chat_history"] == [
{"role": "user", "message": "Original question"}
]
assert "invalid field" in without_history["message"]
assert "object object" in without_history["message"]
assert with_history["chat_history"][-1] == {
"role": "user",
"message": "Original question",
}
assert 'JSON:\n{"answer":"wrong"}' in with_history["message"]
with pytest.raises(ConfigurationError, match="Streaming is not supported"):
handler.parse_response(iter(_stream_events(15)), Answer, stream=True)