diff --git a/docs/api/models/grok.md b/docs/api/models/grok.md new file mode 100644 index 0000000000..c37429873f --- /dev/null +++ b/docs/api/models/grok.md @@ -0,0 +1,7 @@ +# `pydantic_ai.models.grok` + +## Setup + +For details on how to set up authentication with this model, see [model configuration for Grok](../../models/grok.md). + +::: pydantic_ai.models.grok diff --git a/docs/models/overview.md b/docs/models/overview.md index 75cf954b11..8e7b4aadc9 100644 --- a/docs/models/overview.md +++ b/docs/models/overview.md @@ -5,6 +5,7 @@ Pydantic AI is model-agnostic and has built-in support for multiple model provid * [OpenAI](openai.md) * [Anthropic](anthropic.md) * [Gemini](google.md) (via two different APIs: Generative Language API and VertexAI API) +* [Grok](grok.md) * [Groq](groq.md) * [Mistral](mistral.md) * [Cohere](cohere.md) diff --git a/docs/models/xai.md b/docs/models/xai.md new file mode 100644 index 0000000000..a5a9288b7b --- /dev/null +++ b/docs/models/xai.md @@ -0,0 +1,73 @@ +# xAI + +## Install + +To use [`XaiModel`][pydantic_ai.models.xai.XaiModel], you need to either install `pydantic-ai`, or install `pydantic-ai-slim` with the `xai` optional group: + +```bash +pip/uv-add "pydantic-ai-slim[xai]" +``` + +## Configuration + +To use xAI models from [xAI](https://x.ai/api) through their API, go to [console.x.ai](https://console.x.ai/team/default/api-keys) to create an API key. + +[`GrokModelName`][pydantic_ai.providers.grok.GrokModelName] contains a list of available xAI models. + +## Environment variable + +Once you have the API key, you can set it as an environment variable: + +```bash +export XAI_API_KEY='your-api-key' +``` + +You can then use [`XaiModel`][pydantic_ai.models.xai.XaiModel] by name: + +```python +from pydantic_ai import Agent + +agent = Agent('xai:grok-4-1-fast-non-reasoning') +... +``` + +Or initialise the model directly: + +```python +from pydantic_ai import Agent +from pydantic_ai.models.xai import XaiModel + +# Uses XAI_API_KEY environment variable +model = XaiModel('grok-4-1-fast-non-reasoning') +agent = Agent(model) +... +``` + +You can also customize the [`XaiModel`][pydantic_ai.models.xai.XaiModel] with a custom provider: + +```python +from pydantic_ai import Agent +from pydantic_ai.models.xai import XaiModel +from pydantic_ai.providers.xai import XaiProvider + +# Custom API key +provider = XaiProvider(api_key='your-api-key') +model = XaiModel('grok-4-1-fast-non-reasoning', provider=provider) +agent = Agent(model) +... +``` + +Or with a custom `xai_sdk.AsyncClient`: + +```python +from xai_sdk import AsyncClient +from pydantic_ai import Agent +from pydantic_ai.models.xai import XaiModel +from pydantic_ai.providers.xai import XaiProvider + +xai_client = AsyncClient(api_key='your-api-key') +provider = XaiProvider(xai_client=xai_client) +model = XaiModel('grok-4-1-fast-non-reasoning', provider=provider) +agent = Agent(model) +... +``` diff --git a/examples/pydantic_ai_examples/stock_analysis_agent.py b/examples/pydantic_ai_examples/stock_analysis_agent.py new file mode 100644 index 0000000000..a798ece0d1 --- /dev/null +++ b/examples/pydantic_ai_examples/stock_analysis_agent.py @@ -0,0 +1,88 @@ +"""Example of using Grok's server-side web_search tool. + +This agent: +1. Uses web_search to find the hottest performing stock yesterday +2. Provides buy analysis for the user +""" + +import logfire +from pydantic import BaseModel, Field + +from pydantic_ai import ( + Agent, + BuiltinToolCallPart, + WebSearchTool, +) +from pydantic_ai.models.xai import XaiModel + +logfire.configure() +logfire.instrument_pydantic_ai() + +# Configure for xAI API - XAI_API_KEY environment variable is required +# The model will automatically use XaiProvider with the API key from the environment + +# Create the model using XaiModel with server-side tools +model = XaiModel('grok-4-1-fast-non-reasoning') + + +class StockAnalysis(BaseModel): + """Analysis of top performing stock.""" + + stock_symbol: str = Field(description='Stock ticker symbol') + current_price: float = Field(description='Current stock price') + buy_analysis: str = Field(description='Brief analysis for whether to buy the stock') + + +# This agent uses server-side web search to research stocks +stock_analysis_agent = Agent[None, StockAnalysis]( + model=model, + output_type=StockAnalysis, + builtin_tools=[WebSearchTool()], + system_prompt=( + 'You are a stock analysis assistant. ' + 'Use web_search to find the hottest performing stock from yesterday on NASDAQ. ' + 'Provide the current price and a brief buy analysis explaining whether this is a good buy.' + ), +) + + +async def main(): + """Run the stock analysis agent.""" + query = 'What was the hottest performing stock on NASDAQ yesterday?' + + print('šŸ” Starting stock analysis...\n') + print(f'Query: {query}\n') + + async with stock_analysis_agent.run_stream(query) as result: + # Stream responses as they happen + async for message, _is_last in result.stream_responses(): + for part in message.parts: + if isinstance(part, BuiltinToolCallPart): + print(f'šŸ”§ Server-side tool: {part.tool_name}') + + # Access output after streaming is complete + output = await result.get_output() + + print('\nāœ… Analysis complete!\n') + + print(f'šŸ“Š Top Stock: {output.stock_symbol}') + print(f'šŸ’° Current Price: ${output.current_price:.2f}') + print(f'\nšŸ“ˆ Buy Analysis:\n{output.buy_analysis}') + + # Show usage statistics + usage = result.usage() + print('\nšŸ“Š Usage Statistics:') + print(f' Requests: {usage.requests}') + print(f' Input Tokens: {usage.input_tokens}') + print(f' Output Tokens: {usage.output_tokens}') + print(f' Total Tokens: {usage.total_tokens}') + + # Show server-side tools usage if available + if usage.details and 'server_side_tools_used' in usage.details: + print(f' Server-Side Tools: {usage.details["server_side_tools_used"]}') + + +if __name__ == '__main__': + import asyncio + + asyncio.run(main()) diff --git a/pydantic_ai_slim/pydantic_ai/builtin_tools.py b/pydantic_ai_slim/pydantic_ai/builtin_tools.py index 5559b3124a..3977f791d7 100644 --- a/pydantic_ai_slim/pydantic_ai/builtin_tools.py +++ b/pydantic_ai_slim/pydantic_ai/builtin_tools.py @@ -75,6 +75,7 @@ class WebSearchTool(AbstractBuiltinTool): * OpenAI Responses * Groq * Google + * xAI """ search_context_size: Literal['low', 'medium', 'high'] = 'medium' @@ -103,6 +104,7 @@ class WebSearchTool(AbstractBuiltinTool): * Anthropic, see * Groq, see + * xAI, see """ allowed_domains: list[str] | None = None @@ -114,6 +116,7 @@ class WebSearchTool(AbstractBuiltinTool): * Anthropic, see * Groq, see + * xAI, see """ max_uses: int | None = None @@ -159,6 +162,7 @@ class CodeExecutionTool(AbstractBuiltinTool): * Anthropic * OpenAI Responses * Google + * xAI """ kind: str = 'code_execution' @@ -280,6 +284,7 @@ class MCPServerTool(AbstractBuiltinTool): * OpenAI Responses * Anthropic + * xAI """ id: str @@ -298,6 +303,7 @@ class MCPServerTool(AbstractBuiltinTool): * OpenAI Responses * Anthropic + * xAI """ description: str | None = None @@ -306,6 +312,7 @@ class MCPServerTool(AbstractBuiltinTool): Supported by: * OpenAI Responses + * xAI """ allowed_tools: list[str] | None = None @@ -315,6 +322,7 @@ class MCPServerTool(AbstractBuiltinTool): * OpenAI Responses * Anthropic + * xAI """ headers: dict[str, str] | None = None @@ -325,6 +333,7 @@ class MCPServerTool(AbstractBuiltinTool): Supported by: * OpenAI Responses + * xAI """ kind: str = 'mcp_server' diff --git a/pydantic_ai_slim/pydantic_ai/models/__init__.py b/pydantic_ai_slim/pydantic_ai/models/__init__.py index b43681b0a4..ec5f3dc557 100644 --- a/pydantic_ai_slim/pydantic_ai/models/__init__.py +++ b/pydantic_ai_slim/pydantic_ai/models/__init__.py @@ -175,6 +175,24 @@ 'grok:grok-3-mini-fast', 'grok:grok-4', 'grok:grok-4-0709', + 'grok:grok-4-1-fast-non-reasoning', + 'grok:grok-4-1-fast-reasoning', + 'grok:grok-4-fast-non-reasoning', + 'grok:grok-4-fast-reasoning', + 'grok:grok-code-fast-1', + 'xai:grok-2-image-1212', + 'xai:grok-2-vision-1212', + 'xai:grok-3', + 'xai:grok-3-fast', + 'xai:grok-3-mini', + 'xai:grok-3-mini-fast', + 'xai:grok-4', + 'xai:grok-4-0709', + 'xai:grok-4-1-fast-non-reasoning', + 'xai:grok-4-1-fast-reasoning', + 'xai:grok-4-fast-non-reasoning', + 'xai:grok-4-fast-reasoning', + 'xai:grok-code-fast-1', 'groq:deepseek-r1-distill-llama-70b', 'groq:deepseek-r1-distill-qwen-32b', 'groq:distil-whisper-large-v3-en', @@ -804,6 +822,7 @@ def infer_model( # noqa: C901 'fireworks', 'github', 'grok', + 'xai', 'heroku', 'moonshotai', 'ollama', diff --git a/pydantic_ai_slim/pydantic_ai/models/xai.py b/pydantic_ai_slim/pydantic_ai/models/xai.py new file mode 100644 index 0000000000..b956191699 --- /dev/null +++ b/pydantic_ai_slim/pydantic_ai/models/xai.py @@ -0,0 +1,837 @@ +"""xAI model implementation using xAI SDK.""" + +from collections.abc import AsyncIterator, Iterator, Sequence +from contextlib import asynccontextmanager +from dataclasses import dataclass +from typing import Any, Literal, cast + +from typing_extensions import assert_never + +try: + import xai_sdk.chat as chat_types + + # Import xai_sdk components + from xai_sdk import AsyncClient + from xai_sdk.chat import assistant, file, image, system, tool, tool_result, user + from xai_sdk.tools import code_execution, get_tool_call_type, mcp, web_search # x_search not yet supported +except ImportError as _import_error: + raise ImportError( + 'Please install `xai-sdk` to use the xAI model, ' + 'you can use the `xai` optional group — `pip install "pydantic-ai-slim[xai]"`' + ) from _import_error + +from .._run_context import RunContext +from .._utils import now_utc +from ..builtin_tools import CodeExecutionTool, MCPServerTool, WebSearchTool +from ..exceptions import UserError +from ..messages import ( + AudioUrl, + BinaryContent, + BuiltinToolCallPart, + BuiltinToolReturnPart, + CachePoint, + DocumentUrl, + FilePart, + FinishReason, + ImageUrl, + ModelMessage, + ModelRequest, + ModelRequestPart, + ModelResponse, + ModelResponsePart, + ModelResponseStreamEvent, + RetryPromptPart, + SystemPromptPart, + TextPart, + ThinkingPart, + ToolCallPart, + ToolReturnPart, + UserPromptPart, + VideoUrl, +) +from ..models import ( + Model, + ModelRequestParameters, + StreamedResponse, + check_allow_model_requests, + download_item, +) +from ..profiles import ModelProfileSpec +from ..providers import Provider, infer_provider +from ..providers.grok import GrokModelName +from ..settings import ModelSettings +from ..usage import RequestUsage + +# Type alias for consistency +XaiModelName = GrokModelName + + +class XaiModelSettings(ModelSettings, total=False): + """Settings specific to xAI models. + + See [xAI SDK documentation](https://docs.x.ai/docs) for more details on these parameters. + """ + + logprobs: bool + """Whether to return log probabilities of the output tokens or not.""" + + top_logprobs: int + """An integer between 0 and 20 specifying the number of most likely tokens to return at each position.""" + + use_encrypted_content: bool + """Whether to use encrypted content for reasoning continuity.""" + + store_messages: bool + """Whether to store messages on xAI's servers for conversation continuity.""" + + user: str + """A unique identifier representing your end-user, which can help xAI to monitor and detect abuse.""" + + +class XaiModel(Model): + """A model that uses the xAI SDK to interact with xAI models.""" + + _model_name: str + _provider: Provider[AsyncClient] + + def __init__( + self, + model_name: XaiModelName, + *, + provider: Literal['xai'] | Provider[AsyncClient] = 'xai', + profile: ModelProfileSpec | None = None, + settings: ModelSettings | None = None, + ): + """Initialize the xAI model. + + Args: + model_name: The name of the xAI model to use (e.g., "grok-4-1-fast-non-reasoning") + provider: The provider to use for API calls. Defaults to `'xai'`. + profile: Optional model profile specification. Defaults to a profile picked by the provider based on the model name. + settings: Optional model settings. + """ + self._model_name = model_name + + if isinstance(provider, str): + provider = infer_provider(provider) + self._provider = provider + self.client = provider.client + + super().__init__(settings=settings, profile=profile or provider.model_profile) + + @property + def model_name(self) -> str: + """The model name.""" + return self._model_name + + @property + def system(self) -> str: + """The model provider.""" + return 'xai' + + async def _map_messages(self, messages: list[ModelMessage]) -> list[chat_types.chat_pb2.Message]: + """Convert pydantic_ai messages to xAI SDK messages.""" + xai_messages: list[chat_types.chat_pb2.Message] = [] + + for message in messages: + if isinstance(message, ModelRequest): + xai_messages.extend(await self._map_request_parts(message.parts)) + elif isinstance(message, ModelResponse): + if response_msg := self._map_response_parts(message.parts): + xai_messages.append(response_msg) + else: + assert_never(message) + + return xai_messages + + async def _map_request_parts(self, parts: Sequence[ModelRequestPart]) -> list[chat_types.chat_pb2.Message]: + """Map ModelRequest parts to xAI messages.""" + xai_messages: list[chat_types.chat_pb2.Message] = [] + + for part in parts: + if isinstance(part, SystemPromptPart): + xai_messages.append(system(part.content)) + elif isinstance(part, UserPromptPart): + if user_msg := await self._map_user_prompt(part): + xai_messages.append(user_msg) + elif isinstance(part, ToolReturnPart): + xai_messages.append(tool_result(part.model_response_str())) + elif isinstance(part, RetryPromptPart): + if part.tool_name is None: + # Retry prompt as user message + xai_messages.append(user(part.model_response())) + else: + # Retry prompt as tool result + xai_messages.append(tool_result(part.model_response())) + else: + assert_never(part) + + return xai_messages + + def _map_response_parts(self, parts: Sequence[ModelResponsePart]) -> chat_types.chat_pb2.Message | None: + """Map ModelResponse parts to an xAI assistant message.""" + # Collect content from response parts + texts: list[str] = [] + reasoning_texts: list[str] = [] + tool_calls: list[chat_types.chat_pb2.ToolCall] = [] + + # Track builtin tool calls to update their status with corresponding return parts + code_execution_tool_call: chat_types.chat_pb2.ToolCall | None = None + web_search_tool_call: chat_types.chat_pb2.ToolCall | None = None + + for item in parts: + if isinstance(item, TextPart): + texts.append(item.content) + elif isinstance(item, ThinkingPart): + # xAI models (grok) support reasoning_content directly + reasoning_texts.append(item.content) + elif isinstance(item, ToolCallPart): + tool_calls.append(self._map_tool_call(item)) + elif isinstance(item, BuiltinToolCallPart): + # Map builtin tool calls with appropriate status + builtin_call = self._map_builtin_tool_call_part(item) + if builtin_call: + tool_calls.append(builtin_call) + # Track specific tool calls for status updates + if item.tool_name == CodeExecutionTool.kind: + code_execution_tool_call = builtin_call + elif item.tool_name == WebSearchTool.kind: + web_search_tool_call = builtin_call + elif isinstance(item, BuiltinToolReturnPart): + # Update tool call status based on return part + self._update_builtin_tool_status(item, code_execution_tool_call, web_search_tool_call) + elif isinstance(item, FilePart): # pragma: no cover + # Files generated by models (e.g., from CodeExecutionTool) are not sent back + pass + else: + assert_never(item) + + # Create assistant message with content, reasoning_content, and tool_calls + return self._build_assistant_message(texts, reasoning_texts, tool_calls) + + def _map_tool_call(self, tool_call_part: ToolCallPart) -> chat_types.chat_pb2.ToolCall: + """Map a ToolCallPart to an xAI SDK ToolCall.""" + return chat_types.chat_pb2.ToolCall( + id=tool_call_part.tool_call_id, + function=chat_types.chat_pb2.FunctionCall( + name=tool_call_part.tool_name, + arguments=tool_call_part.args_as_json_str(), + ), + ) + + def _map_builtin_tool_call_part(self, item: BuiltinToolCallPart) -> chat_types.chat_pb2.ToolCall | None: + """Map a BuiltinToolCallPart to an xAI SDK ToolCall with appropriate type and status.""" + if not item.tool_call_id: + return None + + if item.tool_name == CodeExecutionTool.kind: + return chat_types.chat_pb2.ToolCall( + id=item.tool_call_id, + type=chat_types.chat_pb2.TOOL_CALL_TYPE_CODE_EXECUTION_TOOL, + status=chat_types.chat_pb2.TOOL_CALL_STATUS_COMPLETED, + function=chat_types.chat_pb2.FunctionCall( + name=CodeExecutionTool.kind, + arguments=item.args_as_json_str(), + ), + ) + elif item.tool_name == WebSearchTool.kind: + return chat_types.chat_pb2.ToolCall( + id=item.tool_call_id, + type=chat_types.chat_pb2.TOOL_CALL_TYPE_WEB_SEARCH_TOOL, + status=chat_types.chat_pb2.TOOL_CALL_STATUS_COMPLETED, + function=chat_types.chat_pb2.FunctionCall( + name=WebSearchTool.kind, + arguments=item.args_as_json_str(), + ), + ) + elif item.tool_name.startswith(MCPServerTool.kind): + return chat_types.chat_pb2.ToolCall( + id=item.tool_call_id, + type=chat_types.chat_pb2.TOOL_CALL_TYPE_MCP_TOOL, + status=chat_types.chat_pb2.TOOL_CALL_STATUS_COMPLETED, + function=chat_types.chat_pb2.FunctionCall( + name=item.tool_name, + arguments=item.args_as_json_str(), + ), + ) + return None + + def _update_builtin_tool_status( + self, + item: BuiltinToolReturnPart, + code_execution_tool_call: chat_types.chat_pb2.ToolCall | None, + web_search_tool_call: chat_types.chat_pb2.ToolCall | None, + ) -> None: + """Update the status of builtin tool calls based on their return parts.""" + if not isinstance(item.content, dict): + return + + content = cast(dict[str, Any], item.content) + status = content.get('status') + + # Update status if it failed or has an error + if status == 'failed' or 'error' in content: + if item.tool_name == CodeExecutionTool.kind and code_execution_tool_call is not None: + code_execution_tool_call.status = chat_types.chat_pb2.TOOL_CALL_STATUS_FAILED + if error_msg := content.get('error'): + code_execution_tool_call.error_message = str(error_msg) + elif item.tool_name == WebSearchTool.kind and web_search_tool_call is not None: + web_search_tool_call.status = chat_types.chat_pb2.TOOL_CALL_STATUS_FAILED + if error_msg := content.get('error'): + web_search_tool_call.error_message = str(error_msg) + + def _build_assistant_message( + self, + texts: list[str], + reasoning_texts: list[str], + tool_calls: list[chat_types.chat_pb2.ToolCall], + ) -> chat_types.chat_pb2.Message | None: + """Build an assistant message from collected parts.""" + if not (texts or reasoning_texts or tool_calls): + return None + + # Simple text-only message + if texts and not (reasoning_texts or tool_calls): + return assistant('\n\n'.join(texts)) + + # Message with reasoning and/or tool calls + if texts: + msg = assistant('\n\n'.join(texts)) + else: + msg = chat_types.chat_pb2.Message(role=chat_types.chat_pb2.MessageRole.ROLE_ASSISTANT) + + if reasoning_texts: + msg.reasoning_content = '\n\n'.join(reasoning_texts) + if tool_calls: + msg.tool_calls.extend(tool_calls) + + return msg + + async def _upload_file_to_xai(self, data: bytes, filename: str) -> str: + """Upload a file to xAI files API and return the file ID. + + Args: + data: The file content as bytes + filename: The filename to use for the upload + + Returns: + The file ID from xAI + """ + uploaded_file = await self._provider.client.files.upload(data, filename=filename) + return uploaded_file.id + + async def _map_user_prompt(self, part: UserPromptPart) -> chat_types.chat_pb2.Message | None: # noqa: C901 + """Map a UserPromptPart to an xAI user message.""" + if isinstance(part.content, str): + return user(part.content) + + # Handle complex content (images, text, etc.) + content_items: list[chat_types.Content] = [] + + for item in part.content: + if isinstance(item, str): + content_items.append(item) + elif isinstance(item, ImageUrl): + # Get detail from vendor_metadata if available + detail: chat_types.ImageDetail = 'auto' + if item.vendor_metadata and 'detail' in item.vendor_metadata: + detail = item.vendor_metadata['detail'] + content_items.append(image(item.url, detail=detail)) + elif isinstance(item, BinaryContent): + if item.is_image: + # Convert binary content to data URI and use image() + content_items.append(image(item.data_uri, detail='auto')) + elif item.is_audio: + raise NotImplementedError('AudioUrl/BinaryContent with audio is not supported by xAI SDK') + elif item.is_document: + # Upload document to xAI files API and reference it + filename = item.identifier or f'document.{item.format}' + file_id = await self._upload_file_to_xai(item.data, filename) + content_items.append(file(file_id)) + else: # pragma: no cover + raise RuntimeError(f'Unsupported binary content type: {item.media_type}') + elif isinstance(item, AudioUrl): + raise NotImplementedError('AudioUrl is not supported by xAI SDK') + elif isinstance(item, DocumentUrl): + # Download and upload to xAI files API + downloaded = await download_item(item, data_format='bytes') + filename = item.identifier or 'document' + if 'data_type' in downloaded and downloaded['data_type']: + filename = f'{filename}.{downloaded["data_type"]}' + + file_id = await self._upload_file_to_xai(downloaded['data'], filename) + content_items.append(file(file_id)) + elif isinstance(item, VideoUrl): + raise NotImplementedError('VideoUrl is not supported by xAI SDK') + elif isinstance(item, CachePoint): + # xAI doesn't support prompt caching via CachePoint, so we filter it out + pass + else: + assert_never(item) + + if content_items: + return user(*content_items) + + return None + + def _map_tools(self, model_request_parameters: ModelRequestParameters) -> list[chat_types.chat_pb2.Tool]: + """Convert pydantic_ai tool definitions to xAI SDK tools.""" + tools: list[chat_types.chat_pb2.Tool] = [] + for tool_def in model_request_parameters.tool_defs.values(): + xai_tool = tool( + name=tool_def.name, + description=tool_def.description or '', + parameters=tool_def.parameters_json_schema, + ) + tools.append(xai_tool) + return tools + + def _get_builtin_tools(self, model_request_parameters: ModelRequestParameters) -> list[chat_types.chat_pb2.Tool]: + """Convert pydantic_ai built-in tools to xAI SDK server-side tools.""" + tools: list[chat_types.chat_pb2.Tool] = [] + for builtin_tool in model_request_parameters.builtin_tools: + if isinstance(builtin_tool, WebSearchTool): + # xAI web_search supports: + # - excluded_domains (from blocked_domains) + # - allowed_domains + # Note: user_location and search_context_size are not supported by xAI SDK + tools.append( + web_search( + excluded_domains=builtin_tool.blocked_domains, + allowed_domains=builtin_tool.allowed_domains, + enable_image_understanding=False, # Not supported by PydanticAI + ) + ) + elif isinstance(builtin_tool, CodeExecutionTool): + # xAI code_execution takes no parameters + tools.append(code_execution()) + elif isinstance(builtin_tool, MCPServerTool): + # xAI mcp supports: + # - server_url, server_label, server_description + # - allowed_tool_names, authorization, extra_headers + tools.append( + mcp( + server_url=builtin_tool.url, + server_label=builtin_tool.id, + server_description=builtin_tool.description, + allowed_tool_names=builtin_tool.allowed_tools, + authorization=builtin_tool.authorization_token, + extra_headers=builtin_tool.headers, + ) + ) + else: + raise UserError( + f'`{builtin_tool.__class__.__name__}` is not supported by `XaiModel`. ' + f'Supported built-in tools: WebSearchTool, CodeExecutionTool, MCPServerTool. ' + f'If XSearchTool should be supported, please file an issue.' + ) + return tools + + def _map_model_settings(self, model_settings: ModelSettings | None) -> dict[str, Any]: + """Map pydantic_ai ModelSettings to xAI SDK parameters.""" + if not model_settings: + return {} + + # Mapping of pydantic_ai setting keys to xAI SDK parameter names + # Most keys are the same, but 'stop_sequences' maps to 'stop' + setting_mapping = { + 'temperature': 'temperature', + 'top_p': 'top_p', + 'max_tokens': 'max_tokens', + 'stop_sequences': 'stop', + 'seed': 'seed', + 'parallel_tool_calls': 'parallel_tool_calls', + 'presence_penalty': 'presence_penalty', + 'frequency_penalty': 'frequency_penalty', + 'logprobs': 'logprobs', + 'top_logprobs': 'top_logprobs', + 'reasoning_effort': 'reasoning_effort', + 'use_encrypted_content': 'use_encrypted_content', + 'store_messages': 'store_messages', + 'user': 'user', + } + + # Build the settings dict, only including keys that are present in the input + # TypedDict is just a dict at runtime, so we can iterate over it directly + return {setting_mapping[key]: value for key, value in model_settings.items() if key in setting_mapping} + + async def request( + self, + messages: list[ModelMessage], + model_settings: ModelSettings | None, + model_request_parameters: ModelRequestParameters, + ) -> ModelResponse: + """Make a request to the xAI model.""" + check_allow_model_requests() + model_settings, model_request_parameters = self.prepare_request( + model_settings, + model_request_parameters, + ) + client = self._provider.client + + # Convert messages to xAI format + xai_messages = await self._map_messages(messages) + + # Convert tools: combine built-in (server-side) tools and custom (client-side) tools + tools: list[chat_types.chat_pb2.Tool] = [] + if model_request_parameters.builtin_tools: + tools.extend(self._get_builtin_tools(model_request_parameters)) + if model_request_parameters.tool_defs: + tools.extend(self._map_tools(model_request_parameters)) + tools_param = tools if tools else None + + # Map model settings to xAI SDK parameters + xai_settings = self._map_model_settings(model_settings) + + # Create chat instance + chat = client.chat.create(model=self._model_name, messages=xai_messages, tools=tools_param, **xai_settings) + + # Sample the response + response = await chat.sample() + + # Convert response to pydantic_ai format + return self._process_response(response) + + @asynccontextmanager + async def request_stream( + self, + messages: list[ModelMessage], + model_settings: ModelSettings | None, + model_request_parameters: ModelRequestParameters, + run_context: RunContext[Any] | None = None, + ) -> AsyncIterator[StreamedResponse]: + """Make a streaming request to the xAI model.""" + check_allow_model_requests() + model_settings, model_request_parameters = self.prepare_request( + model_settings, + model_request_parameters, + ) + client = self._provider.client + + # Convert messages to xAI format + xai_messages = await self._map_messages(messages) + + # Convert tools: combine built-in (server-side) tools and custom (client-side) tools + tools: list[chat_types.chat_pb2.Tool] = [] + if model_request_parameters.builtin_tools: + tools.extend(self._get_builtin_tools(model_request_parameters)) + if model_request_parameters.tool_defs: + tools.extend(self._map_tools(model_request_parameters)) + tools_param = tools if tools else None + + # Map model settings to xAI SDK parameters + xai_settings = self._map_model_settings(model_settings) + + # Create chat instance + chat = client.chat.create(model=self._model_name, messages=xai_messages, tools=tools_param, **xai_settings) + + # Stream the response + response_stream = chat.stream() + streamed_response = XaiStreamedResponse( + model_request_parameters=model_request_parameters, + _model_name=self._model_name, + _response=response_stream, + _timestamp=now_utc(), + _provider_name='xai', + ) + yield streamed_response + + def _process_response(self, response: chat_types.Response) -> ModelResponse: + """Convert xAI SDK response to pydantic_ai ModelResponse.""" + parts: list[ModelResponsePart] = [] + + # Add reasoning/thinking content first if present + if response.reasoning_content or response.encrypted_content: + parts.append( + ThinkingPart( + content=response.reasoning_content or '', # Empty string if only encrypted + signature=response.encrypted_content or None, + provider_name='xai', + ) + ) + + # Add tool calls (both client-side and server-side) first + # For server-side tools, these were executed before generating the final content + for tool_call in response.tool_calls: + # Try to determine if this is a server-side tool + # In real responses, we can use get_tool_call_type() + # In mock responses, we default to client-side tools + is_server_side_tool = False + try: + tool_type = get_tool_call_type(tool_call) + # If it's not a client-side tool, it's a server-side tool + is_server_side_tool = tool_type != 'client_side_tool' + except Exception: + # If we can't determine the type, treat as client-side + pass + + if is_server_side_tool: + # Server-side tools are executed by xAI, so we add both call and return parts + # The final result is in response.content + parts.append( + BuiltinToolCallPart( + tool_name=tool_call.function.name, + args=tool_call.function.arguments, + tool_call_id=tool_call.id, + provider_name='xai', + ) + ) + # Always add the return part for server-side tools since they're already executed + parts.append( + BuiltinToolReturnPart( + tool_name=tool_call.function.name, + content={'status': 'completed'}, + tool_call_id=tool_call.id, + provider_name='xai', + ) + ) + else: + # Client-side tool call (or mock) + parts.append( + ToolCallPart( + tool_name=tool_call.function.name, + args=tool_call.function.arguments, + tool_call_id=tool_call.id, + ) + ) + + # Add text content after tool calls (for server-side tools, this is the final result) + if response.content: + parts.append(TextPart(content=response.content)) + + # Convert usage with detailed token information + usage = self._map_usage(response) + + # Map finish reason + finish_reason_map = { + 'stop': 'stop', + 'length': 'length', + 'content_filter': 'content_filter', + 'max_output_tokens': 'length', + 'cancelled': 'error', + 'failed': 'error', + } + raw_finish_reason = response.finish_reason + mapped_reason = ( + finish_reason_map.get(raw_finish_reason, 'stop') if isinstance(raw_finish_reason, str) else 'stop' + ) + finish_reason = cast(FinishReason, mapped_reason) + + return ModelResponse( + parts=parts, + usage=usage, + model_name=self._model_name, + timestamp=now_utc(), + provider_name='xai', + provider_response_id=response.id, + finish_reason=finish_reason, + ) + + def _map_usage(self, response: chat_types.Response) -> RequestUsage: + """Extract usage information from xAI SDK response, including reasoning tokens and cache tokens.""" + return XaiModel.extract_usage(response) + + @staticmethod + def extract_usage(response: chat_types.Response) -> RequestUsage: + """Extract usage information from xAI SDK response. + + Extracts token counts and additional usage details including: + - reasoning_tokens: Tokens used for model reasoning/thinking + - cache_read_tokens: Tokens read from prompt cache + - server_side_tools_used: Count of server-side (built-in) tools executed + """ + if not response.usage: + return RequestUsage() + + usage_obj = response.usage + + prompt_tokens = usage_obj.prompt_tokens or 0 + completion_tokens = usage_obj.completion_tokens or 0 + + # Build details dict for additional usage metrics + details: dict[str, int] = {} + + # Add reasoning tokens if available (optional attribute) + reasoning_tokens = getattr(usage_obj, 'reasoning_tokens', None) + if reasoning_tokens: + details['reasoning_tokens'] = reasoning_tokens + + # Add cached prompt tokens if available (optional attribute) + cached_tokens = getattr(usage_obj, 'cached_prompt_text_tokens', None) + if cached_tokens: + details['cache_read_tokens'] = cached_tokens + + # Add server-side tools used count if available (optional attribute) + server_side_tools = getattr(usage_obj, 'server_side_tools_used', None) + if server_side_tools: + # server_side_tools_used is a repeated field (list-like) in the real SDK + # but may be an int in mocks for simplicity + if isinstance(server_side_tools, int): + tools_count = server_side_tools + else: + tools_count = len(server_side_tools) + if tools_count: + details['server_side_tools_used'] = tools_count + + if details: + return RequestUsage( + input_tokens=prompt_tokens, + output_tokens=completion_tokens, + details=details, + ) + else: + return RequestUsage( + input_tokens=prompt_tokens, + output_tokens=completion_tokens, + ) + + +@dataclass +class XaiStreamedResponse(StreamedResponse): + """Implementation of `StreamedResponse` for xAI SDK.""" + + _model_name: str + _response: Any # xai_sdk chat stream + _timestamp: Any + _provider_name: str + + def _update_response_state(self, response: Any) -> None: + """Update response state including usage, response ID, and finish reason.""" + # Update usage + if response.usage: + self._usage = XaiModel.extract_usage(response) + + # Set provider response ID + if response.id and self.provider_response_id is None: + self.provider_response_id = response.id + + # Handle finish reason + if response.finish_reason: + finish_reason_map = { + 'stop': 'stop', + 'length': 'length', + 'content_filter': 'content_filter', + 'max_output_tokens': 'length', + 'cancelled': 'error', + 'failed': 'error', + } + mapped_reason = finish_reason_map.get(response.finish_reason, 'stop') + self.finish_reason = cast(FinishReason, mapped_reason) + + def _handle_reasoning_content(self, response: Any, reasoning_handled: bool) -> Iterator[ModelResponseStreamEvent]: + """Handle reasoning content (both readable and encrypted).""" + if reasoning_handled: + return + + if response.reasoning_content: + # reasoning_content is the human-readable summary + thinking_part = ThinkingPart( + content=response.reasoning_content, + signature=None, + provider_name='xai', + ) + yield self._parts_manager.handle_part(vendor_part_id='reasoning', part=thinking_part) + elif response.encrypted_content: + # encrypted_content is a signature that can be sent back for reasoning continuity + thinking_part = ThinkingPart( + content='', # No readable content for encrypted-only reasoning + signature=response.encrypted_content, + provider_name='xai', + ) + yield self._parts_manager.handle_part(vendor_part_id='encrypted_reasoning', part=thinking_part) + + def _handle_text_delta(self, chunk: Any) -> Iterator[ModelResponseStreamEvent]: + """Handle text content delta from chunk.""" + if chunk.content: + event = self._parts_manager.handle_text_delta( + vendor_part_id='content', + content=chunk.content, + ) + if event is not None: + yield event + + def _handle_single_tool_call(self, tool_call: Any) -> Iterator[ModelResponseStreamEvent]: + """Handle a single tool call, routing to server-side or client-side handler.""" + if not tool_call.function.name: + return + + # Determine if this is a server-side (built-in) tool + is_server_side_tool = False + try: + tool_type = get_tool_call_type(tool_call) + is_server_side_tool = tool_type != 'client_side_tool' + except Exception: + pass # Treat as client-side if we can't determine + + if is_server_side_tool: + # Server-side tools - create BuiltinToolCallPart and BuiltinToolReturnPart + # These tools are already executed by xAI's infrastructure + call_part = BuiltinToolCallPart( + tool_name=tool_call.function.name, + args=tool_call.function.arguments, + tool_call_id=tool_call.id, + provider_name='xai', + ) + yield self._parts_manager.handle_part(vendor_part_id=tool_call.id, part=call_part) + + # Immediately yield the return part since the tool was already executed + return_part = BuiltinToolReturnPart( + tool_name=tool_call.function.name, + content={'status': 'completed'}, + tool_call_id=tool_call.id, + provider_name='xai', + ) + yield self._parts_manager.handle_part(vendor_part_id=f'{tool_call.id}_return', part=return_part) + else: + # Client-side tools - use standard handler + yield self._parts_manager.handle_tool_call_part( + vendor_part_id=tool_call.id, + tool_name=tool_call.function.name, + args=tool_call.function.arguments, + tool_call_id=tool_call.id, + ) + + def _handle_tool_calls(self, response: Any) -> Iterator[ModelResponseStreamEvent]: + """Handle tool calls (both client-side and server-side).""" + if not response.tool_calls: + return + + for tool_call in response.tool_calls: + yield from self._handle_single_tool_call(tool_call) + + async def _get_event_iterator(self) -> AsyncIterator[ModelResponseStreamEvent]: + """Iterate over streaming events from xAI SDK.""" + reasoning_handled = False # Track if we've already handled reasoning content + + async for response, chunk in self._response: + self._update_response_state(response) + + # Handle reasoning content (only emit once) + reasoning_events = list(self._handle_reasoning_content(response, reasoning_handled)) + if reasoning_events: + reasoning_handled = True + for event in reasoning_events: + yield event + + # Handle text content delta + for event in self._handle_text_delta(chunk): + yield event + + # Handle tool calls + for event in self._handle_tool_calls(response): + yield event + + @property + def model_name(self) -> str: + """Get the model name of the response.""" + return self._model_name + + @property + def provider_name(self) -> str: + """Get the provider name.""" + return self._provider_name + + @property + def timestamp(self): + """Get the timestamp of the response.""" + return self._timestamp diff --git a/pydantic_ai_slim/pydantic_ai/profiles/grok.py b/pydantic_ai_slim/pydantic_ai/profiles/grok.py index 3b7c4a3746..c99da07f0a 100644 --- a/pydantic_ai_slim/pydantic_ai/profiles/grok.py +++ b/pydantic_ai_slim/pydantic_ai/profiles/grok.py @@ -1,8 +1,43 @@ from __future__ import annotations as _annotations +from dataclasses import dataclass + from . import ModelProfile +@dataclass(kw_only=True) +class GrokModelProfile(ModelProfile): + """Profile for Grok models (used with both GrokProvider and XaiProvider). + + ALL FIELDS MUST BE `grok_` PREFIXED SO YOU CAN MERGE THEM WITH OTHER MODELS. + """ + + grok_supports_builtin_tools: bool = False + """Whether the model supports builtin tools (web_search, code_execution, mcp).""" + + grok_is_reasoning_model: bool = False + """Whether the model is a reasoning model (supports extended thinking/reasoning).""" + + def grok_model_profile(model_name: str) -> ModelProfile | None: """Get the model profile for a Grok model.""" - return None + # Grok-4 models support builtin tools + grok_supports_builtin_tools = model_name.startswith('grok-4') + + # Reasoning models have 'reasoning' in their name but not 'non-reasoning' + grok_is_reasoning_model = model_name == 'grok-4' or 'reasoning' in model_name and 'non-reasoning' not in model_name + + return GrokModelProfile( + # xAI supports tool calling + supports_tools=True, + # xAI supports JSON schema output for structured responses + supports_json_schema_output=True, + # xAI supports JSON object output + supports_json_object_output=True, + # Default to 'native' for structured output since xAI supports it well + default_structured_output_mode='native', + # Support for builtin tools (web_search, code_execution, mcp) + grok_supports_builtin_tools=grok_supports_builtin_tools, + # Whether this is a reasoning model + grok_is_reasoning_model=grok_is_reasoning_model, + ) diff --git a/pydantic_ai_slim/pydantic_ai/providers/__init__.py b/pydantic_ai_slim/pydantic_ai/providers/__init__.py index 9557e8e87b..0ca388fcb4 100644 --- a/pydantic_ai_slim/pydantic_ai/providers/__init__.py +++ b/pydantic_ai_slim/pydantic_ai/providers/__init__.py @@ -105,6 +105,10 @@ def infer_provider_class(provider: str) -> type[Provider[Any]]: # noqa: C901 from .grok import GrokProvider return GrokProvider + elif provider == 'xai': + from .xai import XaiProvider + + return XaiProvider elif provider == 'moonshotai': from .moonshotai import MoonshotAIProvider diff --git a/pydantic_ai_slim/pydantic_ai/providers/grok.py b/pydantic_ai_slim/pydantic_ai/providers/grok.py index 604a38abbf..93a46bb78b 100644 --- a/pydantic_ai_slim/pydantic_ai/providers/grok.py +++ b/pydantic_ai_slim/pydantic_ai/providers/grok.py @@ -25,6 +25,11 @@ GrokModelName = Literal[ 'grok-4', 'grok-4-0709', + 'grok-4-1-fast-reasoning', + 'grok-4-1-fast-non-reasoning', + 'grok-4-fast-reasoning', + 'grok-4-fast-non-reasoning', + 'grok-code-fast-1', 'grok-3', 'grok-3-mini', 'grok-3-fast', @@ -35,7 +40,7 @@ class GrokProvider(Provider[AsyncOpenAI]): - """Provider for Grok API.""" + """Provider for Grok API (OpenAI-compatible interface).""" @property def name(self) -> str: diff --git a/pydantic_ai_slim/pydantic_ai/providers/xai.py b/pydantic_ai_slim/pydantic_ai/providers/xai.py new file mode 100644 index 0000000000..7a24749fe3 --- /dev/null +++ b/pydantic_ai_slim/pydantic_ai/providers/xai.py @@ -0,0 +1,62 @@ +from __future__ import annotations as _annotations + +import os +from typing import overload + +from pydantic_ai import ModelProfile +from pydantic_ai.exceptions import UserError +from pydantic_ai.profiles.grok import grok_model_profile +from pydantic_ai.providers import Provider + +try: + from xai_sdk import AsyncClient +except ImportError as _import_error: # pragma: no cover + raise ImportError( + 'Please install the `xai-sdk` package to use the xAI provider, ' + 'you can use the `xai` optional group — `pip install "pydantic-ai-slim[xai]"`' + ) from _import_error + + +class XaiProvider(Provider[AsyncClient]): + """Provider for xAI API (native xAI SDK).""" + + @property + def name(self) -> str: + return 'xai' + + @property + def base_url(self) -> str: + return 'https://api.x.ai/v1' + + @property + def client(self) -> AsyncClient: + return self._client + + def model_profile(self, model_name: str) -> ModelProfile | None: + return grok_model_profile(model_name) + + @overload + def __init__(self) -> None: ... + + @overload + def __init__(self, *, api_key: str) -> None: ... + + @overload + def __init__(self, *, xai_client: AsyncClient) -> None: ... + + def __init__( + self, + *, + api_key: str | None = None, + xai_client: AsyncClient | None = None, + ) -> None: + if xai_client is not None: + self._client = xai_client + else: + api_key = api_key or os.getenv('XAI_API_KEY') + if not api_key: + raise UserError( + 'Set the `XAI_API_KEY` environment variable or pass it via `XaiProvider(api_key=...)`' + 'to use the xAI provider.' + ) + self._client = AsyncClient(api_key=api_key) diff --git a/pydantic_ai_slim/pydantic_ai/settings.py b/pydantic_ai_slim/pydantic_ai/settings.py index 6941eb1ab3..3d8c6adc3d 100644 --- a/pydantic_ai_slim/pydantic_ai/settings.py +++ b/pydantic_ai_slim/pydantic_ai/settings.py @@ -25,6 +25,7 @@ class ModelSettings(TypedDict, total=False): * Bedrock * MCP Sampling * Outlines (all providers) + * xAI """ temperature: float @@ -45,6 +46,7 @@ class ModelSettings(TypedDict, total=False): * Mistral * Bedrock * Outlines (Transformers, LlamaCpp, SgLang, VLLMOffline) + * xAI """ top_p: float @@ -64,6 +66,7 @@ class ModelSettings(TypedDict, total=False): * Mistral * Bedrock * Outlines (Transformers, LlamaCpp, SgLang, VLLMOffline) + * xAI """ timeout: float | Timeout @@ -76,6 +79,7 @@ class ModelSettings(TypedDict, total=False): * OpenAI * Groq * Mistral + * xAI """ parallel_tool_calls: bool @@ -86,6 +90,8 @@ class ModelSettings(TypedDict, total=False): * OpenAI (some models, not o1) * Groq * Anthropic + * Grok + * xAI """ seed: int @@ -112,6 +118,7 @@ class ModelSettings(TypedDict, total=False): * Gemini * Mistral * Outlines (LlamaCpp, SgLang, VLLMOffline) + * xAI """ frequency_penalty: float @@ -125,6 +132,7 @@ class ModelSettings(TypedDict, total=False): * Gemini * Mistral * Outlines (LlamaCpp, SgLang, VLLMOffline) + * xAI """ logit_bias: dict[str, int] @@ -149,6 +157,7 @@ class ModelSettings(TypedDict, total=False): * Groq * Cohere * Google + * xAI """ extra_headers: dict[str, str] @@ -159,6 +168,7 @@ class ModelSettings(TypedDict, total=False): * OpenAI * Anthropic * Groq + * xAI """ extra_body: object diff --git a/pydantic_ai_slim/pyproject.toml b/pydantic_ai_slim/pyproject.toml index 6e815a4f52..5ebaa21beb 100644 --- a/pydantic_ai_slim/pyproject.toml +++ b/pydantic_ai_slim/pyproject.toml @@ -72,6 +72,7 @@ cohere = ["cohere>=5.18.0; platform_system != 'Emscripten'"] vertexai = ["google-auth>=2.36.0", "requests>=2.32.2"] google = ["google-genai>=1.51.0"] anthropic = ["anthropic>=0.70.0"] +xai = ["xai-sdk>=1.4.0"] groq = ["groq>=0.25.0"] mistral = ["mistralai>=1.9.10"] bedrock = ["boto3>=1.40.14"] diff --git a/pyproject.toml b/pyproject.toml index 3c13afdece..88bdded4e6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -46,7 +46,7 @@ requires-python = ">=3.10" [tool.hatch.metadata.hooks.uv-dynamic-versioning] dependencies = [ - "pydantic-ai-slim[openai,vertexai,google,groq,anthropic,mistral,cohere,bedrock,huggingface,cli,mcp,fastmcp,evals,ag-ui,retries,temporal,logfire,ui]=={{ version }}", + "pydantic-ai-slim[openai,vertexai,google,xai,groq,anthropic,mistral,cohere,bedrock,huggingface,cli,mcp,fastmcp,evals,ag-ui,retries,temporal,logfire,ui]=={{ version }}", ] [tool.hatch.metadata.hooks.uv-dynamic-versioning.optional-dependencies] diff --git a/tests/conftest.py b/tests/conftest.py index 6b90ecfb28..b7b8d28895 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -412,6 +412,11 @@ def cerebras_api_key() -> str: return os.getenv('CEREBRAS_API_KEY', 'mock-api-key') +@pytest.fixture(scope='session') +def xai_api_key() -> str: + return os.getenv('XAI_API_KEY', 'mock-api-key') + + @pytest.fixture(scope='session') def bedrock_provider(): try: diff --git a/tests/models/cassettes/test_xai/test_xai_document_url_input.yaml b/tests/models/cassettes/test_xai/test_xai_document_url_input.yaml new file mode 100644 index 0000000000..4407846869 --- /dev/null +++ b/tests/models/cassettes/test_xai/test_xai_document_url_input.yaml @@ -0,0 +1,363 @@ +interactions: +- request: + body: '' + headers: + accept: + - '*/*' + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - www.w3.org + method: GET + uri: https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf + response: + body: + string: !!binary | + JVBERi0xLjQKJcOkw7zDtsOfCjIgMCBvYmoKPDwvTGVuZ3RoIDMgMCBSL0ZpbHRlci9GbGF0ZURl + Y29kZT4+CnN0cmVhbQp4nD2OywoCMQxF9/mKu3YRk7bptDAIDuh+oOAP+AAXgrOZ37etjmSTe3IS + IljpDYGwwrKxRwrKGcsNlx1e31mt5UFTIYucMFiqcrlif1ZobP0do6g48eIPKE+ydk6aM0roJG/R + egwcNhDr5tChd+z+miTJnWqoT/3oUabOToVmmvEBy5IoCgplbmRzdHJlYW0KZW5kb2JqCgozIDAg + b2JqCjEzNAplbmRvYmoKCjUgMCBvYmoKPDwvTGVuZ3RoIDYgMCBSL0ZpbHRlci9GbGF0ZURlY29k + ZS9MZW5ndGgxIDIzMTY0Pj4Kc3RyZWFtCnic7Xx5fFvVlf+59z0tdrzIu7xFz1G8Kl7i2HEWE8vx + QlI3iRM71A6ksSwrsYptKZYUE9omYStgloZhaSlMMbTsbSPLAZwEGgNlusxQ0mHa0k4Z8muhlJb8 + ynQoZVpi/b736nkjgWlnfn/8Pp9fpNx3zz33bPecc899T4oVHA55KIEOkUJO96DLvyQxM5WI/omI + pbr3BbU/3J61FPBpItOa3f49g1948t/vI4rLIzL8dM/A/t3vn77ZSpT0LlH8e/0eV98jn3k0mSj7 + bchY2Q/EpdNXm4hyIIOW9g8Gr+gyrq3EeAPGVQM+t+uw5VrQ51yBcc6g6wr/DywvGAHegbE25Br0 + bFR/ezPGR4kq6/y+QPCnVBYl2ijka/5hjz95S8kmok8kEFl8wDG8xQtjZhRjrqgGo8kcF7+I/r98 + GY5TnmwPU55aRIhb9PWZNu2Nvi7mRM9/C2flx5r+itA36KeshGk0wf5MWfQ+y2bLaSOp9CdkyxE6 + S3dSOnXSXSyVllImbaeNTAWNg25m90T3Rd+ii+jv6IHoU+zq6GOY/yL9A70PC/5NZVRHm0G/nTz0 + lvIGdUe/Qma6nhbRWtrGMslFP8H7j7DhdrqDvs0+F30fWtPpasirp0ZqjD4b/YDK6Gb1sOGVuCfo + NjrBjFF31EuLaQmNckf0J9HXqIi66Wv0DdjkYFPqBiqgy+k6+jLLVv4B0J30dZpmCXyn0mQ4CU0b + 6RIaohEapcfoByyVtRteMbwT/Wz0TTJSGpXAJi+9xWrZJv6gmhBdF/05XUrH6HtYr3hPqZeqDxsu + nW6I/n30Ocqgp1g8e5o9a6g23Hr2quj90W8hI4toOTyyGXp66Rp6lr5P/05/4AejB2kDdUDzCyyf + aawIHv8Jz+YH+AHlZarAanfC2hDdR2FE5DidoGfgm3+l0/QGS2e57BOsl93G/sATeB9/SblHOar8 + i8rUR+FvOxXCR0F6kJ7Efn6RXmIGyK9i7ewzzMe+xP6eneZh/jb/k2pWr1H/op41FE2fnv5LdHP0 + j2SlHPokXUkH4duv0QQdpR/Sj+kP9B/0HrOwVayf3c/C7DR7m8fxJXwL9/O7+IP8m8pm5TblWbVW + Xa9err6o/tzwBcNNJpdp+oOHpm+f/ub0j6JPRX+E3EmC/CJqhUevQlY8SCfpZUj/Gb1KvxT5A/lr + 2Q72aWgJsBvYHeyb7AX2I/ZbrJLkewlfy5uh1ceH4aer+e38Dmh/Ce9T/Of8Vf47/kfFoCxRVip7 + lfuVsDKpnFJ+rVrUIrVCXa5uUXeoUUSm2nCxocPwiOFxw3OGd4z1xj6j3/gb09Wma83/dLbs7L9N + 03T/dHh6ArlrRiZdCU98lR5A3h9FDH4Aj/4QFp+mdxGFHFbAimH3atbK2tgm9il2GfOwq9n17O/Y + l9k97AH2LawAa+Am2O7gjbyDu7iHX8uv57fwo3gf59/nP+Gv8DOwPEuxKw5lubJR2aFcqgxhDUHl + gHItPHub8pjykvKy8qbyG+UMopalLlZD6pXq3erD6lH1R4ZPGgbxfsBw0jBl+JHhA8MHRm7MMeYZ + K42fMT5i/KXJaFppajfdaPoX03+Y/SyPlcFybX614NnYg4v5YzxdPcjOAJHPVErGyh2IQwd2xX9Q + gzKNuCSJediWwbPVNMFpdKph8AfZCaplL9BBI1dQidXTFGG/4KfV5/lF9GPWw7LVh5Uhww94AT2O + anSYP81PsPV0lNfzS/i9CrE32CP0BvL9CrqDXc4C9Dg7w9awz7M6dpD+hWcqHexaqo8+wFUWxzay + dwgW0FVqH33646sgW02/oLemv6omqp9DfZqkuxDRb9Br7FH6MzNE30Z1U1CNXKgyNyPfryNR9XZi + nx3EfsxGBRkwvkRHxYliqjOuU6+kd+g/6S3DcWTUelTSN6e96lfVX0XrouXYYdhl9Aj2XT9djB3z + BrLkGYzF6DLs9HjUkmrs6nbaQX30eVS926Lh6L3Ra6L7oz76R/D+mS1jf2Zj2BGT4Kin7+H9RfoZ + uwn78OL/3ikw3UdT9FtmZYWsGvvhjGGf4bDhMcNRw7cNLxqXw9vX0j3I6F8im+OxAjf9iH5Lf2Jm + xCabllEN7F0F27togHcrz1ATyyE/9mwJ6vh6fSUBSLka3rsX+/kZ7I13UCcuo2/TK4yzLKzIDf1m + yGmDn3eB+iFE8Bo2AUwfqnYZ/Q7rTmKreBD6nJB0F6rWFGz6Bf0a3o5Ku5ahLjSzSyDrT/Qp6oOG + ldTOxhGBJ2k1Kmuz8k/w91JmofVsCfs6+HqwQ5Mon1YbfsU4LZveHF3FvcozOGOiwI/h9Mqli9he + WJGMdZylDLaFaqe3wYaXiZyNnc6GdRfVr12zelVdbc2K6uVVlRXlyxxlpSXFRYVL7UsKNNvi/Lzc + nGxrVmZGelpqiiU5KTFhUXyc2WQ0qApntKzF3tqjhYt6wmqRfcOGcjG2u4BwzUP0hDWgWhfShLUe + SaYtpHSCcveHKJ0xSucsJbNo9VRfvkxrsWvhF5vt2iTbsbUL8C3N9m4tfEbCmyR8WMKJgAsKwKC1 + WPubtTDr0VrCrfv6R1t6miFufFF8k73JE1++jMbjFwFcBCicZfePs6x1TAI8q2XNOCdzIowK59ib + W8LZ9mZhQVgpbHH1hdu3drU05xYUdJcvC7Mmt703TPb14WSHJKEmqSZsbAqbpBrNK1ZDN2njy6ZG + b560UG+PI6HP3ue6rCusuLqFjhQH9DaHs6583To3hPDUpq7r58/mKqMtVq8mhqOj12vhqa1d82cL + xLW7GzLAywtbe0ZbofpmOLGtQ4M2fl13V5hdB5WaWIlYVWx9HnuLwPR8RgvH2dfb+0c/04PQ5IyG + adv+gkhOjvNY9DTltGijnV32gnBDrr3b1Zw3nk6j2/ZPZDu17IUz5cvGLSkxx44nJetAQuJ8wDM7 + JyFJLqC2bbOeZcIi+0YkRFhza7Cky441rRIXzyoada8CGV7dDFzhPkTEG45r6hm1rBF4wR82FFrs + 2ugfCRlgP/P2QoxLxxgLLX8kAYo8mU01zM/AYYcjXFYmUsTUhJjCxnVyXFu+bN8kX2n3WzR0cB+1 + w7eu7jWVcH9BgQjwTZNO6sUgfGhrV2ysUW9uhJyVju4w7xEzUzMzGdvFzKGZmVn2Hjsy+ah8EMgI + m4tm/yVbMtNa+teEWebHTHti820d9ratO7q0ltEe3bdtnQtGsflVs3M6FE5r6lJyuQ7xXEXOIikv + myUWg66EsFqIf0aZ1H1hBUkpEUxrDVt6NsSu3fEFBR/JM2kyz2OajL4juGQ3x6ZbGV7jWDheu2C8 + wLqEUQX2qkW8rXPH6Gj8grlWFKDR0Va71jraM+qajB7qtWsW++gx/jB/eNTf0jMT0Mno8Ztyw603 + d2MR/WwNkpXT+nE7u2HruJPd0LGj65gFT283dHZFOONNPeu7x5dirusYbkWcEstnsWKkiRG1MSR6 + hJvlVO4xJ9EhOatKhBy7JxlJnHkGx8g9yWM4i8ThVY7bFBF8A9449U20/ihn00bTJG9wppFBnVYo + 3qROM8o2Gw3TXHmaFVEcbnatZHVY3qs/W7/Z8m79prP11ADY8gEuy6sKUgpSCnFhuIH4QFOmPnAa + 6C+kqVPQhScYMrjwnGUhGx10rigxlMRfnOVRPQmGsqzVWRsyuzP7Mw2rs1bmXp97t+GuRQZbSiEj + npZamGwxZxcfMTHTZHRqIm5RDUy82Zl2qIBpBVUFvCAlVSPNUmXhlkl+04S2vMPqgGk7hW2bLDv3 + vufYu+mMNLJB2kg797KdaQXVWZmZqRnpuBfE217AUlZU163jtTVFRcVF9jt4/lM9V032lNft3nRN + 79fPvsxKXv1c3YZd9fUDHeueMBzPK3pu+s0fPnHNmLutzKY+90FtUuolLzz22JO7U5PEs/ct0d+o + Hbivy6R7nVmfStmTcpdBiTNmG+t5fUobb0t5k5uSJ3nQmaIuyqT4jPT0+DhjWnpRRgZNslJnUqZT + W1pzJJNFM1lmjhWLdmYuWVpz2Dpm5X7rO1b+eyuzxi8qijOLqWTQjpnZO2Zmzs5qqJdr3zvsEKvf + jNUPO95D23Sm3iIjVW+BFxrOCC+wnQW1RqN9SVFRLaKWnpm5onrlSgEqm9c84738sU+ybNu2hg3D + ZSz7vu29n37sLj42bT3tWbsl9Dqb+svPxToP4H73y+o6KmZrj1EpjNmZEt9gMBoTMoyZCTVKjbnG + WmNv5i3mFmuzPUFTKks74npKD5XeV/p148OmhxKeMD6REC49VXq6NIlKK0vbMXGy9LVSY6kzJ6+m + AeNDctJgKlBNOfmZcFkk3lQgPLdYNVlSUopz8/KKiuMZGZMtRakpzh21PSnMl8JSJnmrMzkntyg/ + DzhfHuvJY3nAHS1EdBl8HCEqFsmUHNcgeudK2F0M0mJnI1o92tLimmLnmotqKotfKn6tWEkuthUf + KlaoWCuuKo4Wq8XZJb+K+Vq4OPZCtp2Bl9/budeBRHtv707RwefS6+LdcKbhDEtJXU1oy6vYsGPv + ToTBkVaQsXJFdWbWSnnNzEAIapCDS4xGCRbNgAeYctPU7ruqWh+4LPRASf70m/nFW9f2V0y/ubhh + ZWN/+fSbatFtj3Zu396567LmL5/t5ru+WlG/4aa7pjlvvWfHstZr7z77AWKWNL1V3YbcTGM1R1NL + DCxtMnraaU1IrjFnJibXmMTFKC6GTOC4cI4tZ00NgqomLkoyWjilGdU0rioKg9vTeizMMsmOOFMX + JSdWJpWQllGV0ZOhvJPBMoR/lxTViN6Zmre4JiMrK0ddrTit2TUHFaZMsmJnHJcjVD8xSsXTiTNv + ZY1GVagW2enfGYs52LHpbDau+Gc9u7nF0/xrh2Pv8CbLu69Tw5mdlQ3StSx1dYr0a+pqAKYki9jo + DibjsrMtbOloC69BxY+oFjoefYdY9J1xBc/veHXjRDlGhuhvnEmJKQ1plrRsXFKtDQacIRMYiD6C + cUxWd1pBWloBMyUp9iXFxWLL1CUxx/T7zD59Y1Nh06cOtm/dnL2+tvfT2WrR2ST+hw/4sZ29Fy1J + +UVioFvUwDvxLPg+amAy7rdHnIVGw7H0Y1blYgPbY/iJgaemFCYmJVGupRAuSSZz5jlVL9OWX5Xf + k+/PP5RvyLckayzmLFH48hYWvtm6J6pe6urKudq3IqVAQ/HLSDeKymfP5nLj14i6dyf7V5a07cBj + vV/a/JnvP/vAkX1Nn95QO2Y4nlnw6pHrJ70pGWd/qj433VPR29jenxiPbPoS1nMt1hNHw84Gs0E1 + GgpNmrnKfNL8mlmtNB82c7OZFFWsJ47MpgbjFjyKb1Nw8vAcbVHVIr5IjZu/iPj5i0D9eg8ABnPL + 2LkXvWKw1GM1WEhGgWxfUs6cXcv7zt5rOP7+9IPvn71NVCcrHP5rw8uowpPO6pUqK1M1i5bSrR6y + GszqSSvPyEzh6amZKUlpyWRJSmNk4elx5uRFbNeiKAwTZSbeyFKSY4VYVh2c13jYFomPkr2iwbzF + 3G5WzCWWypRdKTxlkqnOxKS0Ip6+i8YypzJ5JkL3ZFxCTWZ21hXHuJfk0hx76zeJ0/KDnfXv7sx+ + naxYm1gVWgMuq6uT8UJ5EMUhbUVtjSgLWSZRBDIyVmTYURLs1ntX3x26IlDUtO6i2n/+5+k371WL + 2r9wbcfS71hWb2179YOnlI0i126Hsd9AbMTZPnKM4rAPG1DnnHHtcfxQXDhuKu5U3O/jDLa4nriD + cWNAGBSjCQe/kkzMSafwxKjQTtwiGA1GkxrPTUVMFXs5rmBpjZpt1o8ah34LIAOEJcjQyOhgAcOO + NJjL0G5n2dNvsmz1SaZOf/CXT6hFOEDYPAs7xBaccpYK+wztBn7IEDZMGU4Zfm8w2Aw9hoOGMSAM + MAY3JVwpYjRjCWWr51ii614R02s4/udWeKMRZ3Ixzqp0ymNfO0aW6PvO1kWr7477SuJdlkcMD8ef + iDuROJljNqezDfxiY2v8lsWPJD5pfDLnu/HfS/hJ/CsJ75v+lJiYl5yX4czNr8lwJqXUJGeczHgp + Q5GFLnlxg+yTstDzW5wJyUmp7Uk9STzJmspEFmTn1rAVqcLsiXytRvZLSmO9ozzWW/Nk70xOSq4Z + E/flFpi9KzUVmTehLkq1igxcushEBawyo2BLEkvKqVy8a7Fv8X2L1cXJBWYnirY5O9/bGPPGpjNy + +2w68y6KwBkUOWe61VmS3mB1Lk7GJdeCS15KgyxqDWdlEUyFEaBIFcaASPagE31khhTnnSyEkoEw + geNMzGeJLjwRF79ODhsLGhwk6F93oCjvlOqTnPBSklCaJNQnOeEskkJRnBwOHKP1uAtD8HbupZ0O + hiPHrhUX1VpoRTUpBfL+JE0chiZjFv8zs65868j0767zsvSXz7BU41mncrVr/Y5i5YpLLquvZ2xb + 5Vfuf+K2V5kZ1fm70898/qYNbODKg01NAfkxmPiI79d7nvlx/8ldyfV/NGeb5adDD/yqfu5Tf5re + avwyqgdDbWMzH58RmdZNb6amuQ/UPvQBU4IRKMN36Q71V3SLKZ8OqAFK4qtx53sJ3Qncl/hjZMX4 + dtEw1wielfQ4s7H/5JN8UtGUIeV/qw1qyPBZXXoClSANxIsjISppO+65Nlt82AgCu0u9ksTduzRY + XhXJFy9HiuTCnaEOK9TFLDqsUjrr12EDWdnndNgI+A4dNtF32Dd02ExF3K/DcTTK79LhePU5RdPh + RdRr+qUOJ9Buc7MOJxqPmh/T4SS6LPnTs347mHxch+E2y2od5qRa1umwQsss63VYpXjLkA4bKMFy + hQ4bAV+rwybqtRzWYTOlWf6gw3HUkmLQ4XjuSvmEDi+i5WmPz35btiLtFzqcqOxIT9bhJKrI8sIS + pgqvJ2V9SYdVysl6UMIG4OOzTuqwSplZ35ewEXhj1ms6rFJq1hsSNom4ZP1JhxGLrKiEzcAnWNN0 + WCWr1SbhOBFfa50OI77ZtToMOdkNOoz4Zl+sw5CZfZ8OI77ZEzqM+Gb/ow4jvtm/0mHEN+dhHUZ8 + c17UYcQ391M6jPhq2TqM+Gqf1WHEV/tfOoz4Ft8p4Xjhq+J/12H4qji2xkXAp5Zk67BKi0scEk4Q + aynZqMOwv2SrhJNE5pd4dFilvJKQhC1Szm06LOR8TcJpwuclz+owfF7yXQmnC3tKfqbDsKfkTQln + AJ9eynRYJa00Q8KZgr60VodBX9ok4WxJv1OHBf1eCeeKHCi9TYeRA6X3SDhf2FM6rsOwp/QpCdsk + /fd1WNC/LOGlIgdK39Jh5EDpHyVcJvxTlqjD8E9ZzM5yUQnKSnVYnYHN0v+zMOwvk/ljlusq26rD + Ar9LwAkx+v06LPDXS1jGpex+HRZ6H6VO2k9+8tBucpEbvUaPonVSv4Q3kY+G0II6lYaK6aNhwOLq + At4rKTRgBsBfAahZ4l3/Q0mVs5Zp1IGZAQrN0gSA24g+pm85rca7isp1qFpiG8ExgH4bePbAhqDk + 2gZ5AbRh2odrH6iGMe8C5Xqpo+8cO9fMo9FmqdbQJVJKYNbqFdBahbeGKr8JWDdmfZj3wbNBKj2v + lI+SMUdbPs+uznn4b0nPCr/1QcYg+mG6HDih7b/vcw1YD7zlhU1BaZvwkYaxoAnqUrcjHhq1S36N + iqS+Tbhuge7d0vcu0As+D6QKb49ITiGt4jw2xeLsg15hkx+0+z+SyiPzS9CNSKv2zOr16tlbLqPs + o17d6s1ypl960QVrls3aPixnvDJTO3ANSatjEYll1SrkUpO0JCi9POO3Ydiigcql52Iso7zS930y + w0TODUld8+Pu1mW5pG2Cc1BKFHb3Q/+glBjzviatdkl9bj0asRlhdUCPh0uuMca3fzb+Xj3b/XoE + PdI3AZmNsdXNRMil2x+S2jSpYb5VM5EXvhHjESm7f142CFqflBXTPYOPeTuoe8StZ2rgHLogZHqk + V7zoY7LdOiYkPS0yai6nfXLnDkuPDkh+YamI56DONaPBLfn36Vq9+kpj+1FImPPCblAKaTHsnF+9 + und9+kq8kj4kR3NRDcgsHZDWnT8nZmprYHYtYm5QypuTIerF5bq1Lt3/bln1NH2XzvisT+reI7Ex + frHDvHoM++W+8+s54sNV7Oh9urdjEuaqvUvGKpYdmvShW1+/V0ZtQNL45d6LZeOQ5IytZH52e2cz + S+z8K/TIDEprRG7u0/dWrO4MzNoxKEdz2Rv80IkU+ND63LqOXikhJD3dtyA3PbQX+BnPitx2z65w + t8xtTebAFdK3AZl3wdl6Eou6sD2234N61YjtpoCeZXPVMzY7KCPioislf8xqIdctZ+cyLaa9T3rL + L3fJ/tlVzOgekjVTzLukJ4Z1HWIPxbwYlPwzFs9I98scGpR1c8a2Cnn2BTG3BmdqJeSKd4Wkml9h + K2R1GgRFv9xLA4AGAQ3JCHnkKEC7ZA7EIl4xS/l/V8OIzJgYrWeels2o9J0491vRmpB5At4CrDgB + WnH9pMS3ANOBq8jNi3EStOC9SWI7KRFPU6J1ymwKnCfXtFl8bJ/EPOrXfT6Xo3/dKTYXmZmKPBPn + Xjm7H/ShWZ3u2doWy+e582h+tYxVjrk6Gtu/Xr1mBvQ9vUdK8czWRLFbu3VtYnfv02tp7+xpFNMZ + /BjPzNTOkdnq5NF3nGc2p4dl/Qjq+3m3no/n89fMLhQe88yTMreLz9XXp5+AIgN7ZWWMWd2rR2ZI + l3y+CBXLVS30VKwin5sV52qeqW2iirnkvagLWgd0bwf0GvJRuoX3twMzV2f3nxMLj36XMf+eK1a9 + XdIiv/SsV7/T+Wtirum5ODSvts3oFZWkT3raO+8UGZ53r7xslnp4Xt7Ond0f7ylh3aCUP5NXvgXy + RmT8L5fRnH8fOlMf5yh9oI3doYakx4X8/tn1xOyan92DekWN+T+2q/x6fsxV3oU59HErmsuPjXLt + 50Zu5t5LnDke/Q4ttprY/Z5bRnXoQzEY/pC/5yQH5N1qSN71x86hffLeaITm313919GfkTes3/95 + 9Wee893FnRvHmLfm7ljdUua5+3gmYq4P+Xr332TtnJfP1bDwvF9okUe/iw3i7JmRIJ5PGin2JFCC + e/gaqsPzl4brcozK8XxVI5+yxKcj26lNp6zC7HLM1OhwHZ7G6iTXSqrFs4BoQvrfdtb990/GmbnK + D3lv9jzs3O/37Ha5PdqjWme/R9vkG/IFgdKafMN+37Ar6PUNaf4Bd4XW7Aq6/guiSiFM6/ANhAQm + oG0cAt/y1aurynGprtAaBwa0bd49/cGAts0T8Azv8/Q1DntdA+t9A30zMtdIjCZQay7xDAeE6BUV + VVVaySave9gX8O0Ols6RzKeQ2HIpq1PCj2idw64+z6Br+HLNt/tjLdeGPXu8gaBn2NOneYe0IEi3 + d2jtrqBWpHVu0rbs3l2huYb6NM9AwDPSD7KKWUlYs2/PsMvfv38+yqM1D7tGvEN7BK8X7i3Xtvl6 + IXqz193vG3AFlgnpw16316V1uEJDfVgIXLWqusk3FPQMCtuG92sBF7wIR3l3a32egHfP0DIttnY3 + qFxeTA76hj1af2jQNQTzNXe/a9jlxjIw8LoDWIdrSMPcfrF+L9zuxwI9bk8g4IM6sSAX5Ifc/ZpX + FyUWHxryaCPeYL90w6DP1ye4BQyzgzDEDacGZnDBEc9Q0OsBtRtAaHh/hSY97dvnGXYh3sFhjys4 + iCnB4A4h5gGhTMTRMyxN2B0aGAAobYX6QR+UeIf6QoGgXGoguH/AM98TIlsDQotneNA7JCmGfZdD + rAv2u0NQFAtgn9e1xyfmR/rhc63fM+CHR3zaHu8+jySQae/SBuAObdAD3w153SB3+f0euHHI7YGS + mLu9wlma5wosZtAzsF/D2gLInQEhY9A7IN0b1DdSQNfnBkevRwsFkFLSm569IWFsyC38r+32YcmQ + iEUFgyJPsPRhD+IeRGogTAG4TKYnhoOuPa4rvUMQ7Qm6l8WcBvY+b8A/4NovVAjuIc9IwO/ywzSQ + 9MHEoDcgBAty/7Bv0CelVfQHg/41lZUjIyMVg3rCVrh9g5X9wcGBysGg+NuSysHALpdYeIVA/pUM + I54BYD2SZfOWzo2tG5saOzdu2axtadU+ubGpZXNHi9Z48baWlk0tmzsT4xPjO/vh1hmvCReLmMBQ + rCAoPXqeLSYXIxJZrLl3v7bfFxKcbpFt8LPcR7G0RHLIHEV8sf2GQO7aM+zxiEys0LrB1u9CGvh6 + xTYCZ3CBMSI7R0Q6eRA4j/D0sMcdRJx3w49zdokQ+vZ4JIkM8SwfQoPs7Q0FIRpm+rCj5i2oODBj + FBJ51hWzzCLbtH2ugZCrFxnmCiBD5nNXaNuHZM7un1kF1qRXLqS3Swv4PW4vis65K9fgxSGZbYLX + 1dfnFTmBrByWVXmZQA9L38rd/SGjBryDXrEgKJF0I77hywOxJJX5KJG+ERTUUO+AN9Av9EBWzN2D + SFTYj1D592ux5NU9tFCR9MfG3XOLE9Vrb8gTkGpQ99ye4SF9BcO63ZI40O8LDfRhD+3zekZi5eqc + 5Qs6RNKDCtA3V+Jm1wizZGF1B+diLBbm0q3efX6x0uRZBn3f64KgxxVcIwi2dzTiEChZVVNXqtUt + X1VeVVNVFRe3vQ3IquXLa2pwrVtRp9WtrF1duzox/iN23cduRjGq1M2T+xCPqx79Jknc6sz/mGXh + TJBCLBG3Bm8toJnD7qaFH3NrOqZV/9Bj/oyOU25QnlG+o5zEdXz+/AL8ha8NLnxtcOFrgwtfG1z4 + 2uDC1wYXvja48LXBha8NLnxtcOFrgwtfG1z42uDC1wYXvjb4f/hrg9nPD7z0UZ8sxGY+iT6WrT6J + CS2gPXf2Ylk1AguoZnCt9BbGl9N7oH8LuIWfOiycm+GZub/ynVfi3OwlEppPE8NskKN98vOOhfML + Z9r10zckn/18clfOpz7f/HxP+T7Shz7Vpq5T16pN6kp1lepUL1Lb1NXzqc8733neT3TmsK3nrCeG + aRMjthw08+fmsG36venlH7J4Hp6l0C8VO7Jk3vws7q/Nm7/SN3+1vI/LK/3/y1O0mH5K53l9mzqV + r1AyY2SLTilfnrCkVzsnlbsnktOqnY0W5U5qR+MUVjbRFBonn3IbHUTjIG+LlC+vPiaAifikagvo + byIN7RCaQmO4Mjl2ogn6mybSMoX4ayLJKZLvs5GqmhgwYbFWtzemK1cQUzzKENnJphxAvxi9G30+ + +l6lD5VC2OmcSLZUH4K+BpA3KBkoQzalUcmkavTNSg7lSrJQJCmmJxQpKatujFeaFKskSVYSUY9s + ilkxRapt2glF/NmwU7lhIm6RsO+GiCWj+hnlOsVE6aA6BKosW/IzSjxVoomVdE7EJVYfbkxQOrHM + TrjFpoj/rH+fvDqVoQgEQV+LkkeZmLtcyacM9K3K4kiGbeqEcrsk+zshBfrWRcwrRDeRmFQ91Rin + iL8HCCu3wuO3Sm2HJ4pWVVNjkVJCVYr4EwlNOQjooPjP4soooFGEaRShGUVoRmHFKBkR+RsxcyNo + KpUrya+M0GG0+wCrEJkRgQePSWBpSfUxJVuxwhOWE/AdAzZnIi5JWGaNpKZJMutEQlJ1wzNKgLag + cRgfnMiyVvtOKGVyKcsmrLmCwR+JS4DrsmKxAGOmiMEzSp6yWHoiX3og3GjDmFGyYiPGf8BPCe/w + l/mPRXzFT/rI/h/1/kW9/2Gsj07xUxPQ4pzk/yz60415/A0I28VfpfsAcX6CP4+jxsZ/zieFFfxn + /Bg1oH8F4z70x9CvQH88UvA92ySfnEAH2++JJGaKxfLnI45KHbAV6kBWrg6kZlY3FvLn+LOUBxE/ + Rb8U/bN8ipagP4nein6KB+l76J/gtbQW/VG9/w5/WuQ0f4o/iTPTxiciScKEcMQkuiMRo+i+FaHY + qL3S9jT/Fn+cckD6zUhRDrCPTBQttSWfgDzGH+TBSL4ttTGe38+62LsgGqNXRE+p/IFInRByOPK0 + ZjvGD/PDTmuds9BZ7nxIqSqsKq96SNEKtXKtTntIa7TwW8kA52HD8ptwxfnMkT1oTrTD/MaIWhdu + PIs1iXVxOoTrmIR6cPVLiHC1zM6+I6EGfh1tQeOQcQDtINohtKtIxfVKtM+ifQ7t8xITRAuhjaB8 + +MHhB4cfHH7J4QeHHxx+cPglh19qD6EJjh5w9ICjBxw9kqMHHD3g6AFHj+QQ9vaAo0dytIOjHRzt + 4GiXHO3gaAdHOzjaJUc7ONrB0S45nOBwgsMJDqfkcILDCQ4nOJySwwkOJzickqMKHFXgqAJHleSo + AkcVOKrAUSU5qsBRBY4qyaGBQwOHBg5Ncmjg0MChgUOTHBo4NHBoksMCDgs4LOCwSA4LOCzgsIDD + IjksMj4hNMFxGhynwXEaHKclx2lwnAbHaXCclhynwXEaHKf5yLhyqvEFsJwCyymwnJIsp8ByCiyn + wHJKspwCyymwnNKXHpTO4EibA2gH0Q6hCd4p8E6Bdwq8U5J3SqZXCE3whsERBkcYHGHJEQZHGBxh + cIQlRxgcYXCEJccYOMbAMQaOMckxBo4xcIyBY0xyjMnEDaEJjr89Kf/m0PCrWJcZhys/xEplf5De + lv0BekX2n6dx2X+OHpL9Z+lq2V9JdbIfoSLZQ57sg2Qzs4itLrkxEyVgC9ouNB/afWhH0E6imST0 + EtpraFFe61yiJpu2mO4zHTGdNBmOmE6beLJxi/E+4xHjSaPhiPG0kWuNuTxR1lGUFvqivB7E9fdo + OERwbZBQA6+B3hrU2Vq8a3iNM+WM9vsy9lIZO1nGjpSxL5axxjh+MVNlpcOdPofhrMuZULTO9gpa + XVHxOlSmW598O8sWKVppm2RPx7pSpwP922jjaA+hXY1Wh1aNVo5WiGaTuDLQdzmX6CKfRitGK0DT + hArKzMTdTWqK2XmMJ7KHJl5IpDihp7gEfCcixVXoJiPFW9A9FSnutTXGsSepWNwGsScQucfRH4nY + Xsf0N2PdNyK2E+geidhq0O2MFFeguzRS/KKtMZFtJ5sqWDv1vgPrFv22iO0SkG2N2ErROSLFRYK6 + DIoKMVvKuuh19IU619KYJnvEthbdkohttaA2U7EIPDNSuTTPgCZ6ZQIG/f4Y61KZc5HtjO1229tg + /x0ci/T4mTaponupcJJd4oy3PV3+VRA32iKN8YIe58O43odF/4TtocIbbfdAFit80na3rcJ2a/mk + GehbYPeNUkXEdrU2yR93ptkO2apswfLXbQHbJ2wu2zbbzkLgI7bLbE8LM6mbdfHHn7S1Q+BGrKIw + Yru4cFKa2Grbb3Paim2rtaeFf2lVTG5d+dPCA1Qd074M/i0rnBQ5vr1ukqU4y0zvmA6bLjWtN601 + 2U1LTItN+aZ0c6rZYk4yJ5jjzWaz0ayauZnM6eLnHRzizyvTjeKv18moiqsqYQsXVx77S1POzJw+ + QeE0pY23daxnbeEpN7X1auH3OuyTLH7rjrDBvp6FU9uorXN9eJWjbdIU3Rauc7SFTe2Xdo0zdms3 + sGF+wySjzq5JFhWo63LFD1GNM7rultxjxFj2dbd0d5M1c1+DtSF1Xcrq1ubzXHr0q2PuZZ0P5ofv + auvoCj+W3x2uFkA0v7stfJX4mapjPJkntjQf40mi6+46pvp5css2gVf9zd0ge12SIZuTQEbFogOZ + eT1pggz1ZL0gQ4xidEVgB12B6EAXn0hFkq4oPlHSqUzQjb+itTSPa5qkKSR6RdK8UkjzaJAx4G0e + LyqSVHaNdQkq1mXXpGGlUpDNBpJymyTBk5tNCrIxqSxcOUdSqJPUzpLUSl0Km6OxxWjSS2Zo0ktA + 4/gfvjzrHWxieejA8+KXv3rsLR60nvBN+/qt4UO9mjZ+IKT/JFhRT6+7X/QuTzhk9zSHD9ibtfHl + z59n+nkxvdzePE7Pt3R2jT/v9DRHljuXt9hdzd0TDfVdjQt03Tirq6v+PMLqhbAuoauh8TzTjWK6 + QehqFLoaha4GZ4PU1eIVed/eNW6m9eJ3QWQ/wRfFI4d7cgu612da/OtEQh9bW2A9kHtcJfYILXJ0 + hxPs68OJaGKqvLG8UUxhn4mpJPHzbvqU9cDagtzj7BF9ygJ0in09zbiWBFFbuHZrW7igY0eXSJWw + 03X+mAXES05bqcXbjH8YB2XDez4lBc77Cp7vFQqFAuIScuApuS1c1tEWXrkVlphMUNXT3A1cxQxO + USRuPC6uZTI6hUkHjGBBoU5ADiZ+I8AZj6cuEx8zjpm4eFQITuTkV/uewQl+EA3PcXwkUimfl/nI + xJJC8fwSnKisjfV4PhV9JKegWvwUQR1YRV8Y650p5QAOFx4uP1w3VjhWPlZnFD+08BCQtofEURqp + fEihoCMw4wiAwW6K/XQB9N0fycuXiscE4HB0OwLyN17ow6526L8jA6fPOjagSw1I8cGZgMTwAYoR + xyYdoRmmkM4iJ0OSRSr8P1jbNhMKZW5kc3RyZWFtCmVuZG9iagoKNiAwIG9iagoxMDgyNQplbmRv + YmoKCjcgMCBvYmoKPDwvVHlwZS9Gb250RGVzY3JpcHRvci9Gb250TmFtZS9CQUFBQUErQXJpYWwt + Qm9sZE1UCi9GbGFncyA0Ci9Gb250QkJveFstNjI3IC0zNzYgMjAwMCAxMDExXS9JdGFsaWNBbmds + ZSAwCi9Bc2NlbnQgOTA1Ci9EZXNjZW50IDIxMQovQ2FwSGVpZ2h0IDEwMTAKL1N0ZW1WIDgwCi9G + b250RmlsZTIgNSAwIFI+PgplbmRvYmoKCjggMCBvYmoKPDwvTGVuZ3RoIDI3Mi9GaWx0ZXIvRmxh + dGVEZWNvZGU+PgpzdHJlYW0KeJxdkc9uhCAQxu88BcftYQNadbuJMdm62cRD/6S2D6AwWpKKBPHg + 2xcG2yY9QH7DzDf5ZmB1c220cuzVzqIFRwelpYVlXq0A2sOoNElSKpVwe4S3mDpDmNe22+JgavQw + lyVhbz63OLvRw0XOPdwR9mIlWKVHevioWx+3qzFfMIF2lJOqohIG3+epM8/dBAxVx0b6tHLb0Uv+ + Ct43AzTFOIlWxCxhMZ0A2+kRSMl5RcvbrSKg5b9cskv6QXx21pcmvpTzLKs8p8inPPA9cnENnMX3 + c+AcOeWBC+Qc+RT7FIEfohb5HBm1l8h14MfIOZrc3QS7YZ8/a6BitdavAJeOs4eplYbffzGzCSo8 + 3zuVhO0KZW5kc3RyZWFtCmVuZG9iagoKOSAwIG9iago8PC9UeXBlL0ZvbnQvU3VidHlwZS9UcnVl + VHlwZS9CYXNlRm9udC9CQUFBQUErQXJpYWwtQm9sZE1UCi9GaXJzdENoYXIgMAovTGFzdENoYXIg + MTEKL1dpZHRoc1s3NTAgNzIyIDYxMCA4ODkgNTU2IDI3NyA2NjYgNjEwIDMzMyAyNzcgMjc3IDU1 + NiBdCi9Gb250RGVzY3JpcHRvciA3IDAgUgovVG9Vbmljb2RlIDggMCBSCj4+CmVuZG9iagoKMTAg + MCBvYmoKPDwKL0YxIDkgMCBSCj4+CmVuZG9iagoKMTEgMCBvYmoKPDwvRm9udCAxMCAwIFIKL1By + b2NTZXRbL1BERi9UZXh0XT4+CmVuZG9iagoKMSAwIG9iago8PC9UeXBlL1BhZ2UvUGFyZW50IDQg + MCBSL1Jlc291cmNlcyAxMSAwIFIvTWVkaWFCb3hbMCAwIDU5NSA4NDJdL0dyb3VwPDwvUy9UcmFu + c3BhcmVuY3kvQ1MvRGV2aWNlUkdCL0kgdHJ1ZT4+L0NvbnRlbnRzIDIgMCBSPj4KZW5kb2JqCgox + MiAwIG9iago8PC9Db3VudCAxL0ZpcnN0IDEzIDAgUi9MYXN0IDEzIDAgUgo+PgplbmRvYmoKCjEz + IDAgb2JqCjw8L1RpdGxlPEZFRkYwMDQ0MDA3NTAwNkQwMDZEMDA3OTAwMjAwMDUwMDA0NDAwNDYw + MDIwMDA2NjAwNjkwMDZDMDA2NT4KL0Rlc3RbMSAwIFIvWFlaIDU2LjcgNzczLjMgMF0vUGFyZW50 + IDEyIDAgUj4+CmVuZG9iagoKNCAwIG9iago8PC9UeXBlL1BhZ2VzCi9SZXNvdXJjZXMgMTEgMCBS + Ci9NZWRpYUJveFsgMCAwIDU5NSA4NDIgXQovS2lkc1sgMSAwIFIgXQovQ291bnQgMT4+CmVuZG9i + agoKMTQgMCBvYmoKPDwvVHlwZS9DYXRhbG9nL1BhZ2VzIDQgMCBSCi9PdXRsaW5lcyAxMiAwIFIK + Pj4KZW5kb2JqCgoxNSAwIG9iago8PC9BdXRob3I8RkVGRjAwNDUwMDc2MDA2MTAwNkUwMDY3MDA2 + NTAwNkMwMDZGMDA3MzAwMjAwMDU2MDA2QzAwNjEwMDYzMDA2ODAwNkYwMDY3MDA2OTAwNjEwMDZF + MDA2RTAwNjkwMDczPgovQ3JlYXRvcjxGRUZGMDA1NzAwNzIwMDY5MDA3NDAwNjUwMDcyPgovUHJv + ZHVjZXI8RkVGRjAwNEYwMDcwMDA2NTAwNkUwMDRGMDA2NjAwNjYwMDY5MDA2MzAwNjUwMDJFMDA2 + RjAwNzIwMDY3MDAyMDAwMzIwMDJFMDAzMT4KL0NyZWF0aW9uRGF0ZShEOjIwMDcwMjIzMTc1NjM3 + KzAyJzAwJyk+PgplbmRvYmoKCnhyZWYKMCAxNgowMDAwMDAwMDAwIDY1NTM1IGYgCjAwMDAwMTE5 + OTcgMDAwMDAgbiAKMDAwMDAwMDAxOSAwMDAwMCBuIAowMDAwMDAwMjI0IDAwMDAwIG4gCjAwMDAw + MTIzMzAgMDAwMDAgbiAKMDAwMDAwMDI0NCAwMDAwMCBuIAowMDAwMDExMTU0IDAwMDAwIG4gCjAw + MDAwMTExNzYgMDAwMDAgbiAKMDAwMDAxMTM2OCAwMDAwMCBuIAowMDAwMDExNzA5IDAwMDAwIG4g + CjAwMDAwMTE5MTAgMDAwMDAgbiAKMDAwMDAxMTk0MyAwMDAwMCBuIAowMDAwMDEyMTQwIDAwMDAw + IG4gCjAwMDAwMTIxOTYgMDAwMDAgbiAKMDAwMDAxMjQyOSAwMDAwMCBuIAowMDAwMDEyNDk0IDAw + MDAwIG4gCnRyYWlsZXIKPDwvU2l6ZSAxNi9Sb290IDE0IDAgUgovSW5mbyAxNSAwIFIKL0lEIFsg + PEY3RDc3QjNEMjJCOUY5MjgyOUQ0OUZGNUQ3OEI4RjI4Pgo8RjdENzdCM0QyMkI5RjkyODI5RDQ5 + RkY1RDc4QjhGMjg+IF0KPj4Kc3RhcnR4cmVmCjEyNzg3CiUlRU9GCg== + headers: + accept-ranges: + - bytes + age: + - '124514' + alt-svc: + - h3=":443"; ma=86400 + cache-control: + - public, max-age=604800, s-maxage=604800 + connection: + - keep-alive + content-length: + - '13264' + content-security-policy: + - frame-ancestors 'self' https://cms.w3.org/ https://cms-dev.w3.org/; upgrade-insecure-requests + content-type: + - application/pdf; qs=0.001 + etag: + - '"33d0-438b181451e00"' + expires: + - Fri, 18 Apr 2025 15:42:07 GMT + last-modified: + - Mon, 27 Aug 2007 17:15:36 GMT + strict-transport-security: + - max-age=15552000; includeSubdomains; preload + vary: + - Accept-Encoding + status: + code: 200 + message: OK +- request: + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + content-length: + - '17989' + content-type: + - application/json + host: + - api.openai.com + method: POST + parsed_body: + messages: + - content: + - text: What is the main content on this document? + type: text + - file: + file_data: data:application/pdf;base64,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CmVuZG9iagoKOSAwIG9iago8PC9UeXBlL0ZvbnQvU3VidHlwZS9UcnVlVHlwZS9CYXNlRm9udC9CQUFBQUErQXJpYWwtQm9sZE1UCi9GaXJzdENoYXIgMAovTGFzdENoYXIgMTEKL1dpZHRoc1s3NTAgNzIyIDYxMCA4ODkgNTU2IDI3NyA2NjYgNjEwIDMzMyAyNzcgMjc3IDU1NiBdCi9Gb250RGVzY3JpcHRvciA3IDAgUgovVG9Vbmljb2RlIDggMCBSCj4+CmVuZG9iagoKMTAgMCBvYmoKPDwKL0YxIDkgMCBSCj4+CmVuZG9iagoKMTEgMCBvYmoKPDwvRm9udCAxMCAwIFIKL1Byb2NTZXRbL1BERi9UZXh0XT4+CmVuZG9iagoKMSAwIG9iago8PC9UeXBlL1BhZ2UvUGFyZW50IDQgMCBSL1Jlc291cmNlcyAxMSAwIFIvTWVkaWFCb3hbMCAwIDU5NSA4NDJdL0dyb3VwPDwvUy9UcmFuc3BhcmVuY3kvQ1MvRGV2aWNlUkdCL0kgdHJ1ZT4+L0NvbnRlbnRzIDIgMCBSPj4KZW5kb2JqCgoxMiAwIG9iago8PC9Db3VudCAxL0ZpcnN0IDEzIDAgUi9MYXN0IDEzIDAgUgo+PgplbmRvYmoKCjEzIDAgb2JqCjw8L1RpdGxlPEZFRkYwMDQ0MDA3NTAwNkQwMDZEMDA3OTAwMjAwMDUwMDA0NDAwNDYwMDIwMDA2NjAwNjkwMDZDMDA2NT4KL0Rlc3RbMSAwIFIvWFlaIDU2LjcgNzczLjMgMF0vUGFyZW50IDEyIDAgUj4+CmVuZG9iagoKNCAwIG9iago8PC9UeXBlL1BhZ2VzCi9SZXNvdXJjZXMgMTEgMCBSCi9NZWRpYUJveFsgMCAwIDU5NSA4NDIgXQovS2lkc1sgMSAwIFIgXQovQ291bnQgMT4+CmVuZG9iagoKMTQgMCBvYmoKPDwvVHlwZS9DYXRhbG9nL1BhZ2VzIDQgMCBSCi9PdXRsaW5lcyAxMiAwIFIKPj4KZW5kb2JqCgoxNSAwIG9iago8PC9BdXRob3I8RkVGRjAwNDUwMDc2MDA2MTAwNkUwMDY3MDA2NTAwNkMwMDZGMDA3MzAwMjAwMDU2MDA2QzAwNjEwMDYzMDA2ODAwNkYwMDY3MDA2OTAwNjEwMDZFMDA2RTAwNjkwMDczPgovQ3JlYXRvcjxGRUZGMDA1NzAwNzIwMDY5MDA3NDAwNjUwMDcyPgovUHJvZHVjZXI8RkVGRjAwNEYwMDcwMDA2NTAwNkUwMDRGMDA2NjAwNjYwMDY5MDA2MzAwNjUwMDJFMDA2RjAwNzIwMDY3MDAyMDAwMzIwMDJFMDAzMT4KL0NyZWF0aW9uRGF0ZShEOjIwMDcwMjIzMTc1NjM3KzAyJzAwJyk+PgplbmRvYmoKCnhyZWYKMCAxNgowMDAwMDAwMDAwIDY1NTM1IGYgCjAwMDAwMTE5OTcgMDAwMDAgbiAKMDAwMDAwMDAxOSAwMDAwMCBuIAowMDAwMDAwMjI0IDAwMDAwIG4gCjAwMDAwMTIzMzAgMDAwMDAgbiAKMDAwMDAwMDI0NCAwMDAwMCBuIAowMDAwMDExMTU0IDAwMDAwIG4gCjAwMDAwMTExNzYgMDAwMDAgbiAKMDAwMDAxMTM2OCAwMDAwMCBuIAowMDAwMDExNzA5IDAwMDAwIG4gCjAwMDAwMTE5MTAgMDAwMDAgbiAKMDAwMDAxMTk0MyAwMDAwMCBuIAowMDAwMDEyMTQwIDAwMDAwIG4gCjAwMDAwMTIxOTYgMDAwMDAgbiAKMDAwMDAxMjQyOSAwMDAwMCBuIAowMDAwMDEyNDk0IDAwMDAwIG4gCnRyYWlsZXIKPDwvU2l6ZSAxNi9Sb290IDE0IDAgUgovSW5mbyAxNSAwIFIKL0lEIFsgPEY3RDc3QjNEMjJCOUY5MjgyOUQ0OUZGNUQ3OEI4RjI4Pgo8RjdENzdCM0QyMkI5RjkyODI5RDQ5RkY1RDc4QjhGMjg+IF0KPj4Kc3RhcnR4cmVmCjEyNzg3CiUlRU9GCg== + filename: https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf + type: file + role: user + model: gpt-4o + n: 1 + stream: false + uri: https://api.openai.com/v1/chat/completions + response: + headers: + access-control-expose-headers: + - X-Request-ID + alt-svc: + - h3=":443"; ma=86400 + connection: + - keep-alive + content-length: + - '873' + content-type: + - application/json + openai-organization: + - gearheart-io + openai-processing-ms: + - '531' + openai-version: + - '2020-10-01' + strict-transport-security: + - max-age=31536000; includeSubDomains; preload + transfer-encoding: + - chunked + parsed_body: + choices: + - finish_reason: stop + index: 0 + logprobs: null + message: + annotations: [] + content: The document contains the text "Dummy PDF file" on its single page. + refusal: null + role: assistant + created: 1745093899 + id: chatcmpl-BO8vj699pdVsDxRwYD63JJVUxqzfg + model: gpt-4o-2024-08-06 + object: chat.completion + service_tier: default + system_fingerprint: fp_f5bdcc3276 + usage: + completion_tokens: 16 + completion_tokens_details: + accepted_prediction_tokens: 0 + audio_tokens: 0 + reasoning_tokens: 0 + rejected_prediction_tokens: 0 + prompt_tokens: 235 + prompt_tokens_details: + audio_tokens: 0 + cached_tokens: 0 + total_tokens: 251 + status: + code: 200 + message: OK +version: 1 diff --git a/tests/models/mock_xai.py b/tests/models/mock_xai.py new file mode 100644 index 0000000000..98ce452127 --- /dev/null +++ b/tests/models/mock_xai.py @@ -0,0 +1,223 @@ +from __future__ import annotations as _annotations + +from collections.abc import Sequence +from dataclasses import dataclass, field +from functools import cached_property +from typing import Any, cast + +from ..conftest import raise_if_exception, try_import +from .mock_async_stream import MockAsyncStream + +with try_import() as imports_successful: + import xai_sdk.chat as chat_types + from xai_sdk import AsyncClient + + MockResponse = chat_types.Response | Exception + # xai_sdk streaming returns tuples of (Response, chunk) where chunk type is not explicitly defined + MockResponseChunk = tuple[chat_types.Response, Any] | Exception + + +@dataclass +class MockXai: + """Mock for xAI SDK AsyncClient to simulate xAI API responses.""" + + responses: MockResponse | Sequence[MockResponse] | None = None + stream_data: Sequence[MockResponseChunk] | Sequence[Sequence[MockResponseChunk]] | None = None + index: int = 0 + chat_create_kwargs: list[dict[str, Any]] = field(default_factory=list) + api_key: str = 'test-api-key' + + @cached_property + def chat(self) -> Any: + """Create mock chat interface.""" + return type('Chat', (), {'create': self.chat_create}) + + @cached_property + def files(self) -> Any: + """Create mock files interface.""" + return type('Files', (), {'upload': self.files_upload}) + + @classmethod + def create_mock( + cls, responses: MockResponse | Sequence[MockResponse], api_key: str = 'test-api-key' + ) -> AsyncClient: + """Create a mock AsyncClient for non-streaming responses.""" + return cast(AsyncClient, cls(responses=responses, api_key=api_key)) + + @classmethod + def create_mock_stream( + cls, + stream: Sequence[MockResponseChunk] | Sequence[Sequence[MockResponseChunk]], + api_key: str = 'test-api-key', + ) -> AsyncClient: + """Create a mock AsyncClient for streaming responses.""" + return cast(AsyncClient, cls(stream_data=stream, api_key=api_key)) + + def chat_create(self, *_args: Any, **kwargs: Any) -> MockChatInstance: + """Mock the chat.create method.""" + self.chat_create_kwargs.append(kwargs) + return MockChatInstance( + responses=self.responses, + stream_data=self.stream_data, + index=self.index, + parent=self, + ) + + async def files_upload(self, data: bytes, filename: str) -> Any: + """Mock the files.upload method.""" + # Return a mock uploaded file object with an id + return type('UploadedFile', (), {'id': f'file-{filename}'})() + + +@dataclass +class MockChatInstance: + """Mock for the chat instance returned by client.chat.create().""" + + responses: MockResponse | Sequence[MockResponse] | None = None + stream_data: Sequence[MockResponseChunk] | Sequence[Sequence[MockResponseChunk]] | None = None + index: int = 0 + parent: MockXai | None = None + + async def sample(self) -> chat_types.Response: + """Mock the sample() method for non-streaming responses.""" + assert self.responses is not None, 'you can only use sample() if responses are provided' + + if isinstance(self.responses, Sequence): + raise_if_exception(self.responses[self.index]) + response = cast(chat_types.Response, self.responses[self.index]) + else: + raise_if_exception(self.responses) + response = cast(chat_types.Response, self.responses) + + if self.parent: + self.parent.index += 1 + + return response + + def stream(self) -> MockAsyncStream[MockResponseChunk]: + """Mock the stream() method for streaming responses.""" + assert self.stream_data is not None, 'you can only use stream() if stream_data is provided' + + # Check if we have nested sequences (multiple streams) vs single stream + # We need to check if it's a list of tuples (single stream) vs list of lists (multiple streams) + if isinstance(self.stream_data, list) and len(self.stream_data) > 0: + first_item = self.stream_data[0] + # If first item is a list (not a tuple), we have multiple streams + if isinstance(first_item, list): + data = cast(list[MockResponseChunk], self.stream_data[self.index]) + else: + # Single stream - use the data as is + data = cast(list[MockResponseChunk], self.stream_data) + else: + data = cast(list[MockResponseChunk], self.stream_data) + + if self.parent: + self.parent.index += 1 + + return MockAsyncStream(iter(data)) + + +def get_mock_chat_create_kwargs(async_client: AsyncClient) -> list[dict[str, Any]]: + """Extract the kwargs passed to chat.create from a mock client.""" + if isinstance(async_client, MockXai): + return async_client.chat_create_kwargs + else: # pragma: no cover + raise RuntimeError('Not a MockXai instance') + + +@dataclass +class MockXaiResponse: + """Mock Response object that mimics xai_sdk.chat.Response interface.""" + + id: str = 'grok-123' + content: str = '' + tool_calls: list[Any] = field(default_factory=list) + finish_reason: str = 'stop' + usage: Any | None = None # Would be usage_pb2.SamplingUsage in real xai_sdk + reasoning_content: str = '' # Human-readable reasoning trace + encrypted_content: str = '' # Encrypted reasoning signature + + # Note: The real xAI SDK usage object uses protobuf fields: + # - prompt_tokens (not input_tokens) + # - completion_tokens (not output_tokens) + # - reasoning_tokens + # - cached_prompt_text_tokens + + +@dataclass +class MockXaiToolCall: + """Mock ToolCall object that mimics chat_pb2.ToolCall interface.""" + + id: str + function: Any # Would be chat_pb2.Function with name and arguments + + +@dataclass +class MockXaiFunction: + """Mock Function object for tool calls.""" + + name: str + arguments: dict[str, Any] + + +def create_response( + content: str = '', + tool_calls: list[Any] | None = None, + finish_reason: str = 'stop', + usage: Any | None = None, + reasoning_content: str = '', + encrypted_content: str = '', +) -> chat_types.Response: + """Create a mock Response object for testing. + + Returns a MockXaiResponse that mimics the xai_sdk.chat.Response interface. + """ + return cast( + chat_types.Response, + MockXaiResponse( + id='grok-123', + content=content, + tool_calls=tool_calls or [], + finish_reason=finish_reason, + usage=usage, + reasoning_content=reasoning_content, + encrypted_content=encrypted_content, + ), + ) + + +def create_tool_call( + id: str, + name: str, + arguments: dict[str, Any], +) -> MockXaiToolCall: + """Create a mock ToolCall object for testing. + + Returns a MockXaiToolCall that mimics the chat_pb2.ToolCall interface. + """ + return MockXaiToolCall( + id=id, + function=MockXaiFunction(name=name, arguments=arguments), + ) + + +@dataclass +class MockXaiResponseChunk: + """Mock response chunk for streaming.""" + + content: str = '' + tool_calls: list[Any] = field(default_factory=list) + + +def create_response_chunk( + content: str = '', + tool_calls: list[Any] | None = None, +) -> MockXaiResponseChunk: + """Create a mock response chunk object for testing. + + Returns a MockXaiResponseChunk for streaming responses. + """ + return MockXaiResponseChunk( + content=content, + tool_calls=tool_calls or [], + ) diff --git a/tests/models/test_fallback.py b/tests/models/test_fallback.py index d03726330a..62e3454bd3 100644 --- a/tests/models/test_fallback.py +++ b/tests/models/test_fallback.py @@ -415,6 +415,7 @@ def test_all_failed_instrumented(capfire: CaptureLogfire) -> None: 'gen_ai.agent.name': 'agent', 'logfire.msg': 'agent run', 'logfire.span_type': 'span', + 'logfire.exception.fingerprint': '0000000000000000000000000000000000000000000000000000000000000000', 'pydantic_ai.all_messages': [{'role': 'user', 'parts': [{'type': 'text', 'content': 'hello'}]}], 'logfire.json_schema': { 'type': 'object', diff --git a/tests/models/test_model_names.py b/tests/models/test_model_names.py index 058ffe28c7..ac94a8c422 100644 --- a/tests/models/test_model_names.py +++ b/tests/models/test_model_names.py @@ -63,6 +63,7 @@ def get_model_names(model_name_type: Any) -> Iterator[str]: google_names = [f'google-gla:{n}' for n in get_model_names(GoogleModelName)] + [ f'google-vertex:{n}' for n in get_model_names(GoogleModelName) ] + xai_names = [f'xai:{n}' for n in get_model_names(GrokModelName)] grok_names = [f'grok:{n}' for n in get_model_names(GrokModelName)] groq_names = [f'groq:{n}' for n in get_model_names(GroqModelName)] moonshotai_names = [f'moonshotai:{n}' for n in get_model_names(MoonshotAIModelName)] @@ -79,6 +80,7 @@ def get_model_names(model_name_type: Any) -> Iterator[str]: anthropic_names + cohere_names + google_names + + xai_names + grok_names + groq_names + mistral_names diff --git a/tests/models/test_xai.py b/tests/models/test_xai.py new file mode 100644 index 0000000000..4c830bf6cb --- /dev/null +++ b/tests/models/test_xai.py @@ -0,0 +1,1513 @@ +from __future__ import annotations as _annotations + +import json +import os +from datetime import timezone +from types import SimpleNamespace +from typing import Any, cast + +import pytest +from inline_snapshot import snapshot +from typing_extensions import TypedDict + +from pydantic_ai import ( + Agent, + AudioUrl, + BinaryContent, + BuiltinToolCallPart, + DocumentUrl, + ImageUrl, + ModelRequest, + ModelResponse, + ModelRetry, + RetryPromptPart, + SystemPromptPart, + TextPart, + ThinkingPart, + ToolCallPart, + ToolReturnPart, + UserPromptPart, + VideoUrl, +) +from pydantic_ai.output import NativeOutput +from pydantic_ai.result import RunUsage +from pydantic_ai.settings import ModelSettings +from pydantic_ai.usage import RequestUsage + +from ..conftest import IsDatetime, IsNow, IsStr, try_import +from .mock_xai import ( + MockXai, + MockXaiResponse, + MockXaiResponseChunk, + create_response, + create_tool_call, + get_mock_chat_create_kwargs, +) + +with try_import() as imports_successful: + import xai_sdk.chat as chat_types + + from pydantic_ai.models.xai import XaiModel + from pydantic_ai.providers.xai import XaiProvider + + MockResponse = chat_types.Response | Exception + # xai_sdk streaming returns tuples of (Response, chunk) where chunk type is not explicitly defined + MockResponseChunk = tuple[chat_types.Response, Any] | Exception + +pytestmark = [ + pytest.mark.skipif(not imports_successful(), reason='xai_sdk not installed'), + pytest.mark.anyio, + pytest.mark.vcr, + pytest.mark.filterwarnings( + 'ignore:`BuiltinToolCallEvent` is deprecated, look for `PartStartEvent` and `PartDeltaEvent` with `BuiltinToolCallPart` instead.:DeprecationWarning' + ), + pytest.mark.filterwarnings( + 'ignore:`BuiltinToolResultEvent` is deprecated, look for `PartStartEvent` and `PartDeltaEvent` with `BuiltinToolReturnPart` instead.:DeprecationWarning' + ), +] + + +def test_xai_init(): + from pydantic_ai.providers.xai import XaiProvider + + provider = XaiProvider(api_key='foobar') + m = XaiModel('grok-4-1-fast-non-reasoning', provider=provider) + # Check model properties without accessing private attributes + assert m.model_name == 'grok-4-1-fast-non-reasoning' + assert m.system == 'xai' + + +async def test_xai_request_simple_success(allow_model_requests: None): + response = create_response(content='world') + mock_client = MockXai.create_mock(response) + m = XaiModel('grok-4-1-fast-non-reasoning', provider=XaiProvider(xai_client=mock_client)) + agent = Agent(m) + + result = await agent.run('hello') + assert result.output == 'world' + assert result.usage() == snapshot(RunUsage(requests=1)) + + # reset the index so we get the same response again + mock_client.index = 0 # type: ignore + + result = await agent.run('hello', message_history=result.new_messages()) + assert result.output == 'world' + assert result.usage() == snapshot(RunUsage(requests=1)) + assert result.all_messages() == snapshot( + [ + ModelRequest( + parts=[UserPromptPart(content='hello', timestamp=IsNow(tz=timezone.utc))], + run_id=IsStr(), + ), + ModelResponse( + parts=[TextPart(content='world')], + model_name='grok-4-1-fast-non-reasoning', + timestamp=IsDatetime(), + provider_name='xai', + provider_response_id='grok-123', + finish_reason='stop', + run_id=IsStr(), + ), + ModelRequest( + parts=[UserPromptPart(content='hello', timestamp=IsNow(tz=timezone.utc))], + run_id=IsStr(), + ), + ModelResponse( + parts=[TextPart(content='world')], + model_name='grok-4-1-fast-non-reasoning', + timestamp=IsDatetime(), + provider_name='xai', + provider_response_id='grok-123', + finish_reason='stop', + run_id=IsStr(), + ), + ] + ) + + +async def test_xai_request_simple_usage(allow_model_requests: None): + response = create_response( + content='world', + usage=SimpleNamespace(prompt_tokens=2, completion_tokens=1), + ) + mock_client = MockXai.create_mock(response) + m = XaiModel('grok-4-1-fast-non-reasoning', provider=XaiProvider(xai_client=mock_client)) + agent = Agent(m) + + result = await agent.run('Hello') + assert result.output == 'world' + assert result.usage() == snapshot( + RunUsage( + requests=1, + input_tokens=2, + output_tokens=1, + ) + ) + + +async def test_xai_image_input(allow_model_requests: None): + """Test that xAI model handles image inputs (text is extracted from content).""" + response = create_response(content='done') + mock_client = MockXai.create_mock(response) + model = XaiModel('grok-4-1-fast-non-reasoning', provider=XaiProvider(xai_client=mock_client)) + agent = Agent(model) + + image_url = ImageUrl('https://example.com/image.png') + binary_image = BinaryContent(b'\x89PNG', media_type='image/png') + + result = await agent.run(['Describe these inputs.', image_url, binary_image]) + assert result.output == 'done' + + +async def test_xai_request_structured_response(allow_model_requests: None): + tool_call = create_tool_call( + id='123', + name='final_result', + arguments={'response': [1, 2, 123]}, + ) + response = create_response(tool_calls=[tool_call]) + mock_client = MockXai.create_mock(response) + m = XaiModel('grok-4-1-fast-non-reasoning', provider=XaiProvider(xai_client=mock_client)) + agent = Agent(m, output_type=list[int]) + + result = await agent.run('Hello') + assert result.output == [1, 2, 123] + assert result.all_messages() == snapshot( + [ + ModelRequest( + parts=[UserPromptPart(content='Hello', timestamp=IsNow(tz=timezone.utc))], + run_id=IsStr(), + ), + ModelResponse( + parts=[ + ToolCallPart( + tool_name='final_result', + args={'response': [1, 2, 123]}, + tool_call_id='123', + ) + ], + model_name='grok-4-1-fast-non-reasoning', + timestamp=IsDatetime(), + provider_name='xai', + provider_response_id='grok-123', + finish_reason='stop', + run_id=IsStr(), + ), + ModelRequest( + parts=[ + ToolReturnPart( + tool_name='final_result', + content='Final result processed.', + tool_call_id='123', + timestamp=IsNow(tz=timezone.utc), + ) + ], + run_id=IsStr(), + ), + ] + ) + + +async def test_xai_request_tool_call(allow_model_requests: None): + responses = [ + create_response( + tool_calls=[create_tool_call(id='1', name='get_location', arguments={'loc_name': 'San Fransisco'})], + usage=SimpleNamespace(prompt_tokens=2, completion_tokens=1), + ), + create_response( + tool_calls=[create_tool_call(id='2', name='get_location', arguments={'loc_name': 'London'})], + usage=SimpleNamespace(prompt_tokens=3, completion_tokens=2), + ), + create_response(content='final response'), + ] + mock_client = MockXai.create_mock(responses) + m = XaiModel('grok-4-1-fast-non-reasoning', provider=XaiProvider(xai_client=mock_client)) + agent = Agent(m, system_prompt='this is the system prompt') + + @agent.tool_plain + async def get_location(loc_name: str) -> str: + if loc_name == 'London': + return json.dumps({'lat': 51, 'lng': 0}) + else: + raise ModelRetry('Wrong location, please try again') + + result = await agent.run('Hello') + assert result.output == 'final response' + assert result.all_messages() == snapshot( + [ + ModelRequest( + parts=[ + SystemPromptPart(content='this is the system prompt', timestamp=IsNow(tz=timezone.utc)), + UserPromptPart(content='Hello', timestamp=IsNow(tz=timezone.utc)), + ], + run_id=IsStr(), + ), + ModelResponse( + parts=[ + ToolCallPart( + tool_name='get_location', + args={'loc_name': 'San Fransisco'}, + tool_call_id='1', + ) + ], + usage=RequestUsage( + input_tokens=2, + output_tokens=1, + ), + model_name='grok-4-1-fast-non-reasoning', + timestamp=IsDatetime(), + provider_name='xai', + provider_response_id='grok-123', + finish_reason='stop', + run_id=IsStr(), + ), + ModelRequest( + parts=[ + RetryPromptPart( + content='Wrong location, please try again', + tool_name='get_location', + tool_call_id='1', + timestamp=IsNow(tz=timezone.utc), + ) + ], + run_id=IsStr(), + ), + ModelResponse( + parts=[ + ToolCallPart( + tool_name='get_location', + args={'loc_name': 'London'}, + tool_call_id='2', + ) + ], + usage=RequestUsage( + input_tokens=3, + output_tokens=2, + ), + model_name='grok-4-1-fast-non-reasoning', + timestamp=IsDatetime(), + provider_name='xai', + provider_response_id='grok-123', + finish_reason='stop', + run_id=IsStr(), + ), + ModelRequest( + parts=[ + ToolReturnPart( + tool_name='get_location', + content='{"lat": 51, "lng": 0}', + tool_call_id='2', + timestamp=IsNow(tz=timezone.utc), + ) + ], + run_id=IsStr(), + ), + ModelResponse( + parts=[TextPart(content='final response')], + model_name='grok-4-1-fast-non-reasoning', + timestamp=IsDatetime(), + provider_name='xai', + provider_response_id='grok-123', + finish_reason='stop', + run_id=IsStr(), + ), + ] + ) + assert result.usage() == snapshot(RunUsage(requests=3, input_tokens=5, output_tokens=3, tool_calls=1)) + + +# Helpers for creating Grok streaming chunks +def grok_chunk(response: chat_types.Response, chunk: Any) -> tuple[chat_types.Response, Any]: + """Create a Grok streaming chunk (response, chunk) tuple.""" + return (response, chunk) + + +def grok_text_chunk(text: str, finish_reason: str = 'stop') -> tuple[chat_types.Response, Any]: + """Create a text streaming chunk for Grok. + + Note: For streaming, Response accumulates content, Chunk is the delta. + Since we can't easily track state across calls, we pass full accumulated text as response.content + and the delta as chunk.content. + """ + # Create chunk (delta) - just this piece of text + chunk = MockXaiResponseChunk(content=text, tool_calls=[]) + + # Create response (accumulated) - for simplicity in mocks, we'll just use the same text + # In real usage, the Response object would accumulate over multiple chunks + response = MockXaiResponse( + id='grok-123', + content=text, # This will be accumulated by the streaming handler + tool_calls=[], + finish_reason=finish_reason if finish_reason else '', + usage=SimpleNamespace(prompt_tokens=2, completion_tokens=1) if finish_reason else None, + ) + + return (cast(chat_types.Response, response), chunk) + + +def grok_reasoning_text_chunk( + text: str, reasoning_content: str = '', encrypted_content: str = '', finish_reason: str = 'stop' +) -> tuple[chat_types.Response, Any]: + """Create a text streaming chunk for Grok with reasoning content. + + Args: + text: The text content delta + reasoning_content: The reasoning trace (accumulated, not a delta) + encrypted_content: The encrypted reasoning signature (accumulated, not a delta) + finish_reason: The finish reason + """ + # Create chunk (delta) - just this piece of text + chunk = MockXaiResponseChunk(content=text, tool_calls=[]) + + # Create response (accumulated) - includes reasoning content + response = MockXaiResponse( + id='grok-123', + content=text, + tool_calls=[], + finish_reason=finish_reason if finish_reason else '', + usage=SimpleNamespace(prompt_tokens=2, completion_tokens=1) if finish_reason else None, + reasoning_content=reasoning_content, + encrypted_content=encrypted_content, + ) + + return (cast(chat_types.Response, response), chunk) + + +async def test_xai_stream_text(allow_model_requests: None): + stream = [grok_text_chunk('hello '), grok_text_chunk('world')] + mock_client = MockXai.create_mock_stream(stream) + m = XaiModel('grok-4-1-fast-non-reasoning', provider=XaiProvider(xai_client=mock_client)) + agent = Agent(m) + + async with agent.run_stream('') as result: + assert not result.is_complete + assert [c async for c in result.stream_text(debounce_by=None)] == snapshot(['hello ', 'hello world']) + assert result.is_complete + assert result.usage() == snapshot(RunUsage(requests=1, input_tokens=2, output_tokens=1)) + + +async def test_xai_stream_text_finish_reason(allow_model_requests: None): + # Create streaming chunks with finish reasons + stream = [ + grok_text_chunk('hello ', ''), + grok_text_chunk('world', ''), + grok_text_chunk('.', 'stop'), + ] + mock_client = MockXai.create_mock_stream(stream) + m = XaiModel('grok-4-1-fast-non-reasoning', provider=XaiProvider(xai_client=mock_client)) + agent = Agent(m) + + async with agent.run_stream('') as result: + assert not result.is_complete + assert [c async for c in result.stream_text(debounce_by=None)] == snapshot( + ['hello ', 'hello world', 'hello world.'] + ) + assert result.is_complete + async for response, is_last in result.stream_responses(debounce_by=None): + if is_last: + assert response == snapshot( + ModelResponse( + parts=[TextPart(content='hello world.')], + usage=RequestUsage(input_tokens=2, output_tokens=1), + model_name='grok-4-1-fast-non-reasoning', + timestamp=IsDatetime(), + provider_name='xai', + provider_response_id='grok-123', + finish_reason='stop', + ) + ) + + +def grok_tool_chunk( + tool_name: str | None, tool_arguments: str | None, finish_reason: str = '', accumulated_args: str = '' +) -> tuple[chat_types.Response, Any]: + """Create a tool call streaming chunk for Grok. + + Args: + tool_name: The tool name (should be provided in all chunks for proper tracking) + tool_arguments: The delta of arguments for this chunk + finish_reason: The finish reason (only in last chunk) + accumulated_args: The accumulated arguments string up to and including this chunk + + Note: Unlike the real xAI SDK which only sends the tool name in the first chunk, + our mock includes it in every chunk to ensure proper tool call tracking. + """ + # Infer tool name from accumulated state if not provided + effective_tool_name = tool_name or ('final_result' if accumulated_args else None) + + # Create the chunk (delta) - includes tool name for proper tracking + chunk_tool_call = None + if effective_tool_name is not None or tool_arguments is not None: + chunk_tool_call = SimpleNamespace( + id='tool-123', + function=SimpleNamespace( + name=effective_tool_name, + # arguments should be a string (delta JSON), default to empty string + arguments=tool_arguments if tool_arguments is not None else '', + ), + ) + + # Chunk (delta) + chunk = MockXaiResponseChunk( + content='', + tool_calls=[chunk_tool_call] if chunk_tool_call else [], + ) + + # Response (accumulated) - contains the full accumulated tool call + response_tool_call = SimpleNamespace( + id='tool-123', + function=SimpleNamespace( + name=effective_tool_name, + arguments=accumulated_args, # Full accumulated arguments + ), + ) + + response = MockXaiResponse( + id='grok-123', + content='', + tool_calls=[response_tool_call] if (effective_tool_name is not None or accumulated_args) else [], + finish_reason=finish_reason, + usage=SimpleNamespace(prompt_tokens=20, completion_tokens=1) if finish_reason else None, + ) + + return (cast(chat_types.Response, response), chunk) + + +class MyTypedDict(TypedDict, total=False): + first: str + second: str + + +async def test_xai_stream_structured(allow_model_requests: None): + stream = [ + grok_tool_chunk('final_result', None, accumulated_args=''), + grok_tool_chunk(None, '{"first": "One', accumulated_args='{"first": "One'), + grok_tool_chunk(None, '", "second": "Two"', accumulated_args='{"first": "One", "second": "Two"'), + grok_tool_chunk(None, '}', finish_reason='stop', accumulated_args='{"first": "One", "second": "Two"}'), + ] + mock_client = MockXai.create_mock_stream(stream) + m = XaiModel('grok-4-1-fast-non-reasoning', provider=XaiProvider(xai_client=mock_client)) + agent = Agent(m, output_type=MyTypedDict) + + async with agent.run_stream('') as result: + assert not result.is_complete + assert [dict(c) async for c in result.stream_output(debounce_by=None)] == snapshot( + [{'first': 'One', 'second': 'Two'}] + ) + assert result.is_complete + assert result.usage() == snapshot(RunUsage(requests=1, input_tokens=20, output_tokens=1)) + + +async def test_xai_stream_structured_finish_reason(allow_model_requests: None): + stream = [ + grok_tool_chunk('final_result', None, accumulated_args=''), + grok_tool_chunk(None, '{"first": "One', accumulated_args='{"first": "One'), + grok_tool_chunk(None, '", "second": "Two"', accumulated_args='{"first": "One", "second": "Two"'), + grok_tool_chunk(None, '}', accumulated_args='{"first": "One", "second": "Two"}'), + grok_tool_chunk(None, None, finish_reason='stop', accumulated_args='{"first": "One", "second": "Two"}'), + ] + mock_client = MockXai.create_mock_stream(stream) + m = XaiModel('grok-4-1-fast-non-reasoning', provider=XaiProvider(xai_client=mock_client)) + agent = Agent(m, output_type=MyTypedDict) + + async with agent.run_stream('') as result: + assert not result.is_complete + assert [dict(c) async for c in result.stream_output(debounce_by=None)] == snapshot( + [{'first': 'One', 'second': 'Two'}] + ) + assert result.is_complete + + +async def test_xai_stream_native_output(allow_model_requests: None): + stream = [ + grok_text_chunk('{"first": "One'), + grok_text_chunk('", "second": "Two"'), + grok_text_chunk('}'), + ] + mock_client = MockXai.create_mock_stream(stream) + m = XaiModel('grok-4-1-fast-non-reasoning', provider=XaiProvider(xai_client=mock_client)) + agent = Agent(m, output_type=NativeOutput(MyTypedDict)) + + async with agent.run_stream('') as result: + assert not result.is_complete + assert [dict(c) async for c in result.stream_output(debounce_by=None)] == snapshot( + [{'first': 'One'}, {'first': 'One', 'second': 'Two'}, {'first': 'One', 'second': 'Two'}] + ) + assert result.is_complete + + +async def test_xai_stream_tool_call_with_empty_text(allow_model_requests: None): + stream = [ + grok_tool_chunk('final_result', None, accumulated_args=''), + grok_tool_chunk(None, '{"first": "One', accumulated_args='{"first": "One'), + grok_tool_chunk(None, '", "second": "Two"', accumulated_args='{"first": "One", "second": "Two"'), + grok_tool_chunk(None, '}', finish_reason='stop', accumulated_args='{"first": "One", "second": "Two"}'), + ] + mock_client = MockXai.create_mock_stream(stream) + m = XaiModel('grok-4-1-fast-non-reasoning', provider=XaiProvider(xai_client=mock_client)) + agent = Agent(m, output_type=[str, MyTypedDict]) + + async with agent.run_stream('') as result: + assert not result.is_complete + assert [c async for c in result.stream_output(debounce_by=None)] == snapshot( + [{'first': 'One'}, {'first': 'One', 'second': 'Two'}, {'first': 'One', 'second': 'Two'}] + ) + assert await result.get_output() == snapshot({'first': 'One', 'second': 'Two'}) + + +async def test_xai_no_delta(allow_model_requests: None): + stream = [ + grok_text_chunk('hello '), + grok_text_chunk('world'), + ] + mock_client = MockXai.create_mock_stream(stream) + m = XaiModel('grok-4-1-fast-non-reasoning', provider=XaiProvider(xai_client=mock_client)) + agent = Agent(m) + + async with agent.run_stream('') as result: + assert not result.is_complete + assert [c async for c in result.stream_text(debounce_by=None)] == snapshot(['hello ', 'hello world']) + assert result.is_complete + assert result.usage() == snapshot(RunUsage(requests=1, input_tokens=2, output_tokens=1)) + + +async def test_xai_none_delta(allow_model_requests: None): + # Test handling of chunks without deltas + stream = [ + grok_text_chunk('hello '), + grok_text_chunk('world'), + ] + mock_client = MockXai.create_mock_stream(stream) + m = XaiModel('grok-4-1-fast-non-reasoning', provider=XaiProvider(xai_client=mock_client)) + agent = Agent(m) + + async with agent.run_stream('') as result: + assert not result.is_complete + assert [c async for c in result.stream_text(debounce_by=None)] == snapshot(['hello ', 'hello world']) + assert result.is_complete + assert result.usage() == snapshot(RunUsage(requests=1, input_tokens=2, output_tokens=1)) + + +# Skip OpenAI-specific tests that don't apply to Grok +# test_system_prompt_role - OpenAI specific +# test_system_prompt_role_o1_mini - OpenAI specific +# test_openai_pass_custom_system_prompt_role - OpenAI specific +# test_openai_o1_mini_system_role - OpenAI specific + + +@pytest.mark.parametrize('parallel_tool_calls', [True, False]) +async def test_xai_parallel_tool_calls(allow_model_requests: None, parallel_tool_calls: bool) -> None: + tool_call = create_tool_call( + id='123', + name='final_result', + arguments={'response': [1, 2, 3]}, + ) + response = create_response(content='', tool_calls=[tool_call], finish_reason='tool_calls') + mock_client = MockXai.create_mock(response) + m = XaiModel('grok-4-1-fast-non-reasoning', provider=XaiProvider(xai_client=mock_client)) + agent = Agent(m, output_type=list[int], model_settings=ModelSettings(parallel_tool_calls=parallel_tool_calls)) + + await agent.run('Hello') + assert get_mock_chat_create_kwargs(mock_client)[0]['parallel_tool_calls'] == parallel_tool_calls + + +async def test_xai_penalty_parameters(allow_model_requests: None) -> None: + response = create_response(content='test response') + mock_client = MockXai.create_mock(response) + m = XaiModel('grok-4-1-fast-non-reasoning', provider=XaiProvider(xai_client=mock_client)) + + settings = ModelSettings( + temperature=0.7, + presence_penalty=0.5, + frequency_penalty=0.3, + parallel_tool_calls=False, + ) + + agent = Agent(m, model_settings=settings) + result = await agent.run('Hello') + + # Check that all settings were passed to the xAI SDK + kwargs = get_mock_chat_create_kwargs(mock_client)[0] + assert kwargs['temperature'] == 0.7 + assert kwargs['presence_penalty'] == 0.5 + assert kwargs['frequency_penalty'] == 0.3 + assert kwargs['parallel_tool_calls'] is False + assert result.output == 'test response' + + +async def test_xai_image_url_input(allow_model_requests: None): + response = create_response(content='world') + mock_client = MockXai.create_mock(response) + m = XaiModel('grok-4-1-fast-non-reasoning', provider=XaiProvider(xai_client=mock_client)) + agent = Agent(m) + + result = await agent.run( + [ + 'hello', + ImageUrl(url='https://t3.ftcdn.net/jpg/00/85/79/92/360_F_85799278_0BBGV9OAdQDTLnKwAPBCcg1J7QtiieJY.jpg'), + ] + ) + assert result.output == 'world' + # Verify that the image URL was included in the messages + assert len(get_mock_chat_create_kwargs(mock_client)) == 1 + + +@pytest.mark.skipif(os.getenv('XAI_API_KEY') is None, reason='Requires XAI_API_KEY (gRPC, no cassettes)') +async def test_xai_image_url_tool_response(allow_model_requests: None, xai_api_key: str): + m = XaiModel('grok-4-1-fast-non-reasoning', provider=XaiProvider(api_key=xai_api_key)) + agent = Agent(m) + + @agent.tool_plain + async def get_image() -> ImageUrl: + return ImageUrl(url='https://t3.ftcdn.net/jpg/00/85/79/92/360_F_85799278_0BBGV9OAdQDTLnKwAPBCcg1J7QtiieJY.jpg') + + result = await agent.run(['What food is in the image you can get from the get_image tool?']) + + # Verify structure with matchers for dynamic values + messages = result.all_messages() + assert len(messages) == 4 + + # Verify message types and key content + assert isinstance(messages[0], ModelRequest) + assert isinstance(messages[1], ModelResponse) + assert isinstance(messages[2], ModelRequest) + assert isinstance(messages[3], ModelResponse) + + # Verify tool was called + assert isinstance(messages[1].parts[0], ToolCallPart) + assert messages[1].parts[0].tool_name == 'get_image' + + # Verify image was passed back to model + assert isinstance(messages[2].parts[1], UserPromptPart) + assert isinstance(messages[2].parts[1].content, list) + assert any(isinstance(item, ImageUrl) for item in messages[2].parts[1].content) + + # Verify model responded about the image + assert isinstance(messages[3].parts[0], TextPart) + assert 'potato' in messages[3].parts[0].content.lower() + + +@pytest.mark.skipif(os.getenv('XAI_API_KEY') is None, reason='Requires XAI_API_KEY (gRPC, no cassettes)') +async def test_xai_image_as_binary_content_tool_response( + allow_model_requests: None, image_content: BinaryContent, xai_api_key: str +): + m = XaiModel('grok-4-1-fast-non-reasoning', provider=XaiProvider(api_key=xai_api_key)) + agent = Agent(m) + + @agent.tool_plain + async def get_image() -> BinaryContent: + return image_content + + result = await agent.run(['What fruit is in the image you can get from the get_image tool?']) + + # Verify structure with matchers for dynamic values + messages = result.all_messages() + assert len(messages) == 4 + + # Verify message types and key content + assert isinstance(messages[0], ModelRequest) + assert isinstance(messages[1], ModelResponse) + assert isinstance(messages[2], ModelRequest) + assert isinstance(messages[3], ModelResponse) + + # Verify tool was called + assert isinstance(messages[1].parts[0], ToolCallPart) + assert messages[1].parts[0].tool_name == 'get_image' + + # Verify binary image content was passed back to model + assert isinstance(messages[2].parts[1], UserPromptPart) + assert isinstance(messages[2].parts[1].content, list) + has_binary_image = any(isinstance(item, BinaryContent) and item.is_image for item in messages[2].parts[1].content) + assert has_binary_image, 'Expected BinaryContent image in tool response' + + # Verify model responded about the image + assert isinstance(messages[3].parts[0], TextPart) + response_text = messages[3].parts[0].content.lower() + assert 'kiwi' in response_text or 'fruit' in response_text + + +@pytest.mark.skipif(os.getenv('XAI_API_KEY') is None, reason='Requires XAI_API_KEY (gRPC, no cassettes)') +async def test_xai_image_as_binary_content_input( + allow_model_requests: None, image_content: BinaryContent, xai_api_key: str +): + """Test passing binary image content directly as input (not from a tool).""" + m = XaiModel('grok-4-1-fast-non-reasoning', provider=XaiProvider(api_key=xai_api_key)) + agent = Agent(m) + + result = await agent.run(['What fruit is in the image?', image_content]) + + # Verify the model received and processed the image + assert result.output + response_text = result.output.lower() + assert 'kiwi' in response_text or 'fruit' in response_text + + +@pytest.mark.skipif(os.getenv('XAI_API_KEY') is None, reason='Requires XAI_API_KEY (live API test)') +async def test_xai_document_url_input(allow_model_requests: None, xai_api_key: str): + """Test passing a document URL to the xAI model.""" + m = XaiModel('grok-4-1-fast-non-reasoning', provider=XaiProvider(api_key=xai_api_key)) + agent = Agent(m) + + document_url = DocumentUrl(url='https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf') + + result = await agent.run(['What is the main content on this document?', document_url]) + assert result.output + # The document contains "Dummy PDF file" + response_text = result.output.lower() + assert 'dummy' in response_text or 'pdf' in response_text + + +async def test_xai_binary_content_document_input(allow_model_requests: None): + """Test passing a document as BinaryContent to the xAI model.""" + response = create_response(content='The document discusses testing.') + mock_client = MockXai.create_mock(response) + m = XaiModel('grok-4-1-fast-non-reasoning', provider=XaiProvider(xai_client=mock_client)) + agent = Agent(m) + + document_content = BinaryContent( + data=b'%PDF-1.4\nTest document content', + media_type='application/pdf', + ) + + result = await agent.run(['What is in this document?', document_content]) + + # Verify the response + assert result.output == 'The document discusses testing.' + + +async def test_xai_audio_url_not_supported(allow_model_requests: None): + """Test that AudioUrl raises NotImplementedError.""" + response = create_response(content='This should not be reached') + mock_client = MockXai.create_mock(response) + m = XaiModel('grok-4-1-fast-non-reasoning', provider=XaiProvider(xai_client=mock_client)) + agent = Agent(m) + + audio_url = AudioUrl(url='https://example.com/audio.mp3') + + with pytest.raises(NotImplementedError, match='AudioUrl is not supported by xAI SDK'): + await agent.run(['What is in this audio?', audio_url]) + + +async def test_xai_video_url_not_supported(allow_model_requests: None): + """Test that VideoUrl raises NotImplementedError.""" + response = create_response(content='This should not be reached') + mock_client = MockXai.create_mock(response) + m = XaiModel('grok-4-1-fast-non-reasoning', provider=XaiProvider(xai_client=mock_client)) + agent = Agent(m) + + video_url = VideoUrl(url='https://example.com/video.mp4') + + with pytest.raises(NotImplementedError, match='VideoUrl is not supported by xAI SDK'): + await agent.run(['What is in this video?', video_url]) + + +async def test_xai_binary_content_audio_not_supported(allow_model_requests: None): + """Test that BinaryContent with audio raises NotImplementedError.""" + response = create_response(content='This should not be reached') + mock_client = MockXai.create_mock(response) + m = XaiModel('grok-4-1-fast-non-reasoning', provider=XaiProvider(xai_client=mock_client)) + agent = Agent(m) + + audio_content = BinaryContent( + data=b'fake audio data', + media_type='audio/mpeg', + ) + + with pytest.raises(NotImplementedError, match='AudioUrl/BinaryContent with audio is not supported by xAI SDK'): + await agent.run(['What is in this audio?', audio_content]) + + +# Grok built-in tools tests +# Built-in tools are executed server-side by xAI's infrastructure +# Based on: https://github.com/xai-org/xai-sdk-python/blob/main/examples/aio/server_side_tools.py + + +@pytest.mark.skipif(os.getenv('XAI_API_KEY') is None, reason='Requires XAI_API_KEY (gRPC, no cassettes)') +async def test_xai_builtin_web_search_tool(allow_model_requests: None, xai_api_key: str): + """Test xAI's built-in web_search tool.""" + from pydantic_ai import WebSearchTool + + m = XaiModel('grok-4-1-fast-non-reasoning', provider=XaiProvider(api_key=xai_api_key)) + agent = Agent(m, builtin_tools=[WebSearchTool()]) + + result = await agent.run('Return just the day of week for the date of Jan 1 in 2026?') + assert result.output + assert 'thursday' in result.output.lower() + + # Verify that server-side tools were used + usage = result.usage() + assert usage.details is not None + assert 'server_side_tools_used' in usage.details + assert usage.details['server_side_tools_used'] > 0 + + +@pytest.mark.skipif(os.getenv('XAI_API_KEY') is None, reason='Requires XAI_API_KEY (gRPC, no cassettes)') +async def test_xai_builtin_x_search_tool(allow_model_requests: None, xai_api_key: str): + """Test xAI's built-in x_search tool (X/Twitter search).""" + # Note: This test is skipped until XSearchTool is properly implemented + # from pydantic_ai.builtin_tools import AbstractBuiltinTool + # + # class XSearchTool(AbstractBuiltinTool): + # """X (Twitter) search tool - specific to Grok.""" + # kind: str = 'x_search' + # + # m = XaiModel('grok-4-1-fast-non-reasoning', provider=XaiProvider(api_key=xai_api_key)) + # agent = Agent(m, builtin_tools=[XSearchTool()]) + # result = await agent.run('What is the latest post from @elonmusk?') + # assert result.output + pytest.skip('XSearchTool not yet implemented in pydantic-ai') + + +@pytest.mark.skipif(os.getenv('XAI_API_KEY') is None, reason='Requires XAI_API_KEY (gRPC, no cassettes)') +async def test_xai_builtin_code_execution_tool(allow_model_requests: None, xai_api_key: str): + """Test xAI's built-in code_execution tool.""" + from pydantic_ai import CodeExecutionTool + + m = XaiModel('grok-4-1-fast-non-reasoning', provider=XaiProvider(api_key=xai_api_key)) + agent = Agent(m, builtin_tools=[CodeExecutionTool()]) + + # Use a simpler calculation similar to OpenAI tests + result = await agent.run('What is 65465 - 6544 * 65464 - 6 + 1.02255? Use code to calculate this.') + + # Verify the response + assert result.output + # Expected: 65465 - 6544*65464 - 6 + 1.02255 = -428050955.97745 + assert '-428' in result.output or 'million' in result.output.lower() + + messages = result.all_messages() + assert len(messages) >= 2 + + # TODO: Add validation for built-in tool call parts once response parsing is fully tested + # Server-side tools are executed by xAI's infrastructure + + +@pytest.mark.skipif(os.getenv('XAI_API_KEY') is None, reason='Requires XAI_API_KEY (gRPC, no cassettes)') +async def test_xai_builtin_multiple_tools(allow_model_requests: None, xai_api_key: str): + """Test using multiple built-in tools together.""" + from pydantic_ai import CodeExecutionTool, WebSearchTool + + m = XaiModel('grok-4-1-fast-non-reasoning', provider=XaiProvider(api_key=xai_api_key)) + agent = Agent( + m, + instructions='You are a helpful assistant.', + builtin_tools=[WebSearchTool(), CodeExecutionTool()], + ) + + result = await agent.run( + 'Search for the current price of Bitcoin and calculate its percentage change if it was $50000 last week.' + ) + + # Verify the response + assert result.output + messages = result.all_messages() + assert len(messages) >= 2 + + +@pytest.mark.skipif(os.getenv('XAI_API_KEY') is None, reason='Requires XAI_API_KEY (gRPC, no cassettes)') +async def test_xai_builtin_tools_with_custom_tools(allow_model_requests: None, xai_api_key: str): + """Test mixing xAI's built-in tools with custom (client-side) tools.""" + from pydantic_ai import WebSearchTool + + m = XaiModel('grok-4-1-fast-non-reasoning', provider=XaiProvider(api_key=xai_api_key)) + agent = Agent(m, builtin_tools=[WebSearchTool()]) + + @agent.tool_plain + def get_local_temperature(city: str) -> str: + """Get the local temperature for a city (mock).""" + return f'The local temperature in {city} is 72°F' + + result = await agent.run('What is the weather in Tokyo? Use web search and then get the local temperature.') + + # Verify the response + assert result.output + messages = result.all_messages() + + # Should have both built-in tool calls and custom tool calls + assert len(messages) >= 4 # Request, builtin response, request, custom tool response + + +async def test_xai_builtin_tools_wiring(allow_model_requests: None): + """Test that built-in tools are correctly wired to xAI SDK.""" + from pydantic_ai import CodeExecutionTool, MCPServerTool, WebSearchTool + + response = create_response(content='Built-in tools are registered') + mock_client = MockXai.create_mock(response) + m = XaiModel('grok-4-1-fast-non-reasoning', provider=XaiProvider(xai_client=mock_client)) + agent = Agent( + m, + builtin_tools=[ + WebSearchTool(), + CodeExecutionTool(), + MCPServerTool( + id='test-mcp', + url='https://example.com/mcp', + description='Test MCP server', + authorization_token='test-token', + ), + ], + ) + + # If this runs without error, the built-in tools are correctly wired + result = await agent.run('Test built-in tools') + assert result.output == 'Built-in tools are registered' + + +@pytest.mark.skipif( + os.getenv('XAI_API_KEY') is None or os.getenv('LINEAR_ACCESS_TOKEN') is None, + reason='Requires XAI_API_KEY and LINEAR_ACCESS_TOKEN (gRPC, no cassettes)', +) +async def test_xai_builtin_mcp_server_tool(allow_model_requests: None, xai_api_key: str): + """Test xAI's MCP server tool with Linear.""" + from pydantic_ai import MCPServerTool + + linear_token = os.getenv('LINEAR_ACCESS_TOKEN') + m = XaiModel('grok-4-1-fast-non-reasoning', provider=XaiProvider(api_key=xai_api_key)) + agent = Agent( + m, + instructions='You are a helpful assistant.', + builtin_tools=[ + MCPServerTool( + id='linear', + url='https://mcp.linear.app/mcp', + description='MCP server for Linear the project management tool.', + authorization_token=linear_token, + ), + ], + ) + + result = await agent.run('Can you list my Linear issues? Keep your answer brief.') + + # Verify the response + assert result.output + messages = result.all_messages() + assert len(messages) >= 2 + + # Check that we have builtin tool call parts for MCP (server-side tool with server_label prefix) + response_message = messages[-1] + assert isinstance(response_message, ModelResponse) + + # Should have at least one BuiltinToolCallPart for MCP tools (prefixed with server_label, e.g. "linear.list_issues") + mcp_tool_calls = [ + part + for msg in messages + if isinstance(msg, ModelResponse) + for part in msg.parts + if isinstance(part, BuiltinToolCallPart) and part.tool_name.startswith('linear.') + ] + assert len(mcp_tool_calls) > 0, ( + f'Expected MCP tool calls with "linear." prefix, got parts: {[part for msg in messages if isinstance(msg, ModelResponse) for part in msg.parts]}' + ) + + +async def test_xai_model_retries(allow_model_requests: None): + """Test xAI model with retries.""" + # Create error response then success + success_response = create_response(content='Success after retry') + + mock_client = MockXai.create_mock(success_response) + m = XaiModel('grok-4-1-fast-non-reasoning', provider=XaiProvider(xai_client=mock_client)) + agent = Agent(m) + result = await agent.run('hello') + assert result.output == 'Success after retry' + + +async def test_xai_model_settings(allow_model_requests: None): + """Test xAI model with various settings.""" + response = create_response(content='response with settings') + mock_client = MockXai.create_mock(response) + m = XaiModel('grok-4-1-fast-non-reasoning', provider=XaiProvider(xai_client=mock_client)) + agent = Agent( + m, + model_settings=ModelSettings( + temperature=0.5, + max_tokens=100, + top_p=0.9, + ), + ) + + result = await agent.run('hello') + assert result.output == 'response with settings' + + # Verify settings were passed to the mock + kwargs = get_mock_chat_create_kwargs(mock_client) + assert len(kwargs) > 0 + + +async def test_xai_model_multiple_tool_calls(allow_model_requests: None): + """Test xAI model with multiple tool calls in sequence.""" + # Three responses: two tool calls, then final answer + responses = [ + create_response( + tool_calls=[create_tool_call(id='1', name='get_data', arguments={'key': 'value1'})], + ), + create_response( + tool_calls=[create_tool_call(id='2', name='process_data', arguments={'data': 'result1'})], + ), + create_response(content='Final processed result'), + ] + + mock_client = MockXai.create_mock(responses) + m = XaiModel('grok-4-1-fast-non-reasoning', provider=XaiProvider(xai_client=mock_client)) + agent = Agent(m) + + @agent.tool_plain + async def get_data(key: str) -> str: + return f'data for {key}' + + @agent.tool_plain + async def process_data(data: str) -> str: + return f'processed {data}' + + result = await agent.run('Get and process data') + assert result.output == 'Final processed result' + assert result.usage().requests == 3 + assert result.usage().tool_calls == 2 + + +async def test_xai_stream_with_tool_calls(allow_model_requests: None): + """Test xAI streaming with tool calls.""" + # First stream: tool call + stream1 = [ + grok_tool_chunk('get_info', None, accumulated_args=''), + grok_tool_chunk(None, '{"query": "test"}', finish_reason='tool_calls', accumulated_args='{"query": "test"}'), + ] + # Second stream: final response after tool execution + stream2 = [ + grok_text_chunk('Info retrieved: Info about test', finish_reason='stop'), + ] + + mock_client = MockXai.create_mock_stream([stream1, stream2]) + m = XaiModel('grok-4-1-fast-non-reasoning', provider=XaiProvider(xai_client=mock_client)) + agent = Agent(m) + + @agent.tool_plain + async def get_info(query: str) -> str: + return f'Info about {query}' + + async with agent.run_stream('Get information') as result: + # Consume the stream + [c async for c in result.stream_text(debounce_by=None)] + + # Verify the final output includes the tool result + assert result.is_complete + output = await result.get_output() + assert 'Info about test' in output + + +# Test for error handling +@pytest.mark.skipif(os.getenv('XAI_API_KEY') is not None, reason='Skipped when XAI_API_KEY is set') +async def test_xai_model_invalid_api_key(): + """Test xAI provider with invalid API key.""" + from pydantic_ai.exceptions import UserError + + with pytest.raises(UserError, match='Set the `XAI_API_KEY` environment variable'): + XaiProvider(api_key='') + + +async def test_xai_model_properties(): + """Test xAI model properties.""" + m = XaiModel('grok-4-1-fast-non-reasoning', provider=XaiProvider(api_key='test-key')) + + assert m.model_name == 'grok-4-1-fast-non-reasoning' + assert m.system == 'xai' + + +# Tests for reasoning/thinking content (similar to OpenAI Responses tests) + + +async def test_xai_reasoning_simple(allow_model_requests: None): + """Test xAI model with simple reasoning content.""" + response = create_response( + content='The answer is 4', + reasoning_content='Let me think: 2+2 equals 4', + usage=SimpleNamespace(prompt_tokens=10, completion_tokens=20), + ) + mock_client = MockXai.create_mock(response) + m = XaiModel('grok-3', provider=XaiProvider(xai_client=mock_client)) + agent = Agent(m) + + result = await agent.run('What is 2+2?') + assert result.output == 'The answer is 4' + assert result.all_messages() == snapshot( + [ + ModelRequest( + parts=[UserPromptPart(content='What is 2+2?', timestamp=IsNow(tz=timezone.utc))], + run_id=IsStr(), + ), + ModelResponse( + parts=[ + ThinkingPart(content='Let me think: 2+2 equals 4', signature=None, provider_name='xai'), + TextPart(content='The answer is 4'), + ], + usage=RequestUsage(input_tokens=10, output_tokens=20), + model_name='grok-3', + timestamp=IsDatetime(), + provider_name='xai', + provider_response_id='grok-123', + finish_reason='stop', + run_id=IsStr(), + ), + ] + ) + + +async def test_xai_encrypted_content_only(allow_model_requests: None): + """Test xAI model with encrypted content (signature) only.""" + response = create_response( + content='4', + encrypted_content='abc123signature', + usage=SimpleNamespace(prompt_tokens=10, completion_tokens=5), + ) + mock_client = MockXai.create_mock(response) + m = XaiModel('grok-3', provider=XaiProvider(xai_client=mock_client)) + agent = Agent(m) + + result = await agent.run('What is 2+2?') + assert result.output == '4' + assert result.all_messages() == snapshot( + [ + ModelRequest( + parts=[UserPromptPart(content='What is 2+2?', timestamp=IsNow(tz=timezone.utc))], + run_id=IsStr(), + ), + ModelResponse( + parts=[ + ThinkingPart(content='', signature='abc123signature', provider_name='xai'), + TextPart(content='4'), + ], + usage=RequestUsage(input_tokens=10, output_tokens=5), + model_name='grok-3', + timestamp=IsDatetime(), + provider_name='xai', + provider_response_id='grok-123', + finish_reason='stop', + run_id=IsStr(), + ), + ] + ) + + +async def test_xai_reasoning_without_summary(allow_model_requests: None): + """Test xAI model with encrypted content but no reasoning summary.""" + response = create_response( + content='4', + encrypted_content='encrypted123', + ) + mock_client = MockXai.create_mock(response) + model = XaiModel('grok-3', provider=XaiProvider(xai_client=mock_client)) + + agent = Agent(model=model) + result = await agent.run('What is 2+2?') + assert result.all_messages() == snapshot( + [ + ModelRequest( + parts=[ + UserPromptPart( + content='What is 2+2?', + timestamp=IsDatetime(), + ) + ], + run_id=IsStr(), + ), + ModelResponse( + parts=[ + ThinkingPart(content='', signature='encrypted123', provider_name='xai'), + TextPart(content='4'), + ], + model_name='grok-3', + timestamp=IsDatetime(), + provider_name='xai', + provider_response_id='grok-123', + finish_reason='stop', + run_id=IsStr(), + ), + ] + ) + + +async def test_xai_reasoning_with_tool_calls(allow_model_requests: None): + """Test xAI model with reasoning content and tool calls.""" + responses = [ + create_response( + tool_calls=[create_tool_call(id='1', name='calculate', arguments={'expression': '2+2'})], + reasoning_content='I need to use the calculate tool to solve this', + usage=SimpleNamespace(prompt_tokens=10, completion_tokens=30), + ), + create_response( + content='The calculation shows that 2+2 equals 4', + usage=SimpleNamespace(prompt_tokens=15, completion_tokens=10), + ), + ] + mock_client = MockXai.create_mock(responses) + m = XaiModel('grok-3', provider=XaiProvider(xai_client=mock_client)) + agent = Agent(m) + + @agent.tool_plain + async def calculate(expression: str) -> str: + return '4' + + result = await agent.run('What is 2+2?') + assert result.output == 'The calculation shows that 2+2 equals 4' + assert result.all_messages() == snapshot( + [ + ModelRequest( + parts=[UserPromptPart(content='What is 2+2?', timestamp=IsNow(tz=timezone.utc))], + run_id=IsStr(), + ), + ModelResponse( + parts=[ + ThinkingPart( + content='I need to use the calculate tool to solve this', signature=None, provider_name='xai' + ), + ToolCallPart( + tool_name='calculate', + args={'expression': '2+2'}, + tool_call_id='1', + ), + ], + usage=RequestUsage(input_tokens=10, output_tokens=30), + model_name='grok-3', + timestamp=IsDatetime(), + provider_name='xai', + provider_response_id='grok-123', + finish_reason='stop', + run_id=IsStr(), + ), + ModelRequest( + parts=[ + ToolReturnPart( + tool_name='calculate', + content='4', + tool_call_id='1', + timestamp=IsNow(tz=timezone.utc), + ) + ], + run_id=IsStr(), + ), + ModelResponse( + parts=[TextPart(content='The calculation shows that 2+2 equals 4')], + usage=RequestUsage(input_tokens=15, output_tokens=10), + model_name='grok-3', + timestamp=IsDatetime(), + provider_name='xai', + provider_response_id='grok-123', + finish_reason='stop', + run_id=IsStr(), + ), + ] + ) + + +async def test_xai_reasoning_with_encrypted_and_tool_calls(allow_model_requests: None): + """Test xAI model with encrypted reasoning content and tool calls.""" + responses = [ + create_response( + tool_calls=[create_tool_call(id='1', name='get_weather', arguments={'city': 'San Francisco'})], + encrypted_content='encrypted_reasoning_abc123', + usage=SimpleNamespace(prompt_tokens=20, completion_tokens=40), + ), + create_response( + content='The weather in San Francisco is sunny', + usage=SimpleNamespace(prompt_tokens=25, completion_tokens=12), + ), + ] + mock_client = MockXai.create_mock(responses) + m = XaiModel('grok-3', provider=XaiProvider(xai_client=mock_client)) + agent = Agent(m) + + @agent.tool_plain + async def get_weather(city: str) -> str: + return 'sunny' + + result = await agent.run('What is the weather in San Francisco?') + assert result.output == 'The weather in San Francisco is sunny' + assert result.all_messages() == snapshot( + [ + ModelRequest( + parts=[ + UserPromptPart(content='What is the weather in San Francisco?', timestamp=IsNow(tz=timezone.utc)) + ], + run_id=IsStr(), + ), + ModelResponse( + parts=[ + ThinkingPart(content='', signature='encrypted_reasoning_abc123', provider_name='xai'), + ToolCallPart( + tool_name='get_weather', + args={'city': 'San Francisco'}, + tool_call_id='1', + ), + ], + usage=RequestUsage(input_tokens=20, output_tokens=40), + model_name='grok-3', + timestamp=IsDatetime(), + provider_name='xai', + provider_response_id='grok-123', + finish_reason='stop', + run_id=IsStr(), + ), + ModelRequest( + parts=[ + ToolReturnPart( + tool_name='get_weather', + content='sunny', + tool_call_id='1', + timestamp=IsNow(tz=timezone.utc), + ) + ], + run_id=IsStr(), + ), + ModelResponse( + parts=[TextPart(content='The weather in San Francisco is sunny')], + usage=RequestUsage(input_tokens=25, output_tokens=12), + model_name='grok-3', + timestamp=IsDatetime(), + provider_name='xai', + provider_response_id='grok-123', + finish_reason='stop', + run_id=IsStr(), + ), + ] + ) + + +async def test_xai_stream_with_reasoning(allow_model_requests: None): + """Test xAI streaming with reasoning content.""" + stream = [ + grok_reasoning_text_chunk('The answer', reasoning_content='Let me think about this...', finish_reason=''), + grok_reasoning_text_chunk(' is 4', reasoning_content='Let me think about this...', finish_reason='stop'), + ] + mock_client = MockXai.create_mock_stream(stream) + m = XaiModel('grok-3', provider=XaiProvider(xai_client=mock_client)) + agent = Agent(m) + + async with agent.run_stream('What is 2+2?') as result: + assert not result.is_complete + text_chunks = [c async for c in result.stream_text(debounce_by=None)] + assert text_chunks == snapshot(['The answer', 'The answer is 4']) + assert result.is_complete + + # Verify the final response includes both reasoning and text + messages = result.all_messages() + assert len(messages) == 2 + assert isinstance(messages[1], ModelResponse) + assert len(messages[1].parts) == 2 + assert isinstance(messages[1].parts[0], ThinkingPart) + assert messages[1].parts[0].content == 'Let me think about this...' + assert isinstance(messages[1].parts[1], TextPart) + assert messages[1].parts[1].content == 'The answer is 4' + + +async def test_xai_stream_with_encrypted_reasoning(allow_model_requests: None): + """Test xAI streaming with encrypted reasoning content.""" + stream = [ + grok_reasoning_text_chunk('The weather', encrypted_content='encrypted_abc123', finish_reason=''), + grok_reasoning_text_chunk(' is sunny', encrypted_content='encrypted_abc123', finish_reason='stop'), + ] + mock_client = MockXai.create_mock_stream(stream) + m = XaiModel('grok-3', provider=XaiProvider(xai_client=mock_client)) + agent = Agent(m) + + async with agent.run_stream('What is the weather?') as result: + assert not result.is_complete + text_chunks = [c async for c in result.stream_text(debounce_by=None)] + assert text_chunks == snapshot(['The weather', 'The weather is sunny']) + assert result.is_complete + + # Verify the final response includes both encrypted reasoning and text + messages = result.all_messages() + assert len(messages) == 2 + assert isinstance(messages[1], ModelResponse) + assert len(messages[1].parts) == 2 + assert isinstance(messages[1].parts[0], ThinkingPart) + assert messages[1].parts[0].content == '' # No readable content for encrypted-only + assert messages[1].parts[0].signature == 'encrypted_abc123' + assert isinstance(messages[1].parts[1], TextPart) + assert messages[1].parts[1].content == 'The weather is sunny' + + +async def test_xai_usage_with_reasoning_tokens(allow_model_requests: None): + """Test that xAI model properly extracts reasoning_tokens and cache_read_tokens from usage.""" + # Create a mock usage object with reasoning_tokens and cached_prompt_text_tokens + mock_usage = SimpleNamespace( + prompt_tokens=100, + completion_tokens=50, + reasoning_tokens=25, + cached_prompt_text_tokens=30, + ) + response = create_response( + content='The answer is 42', + reasoning_content='Let me think deeply about this...', + usage=mock_usage, + ) + mock_client = MockXai.create_mock(response) + m = XaiModel('grok-3', provider=XaiProvider(xai_client=mock_client)) + agent = Agent(m) + + result = await agent.run('What is the meaning of life?') + assert result.output == 'The answer is 42' + + # Verify usage includes details + usage = result.usage() + assert usage.input_tokens == 100 + assert usage.output_tokens == 50 + assert usage.total_tokens == 150 + assert usage.details == snapshot({'reasoning_tokens': 25, 'cache_read_tokens': 30}) + + +async def test_xai_usage_without_details(allow_model_requests: None): + """Test that xAI model handles usage without reasoning_tokens or cached tokens.""" + mock_usage = SimpleNamespace( + prompt_tokens=20, + completion_tokens=10, + ) + response = create_response( + content='Simple answer', + usage=mock_usage, + ) + mock_client = MockXai.create_mock(response) + m = XaiModel('grok-3', provider=XaiProvider(xai_client=mock_client)) + agent = Agent(m) + + result = await agent.run('Simple question') + assert result.output == 'Simple answer' + + # Verify usage without details + usage = result.usage() + assert usage.input_tokens == 20 + assert usage.output_tokens == 10 + assert usage.total_tokens == 30 + # details should be empty dict when no additional usage info is provided + assert usage.details == snapshot({}) + + +async def test_xai_usage_with_server_side_tools(allow_model_requests: None): + """Test that xAI model properly extracts server_side_tools_used from usage.""" + # Create a mock usage object with server_side_tools_used + # Note: In the real SDK, server_side_tools_used is a repeated field (list-like), + # but we use an int in mocks for simplicity + mock_usage = SimpleNamespace( + prompt_tokens=50, + completion_tokens=30, + server_side_tools_used=2, + ) + response = create_response( + content='The answer based on web search', + usage=mock_usage, + ) + mock_client = MockXai.create_mock(response) + m = XaiModel('grok-4-1-fast-non-reasoning', provider=XaiProvider(xai_client=mock_client)) + agent = Agent(m) + + result = await agent.run('Search for something') + assert result.output == 'The answer based on web search' + + # Verify usage includes server_side_tools_used in details + usage = result.usage() + assert usage.input_tokens == 50 + assert usage.output_tokens == 30 + assert usage.total_tokens == 80 + assert usage.details == snapshot({'server_side_tools_used': 2}) + + +# End of tests diff --git a/tests/providers/test_provider_names.py b/tests/providers/test_provider_names.py index 55c859faef..057fba9276 100644 --- a/tests/providers/test_provider_names.py +++ b/tests/providers/test_provider_names.py @@ -37,6 +37,7 @@ from pydantic_ai.providers.ovhcloud import OVHcloudProvider from pydantic_ai.providers.together import TogetherProvider from pydantic_ai.providers.vercel import VercelProvider + from pydantic_ai.providers.xai import XaiProvider test_infer_provider_params = [ ('anthropic', AnthropicProvider, 'ANTHROPIC_API_KEY'), @@ -51,6 +52,7 @@ ('groq', GroqProvider, 'GROQ_API_KEY'), ('mistral', MistralProvider, 'MISTRAL_API_KEY'), ('grok', GrokProvider, 'GROK_API_KEY'), + ('xai', XaiProvider, 'XAI_API_KEY'), ('moonshotai', MoonshotAIProvider, 'MOONSHOTAI_API_KEY'), ('fireworks', FireworksProvider, 'FIREWORKS_API_KEY'), ('together', TogetherProvider, 'TOGETHER_API_KEY'), diff --git a/tests/providers/test_xai.py b/tests/providers/test_xai.py new file mode 100644 index 0000000000..c773625fa9 --- /dev/null +++ b/tests/providers/test_xai.py @@ -0,0 +1,48 @@ +import re + +import pytest + +from pydantic_ai.exceptions import UserError + +from ..conftest import TestEnv, try_import + +with try_import() as imports_successful: + from xai_sdk import AsyncClient + + from pydantic_ai.providers.xai import XaiProvider + +pytestmark = pytest.mark.skipif(not imports_successful(), reason='xai_sdk not installed') + + +def test_xai_provider(): + provider = XaiProvider(api_key='api-key') + assert provider.name == 'xai' + assert provider.base_url == 'https://api.x.ai/v1' + assert isinstance(provider.client, AsyncClient) + + +def test_xai_provider_need_api_key(env: TestEnv) -> None: + env.remove('XAI_API_KEY') + with pytest.raises( + UserError, + match=re.escape( + 'Set the `XAI_API_KEY` environment variable or pass it via `XaiProvider(api_key=...)`' + 'to use the xAI provider.' + ), + ): + XaiProvider() + + +def test_xai_pass_xai_client() -> None: + xai_client = AsyncClient(api_key='api-key') + provider = XaiProvider(xai_client=xai_client) + assert provider.client == xai_client + + +def test_xai_model_profile(): + from pydantic_ai.profiles.grok import GrokModelProfile + + provider = XaiProvider(api_key='api-key') + profile = provider.model_profile('grok-4-1-fast-non-reasoning') + assert isinstance(profile, GrokModelProfile) + assert profile.grok_supports_builtin_tools is True diff --git a/tests/test_examples.py b/tests/test_examples.py index 85bae688d0..b9237dc551 100644 --- a/tests/test_examples.py +++ b/tests/test_examples.py @@ -180,6 +180,7 @@ def print(self, *args: Any, **kwargs: Any) -> None: env.set('DEEPSEEK_API_KEY', 'testing') env.set('OVHCLOUD_API_KEY', 'testing') env.set('PYDANTIC_AI_GATEWAY_API_KEY', 'testing') + env.set('XAI_API_KEY', 'testing') prefix_settings = example.prefix_settings() opt_test = prefix_settings.get('test', '') diff --git a/uv.lock b/uv.lock index b9bb2f6a6d..0731485115 100644 --- a/uv.lock +++ b/uv.lock @@ -1,5 +1,5 @@ version = 1 -revision = 3 +revision = 2 requires-python = ">=3.10" resolution-markers = [ "python_full_version >= '3.13' and platform_python_implementation == 'PyPy'", @@ -1410,18 +1410,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/07/6c/aa3f2f849e01cb6a001cd8554a88d4c77c5c1a31c95bdf1cf9301e6d9ef4/defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61", size = 25604, upload-time = "2021-03-08T10:59:24.45Z" }, ] -[[package]] -name = "deprecated" -version = "1.2.18" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "wrapt" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/98/97/06afe62762c9a8a86af0cfb7bfdab22a43ad17138b07af5b1a58442690a2/deprecated-1.2.18.tar.gz", hash = "sha256:422b6f6d859da6f2ef57857761bfb392480502a64c3028ca9bbe86085d72115d", size = 2928744, upload-time = "2025-01-27T10:46:25.7Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/6e/c6/ac0b6c1e2d138f1002bcf799d330bd6d85084fece321e662a14223794041/Deprecated-1.2.18-py2.py3-none-any.whl", hash = "sha256:bd5011788200372a32418f888e326a09ff80d0214bd961147cfed01b5c018eec", size = 9998, upload-time = "2025-01-27T10:46:09.186Z" }, -] - [[package]] name = "depyf" version = "0.19.0" @@ -2015,14 +2003,14 @@ wheels = [ [[package]] name = "googleapis-common-protos" -version = "1.68.0" +version = "1.72.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "protobuf" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/54/d2/c08f0d9f94b45faca68e355771329cba2411c777c8713924dd1baee0e09c/googleapis_common_protos-1.68.0.tar.gz", hash = "sha256:95d38161f4f9af0d9423eed8fb7b64ffd2568c3464eb542ff02c5bfa1953ab3c", size = 57367, upload-time = "2025-02-20T19:08:28.426Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e5/7b/adfd75544c415c487b33061fe7ae526165241c1ea133f9a9125a56b39fd8/googleapis_common_protos-1.72.0.tar.gz", hash = "sha256:e55a601c1b32b52d7a3e65f43563e2aa61bcd737998ee672ac9b951cd49319f5", size = 147433, upload-time = "2025-11-06T18:29:24.087Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3f/85/c99a157ee99d67cc6c9ad123abb8b1bfb476fab32d2f3511c59314548e4f/googleapis_common_protos-1.68.0-py2.py3-none-any.whl", hash = "sha256:aaf179b2f81df26dfadac95def3b16a95064c76a5f45f07e4c68a21bb371c4ac", size = 164985, upload-time = "2025-02-20T19:08:26.964Z" }, + { url = "https://files.pythonhosted.org/packages/c4/ab/09169d5a4612a5f92490806649ac8d41e3ec9129c636754575b3553f4ea4/googleapis_common_protos-1.72.0-py3-none-any.whl", hash = "sha256:4299c5a82d5ae1a9702ada957347726b167f9f8d1fc352477702a1e851ff4038", size = 297515, upload-time = "2025-11-06T18:29:13.14Z" }, ] [[package]] @@ -2193,6 +2181,67 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/4d/11/1019a6cfdb2e520cb461cf70d859216be8ca122ddf5ad301fc3b0ee45fd4/groq-0.25.0-py3-none-any.whl", hash = "sha256:aadc78b40b1809cdb196b1aa8c7f7293108767df1508cafa3e0d5045d9328e7a", size = 129371, upload-time = "2025-05-16T19:57:41.786Z" }, ] +[[package]] +name = "grpcio" +version = "1.76.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b6/e0/318c1ce3ae5a17894d5791e87aea147587c9e702f24122cc7a5c8bbaeeb1/grpcio-1.76.0.tar.gz", hash = "sha256:7be78388d6da1a25c0d5ec506523db58b18be22d9c37d8d3a32c08be4987bd73", size = 12785182, upload-time = "2025-10-21T16:23:12.106Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/17/ff4795dc9a34b6aee6ec379f1b66438a3789cd1315aac0cbab60d92f74b3/grpcio-1.76.0-cp310-cp310-linux_armv7l.whl", hash = "sha256:65a20de41e85648e00305c1bb09a3598f840422e522277641145a32d42dcefcc", size = 5840037, upload-time = "2025-10-21T16:20:25.069Z" }, + { url = "https://files.pythonhosted.org/packages/4e/ff/35f9b96e3fa2f12e1dcd58a4513a2e2294a001d64dec81677361b7040c9a/grpcio-1.76.0-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:40ad3afe81676fd9ec6d9d406eda00933f218038433980aa19d401490e46ecde", size = 11836482, upload-time = "2025-10-21T16:20:30.113Z" }, + { url = "https://files.pythonhosted.org/packages/3e/1c/8374990f9545e99462caacea5413ed783014b3b66ace49e35c533f07507b/grpcio-1.76.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:035d90bc79eaa4bed83f524331d55e35820725c9fbb00ffa1904d5550ed7ede3", size = 6407178, upload-time = "2025-10-21T16:20:32.733Z" }, + { url = "https://files.pythonhosted.org/packages/1e/77/36fd7d7c75a6c12542c90a6d647a27935a1ecaad03e0ffdb7c42db6b04d2/grpcio-1.76.0-cp310-cp310-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:4215d3a102bd95e2e11b5395c78562967959824156af11fa93d18fdd18050990", size = 7075684, upload-time = "2025-10-21T16:20:35.435Z" }, + { url = "https://files.pythonhosted.org/packages/38/f7/e3cdb252492278e004722306c5a8935eae91e64ea11f0af3437a7de2e2b7/grpcio-1.76.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:49ce47231818806067aea3324d4bf13825b658ad662d3b25fada0bdad9b8a6af", size = 6611133, upload-time = "2025-10-21T16:20:37.541Z" }, + { url = "https://files.pythonhosted.org/packages/7e/20/340db7af162ccd20a0893b5f3c4a5d676af7b71105517e62279b5b61d95a/grpcio-1.76.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:8cc3309d8e08fd79089e13ed4819d0af72aa935dd8f435a195fd152796752ff2", size = 7195507, upload-time = "2025-10-21T16:20:39.643Z" }, + { url = "https://files.pythonhosted.org/packages/10/f0/b2160addc1487bd8fa4810857a27132fb4ce35c1b330c2f3ac45d697b106/grpcio-1.76.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:971fd5a1d6e62e00d945423a567e42eb1fa678ba89072832185ca836a94daaa6", size = 8160651, upload-time = "2025-10-21T16:20:42.492Z" }, + { url = "https://files.pythonhosted.org/packages/2c/2c/ac6f98aa113c6ef111b3f347854e99ebb7fb9d8f7bb3af1491d438f62af4/grpcio-1.76.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:9d9adda641db7207e800a7f089068f6f645959f2df27e870ee81d44701dd9db3", size = 7620568, upload-time = "2025-10-21T16:20:45.995Z" }, + { url = "https://files.pythonhosted.org/packages/90/84/7852f7e087285e3ac17a2703bc4129fafee52d77c6c82af97d905566857e/grpcio-1.76.0-cp310-cp310-win32.whl", hash = "sha256:063065249d9e7e0782d03d2bca50787f53bd0fb89a67de9a7b521c4a01f1989b", size = 3998879, upload-time = "2025-10-21T16:20:48.592Z" }, + { url = "https://files.pythonhosted.org/packages/10/30/d3d2adcbb6dd3ff59d6ac3df6ef830e02b437fb5c90990429fd180e52f30/grpcio-1.76.0-cp310-cp310-win_amd64.whl", hash = "sha256:a6ae758eb08088d36812dd5d9af7a9859c05b1e0f714470ea243694b49278e7b", size = 4706892, upload-time = "2025-10-21T16:20:50.697Z" }, + { url = "https://files.pythonhosted.org/packages/a0/00/8163a1beeb6971f66b4bbe6ac9457b97948beba8dd2fc8e1281dce7f79ec/grpcio-1.76.0-cp311-cp311-linux_armv7l.whl", hash = "sha256:2e1743fbd7f5fa713a1b0a8ac8ebabf0ec980b5d8809ec358d488e273b9cf02a", size = 5843567, upload-time = "2025-10-21T16:20:52.829Z" }, + { url = "https://files.pythonhosted.org/packages/10/c1/934202f5cf335e6d852530ce14ddb0fef21be612ba9ecbbcbd4d748ca32d/grpcio-1.76.0-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:a8c2cf1209497cf659a667d7dea88985e834c24b7c3b605e6254cbb5076d985c", size = 11848017, upload-time = "2025-10-21T16:20:56.705Z" }, + { url = "https://files.pythonhosted.org/packages/11/0b/8dec16b1863d74af6eb3543928600ec2195af49ca58b16334972f6775663/grpcio-1.76.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:08caea849a9d3c71a542827d6df9d5a69067b0a1efbea8a855633ff5d9571465", size = 6412027, upload-time = "2025-10-21T16:20:59.3Z" }, + { url = "https://files.pythonhosted.org/packages/d7/64/7b9e6e7ab910bea9d46f2c090380bab274a0b91fb0a2fe9b0cd399fffa12/grpcio-1.76.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:f0e34c2079d47ae9f6188211db9e777c619a21d4faba6977774e8fa43b085e48", size = 7075913, upload-time = "2025-10-21T16:21:01.645Z" }, + { url = "https://files.pythonhosted.org/packages/68/86/093c46e9546073cefa789bd76d44c5cb2abc824ca62af0c18be590ff13ba/grpcio-1.76.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8843114c0cfce61b40ad48df65abcfc00d4dba82eae8718fab5352390848c5da", size = 6615417, upload-time = "2025-10-21T16:21:03.844Z" }, + { url = "https://files.pythonhosted.org/packages/f7/b6/5709a3a68500a9c03da6fb71740dcdd5ef245e39266461a03f31a57036d8/grpcio-1.76.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8eddfb4d203a237da6f3cc8a540dad0517d274b5a1e9e636fd8d2c79b5c1d397", size = 7199683, upload-time = "2025-10-21T16:21:06.195Z" }, + { url = "https://files.pythonhosted.org/packages/91/d3/4b1f2bf16ed52ce0b508161df3a2d186e4935379a159a834cb4a7d687429/grpcio-1.76.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:32483fe2aab2c3794101c2a159070584e5db11d0aa091b2c0ea9c4fc43d0d749", size = 8163109, upload-time = "2025-10-21T16:21:08.498Z" }, + { url = "https://files.pythonhosted.org/packages/5c/61/d9043f95f5f4cf085ac5dd6137b469d41befb04bd80280952ffa2a4c3f12/grpcio-1.76.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:dcfe41187da8992c5f40aa8c5ec086fa3672834d2be57a32384c08d5a05b4c00", size = 7626676, upload-time = "2025-10-21T16:21:10.693Z" }, + { url = "https://files.pythonhosted.org/packages/36/95/fd9a5152ca02d8881e4dd419cdd790e11805979f499a2e5b96488b85cf27/grpcio-1.76.0-cp311-cp311-win32.whl", hash = "sha256:2107b0c024d1b35f4083f11245c0e23846ae64d02f40b2b226684840260ed054", size = 3997688, upload-time = "2025-10-21T16:21:12.746Z" }, + { url = "https://files.pythonhosted.org/packages/60/9c/5c359c8d4c9176cfa3c61ecd4efe5affe1f38d9bae81e81ac7186b4c9cc8/grpcio-1.76.0-cp311-cp311-win_amd64.whl", hash = "sha256:522175aba7af9113c48ec10cc471b9b9bd4f6ceb36aeb4544a8e2c80ed9d252d", size = 4709315, upload-time = "2025-10-21T16:21:15.26Z" }, + { url = "https://files.pythonhosted.org/packages/bf/05/8e29121994b8d959ffa0afd28996d452f291b48cfc0875619de0bde2c50c/grpcio-1.76.0-cp312-cp312-linux_armv7l.whl", hash = "sha256:81fd9652b37b36f16138611c7e884eb82e0cec137c40d3ef7c3f9b3ed00f6ed8", size = 5799718, upload-time = "2025-10-21T16:21:17.939Z" }, + { url = "https://files.pythonhosted.org/packages/d9/75/11d0e66b3cdf998c996489581bdad8900db79ebd83513e45c19548f1cba4/grpcio-1.76.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:04bbe1bfe3a68bbfd4e52402ab7d4eb59d72d02647ae2042204326cf4bbad280", size = 11825627, upload-time = "2025-10-21T16:21:20.466Z" }, + { url = "https://files.pythonhosted.org/packages/28/50/2f0aa0498bc188048f5d9504dcc5c2c24f2eb1a9337cd0fa09a61a2e75f0/grpcio-1.76.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d388087771c837cdb6515539f43b9d4bf0b0f23593a24054ac16f7a960be16f4", size = 6359167, upload-time = "2025-10-21T16:21:23.122Z" }, + { url = "https://files.pythonhosted.org/packages/66/e5/bbf0bb97d29ede1d59d6588af40018cfc345b17ce979b7b45424628dc8bb/grpcio-1.76.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:9f8f757bebaaea112c00dba718fc0d3260052ce714e25804a03f93f5d1c6cc11", size = 7044267, upload-time = "2025-10-21T16:21:25.995Z" }, + { url = "https://files.pythonhosted.org/packages/f5/86/f6ec2164f743d9609691115ae8ece098c76b894ebe4f7c94a655c6b03e98/grpcio-1.76.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:980a846182ce88c4f2f7e2c22c56aefd515daeb36149d1c897f83cf57999e0b6", size = 6573963, upload-time = "2025-10-21T16:21:28.631Z" }, + { url = "https://files.pythonhosted.org/packages/60/bc/8d9d0d8505feccfdf38a766d262c71e73639c165b311c9457208b56d92ae/grpcio-1.76.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f92f88e6c033db65a5ae3d97905c8fea9c725b63e28d5a75cb73b49bda5024d8", size = 7164484, upload-time = "2025-10-21T16:21:30.837Z" }, + { url = "https://files.pythonhosted.org/packages/67/e6/5d6c2fc10b95edf6df9b8f19cf10a34263b7fd48493936fffd5085521292/grpcio-1.76.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:4baf3cbe2f0be3289eb68ac8ae771156971848bb8aaff60bad42005539431980", size = 8127777, upload-time = "2025-10-21T16:21:33.577Z" }, + { url = "https://files.pythonhosted.org/packages/3f/c8/dce8ff21c86abe025efe304d9e31fdb0deaaa3b502b6a78141080f206da0/grpcio-1.76.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:615ba64c208aaceb5ec83bfdce7728b80bfeb8be97562944836a7a0a9647d882", size = 7594014, upload-time = "2025-10-21T16:21:41.882Z" }, + { url = "https://files.pythonhosted.org/packages/e0/42/ad28191ebf983a5d0ecef90bab66baa5a6b18f2bfdef9d0a63b1973d9f75/grpcio-1.76.0-cp312-cp312-win32.whl", hash = "sha256:45d59a649a82df5718fd9527ce775fd66d1af35e6d31abdcdc906a49c6822958", size = 3984750, upload-time = "2025-10-21T16:21:44.006Z" }, + { url = "https://files.pythonhosted.org/packages/9e/00/7bd478cbb851c04a48baccaa49b75abaa8e4122f7d86da797500cccdd771/grpcio-1.76.0-cp312-cp312-win_amd64.whl", hash = "sha256:c088e7a90b6017307f423efbb9d1ba97a22aa2170876223f9709e9d1de0b5347", size = 4704003, upload-time = "2025-10-21T16:21:46.244Z" }, + { url = "https://files.pythonhosted.org/packages/fc/ed/71467ab770effc9e8cef5f2e7388beb2be26ed642d567697bb103a790c72/grpcio-1.76.0-cp313-cp313-linux_armv7l.whl", hash = "sha256:26ef06c73eb53267c2b319f43e6634c7556ea37672029241a056629af27c10e2", size = 5807716, upload-time = "2025-10-21T16:21:48.475Z" }, + { url = "https://files.pythonhosted.org/packages/2c/85/c6ed56f9817fab03fa8a111ca91469941fb514e3e3ce6d793cb8f1e1347b/grpcio-1.76.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:45e0111e73f43f735d70786557dc38141185072d7ff8dc1829d6a77ac1471468", size = 11821522, upload-time = "2025-10-21T16:21:51.142Z" }, + { url = "https://files.pythonhosted.org/packages/ac/31/2b8a235ab40c39cbc141ef647f8a6eb7b0028f023015a4842933bc0d6831/grpcio-1.76.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:83d57312a58dcfe2a3a0f9d1389b299438909a02db60e2f2ea2ae2d8034909d3", size = 6362558, upload-time = "2025-10-21T16:21:54.213Z" }, + { url = "https://files.pythonhosted.org/packages/bd/64/9784eab483358e08847498ee56faf8ff6ea8e0a4592568d9f68edc97e9e9/grpcio-1.76.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:3e2a27c89eb9ac3d81ec8835e12414d73536c6e620355d65102503064a4ed6eb", size = 7049990, upload-time = "2025-10-21T16:21:56.476Z" }, + { url = "https://files.pythonhosted.org/packages/2b/94/8c12319a6369434e7a184b987e8e9f3b49a114c489b8315f029e24de4837/grpcio-1.76.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:61f69297cba3950a524f61c7c8ee12e55c486cb5f7db47ff9dcee33da6f0d3ae", size = 6575387, upload-time = "2025-10-21T16:21:59.051Z" }, + { url = "https://files.pythonhosted.org/packages/15/0f/f12c32b03f731f4a6242f771f63039df182c8b8e2cf8075b245b409259d4/grpcio-1.76.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6a15c17af8839b6801d554263c546c69c4d7718ad4321e3166175b37eaacca77", size = 7166668, upload-time = "2025-10-21T16:22:02.049Z" }, + { url = "https://files.pythonhosted.org/packages/ff/2d/3ec9ce0c2b1d92dd59d1c3264aaec9f0f7c817d6e8ac683b97198a36ed5a/grpcio-1.76.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:25a18e9810fbc7e7f03ec2516addc116a957f8cbb8cbc95ccc80faa072743d03", size = 8124928, upload-time = "2025-10-21T16:22:04.984Z" }, + { url = "https://files.pythonhosted.org/packages/1a/74/fd3317be5672f4856bcdd1a9e7b5e17554692d3db9a3b273879dc02d657d/grpcio-1.76.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:931091142fd8cc14edccc0845a79248bc155425eee9a98b2db2ea4f00a235a42", size = 7589983, upload-time = "2025-10-21T16:22:07.881Z" }, + { url = "https://files.pythonhosted.org/packages/45/bb/ca038cf420f405971f19821c8c15bcbc875505f6ffadafe9ffd77871dc4c/grpcio-1.76.0-cp313-cp313-win32.whl", hash = "sha256:5e8571632780e08526f118f74170ad8d50fb0a48c23a746bef2a6ebade3abd6f", size = 3984727, upload-time = "2025-10-21T16:22:10.032Z" }, + { url = "https://files.pythonhosted.org/packages/41/80/84087dc56437ced7cdd4b13d7875e7439a52a261e3ab4e06488ba6173b0a/grpcio-1.76.0-cp313-cp313-win_amd64.whl", hash = "sha256:f9f7bd5faab55f47231ad8dba7787866b69f5e93bc306e3915606779bbfb4ba8", size = 4702799, upload-time = "2025-10-21T16:22:12.709Z" }, + { url = "https://files.pythonhosted.org/packages/b4/46/39adac80de49d678e6e073b70204091e76631e03e94928b9ea4ecf0f6e0e/grpcio-1.76.0-cp314-cp314-linux_armv7l.whl", hash = "sha256:ff8a59ea85a1f2191a0ffcc61298c571bc566332f82e5f5be1b83c9d8e668a62", size = 5808417, upload-time = "2025-10-21T16:22:15.02Z" }, + { url = "https://files.pythonhosted.org/packages/9c/f5/a4531f7fb8b4e2a60b94e39d5d924469b7a6988176b3422487be61fe2998/grpcio-1.76.0-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:06c3d6b076e7b593905d04fdba6a0525711b3466f43b3400266f04ff735de0cd", size = 11828219, upload-time = "2025-10-21T16:22:17.954Z" }, + { url = "https://files.pythonhosted.org/packages/4b/1c/de55d868ed7a8bd6acc6b1d6ddc4aa36d07a9f31d33c912c804adb1b971b/grpcio-1.76.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fd5ef5932f6475c436c4a55e4336ebbe47bd3272be04964a03d316bbf4afbcbc", size = 6367826, upload-time = "2025-10-21T16:22:20.721Z" }, + { url = "https://files.pythonhosted.org/packages/59/64/99e44c02b5adb0ad13ab3adc89cb33cb54bfa90c74770f2607eea629b86f/grpcio-1.76.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:b331680e46239e090f5b3cead313cc772f6caa7d0fc8de349337563125361a4a", size = 7049550, upload-time = "2025-10-21T16:22:23.637Z" }, + { url = "https://files.pythonhosted.org/packages/43/28/40a5be3f9a86949b83e7d6a2ad6011d993cbe9b6bd27bea881f61c7788b6/grpcio-1.76.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2229ae655ec4e8999599469559e97630185fdd53ae1e8997d147b7c9b2b72cba", size = 6575564, upload-time = "2025-10-21T16:22:26.016Z" }, + { url = "https://files.pythonhosted.org/packages/4b/a9/1be18e6055b64467440208a8559afac243c66a8b904213af6f392dc2212f/grpcio-1.76.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:490fa6d203992c47c7b9e4a9d39003a0c2bcc1c9aa3c058730884bbbb0ee9f09", size = 7176236, upload-time = "2025-10-21T16:22:28.362Z" }, + { url = "https://files.pythonhosted.org/packages/0f/55/dba05d3fcc151ce6e81327541d2cc8394f442f6b350fead67401661bf041/grpcio-1.76.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:479496325ce554792dba6548fae3df31a72cef7bad71ca2e12b0e58f9b336bfc", size = 8125795, upload-time = "2025-10-21T16:22:31.075Z" }, + { url = "https://files.pythonhosted.org/packages/4a/45/122df922d05655f63930cf42c9e3f72ba20aadb26c100ee105cad4ce4257/grpcio-1.76.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:1c9b93f79f48b03ada57ea24725d83a30284a012ec27eab2cf7e50a550cbbbcc", size = 7592214, upload-time = "2025-10-21T16:22:33.831Z" }, + { url = "https://files.pythonhosted.org/packages/4a/6e/0b899b7f6b66e5af39e377055fb4a6675c9ee28431df5708139df2e93233/grpcio-1.76.0-cp314-cp314-win32.whl", hash = "sha256:747fa73efa9b8b1488a95d0ba1039c8e2dca0f741612d80415b1e1c560febf4e", size = 4062961, upload-time = "2025-10-21T16:22:36.468Z" }, + { url = "https://files.pythonhosted.org/packages/19/41/0b430b01a2eb38ee887f88c1f07644a1df8e289353b78e82b37ef988fb64/grpcio-1.76.0-cp314-cp314-win_amd64.whl", hash = "sha256:922fa70ba549fce362d2e2871ab542082d66e2aaf0c19480ea453905b01f384e", size = 4834462, upload-time = "2025-10-21T16:22:39.772Z" }, +] + [[package]] name = "grpclib" version = "0.4.7" @@ -2763,6 +2812,7 @@ version = "0.7.30" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/bf/38/d1ef3ae08d8d857e5e0690c5b1e07bf7eb4a1cae5881d87215826dc6cadb/llguidance-0.7.30.tar.gz", hash = "sha256:e93bf75f2b6e48afb86a5cee23038746975e1654672bf5ba0ae75f7d4d4a2248", size = 1055528, upload-time = "2025-06-23T00:23:49.247Z" } wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/e1/694c89986fcae7777184fc8b22baa0976eba15a6847221763f6ad211fc1f/llguidance-0.7.30-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:c80af02c118d2b0526bcecaab389af2ed094537a069b0fc724cd2a2f2ba3990f", size = 3327974, upload-time = "2025-06-23T00:23:47.556Z" }, { url = "https://files.pythonhosted.org/packages/fd/77/ab7a548ae189dc23900fdd37803c115c2339b1223af9e8eb1f4329b5935a/llguidance-0.7.30-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:00a256d532911d2cf5ba4ef63e182944e767dd2402f38d63002016bc37755958", size = 3210709, upload-time = "2025-06-23T00:23:45.872Z" }, { url = "https://files.pythonhosted.org/packages/9c/5b/6a166564b14f9f805f0ea01ec233a84f55789cb7eeffe1d6224ccd0e6cdd/llguidance-0.7.30-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:af8741c867e4bc7e42f7cdc68350c076b4edd0ca10ecefbde75f15a9f6bc25d0", size = 14867038, upload-time = "2025-06-23T00:23:39.571Z" }, { url = "https://files.pythonhosted.org/packages/af/80/5a40b9689f17612434b820854cba9b8cabd5142072c491b5280fe5f7a35e/llguidance-0.7.30-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9edc409b9decd6cffba5f5bf3b4fbd7541f95daa8cbc9510cbf96c6ab1ffc153", size = 15004926, upload-time = "2025-06-23T00:23:43.965Z" }, @@ -2854,7 +2904,7 @@ wheels = [ [[package]] name = "logfire" -version = "4.0.0" +version = "4.14.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "executing" }, @@ -2866,9 +2916,9 @@ dependencies = [ { name = "tomli", marker = "python_full_version < '3.11'" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/d8/85/4ee1ced49f2c378fd7df9f507d6426da3c3520957bfe56e6c049ccacd4e4/logfire-4.0.0.tar.gz", hash = "sha256:64d95fbf0f05c99a8b4c99a35b5b2971f11adbfbe9a73726df11d01c12f9959c", size = 512056, upload-time = "2025-07-22T15:12:05.951Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5c/89/d26951b6b21790641720c12cfd6dca0cf7ead0f5ddd7de4299837b90b8b1/logfire-4.14.2.tar.gz", hash = "sha256:8dcedbd59c3d06a8794a93bbf09add788de3b74c45afa821750992f0c822c628", size = 548291, upload-time = "2025-10-24T20:14:39.115Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b3/06/377ff0eb5d78ba893025eafed6104088eccefb0e538a9bed24e1f5d4fe53/logfire-4.0.0-py3-none-any.whl", hash = "sha256:4e50887d61954f849ec05343ca71b29fec5c0b6e4e945cabbceed664e37966e7", size = 211515, upload-time = "2025-07-22T15:12:02.113Z" }, + { url = "https://files.pythonhosted.org/packages/a7/92/4fba7b8f4f56f721ad279cb0c08164bffa14e93cfd184d1a4cc7151c52a2/logfire-4.14.2-py3-none-any.whl", hash = "sha256:caa8111b20f263f4ebb0ae380a62f2a214aeb07d5e2f03c9300fa096d0a8e692", size = 228364, upload-time = "2025-10-24T20:14:34.495Z" }, ] [package.optional-dependencies] @@ -4177,50 +4227,50 @@ wheels = [ [[package]] name = "opentelemetry-api" -version = "1.30.0" +version = "1.38.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "deprecated" }, { name = "importlib-metadata" }, + { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/2b/6d/bbbf879826b7f3c89a45252010b5796fb1f1a0d45d9dc4709db0ef9a06c8/opentelemetry_api-1.30.0.tar.gz", hash = "sha256:375893400c1435bf623f7dfb3bcd44825fe6b56c34d0667c542ea8257b1a1240", size = 63703, upload-time = "2025-02-04T18:17:13.789Z" } +sdist = { url = "https://files.pythonhosted.org/packages/08/d8/0f354c375628e048bd0570645b310797299754730079853095bf000fba69/opentelemetry_api-1.38.0.tar.gz", hash = "sha256:f4c193b5e8acb0912b06ac5b16321908dd0843d75049c091487322284a3eea12", size = 65242, upload-time = "2025-10-16T08:35:50.25Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/36/0a/eea862fae6413d8181b23acf8e13489c90a45f17986ee9cf4eab8a0b9ad9/opentelemetry_api-1.30.0-py3-none-any.whl", hash = "sha256:d5f5284890d73fdf47f843dda3210edf37a38d66f44f2b5aedc1e89ed455dc09", size = 64955, upload-time = "2025-02-04T18:16:46.167Z" }, + { url = "https://files.pythonhosted.org/packages/ae/a2/d86e01c28300bd41bab8f18afd613676e2bd63515417b77636fc1add426f/opentelemetry_api-1.38.0-py3-none-any.whl", hash = "sha256:2891b0197f47124454ab9f0cf58f3be33faca394457ac3e09daba13ff50aa582", size = 65947, upload-time = "2025-10-16T08:35:30.23Z" }, ] [[package]] name = "opentelemetry-exporter-otlp-proto-common" -version = "1.30.0" +version = "1.38.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-proto" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a2/d7/44098bf1ef89fc5810cdbda05faa2ae9322a0dbda4921cdc965dc68a9856/opentelemetry_exporter_otlp_proto_common-1.30.0.tar.gz", hash = "sha256:ddbfbf797e518411857d0ca062c957080279320d6235a279f7b64ced73c13897", size = 19640, upload-time = "2025-02-04T18:17:16.234Z" } +sdist = { url = "https://files.pythonhosted.org/packages/19/83/dd4660f2956ff88ed071e9e0e36e830df14b8c5dc06722dbde1841accbe8/opentelemetry_exporter_otlp_proto_common-1.38.0.tar.gz", hash = "sha256:e333278afab4695aa8114eeb7bf4e44e65c6607d54968271a249c180b2cb605c", size = 20431, upload-time = "2025-10-16T08:35:53.285Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ee/54/f4b3de49f8d7d3a78fd6e6e1a6fd27dd342eb4d82c088b9078c6a32c3808/opentelemetry_exporter_otlp_proto_common-1.30.0-py3-none-any.whl", hash = "sha256:5468007c81aa9c44dc961ab2cf368a29d3475977df83b4e30aeed42aa7bc3b38", size = 18747, upload-time = "2025-02-04T18:16:51.512Z" }, + { url = "https://files.pythonhosted.org/packages/a7/9e/55a41c9601191e8cd8eb626b54ee6827b9c9d4a46d736f32abc80d8039fc/opentelemetry_exporter_otlp_proto_common-1.38.0-py3-none-any.whl", hash = "sha256:03cb76ab213300fe4f4c62b7d8f17d97fcfd21b89f0b5ce38ea156327ddda74a", size = 18359, upload-time = "2025-10-16T08:35:34.099Z" }, ] [[package]] name = "opentelemetry-exporter-otlp-proto-http" -version = "1.30.0" +version = "1.38.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "deprecated" }, { name = "googleapis-common-protos" }, { name = "opentelemetry-api" }, { name = "opentelemetry-exporter-otlp-proto-common" }, { name = "opentelemetry-proto" }, { name = "opentelemetry-sdk" }, { name = "requests" }, + { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/04/f9/abb9191d536e6a2e2b7903f8053bf859a76bf784e3ca19a5749550ef19e4/opentelemetry_exporter_otlp_proto_http-1.30.0.tar.gz", hash = "sha256:c3ae75d4181b1e34a60662a6814d0b94dd33b628bee5588a878bed92cee6abdc", size = 15073, upload-time = "2025-02-04T18:17:18.446Z" } +sdist = { url = "https://files.pythonhosted.org/packages/81/0a/debcdfb029fbd1ccd1563f7c287b89a6f7bef3b2902ade56797bfd020854/opentelemetry_exporter_otlp_proto_http-1.38.0.tar.gz", hash = "sha256:f16bd44baf15cbe07633c5112ffc68229d0edbeac7b37610be0b2def4e21e90b", size = 17282, upload-time = "2025-10-16T08:35:54.422Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e1/3c/cdf34bc459613f2275aff9b258f35acdc4c4938dad161d17437de5d4c034/opentelemetry_exporter_otlp_proto_http-1.30.0-py3-none-any.whl", hash = "sha256:9578e790e579931c5ffd50f1e6975cbdefb6a0a0a5dea127a6ae87df10e0a589", size = 17245, upload-time = "2025-02-04T18:16:53.514Z" }, + { url = "https://files.pythonhosted.org/packages/e5/77/154004c99fb9f291f74aa0822a2f5bbf565a72d8126b3a1b63ed8e5f83c7/opentelemetry_exporter_otlp_proto_http-1.38.0-py3-none-any.whl", hash = "sha256:84b937305edfc563f08ec69b9cb2298be8188371217e867c1854d77198d0825b", size = 19579, upload-time = "2025-10-16T08:35:36.269Z" }, ] [[package]] name = "opentelemetry-instrumentation" -version = "0.51b0" +version = "0.59b0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-api" }, @@ -4228,14 +4278,14 @@ dependencies = [ { name = "packaging" }, { name = "wrapt" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ec/5a/4c7f02235ac1269b48f3855f6be1afc641f31d4888d28b90b732fbce7141/opentelemetry_instrumentation-0.51b0.tar.gz", hash = "sha256:4ca266875e02f3988536982467f7ef8c32a38b8895490ddce9ad9604649424fa", size = 27760, upload-time = "2025-02-04T18:21:09.279Z" } +sdist = { url = "https://files.pythonhosted.org/packages/04/ed/9c65cd209407fd807fa05be03ee30f159bdac8d59e7ea16a8fe5a1601222/opentelemetry_instrumentation-0.59b0.tar.gz", hash = "sha256:6010f0faaacdaf7c4dff8aac84e226d23437b331dcda7e70367f6d73a7db1adc", size = 31544, upload-time = "2025-10-16T08:39:31.959Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/40/2c/48fa93f1acca9f79a06da0df7bfe916632ecc7fce1971067b3e46bcae55b/opentelemetry_instrumentation-0.51b0-py3-none-any.whl", hash = "sha256:c6de8bd26b75ec8b0e54dff59e198946e29de6a10ec65488c357d4b34aa5bdcf", size = 30923, upload-time = "2025-02-04T18:19:37.829Z" }, + { url = "https://files.pythonhosted.org/packages/10/f5/7a40ff3f62bfe715dad2f633d7f1174ba1a7dd74254c15b2558b3401262a/opentelemetry_instrumentation-0.59b0-py3-none-any.whl", hash = "sha256:44082cc8fe56b0186e87ee8f7c17c327c4c2ce93bdbe86496e600985d74368ee", size = 33020, upload-time = "2025-10-16T08:38:31.463Z" }, ] [[package]] name = "opentelemetry-instrumentation-asgi" -version = "0.51b0" +version = "0.59b0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "asgiref" }, @@ -4244,28 +4294,28 @@ dependencies = [ { name = "opentelemetry-semantic-conventions" }, { name = "opentelemetry-util-http" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/9e/67/8aa6e1129f641f0f3f8786e6c5d18c1f2bbe490bd4b0e91a6879e85154d2/opentelemetry_instrumentation_asgi-0.51b0.tar.gz", hash = "sha256:b3fe97c00f0bfa934371a69674981d76591c68d937b6422a5716ca21081b4148", size = 24201, upload-time = "2025-02-04T18:21:14.321Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b7/a4/cfbb6fc1ec0aa9bf5a93f548e6a11ab3ac1956272f17e0d399aa2c1f85bc/opentelemetry_instrumentation_asgi-0.59b0.tar.gz", hash = "sha256:2509d6fe9fd829399ce3536e3a00426c7e3aa359fc1ed9ceee1628b56da40e7a", size = 25116, upload-time = "2025-10-16T08:39:36.092Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/54/7e/0a95ab37302729543631a789ba8e71dea75c520495739dbbbdfdc580b401/opentelemetry_instrumentation_asgi-0.51b0-py3-none-any.whl", hash = "sha256:e8072993db47303b633c6ec1bc74726ba4d32bd0c46c28dfadf99f79521a324c", size = 16340, upload-time = "2025-02-04T18:19:49.924Z" }, + { url = "https://files.pythonhosted.org/packages/f3/88/fe02d809963b182aafbf5588685d7a05af8861379b0ec203d48e360d4502/opentelemetry_instrumentation_asgi-0.59b0-py3-none-any.whl", hash = "sha256:ba9703e09d2c33c52fa798171f344c8123488fcd45017887981df088452d3c53", size = 16797, upload-time = "2025-10-16T08:38:37.214Z" }, ] [[package]] name = "opentelemetry-instrumentation-asyncpg" -version = "0.51b0" +version = "0.59b0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-api" }, { name = "opentelemetry-instrumentation" }, { name = "opentelemetry-semantic-conventions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/1f/fe/95eb7747a37d980787440db8001ab991f54ba4f47ea8635b43644eb2df5f/opentelemetry_instrumentation_asyncpg-0.51b0.tar.gz", hash = "sha256:366fb7f7e2c3a66de28b3770e7e795fd2612eace346dd842b77bbe61a97b7ff1", size = 8656, upload-time = "2025-02-04T18:21:16.107Z" } +sdist = { url = "https://files.pythonhosted.org/packages/64/2b/9bad3483380513b1c4c232dffbc8e54d1f38bad275f86462883b355f0d8e/opentelemetry_instrumentation_asyncpg-0.59b0.tar.gz", hash = "sha256:fada2fa14c8ee77b25c1f4ed37aa21a581449b456a78d814b54c6e5b051d3618", size = 8725, upload-time = "2025-10-16T08:39:38Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/02/35/ec8638338a1b4623172f86fa7c01a58f30fd5f39c053bbb3fabc9514d7fd/opentelemetry_instrumentation_asyncpg-0.51b0-py3-none-any.whl", hash = "sha256:6180c57c497cee1c787aeb5b090f92b1bb9ee90cb606932adfaf6bf3fdb494a5", size = 9992, upload-time = "2025-02-04T18:19:53.239Z" }, + { url = "https://files.pythonhosted.org/packages/c2/4e/27430be77f066b8c457e2e85d68009a7ff28d635298bce2486b7429da3dd/opentelemetry_instrumentation_asyncpg-0.59b0-py3-none-any.whl", hash = "sha256:538af20d9423bd05f2bdf4c1cab063539cb4db0835340c0b7f45836725e31cb0", size = 10087, upload-time = "2025-10-16T08:38:39.727Z" }, ] [[package]] name = "opentelemetry-instrumentation-dbapi" -version = "0.51b0" +version = "0.59b0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-api" }, @@ -4273,14 +4323,14 @@ dependencies = [ { name = "opentelemetry-semantic-conventions" }, { name = "wrapt" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/84/b7/fdc107617b9f626632f5fbe444a6a91efa4a9d1e38447500802b8a12010c/opentelemetry_instrumentation_dbapi-0.51b0.tar.gz", hash = "sha256:740b5e17eef02a91a8d3966f06e5605817a7d875ae4d9dec8318ef652ccfc1fe", size = 13860, upload-time = "2025-02-04T18:21:23.948Z" } +sdist = { url = "https://files.pythonhosted.org/packages/60/aa/36a09652c98c65b42408d40f222fba031a3a281f1b6682e1b141b20b508d/opentelemetry_instrumentation_dbapi-0.59b0.tar.gz", hash = "sha256:c50112ae1cdb7f55bddcf57eca96aaa0f2dd78732be2b00953183439a4740493", size = 16308, upload-time = "2025-10-16T08:39:43.192Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/34/13/d3cd0292680ebd54ed6d55d7a81434bc2c6f7327d971c6690c98114d6abc/opentelemetry_instrumentation_dbapi-0.51b0-py3-none-any.whl", hash = "sha256:1b4dfb4f25b4ef509b70fb24c637436a40fe5fc8204933b956f1d0ccaa61735f", size = 12373, upload-time = "2025-02-04T18:20:09.771Z" }, + { url = "https://files.pythonhosted.org/packages/e5/9b/1739b5b7926cbae342880d7a56d59a847313e6568a96ba7d4873ce0c0996/opentelemetry_instrumentation_dbapi-0.59b0-py3-none-any.whl", hash = "sha256:672d59caa06754b42d4e722644d9fcd00a1f9f862e9ea5cef6d4da454515ac67", size = 13970, upload-time = "2025-10-16T08:38:48.342Z" }, ] [[package]] name = "opentelemetry-instrumentation-fastapi" -version = "0.51b0" +version = "0.59b0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-api" }, @@ -4289,14 +4339,14 @@ dependencies = [ { name = "opentelemetry-semantic-conventions" }, { name = "opentelemetry-util-http" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/2d/dc/8db4422b5084177d1ef6c7855c69bf2e9e689f595a4a9b59e60588e0d427/opentelemetry_instrumentation_fastapi-0.51b0.tar.gz", hash = "sha256:1624e70f2f4d12ceb792d8a0c331244cd6723190ccee01336273b4559bc13abc", size = 19249, upload-time = "2025-02-04T18:21:28.379Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ab/a7/7a6ce5009584ce97dbfd5ce77d4f9d9570147507363349d2cb705c402bcf/opentelemetry_instrumentation_fastapi-0.59b0.tar.gz", hash = "sha256:e8fe620cfcca96a7d634003df1bc36a42369dedcdd6893e13fb5903aeeb89b2b", size = 24967, upload-time = "2025-10-16T08:39:46.056Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/55/1c/ec2d816b78edf2404d7b3df6d09eefb690b70bfd191b7da06f76634f1bdc/opentelemetry_instrumentation_fastapi-0.51b0-py3-none-any.whl", hash = "sha256:10513bbc11a1188adb9c1d2c520695f7a8f2b5f4de14e8162098035901cd6493", size = 12117, upload-time = "2025-02-04T18:20:15.267Z" }, + { url = "https://files.pythonhosted.org/packages/35/27/5914c8bf140ffc70eff153077e225997c7b054f0bf28e11b9ab91b63b18f/opentelemetry_instrumentation_fastapi-0.59b0-py3-none-any.whl", hash = "sha256:0d8d00ff7d25cca40a4b2356d1d40a8f001e0668f60c102f5aa6bb721d660c4f", size = 13492, upload-time = "2025-10-16T08:38:52.312Z" }, ] [[package]] name = "opentelemetry-instrumentation-httpx" -version = "0.51b0" +version = "0.59b0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-api" }, @@ -4305,71 +4355,71 @@ dependencies = [ { name = "opentelemetry-util-http" }, { name = "wrapt" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b7/d5/4a3990c461ae7e55212115e0f8f3aa412b5ce6493579e85c292245ac69ea/opentelemetry_instrumentation_httpx-0.51b0.tar.gz", hash = "sha256:061d426a04bf5215a859fea46662e5074f920e5cbde7e6ad6825a0a1b595802c", size = 17700, upload-time = "2025-02-04T18:21:31.685Z" } +sdist = { url = "https://files.pythonhosted.org/packages/18/6b/1bdf36b68cace9b4eae3cbbade4150c71c90aa392b127dda5bb5c2a49307/opentelemetry_instrumentation_httpx-0.59b0.tar.gz", hash = "sha256:a1cb9b89d9f05a82701cc9ab9cfa3db54fd76932489449778b350bc1b9f0e872", size = 19886, upload-time = "2025-10-16T08:39:48.428Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c3/ba/23d4ab6402408c01f1c3f32e0c04ea6dae575bf19bcb9a0049c9e768c983/opentelemetry_instrumentation_httpx-0.51b0-py3-none-any.whl", hash = "sha256:2e3fdf755ba6ead6ab43031497c3d55d4c796d0368eccc0ce48d304b7ec6486a", size = 14109, upload-time = "2025-02-04T18:20:19.947Z" }, + { url = "https://files.pythonhosted.org/packages/58/16/c1e0745d20af392ec9060693531d7f01239deb2d81e460d0c379719691b8/opentelemetry_instrumentation_httpx-0.59b0-py3-none-any.whl", hash = "sha256:7dc9f66aef4ca3904d877f459a70c78eafd06131dc64d713b9b1b5a7d0a48f05", size = 15197, upload-time = "2025-10-16T08:38:55.507Z" }, ] [[package]] name = "opentelemetry-instrumentation-sqlite3" -version = "0.51b0" +version = "0.59b0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-api" }, { name = "opentelemetry-instrumentation" }, { name = "opentelemetry-instrumentation-dbapi" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e8/2a/1755f34fd1d58858272970ce9f8386a488ce2aa16c2673373ed31cc60d33/opentelemetry_instrumentation_sqlite3-0.51b0.tar.gz", hash = "sha256:3bd5dbe2292a68b27b79c44a13a03b1443341404e02351d3886ee6526792ead1", size = 7930, upload-time = "2025-02-04T18:21:47.709Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5d/c9/316d9800fbb64ac2b5474d17d13f96a37df86e5c06e348a7d143b3eb377f/opentelemetry_instrumentation_sqlite3-0.59b0.tar.gz", hash = "sha256:7b9989d805336a1e78a907b3863376cf4ff1dc96dd8a9e0d385f6bb3686c27ac", size = 7923, upload-time = "2025-10-16T08:40:01.625Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d8/d0/6288eb2b6065b7766eee545729e6e68ac241ce82ec60a8452742414536c7/opentelemetry_instrumentation_sqlite3-0.51b0-py3-none-any.whl", hash = "sha256:77418bfec1b45f4d44a9a316c355aab33d36eb7cc1cd5d871f40acae36ae5c96", size = 9339, upload-time = "2025-02-04T18:20:51.607Z" }, + { url = "https://files.pythonhosted.org/packages/e5/ef/daf9075b22f59f45c8839dcde8d1c4fd3061b6a6692a61150fad6ca7a1a5/opentelemetry_instrumentation_sqlite3-0.59b0-py3-none-any.whl", hash = "sha256:ec13867102687426b835f6c499a287ee2f4195abfba85d372e011a795661914c", size = 9338, upload-time = "2025-10-16T08:39:11.545Z" }, ] [[package]] name = "opentelemetry-proto" -version = "1.30.0" +version = "1.38.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "protobuf" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/31/6e/c1ff2e3b0cd3a189a6be03fd4d63441d73d7addd9117ab5454e667b9b6c7/opentelemetry_proto-1.30.0.tar.gz", hash = "sha256:afe5c9c15e8b68d7c469596e5b32e8fc085eb9febdd6fb4e20924a93a0389179", size = 34362, upload-time = "2025-02-04T18:17:28.099Z" } +sdist = { url = "https://files.pythonhosted.org/packages/51/14/f0c4f0f6371b9cb7f9fa9ee8918bfd59ac7040c7791f1e6da32a1839780d/opentelemetry_proto-1.38.0.tar.gz", hash = "sha256:88b161e89d9d372ce723da289b7da74c3a8354a8e5359992be813942969ed468", size = 46152, upload-time = "2025-10-16T08:36:01.612Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/56/d7/85de6501f7216995295f7ec11e470142e6a6e080baacec1753bbf272e007/opentelemetry_proto-1.30.0-py3-none-any.whl", hash = "sha256:c6290958ff3ddacc826ca5abbeb377a31c2334387352a259ba0df37c243adc11", size = 55854, upload-time = "2025-02-04T18:17:08.024Z" }, + { url = "https://files.pythonhosted.org/packages/b6/6a/82b68b14efca5150b2632f3692d627afa76b77378c4999f2648979409528/opentelemetry_proto-1.38.0-py3-none-any.whl", hash = "sha256:b6ebe54d3217c42e45462e2a1ae28c3e2bf2ec5a5645236a490f55f45f1a0a18", size = 72535, upload-time = "2025-10-16T08:35:45.749Z" }, ] [[package]] name = "opentelemetry-sdk" -version = "1.30.0" +version = "1.38.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-api" }, { name = "opentelemetry-semantic-conventions" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/93/ee/d710062e8a862433d1be0b85920d0c653abe318878fef2d14dfe2c62ff7b/opentelemetry_sdk-1.30.0.tar.gz", hash = "sha256:c9287a9e4a7614b9946e933a67168450b9ab35f08797eb9bc77d998fa480fa18", size = 158633, upload-time = "2025-02-04T18:17:28.908Z" } +sdist = { url = "https://files.pythonhosted.org/packages/85/cb/f0eee1445161faf4c9af3ba7b848cc22a50a3d3e2515051ad8628c35ff80/opentelemetry_sdk-1.38.0.tar.gz", hash = "sha256:93df5d4d871ed09cb4272305be4d996236eedb232253e3ab864c8620f051cebe", size = 171942, upload-time = "2025-10-16T08:36:02.257Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/97/28/64d781d6adc6bda2260067ce2902bd030cf45aec657e02e28c5b4480b976/opentelemetry_sdk-1.30.0-py3-none-any.whl", hash = "sha256:14fe7afc090caad881addb6926cec967129bd9260c4d33ae6a217359f6b61091", size = 118717, upload-time = "2025-02-04T18:17:09.353Z" }, + { url = "https://files.pythonhosted.org/packages/2f/2e/e93777a95d7d9c40d270a371392b6d6f1ff170c2a3cb32d6176741b5b723/opentelemetry_sdk-1.38.0-py3-none-any.whl", hash = "sha256:1c66af6564ecc1553d72d811a01df063ff097cdc82ce188da9951f93b8d10f6b", size = 132349, upload-time = "2025-10-16T08:35:46.995Z" }, ] [[package]] name = "opentelemetry-semantic-conventions" -version = "0.51b0" +version = "0.59b0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "deprecated" }, { name = "opentelemetry-api" }, + { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/1e/c0/0f9ef4605fea7f2b83d55dd0b0d7aebe8feead247cd6facd232b30907b4f/opentelemetry_semantic_conventions-0.51b0.tar.gz", hash = "sha256:3fabf47f35d1fd9aebcdca7e6802d86bd5ebc3bc3408b7e3248dde6e87a18c47", size = 107191, upload-time = "2025-02-04T18:17:29.903Z" } +sdist = { url = "https://files.pythonhosted.org/packages/40/bc/8b9ad3802cd8ac6583a4eb7de7e5d7db004e89cb7efe7008f9c8a537ee75/opentelemetry_semantic_conventions-0.59b0.tar.gz", hash = "sha256:7a6db3f30d70202d5bf9fa4b69bc866ca6a30437287de6c510fb594878aed6b0", size = 129861, upload-time = "2025-10-16T08:36:03.346Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2e/75/d7bdbb6fd8630b4cafb883482b75c4fc276b6426619539d266e32ac53266/opentelemetry_semantic_conventions-0.51b0-py3-none-any.whl", hash = "sha256:fdc777359418e8d06c86012c3dc92c88a6453ba662e941593adb062e48c2eeae", size = 177416, upload-time = "2025-02-04T18:17:11.305Z" }, + { url = "https://files.pythonhosted.org/packages/24/7d/c88d7b15ba8fe5c6b8f93be50fc11795e9fc05386c44afaf6b76fe191f9b/opentelemetry_semantic_conventions-0.59b0-py3-none-any.whl", hash = "sha256:35d3b8833ef97d614136e253c1da9342b4c3c083bbaf29ce31d572a1c3825eed", size = 207954, upload-time = "2025-10-16T08:35:48.054Z" }, ] [[package]] name = "opentelemetry-util-http" -version = "0.51b0" +version = "0.59b0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/58/64/32510c0a803465eb6ef1f5bd514d0f5627f8abc9444ed94f7240faf6fcaa/opentelemetry_util_http-0.51b0.tar.gz", hash = "sha256:05edd19ca1cc3be3968b1e502fd94816901a365adbeaab6b6ddb974384d3a0b9", size = 8043, upload-time = "2025-02-04T18:21:59.811Z" } +sdist = { url = "https://files.pythonhosted.org/packages/34/f7/13cd081e7851c42520ab0e96efb17ffbd901111a50b8252ec1e240664020/opentelemetry_util_http-0.59b0.tar.gz", hash = "sha256:ae66ee91be31938d832f3b4bc4eb8a911f6eddd38969c4a871b1230db2a0a560", size = 9412, upload-time = "2025-10-16T08:40:11.335Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/48/dd/c371eeb9cc78abbdad231a27ce1a196a37ef96328d876ccbb381dea4c8ee/opentelemetry_util_http-0.51b0-py3-none-any.whl", hash = "sha256:0561d7a6e9c422b9ef9ae6e77eafcfcd32a2ab689f5e801475cbb67f189efa20", size = 7304, upload-time = "2025-02-04T18:21:05.483Z" }, + { url = "https://files.pythonhosted.org/packages/20/56/62282d1d4482061360449dacc990c89cad0fc810a2ed937b636300f55023/opentelemetry_util_http-0.59b0-py3-none-any.whl", hash = "sha256:6d036a07563bce87bf521839c0671b507a02a0d39d7ea61b88efa14c6e25355d", size = 7648, upload-time = "2025-10-16T08:39:25.706Z" }, ] [[package]] @@ -4519,11 +4569,11 @@ wheels = [ [[package]] name = "packaging" -version = "24.2" +version = "25.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d0/63/68dbb6eb2de9cb10ee4c9c14a0148804425e13c4fb20d61cce69f53106da/packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f", size = 163950, upload-time = "2024-11-08T09:47:47.202Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", size = 65451, upload-time = "2024-11-08T09:47:44.722Z" }, + { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, ] [[package]] @@ -4980,16 +5030,17 @@ wheels = [ [[package]] name = "protobuf" -version = "5.29.3" +version = "6.33.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f7/d1/e0a911544ca9993e0f17ce6d3cc0932752356c1b0a834397f28e63479344/protobuf-5.29.3.tar.gz", hash = "sha256:5da0f41edaf117bde316404bad1a486cb4ededf8e4a54891296f648e8e076620", size = 424945, upload-time = "2025-01-08T21:38:51.572Z" } +sdist = { url = "https://files.pythonhosted.org/packages/19/ff/64a6c8f420818bb873713988ca5492cba3a7946be57e027ac63495157d97/protobuf-6.33.0.tar.gz", hash = "sha256:140303d5c8d2037730c548f8c7b93b20bb1dc301be280c378b82b8894589c954", size = 443463, upload-time = "2025-10-15T20:39:52.159Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/dc/7a/1e38f3cafa022f477ca0f57a1f49962f21ad25850c3ca0acd3b9d0091518/protobuf-5.29.3-cp310-abi3-win32.whl", hash = "sha256:3ea51771449e1035f26069c4c7fd51fba990d07bc55ba80701c78f886bf9c888", size = 422708, upload-time = "2025-01-08T21:38:31.799Z" }, - { url = "https://files.pythonhosted.org/packages/61/fa/aae8e10512b83de633f2646506a6d835b151edf4b30d18d73afd01447253/protobuf-5.29.3-cp310-abi3-win_amd64.whl", hash = "sha256:a4fa6f80816a9a0678429e84973f2f98cbc218cca434abe8db2ad0bffc98503a", size = 434508, upload-time = "2025-01-08T21:38:35.489Z" }, - { url = "https://files.pythonhosted.org/packages/dd/04/3eaedc2ba17a088961d0e3bd396eac764450f431621b58a04ce898acd126/protobuf-5.29.3-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:a8434404bbf139aa9e1300dbf989667a83d42ddda9153d8ab76e0d5dcaca484e", size = 417825, upload-time = "2025-01-08T21:38:36.642Z" }, - { url = "https://files.pythonhosted.org/packages/4f/06/7c467744d23c3979ce250397e26d8ad8eeb2bea7b18ca12ad58313c1b8d5/protobuf-5.29.3-cp38-abi3-manylinux2014_aarch64.whl", hash = "sha256:daaf63f70f25e8689c072cfad4334ca0ac1d1e05a92fc15c54eb9cf23c3efd84", size = 319573, upload-time = "2025-01-08T21:38:37.896Z" }, - { url = "https://files.pythonhosted.org/packages/a8/45/2ebbde52ad2be18d3675b6bee50e68cd73c9e0654de77d595540b5129df8/protobuf-5.29.3-cp38-abi3-manylinux2014_x86_64.whl", hash = "sha256:c027e08a08be10b67c06bf2370b99c811c466398c357e615ca88c91c07f0910f", size = 319672, upload-time = "2025-01-08T21:38:40.204Z" }, - { url = "https://files.pythonhosted.org/packages/fd/b2/ab07b09e0f6d143dfb839693aa05765257bceaa13d03bf1a696b78323e7a/protobuf-5.29.3-py3-none-any.whl", hash = "sha256:0a18ed4a24198528f2333802eb075e59dea9d679ab7a6c5efb017a59004d849f", size = 172550, upload-time = "2025-01-08T21:38:50.439Z" }, + { url = "https://files.pythonhosted.org/packages/7e/ee/52b3fa8feb6db4a833dfea4943e175ce645144532e8a90f72571ad85df4e/protobuf-6.33.0-cp310-abi3-win32.whl", hash = "sha256:d6101ded078042a8f17959eccd9236fb7a9ca20d3b0098bbcb91533a5680d035", size = 425593, upload-time = "2025-10-15T20:39:40.29Z" }, + { url = "https://files.pythonhosted.org/packages/7b/c6/7a465f1825872c55e0341ff4a80198743f73b69ce5d43ab18043699d1d81/protobuf-6.33.0-cp310-abi3-win_amd64.whl", hash = "sha256:9a031d10f703f03768f2743a1c403af050b6ae1f3480e9c140f39c45f81b13ee", size = 436882, upload-time = "2025-10-15T20:39:42.841Z" }, + { url = "https://files.pythonhosted.org/packages/e1/a9/b6eee662a6951b9c3640e8e452ab3e09f117d99fc10baa32d1581a0d4099/protobuf-6.33.0-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:905b07a65f1a4b72412314082c7dbfae91a9e8b68a0cc1577515f8df58ecf455", size = 427521, upload-time = "2025-10-15T20:39:43.803Z" }, + { url = "https://files.pythonhosted.org/packages/10/35/16d31e0f92c6d2f0e77c2a3ba93185130ea13053dd16200a57434c882f2b/protobuf-6.33.0-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:e0697ece353e6239b90ee43a9231318302ad8353c70e6e45499fa52396debf90", size = 324445, upload-time = "2025-10-15T20:39:44.932Z" }, + { url = "https://files.pythonhosted.org/packages/e6/eb/2a981a13e35cda8b75b5585aaffae2eb904f8f351bdd3870769692acbd8a/protobuf-6.33.0-cp39-abi3-manylinux2014_s390x.whl", hash = "sha256:e0a1715e4f27355afd9570f3ea369735afc853a6c3951a6afe1f80d8569ad298", size = 339159, upload-time = "2025-10-15T20:39:46.186Z" }, + { url = "https://files.pythonhosted.org/packages/21/51/0b1cbad62074439b867b4e04cc09b93f6699d78fd191bed2bbb44562e077/protobuf-6.33.0-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:35be49fd3f4fefa4e6e2aacc35e8b837d6703c37a2168a55ac21e9b1bc7559ef", size = 323172, upload-time = "2025-10-15T20:39:47.465Z" }, + { url = "https://files.pythonhosted.org/packages/07/d1/0a28c21707807c6aacd5dc9c3704b2aa1effbf37adebd8caeaf68b17a636/protobuf-6.33.0-py3-none-any.whl", hash = "sha256:25c9e1963c6734448ea2d308cfa610e692b801304ba0908d7bfa564ac5132995", size = 170477, upload-time = "2025-10-15T20:39:51.311Z" }, ] [[package]] @@ -5368,7 +5419,7 @@ email = [ name = "pydantic-ai" source = { editable = "." } dependencies = [ - { name = "pydantic-ai-slim", extra = ["ag-ui", "anthropic", "bedrock", "cli", "cohere", "evals", "fastmcp", "google", "groq", "huggingface", "logfire", "mcp", "mistral", "openai", "retries", "temporal", "ui", "vertexai"] }, + { name = "pydantic-ai-slim", extra = ["ag-ui", "anthropic", "bedrock", "cli", "cohere", "evals", "fastmcp", "google", "groq", "huggingface", "logfire", "mcp", "mistral", "openai", "retries", "temporal", "ui", "vertexai", "xai"] }, ] [package.optional-dependencies] @@ -5447,7 +5498,7 @@ lint = [ requires-dist = [ { name = "fasta2a", marker = "extra == 'a2a'", specifier = ">=0.4.1" }, { name = "pydantic-ai-examples", marker = "extra == 'examples'", editable = "examples" }, - { name = "pydantic-ai-slim", extras = ["ag-ui", "anthropic", "bedrock", "cli", "cohere", "evals", "fastmcp", "google", "groq", "huggingface", "logfire", "mcp", "mistral", "openai", "retries", "temporal", "ui", "vertexai"], editable = "pydantic_ai_slim" }, + { name = "pydantic-ai-slim", extras = ["ag-ui", "anthropic", "bedrock", "cli", "cohere", "evals", "fastmcp", "google", "groq", "huggingface", "logfire", "mcp", "mistral", "openai", "retries", "temporal", "ui", "vertexai", "xai"], editable = "pydantic_ai_slim" }, { name = "pydantic-ai-slim", extras = ["dbos"], marker = "extra == 'dbos'", editable = "pydantic_ai_slim" }, { name = "pydantic-ai-slim", extras = ["outlines-llamacpp"], marker = "extra == 'outlines-llamacpp'", editable = "pydantic_ai_slim" }, { name = "pydantic-ai-slim", extras = ["outlines-mlxlm"], marker = "extra == 'outlines-mlxlm'", editable = "pydantic_ai_slim" }, @@ -5655,6 +5706,9 @@ vertexai = [ { name = "google-auth" }, { name = "requests" }, ] +xai = [ + { name = "xai-sdk" }, +] [package.metadata] requires-dist = [ @@ -5705,8 +5759,9 @@ requires-dist = [ { name = "transformers", marker = "extra == 'outlines-transformers'", specifier = ">=4.0.0" }, { name = "typing-inspection", specifier = ">=0.4.0" }, { name = "vllm", marker = "(python_full_version < '3.12' and platform_machine != 'x86_64' and extra == 'outlines-vllm-offline') or (python_full_version < '3.12' and sys_platform != 'darwin' and extra == 'outlines-vllm-offline')" }, + { name = "xai-sdk", marker = "extra == 'xai'", specifier = ">=1.4.0" }, ] -provides-extras = ["a2a", "ag-ui", "anthropic", "bedrock", "cli", "cohere", "dbos", "duckduckgo", "evals", "fastmcp", "google", "groq", "huggingface", "logfire", "mcp", "mistral", "openai", "outlines-llamacpp", "outlines-mlxlm", "outlines-sglang", "outlines-transformers", "outlines-vllm-offline", "prefect", "retries", "tavily", "temporal", "ui", "vertexai"] +provides-extras = ["a2a", "ag-ui", "anthropic", "bedrock", "cli", "cohere", "dbos", "duckduckgo", "evals", "fastmcp", "google", "groq", "huggingface", "logfire", "mcp", "mistral", "openai", "outlines-llamacpp", "outlines-mlxlm", "outlines-sglang", "outlines-transformers", "outlines-vllm-offline", "prefect", "retries", "tavily", "temporal", "ui", "vertexai", "xai"] [[package]] name = "pydantic-core" @@ -8562,6 +8617,24 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/2d/82/f56956041adef78f849db6b289b282e72b55ab8045a75abad81898c28d19/wrapt-1.17.2-py3-none-any.whl", hash = "sha256:b18f2d1533a71f069c7f82d524a52599053d4c7166e9dd374ae2136b7f40f7c8", size = 23594, upload-time = "2025-01-14T10:35:44.018Z" }, ] +[[package]] +name = "xai-sdk" +version = "1.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohttp" }, + { name = "grpcio" }, + { name = "opentelemetry-sdk" }, + { name = "packaging" }, + { name = "protobuf" }, + { name = "pydantic" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c3/cf/c9ccc20bd419f4fce088cd3e1778fb6b3420526ff4599c2bf6caf1427e99/xai_sdk-1.4.0.tar.gz", hash = "sha256:90e6e0b929395816a8474a332e6d996fbd7c56c3e9922b3894d14ef90b4adc37", size = 314502, upload-time = "2025-11-07T23:55:07.722Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/e5/8cbdd56008e8194880151f151db62b1b2331d51de8f8e788b91524279611/xai_sdk-1.4.0-py3-none-any.whl", hash = "sha256:2635d661995ef1424fd5b5de6a9b7d6a11bad49a34afb19b04a330c40d90e0d1", size = 185691, upload-time = "2025-11-07T23:55:06.168Z" }, +] + [[package]] name = "xformers" version = "0.0.32.post1" @@ -8591,14 +8664,17 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/f2/a9/dc3c63cf7f082d183711e46ef34d10d8a135c2319dc581905d79449f52ea/xgrammar-0.1.25.tar.gz", hash = "sha256:70ce16b27e8082f20808ed759b0733304316facc421656f0f30cfce514b5b77a", size = 2297187, upload-time = "2025-09-21T05:58:58.942Z" } wheels = [ + { url = "https://files.pythonhosted.org/packages/c0/b4/8f78b56ebf64f161258f339cc5898bf761b4fb6c6805d0bca1bcaaaef4a1/xgrammar-0.1.25-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:d12d1078ee2b5c1531610489b433b77694a7786210ceb2c0c1c1eb058e9053c7", size = 679074, upload-time = "2025-09-21T05:58:20.344Z" }, { url = "https://files.pythonhosted.org/packages/52/38/b57120b73adcd342ef974bff14b2b584e7c47edf28d91419cb9325fd5ef2/xgrammar-0.1.25-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c2e940541b7cddf3ef55a70f20d4c872af7f0d900bc0ed36f434bf7212e2e729", size = 622668, upload-time = "2025-09-21T05:58:22.269Z" }, { url = "https://files.pythonhosted.org/packages/19/8d/64430d01c21ca2b1d8c5a1ed47c90f8ac43717beafc9440d01d81acd5cfc/xgrammar-0.1.25-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2063e1c72f0c00f47ac8ce7ce0fcbff6fa77f79012e063369683844e2570c266", size = 8517569, upload-time = "2025-09-21T05:58:23.77Z" }, { url = "https://files.pythonhosted.org/packages/b1/c4/137d0e9cd038ff4141752c509dbeea0ec5093eb80815620c01b1f1c26d0a/xgrammar-0.1.25-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9785eafa251c996ebaa441f3b8a6c037538930104e265a64a013da0e6fd2ad86", size = 8709188, upload-time = "2025-09-21T05:58:26.246Z" }, { url = "https://files.pythonhosted.org/packages/6c/3d/c228c470d50865c9db3fb1e75a95449d0183a8248519b89e86dc481d6078/xgrammar-0.1.25-cp310-cp310-win_amd64.whl", hash = "sha256:42ecefd020038b3919a473fe5b9bb9d8d809717b8689a736b81617dec4acc59b", size = 698919, upload-time = "2025-09-21T05:58:28.368Z" }, + { url = "https://files.pythonhosted.org/packages/9e/b7/ca0ff7c91f24b2302e94b0e6c2a234cc5752b10da51eb937e7f2aa257fde/xgrammar-0.1.25-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:27d7ac4be05cf9aa258c109a8647092ae47cb1e28df7d27caced6ab44b72b799", size = 678801, upload-time = "2025-09-21T05:58:29.936Z" }, { url = "https://files.pythonhosted.org/packages/43/cd/fdf4fb1b5f9c301d381656a600ad95255a76fa68132978af6f06e50a46e1/xgrammar-0.1.25-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:151c1636188bc8c5cdf318cefc5ba23221c9c8cc07cb392317fb3f7635428150", size = 622565, upload-time = "2025-09-21T05:58:31.185Z" }, { url = "https://files.pythonhosted.org/packages/55/04/55a87e814bcab771d3e4159281fa382b3d5f14a36114f2f9e572728da831/xgrammar-0.1.25-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:35fc135650aa204bf84db7fe9c0c0f480b6b11419fe47d89f4bd21602ac33be9", size = 8517238, upload-time = "2025-09-21T05:58:32.835Z" }, { url = "https://files.pythonhosted.org/packages/31/f6/3c5210bc41b61fb32b66bf5c9fd8ec5edacfeddf9860e95baa9caa9a2c82/xgrammar-0.1.25-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc19d6d7e8e51b6c9a266e949ac7fb3d2992447efeec7df32cca109149afac18", size = 8709514, upload-time = "2025-09-21T05:58:34.727Z" }, { url = "https://files.pythonhosted.org/packages/21/de/85714f307536b328cc16cc6755151865e8875378c8557c15447ca07dff98/xgrammar-0.1.25-cp311-cp311-win_amd64.whl", hash = "sha256:8fcb24f5a7acd5876165c50bd51ce4bf8e6ff897344a5086be92d1fe6695f7fe", size = 698722, upload-time = "2025-09-21T05:58:36.411Z" }, + { url = "https://files.pythonhosted.org/packages/bf/d7/a7bdb158afa88af7e6e0d312e9677ba5fb5e423932008c9aa2c45af75d5d/xgrammar-0.1.25-cp312-cp312-macosx_10_14_x86_64.whl", hash = "sha256:96500d7578c46e8551253b9211b02e02f54e147bc290479a64717d80dcf4f7e3", size = 678250, upload-time = "2025-09-21T05:58:37.936Z" }, { url = "https://files.pythonhosted.org/packages/10/9d/b20588a3209d544a3432ebfcf2e3b1a455833ee658149b08c18eef0c6f59/xgrammar-0.1.25-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:73ba9031e359447af53ce89dfb0775e7b9f4b358d513bcc28a6b4deace661dd5", size = 621550, upload-time = "2025-09-21T05:58:39.464Z" }, { url = "https://files.pythonhosted.org/packages/99/9c/39bb38680be3b6d6aa11b8a46a69fb43e2537d6728710b299fa9fc231ff0/xgrammar-0.1.25-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c519518ebc65f75053123baaf23776a21bda58f64101a64c2fc4aa467c9cd480", size = 8519097, upload-time = "2025-09-21T05:58:40.831Z" }, { url = "https://files.pythonhosted.org/packages/c6/c2/695797afa9922c30c45aa94e087ad33a9d87843f269461b622a65a39022a/xgrammar-0.1.25-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47fdbfc6007df47de2142613220292023e88e4a570546b39591f053e4d9ec33f", size = 8712184, upload-time = "2025-09-21T05:58:43.142Z" },