Files
567-labs--instructor/tests/coverage/test_xai_handlers_coverage.py
wehub-resource-sync 97e91a83f3
Ruff / Ruff (push) Has been cancelled
Test / Core Tests (push) Has been cancelled
Test / Offline Coverage Tests (Python 3.10) (push) Has been cancelled
Test / Offline Coverage Tests (Python 3.11) (push) Has been cancelled
Test / Offline Coverage Tests (Python 3.12) (push) Has been cancelled
Test / Offline Coverage Tests (Python 3.13) (push) Has been cancelled
Test / Offline Coverage Tests (Python 3.9) (push) Has been cancelled
Test / Full Coverage (Python 3.11) (push) Has been cancelled
Test / Core Provider Tests (OpenAI) (push) Has been cancelled
Test / Core Provider Tests (Anthropic) (push) Has been cancelled
Test / Core Provider Tests (Google) (push) Has been cancelled
Test / Core Provider Tests (Other) (push) Has been cancelled
Test / Anthropic Tests (push) Has been cancelled
Test / Gemini Tests (push) Has been cancelled
Test / Google GenAI Tests (push) Has been cancelled
Test / Vertex AI Tests (push) Has been cancelled
Test / OpenAI Tests (push) Has been cancelled
Test / Writer Tests (push) Has been cancelled
Test / Auto Client Tests (push) Has been cancelled
ty / type-check (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:36:38 +08:00

625 lines
21 KiB
Python

"""Offline coverage for the xAI v2 request, retry, and response handlers."""
from __future__ import annotations
import runpy
import sys
from collections.abc import Iterable
from types import SimpleNamespace
from typing import Any, Union, cast
import pytest
pytest.importorskip("xai_sdk")
from pydantic import BaseModel, ValidationError
import instructor.v2.core.decorators as decorators
from instructor.v2.core.errors import ConfigurationError
from instructor.v2.dsl.iterable import IterableBase, IterableModel
from instructor.v2.dsl.parallel import ParallelBase
from instructor.v2.dsl.partial import Partial
from instructor.v2.dsl.simple_type import ModelAdapter
from instructor.v2.providers.xai import handlers
from tests.coverage._streams import async_items
class Answer(BaseModel):
"""A numeric answer returned by Grok."""
answer: float
class User(BaseModel):
"""A user returned by Grok."""
name: str
age: int
def _tool_call(
name: str,
arguments: dict[str, Any] | str | None,
tool_id: str | None = None,
) -> SimpleNamespace:
return SimpleNamespace(
id=tool_id,
function=SimpleNamespace(name=name, arguments=arguments),
)
def _chunk(
*,
content: str | None = None,
tool_calls: list[Any] | None = None,
wrapped: bool = True,
) -> SimpleNamespace:
delta = SimpleNamespace(content=content, tool_calls=tool_calls)
if wrapped:
return SimpleNamespace(choices=[SimpleNamespace(delta=delta)])
return SimpleNamespace(delta=delta)
def test_optional_xai_sdk_import_has_a_clear_runtime_error(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Importing the provider without its optional SDK must fail only on use."""
monkeypatch.setitem(sys.modules, "xai_sdk", None)
monkeypatch.setattr(
decorators,
"register_mode_handler",
lambda *_args: lambda handler: handler,
)
isolated = runpy.run_path(handlers.__file__, run_name="xai_without_sdk")
assert isolated["xchat"] is None
with pytest.raises(ImportError, match="xai_sdk is required"):
isolated["_convert_messages"]([{"role": "user", "content": "hello"}])
def test_convert_messages_maps_all_xai_roles_and_rejects_bad_content(
monkeypatch: pytest.MonkeyPatch,
) -> None:
fake_chat = SimpleNamespace(
text=lambda value: ("text", value),
user=lambda value: ("user", value),
assistant=lambda value: ("assistant", value),
system=lambda value: ("system", value),
tool_result=lambda value: ("tool_result", value),
)
monkeypatch.setattr(handlers, "xchat", fake_chat)
converted = handlers._convert_messages(
[
{"role": "user", "content": "question"},
{"role": "assistant", "content": "thinking"},
{"role": "system", "content": "be exact"},
{"role": "tool", "content": '{"answer": 4}'},
]
)
assert converted == [
("user", ("text", "question")),
("assistant", ("text", "thinking")),
("system", ("text", "be exact")),
("tool_result", '{"answer": 4}'),
]
with pytest.raises(ValueError, match="Only string content"):
handlers._convert_messages(
[
{
"role": "user",
"content": [
{"type": "text", "text": "What is here?"},
{"type": "image_url", "image_url": "data:image/png;base64,AA"},
],
}
]
)
with pytest.raises(ValueError, match="Unsupported role: developer"):
handlers._convert_messages([{"role": "developer", "content": "rules"}])
def test_legacy_request_helpers_convert_messages_strip_retry_fields_and_make_tool(
monkeypatch: pytest.MonkeyPatch,
) -> None:
fake_chat = SimpleNamespace(
text=lambda value: value,
user=lambda value: {"xai_role": "user", "text": value},
assistant=lambda value: {"xai_role": "assistant", "text": value},
system=lambda value: {"xai_role": "system", "text": value},
tool_result=lambda value: {"xai_role": "tool", "text": value},
tool=lambda **value: value,
)
monkeypatch.setattr(handlers, "xchat", fake_chat)
request = {
"messages": [{"role": "user", "content": "2 + 2?"}],
"max_retries": 3,
"context": {"tenant": "test"},
"hooks": object(),
"temperature": 0,
}
json_model, json_kwargs = handlers.handle_xai_json(Answer, request.copy())
tool_model, tool_kwargs = handlers.handle_xai_tools(Answer, request.copy())
no_model, no_model_kwargs = handlers.handle_xai_tools(None, request.copy())
assert json_model is Answer
assert json_kwargs["x_messages"] == [{"xai_role": "user", "text": "2 + 2?"}]
assert {"max_retries", "context", "hooks"}.isdisjoint(json_kwargs)
assert json_kwargs["temperature"] == 0
assert tool_model is Answer
assert tool_kwargs["tool"]["name"] == "Answer"
assert tool_kwargs["tool"]["description"] == "A numeric answer returned by Grok."
assert tool_kwargs["tool"]["parameters"]["properties"]["answer"]["type"] == "number"
assert no_model is None
assert "tool" not in no_model_kwargs
def test_legacy_reask_helpers_preserve_the_failed_response_and_validation_error() -> (
None
):
response = SimpleNamespace(text="not a number")
original = {"messages": [{"role": "user", "content": "2 + 2?"}]}
error = ValueError("answer must be a number")
json_retry = handlers.reask_xai_json(original, response, error)
tools_retry = handlers.reask_xai_tools(original, response, error)
assert json_retry is not original
assert json_retry["messages"][-3]["role"] == "user"
assert "Validation Errors found" in json_retry["messages"][-3]["content"]
assert "answer must be a number" in json_retry["messages"][-3]["content"]
assert "not a number" in json_retry["messages"][-3]["content"]
assert tools_retry is not original
assert tools_retry["messages"][-2] == {
"role": "assistant",
"content": str(response),
}
assert tools_retry["messages"][-1]["role"] == "user"
assert "Validation Error found" in tools_retry["messages"][-1]["content"]
assert "answer must be a number" in tools_retry["messages"][-1]["content"]
def test_streaming_flags_only_track_streamable_models_and_are_consumed_once() -> None:
handler = handlers.XAIToolsHandler()
users = IterableModel(User)
partial_answer = Partial[Answer]
handler._register_streaming_from_kwargs(None, {"stream": True})
handler.mark_streaming_model(None, True)
handler.mark_streaming_model(Answer, False)
handler.mark_streaming_model(Answer, True)
assert handler._consume_streaming_flag(None) is False
assert handler._consume_streaming_flag(ParallelBase(Answer)) is False
assert handler._consume_streaming_flag(Answer) is False
handler._register_streaming_from_kwargs(users, {"stream": True})
handler._register_streaming_from_kwargs(partial_answer, {"stream": True})
assert handler._consume_streaming_flag(users) is True
assert handler._consume_streaming_flag(users) is False
assert handler._consume_streaming_flag(partial_answer) is True
def test_sync_stream_extractors_handle_content_fences_and_cumulative_tool_deltas() -> (
None
):
tool_handler = handlers.XAIToolsHandler()
schema_handler = handlers.XAIJSONSchemaHandler()
markdown_handler = handlers.XAIMDJSONHandler()
tool_stream = [
SimpleNamespace(choices=[SimpleNamespace(delta=None)]),
_chunk(tool_calls=[SimpleNamespace(id="ignored", function=None)]),
_chunk(tool_calls=[_tool_call("Answer", '{"answer":', "call-1")]),
_chunk(tool_calls=[_tool_call("Answer", '{"answer": 4', "call-1")]),
_chunk(tool_calls=[_tool_call("Answer", '{"answer": 4.0}', "call-2")]),
_chunk(tool_calls=[_tool_call("Answer", '{"answer": 4.0}', "call-2")]),
]
schema_stream = [
SimpleNamespace(delta=None),
_chunk(content=None, wrapped=False),
_chunk(content='{"answer": ', wrapped=False),
_chunk(content="5.0}", wrapped=False),
]
markdown_stream = [
_chunk(content="Here is the result:\n```json\n"),
_chunk(content='{"answer": 6.0}'),
_chunk(content="\n```"),
]
assert (
"".join(tool_handler.extract_streaming_json(tool_stream)) == '{"answer": 4.0}'
)
assert (
"".join(schema_handler.extract_streaming_json(schema_stream))
== '{"answer": 5.0}'
)
assert (
"".join(markdown_handler.extract_streaming_json(markdown_stream))
== '{"answer": 6.0}'
)
assert (
list(
handlers.XAIParallelToolsHandler().extract_streaming_json(
[_chunk(content='{"answer": 6.0}')]
)
)
== []
)
@pytest.mark.asyncio
async def test_async_stream_extractors_handle_content_fences_and_cumulative_tool_deltas() -> (
None
):
tool_handler = handlers.XAIToolsHandler()
schema_handler = handlers.XAIJSONSchemaHandler()
markdown_handler = handlers.XAIMDJSONHandler()
tool_stream = [
SimpleNamespace(choices=[SimpleNamespace(delta=None)]),
_chunk(tool_calls=[SimpleNamespace(id="ignored", function=None)]),
_chunk(tool_calls=[_tool_call("Answer", '{"answer":', None)]),
_chunk(tool_calls=[_tool_call("Answer", '{"answer": 7', None)]),
_chunk(tool_calls=[_tool_call("Answer", '{"answer": 7.0}', "call-2")]),
_chunk(tool_calls=[_tool_call("Answer", '{"answer": 7.0}', "call-2")]),
]
schema_stream = [
SimpleNamespace(delta=None),
_chunk(content=None, wrapped=False),
_chunk(content='{"answer": ', wrapped=False),
_chunk(content="8.0}", wrapped=False),
]
markdown_stream = [
_chunk(content="```json\n"),
_chunk(content='{"answer": 9.0}'),
_chunk(content="\n```"),
]
tool_chunks = [
chunk
async for chunk in tool_handler.extract_streaming_json_async(
async_items(tool_stream)
)
]
schema_chunks = [
chunk
async for chunk in schema_handler.extract_streaming_json_async(
async_items(schema_stream)
)
]
markdown_chunks = [
chunk
async for chunk in markdown_handler.extract_streaming_json_async(
async_items(markdown_stream)
)
]
assert "".join(tool_chunks) == '{"answer": 7.0}'
assert "".join(schema_chunks) == '{"answer": 8.0}'
assert "".join(markdown_chunks) == '{"answer": 9.0}'
assert [
chunk
async for chunk in handlers.XAIParallelToolsHandler().extract_streaming_json_async(
async_items([_chunk(content='{"answer": 9.0}')])
)
] == []
def test_tools_streaming_parser_returns_complete_iterable_items_with_validation_options() -> (
None
):
handler = handlers.XAIToolsHandler()
users = IterableModel(User)
prepared, request = handler.prepare_request(users, {"stream": True})
assert prepared is not None
stream = [
_chunk(
tool_calls=[
_tool_call("IterableUser", '{"tasks": [{"name": "Ada"', "users")
]
),
_chunk(
tool_calls=[
_tool_call(
"IterableUser",
'{"tasks": [{"name": "Ada", "age": 31}, {"name": "Lin", "age": 29}]}',
"users",
)
]
),
]
parsed = handler.parse_response(
stream,
prepared,
validation_context={"request_id": "xai-sync"},
strict=True,
)
assert request["stream"] is True
assert [(item.name, item.age) for item in parsed] == [("Ada", 31), ("Lin", 29)]
@pytest.mark.asyncio
async def test_json_schema_streaming_parser_returns_complete_async_iterable_items() -> (
None
):
handler = handlers.XAIJSONSchemaHandler()
users = IterableModel(User)
prepared, request = handler.prepare_request(users, {"stream": True})
assert prepared is not None
stream = [
_chunk(content='{"tasks": [{"name": "Ada"'),
_chunk(content=', "age": 31}, {"name": "Lin", "age": 29}]}'),
]
parsed = handler.parse_response(
async_items(stream),
prepared,
validation_context={"request_id": "xai-async"},
strict=True,
)
items = [item async for item in parsed]
assert request["_xai_json_schema"]["name"] == prepared.__name__
assert [(item.name, item.age) for item in items] == [("Ada", 31), ("Lin", 29)]
def test_markdown_streaming_parser_returns_partial_updates_and_a_complete_answer() -> (
None
):
handler = handlers.XAIMDJSONHandler()
partial_answer = Partial[Answer]
prepared, request = handler.prepare_request(
partial_answer,
{"messages": [{"role": "user", "content": "2 + 2?"}], "stream": True},
)
assert prepared is not None
stream = [
_chunk(content="```json\n"),
_chunk(content='{"answer": 4'),
_chunk(content=".0}\n```"),
]
parsed = handler.parse_response(
stream,
prepared,
validation_context={"request_id": "xai-partial"},
strict=True,
)
assert request["messages"][0]["role"] == "system"
assert isinstance(parsed, list)
assert parsed[-1].answer == 4.0
@pytest.mark.asyncio
async def test_async_partial_parser_and_custom_sync_stream_model_use_the_stream_extractor() -> (
None
):
handler = handlers.XAIJSONSchemaHandler()
partial_answer = Partial[Answer]
async_result = handler._parse_streaming_response(
partial_answer,
async_items([_chunk(content='{"answer": 1'), _chunk(content="2.0}")]),
validation_context={"request_id": "partial"},
strict=True,
)
async_updates = [item async for item in async_result]
class StreamingAnswer(BaseModel):
answer: float
@classmethod
def from_streaming_response(
cls,
completion: Iterable[Any],
stream_extractor: Any,
**kwargs: Any,
) -> Iterable[StreamingAnswer]:
assert kwargs == {"context": {"request_id": "custom"}, "strict": False}
payload = "".join(stream_extractor(completion))
yield cls.model_validate_json(payload, **kwargs)
sync_result = handler._parse_streaming_response(
StreamingAnswer,
[_chunk(content='{"answer": 13.0}')],
validation_context={"request_id": "custom"},
strict=False,
)
assert async_updates[-1].answer == 12.0
assert [item.answer for item in sync_result] == [13.0]
@pytest.mark.asyncio
async def test_streaming_iterables_allow_empty_streams_without_validation_options() -> (
None
):
handler = handlers.XAIJSONSchemaHandler()
users = IterableModel(User)
sync_result = handler._parse_streaming_response(
users, [], validation_context=None, strict=None
)
async_result = handler._parse_streaming_response(
users, async_items([]), validation_context=None, strict=False
)
assert list(sync_result) == []
assert [item async for item in async_result] == []
def test_finalize_unwraps_iterable_parallel_and_simple_type_results() -> None:
handler = handlers.XAIToolsHandler()
users = IterableModel(User)
parsed_users = users(tasks=[User(name="Ada", age=31), User(name="Lin", age=29)])
parallel = ParallelBase(Answer, User)
adapter_model = cast(type[BaseModel], ModelAdapter[int])
adapted = adapter_model(content=42)
iterable_result = handler._finalize_parsed_result(users, object(), parsed_users)
parallel_result = handler._finalize_parsed_result(parallel, object(), ["kept"])
adapted_result = handler._finalize_parsed_result(adapter_model, object(), adapted)
assert isinstance(parsed_users, IterableBase)
assert [(item.name, item.age) for item in iterable_result] == [
("Ada", 31),
("Lin", 29),
]
assert parallel_result == ["kept"]
assert adapted_result == 42
assert handler._finalize_parsed_result(Answer, object(), {"answer": 4.0}) == {
"answer": 4.0
}
def test_parallel_tools_build_schemas_parse_known_calls_and_skip_unknown_calls() -> (
None
):
handler = handlers.XAIParallelToolsHandler()
model = cast(type[BaseModel], Iterable[Union[Answer, User]])
request = {"messages": [{"role": "user", "content": "answer and user"}]}
no_model, unchanged = handler.prepare_request(None, request)
prepared, prepared_request = handler.prepare_request(model, request)
parsed = list(
handler.parse_response(
SimpleNamespace(
tool_calls=[
_tool_call("Answer", {"answer": 4.0}),
_tool_call("Ignored", '{"anything": true}'),
_tool_call("User", '{"name": "Ada", "age": 31}'),
]
),
model,
validation_context={"request_id": "parallel"},
strict=True,
)
)
empty = list(handler.parse_response(SimpleNamespace(tool_calls=None), model))
assert no_model is None
assert unchanged is request
assert isinstance(prepared, ParallelBase)
assert {tool["function"]["name"] for tool in prepared_request["_xai_tools"]} == {
"Answer",
"User",
}
assert [type(item) for item in parsed] == [Answer, User]
assert parsed[0].answer == 4.0
assert (parsed[1].name, parsed[1].age) == ("Ada", 31)
assert empty == []
def test_parallel_tools_reject_streaming_and_reask_with_the_failed_response() -> None:
handler = handlers.XAIParallelToolsHandler()
model = cast(type[BaseModel], Iterable[Union[Answer, User]])
response = SimpleNamespace(tool_calls=[_tool_call("Answer", '{"answer": "bad"}')])
with pytest.raises(ConfigurationError, match="stream=True is not supported"):
handler.prepare_request(model, {"stream": True})
retry = handler.handle_reask(
{"messages": [{"role": "user", "content": "answer"}]},
response,
ValueError("answer must be a number"),
)
assert retry["messages"][-2] == {"role": "assistant", "content": str(response)}
assert retry["messages"][-1]["role"] == "user"
assert "answer must be a number" in retry["messages"][-1]["content"]
def test_schema_and_markdown_parsers_accept_remaining_content_shapes_and_reject_refusal() -> (
None
):
schema_handler = handlers.XAIJSONSchemaHandler()
markdown_handler = handlers.XAIMDJSONHandler()
schema = schema_handler.parse_response(
SimpleNamespace(text=None, content=['{"answer": 16.0}']),
Answer,
strict=True,
)
markdown = markdown_handler.parse_response(
SimpleNamespace(text=None, content='```json\n{"answer": 17.0}\n```'),
Answer,
strict=True,
)
assert schema.answer == 16.0
assert markdown.answer == 17.0
with pytest.raises(ValueError, match="Could not parse xAI response"):
schema_handler.parse_response(
SimpleNamespace(text=None, content=None, refusal="I cannot help with that"),
Answer,
)
with pytest.raises(ValidationError, match="answer"):
markdown_handler.parse_response(
SimpleNamespace(text='```json\n{"answer": "not-a-number"}\n```'),
Answer,
strict=True,
)
@pytest.mark.parametrize(
("handler", "match"),
[
(handlers.XAIToolsHandler(), "No tool calls returned from xAI"),
(handlers.XAIJSONSchemaHandler(), "Could not parse xAI response"),
(handlers.XAIMDJSONHandler(), "Could not extract JSON from xAI response"),
],
)
def test_response_handlers_reject_unsupported_content_objects(
handler: Any, match: str
) -> None:
response = SimpleNamespace(tool_calls=[], text=None, content={"answer": 4.0})
with pytest.raises(ValueError, match=match):
handler.parse_response(response, Answer)
def test_json_schema_handler_rejects_an_unparsed_sdk_tuple() -> None:
raw = SimpleNamespace(id="raw-xai-response")
with pytest.raises(ValueError, match="Could not parse xAI response"):
handlers.XAIJSONSchemaHandler().parse_response((raw, {"answer": 4.0}), Answer)
def test_markdown_request_preserves_multimodal_system_text_and_handles_empty_messages() -> (
None
):
handler = handlers.XAIMDJSONHandler()
multimodal_messages = [
{
"role": "system",
"content": [
{"type": "text", "text": "Keep this instruction."},
{"type": "image_url", "image_url": "data:image/png;base64,AA"},
],
},
{"role": "user", "content": "What is in the image?"},
]
_, multimodal = handler.prepare_request(Answer, {"messages": multimodal_messages})
_, empty = handler.prepare_request(Answer, {"messages": []})
_, empty_system = handler.prepare_request(
Answer,
{"messages": [{"role": "system", "content": []}]},
)
first_part = multimodal["messages"][0]["content"][0]
assert first_part["text"].startswith("Keep this instruction.")
assert "json_schema" in first_part["text"]
assert multimodal["messages"][0]["content"][1] == {
"type": "image_url",
"image_url": "data:image/png;base64,AA",
}
assert multimodal["messages"][-1]["role"] == "user"
assert "```json codeblock" in multimodal["messages"][-1]["content"]
assert empty["messages"][0]["role"] == "system"
assert empty_system["messages"][0]["role"] == "system"
assert "json_schema" in empty_system["messages"][0]["content"]