From 143c10c04f8ba7e94ace505b9ec00075b8b5ddc8 Mon Sep 17 00:00:00 2001 From: AstroAir Date: Sat, 4 Oct 2025 17:14:14 +0800 Subject: [PATCH 1/3] Add Zhipu AI support with models, provider, and documentation --- docs/models/overview.md | 1 + docs/models/zhipu.md | 202 ++++++++++++++++++ examples/zhipu_example.py | 30 +++ mkdocs.yml | 1 + .../pydantic_ai/models/__init__.py | 9 + pydantic_ai_slim/pydantic_ai/models/openai.py | 9 + .../pydantic_ai/profiles/zhipu.py | 41 ++++ .../pydantic_ai/providers/__init__.py | 4 + .../pydantic_ai/providers/zhipu.py | 94 ++++++++ tests/providers/test_zhipu.py | 86 ++++++++ 10 files changed, 477 insertions(+) create mode 100644 docs/models/zhipu.md create mode 100644 examples/zhipu_example.py create mode 100644 pydantic_ai_slim/pydantic_ai/profiles/zhipu.py create mode 100644 pydantic_ai_slim/pydantic_ai/providers/zhipu.py create mode 100644 tests/providers/test_zhipu.py diff --git a/docs/models/overview.md b/docs/models/overview.md index 36137db6e5..8e7436da79 100644 --- a/docs/models/overview.md +++ b/docs/models/overview.md @@ -10,6 +10,7 @@ Pydantic AI is model-agnostic and has built-in support for multiple model provid * [Cohere](cohere.md) * [Bedrock](bedrock.md) * [Hugging Face](huggingface.md) +* [Zhipu AI](zhipu.md) ## OpenAI-compatible Providers diff --git a/docs/models/zhipu.md b/docs/models/zhipu.md new file mode 100644 index 0000000000..dbd2938f9b --- /dev/null +++ b/docs/models/zhipu.md @@ -0,0 +1,202 @@ +# Zhipu AI + +## Install + +To use Zhipu AI models, you need to either install `pydantic-ai`, or install `pydantic-ai-slim` with the `openai` optional group (since Zhipu AI provides an OpenAI-compatible API): + +```bash +pip/uv-add "pydantic-ai-slim[openai]" +``` + +## Configuration + +To use [Zhipu AI](https://bigmodel.cn/) (智谱AI) through their API, you need to: + +1. Visit [bigmodel.cn](https://bigmodel.cn) and create an account +2. Go to [API Keys management](https://bigmodel.cn/usercenter/proj-mgmt/apikeys) +3. Create a new API key + +## Environment variable + +Once you have the API key, you can set it as an environment variable: + +```bash +export ZHIPU_API_KEY='your-api-key' +``` + +You can then use Zhipu AI models by name: + +```python +from pydantic_ai import Agent + +agent = Agent('zhipu:glm-4.5') +... +``` + +Or initialise the model directly: + +```python +from pydantic_ai import Agent +from pydantic_ai.models.openai import OpenAIChatModel + +model = OpenAIChatModel('glm-4.5', provider='zhipu') +agent = Agent(model) +... +``` + +## `provider` argument + +You can provide a custom `ZhipuProvider` via the `provider` argument: + +```python +from pydantic_ai import Agent +from pydantic_ai.models.openai import OpenAIChatModel +from pydantic_ai.providers.zhipu import ZhipuProvider + +model = OpenAIChatModel( + 'glm-4.5', provider=ZhipuProvider(api_key='your-api-key') +) +agent = Agent(model) +... +``` + +You can also customize the `ZhipuProvider` with a custom `httpx.AsyncClient`: + +```python +from httpx import AsyncClient + +from pydantic_ai import Agent +from pydantic_ai.models.openai import OpenAIChatModel +from pydantic_ai.providers.zhipu import ZhipuProvider + +custom_http_client = AsyncClient(timeout=30) +model = OpenAIChatModel( + 'glm-4.5', + provider=ZhipuProvider(api_key='your-api-key', http_client=custom_http_client), +) +agent = Agent(model) +... +``` + +## Available Models + +Zhipu AI offers several models through their OpenAI-compatible API: + +### GLM-4 Series + +- **`glm-4.6`**: Latest flagship model with 205K context window +- **`glm-4.5`**: High-performance model with 131K context window +- **`glm-4.5-air`**: Balanced performance and cost with 131K context +- **`glm-4.5-flash`**: Fast response model with 131K context + +### Vision Models + +- **`glm-4v-plus`**: Advanced vision understanding model +- **`glm-4v`**: Vision model for image analysis +- **`glm-4.5v`**: Vision-enabled variant with 64K context + +### Specialized Models + +- **`codegeex-4`**: Code generation and understanding model + +## Features + +### Function Calling + +Zhipu AI models support function calling (tool use): + +```python +from pydantic_ai import Agent, RunContext + +agent = Agent( + 'zhipu:glm-4.5', + system_prompt='You are a helpful assistant with access to tools.', +) + +@agent.tool +async def get_weather(ctx: RunContext[None], location: str) -> str: + """Get the weather for a location.""" + return f"The weather in {location} is sunny, 25°C" + +result = await agent.run('What is the weather in Beijing?') +print(result.output) +``` + +### Streaming + +Zhipu AI supports streaming responses: + +```python +from pydantic_ai import Agent + +agent = Agent('zhipu:glm-4.5') + +async with agent.run_stream('Tell me a story') as response: + async for message in response.stream_text(): + print(message, end='', flush=True) +``` + +### Vision Understanding + +Use vision models to analyze images: + +```python +from pydantic_ai import Agent + +agent = Agent('zhipu:glm-4v-plus') + +result = await agent.run( + 'What is in this image?', + message_history=[ + { + 'role': 'user', + 'content': [ + {'type': 'text', 'text': 'Describe this image:'}, + {'type': 'image_url', 'image_url': {'url': 'https://example.com/image.jpg'}} + ] + } + ] +) +print(result.output) +``` + +## Important Notes + +### Temperature Range + +Unlike OpenAI, Zhipu AI requires temperature to be in the range `(0, 1)` (exclusive). Setting `temperature=0` is not supported and will cause an error. + +```python +# This will work +agent = Agent('zhipu:glm-4.5', model_settings={'temperature': 0.1}) + +# This will NOT work with Zhipu AIs +# agent = Agent('zhipu:glm-4.5', model_settings={'temperature': 0}) +``` + +### Strict Mode + +Zhipu AI does not support OpenAI's strict mode for tool definitions. The framework automatically handles this by setting `openai_supports_strict_tool_definition=False` in the model profile. + +## Advanced Features + +### Thinking Mode + +GLM-4.5 and GLM-4.5-Air support a "thinking" mode for complex reasoning tasks. This can be enabled using the `extra_body` parameter: + +```python +from pydantic_ai.models.openai import OpenAIChatModel +from pydantic_ai.providers.zhipu import ZhipuProvider + +model = OpenAIChatModel('glm-4.5', provider=ZhipuProvider(api_key='your-api-key')) + +# Note: Thinking mode requires using the OpenAI client directly +# or passing extra_body through model_settings +``` + +## API Reference + +For more details, see: + +- [ZhipuProvider API Reference][pydantic_ai.providers.zhipu.ZhipuProvider] +- [Zhipu AI Official Documentation](https://docs.bigmodel.cn/) diff --git a/examples/zhipu_example.py b/examples/zhipu_example.py new file mode 100644 index 0000000000..a716357f55 --- /dev/null +++ b/examples/zhipu_example.py @@ -0,0 +1,30 @@ +"""Example of using Zhipu AI with Pydantic AI. + +This example demonstrates how to use Zhipu AI models with the Pydantic AI framework. + +To run this example, you need to: +1. Install pydantic-ai with openai support: pip install "pydantic-ai-slim[openai]" +2. Set your Zhipu API key: export ZHIPU_API_KEY='your-api-key' +3. Run the script: python examples/zhipu_example.py +""" + +from __future__ import annotations as _annotations + +import asyncio + +from pydantic_ai import Agent + + +async def main(): + """Run a simple example with Zhipu AI.""" + # Create an agent using Zhipu AI's GLM-4.5 model + agent = Agent('zhipu:glm-4.5', system_prompt='You are a helpful assistant.') + + # Run a simple query + result = await agent.run('What is the capital of China?') + print(f'Response: {result.output}') + print(f'Model used: {result.all_messages()[-1].model_name}') + + +if __name__ == '__main__': + asyncio.run(main()) diff --git a/mkdocs.yml b/mkdocs.yml index b35189a8ab..89051b71d7 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -34,6 +34,7 @@ nav: - models/groq.md - models/mistral.md - models/huggingface.md + - models/zhipu.md - Tools & Toolsets: - tools.md - tools-advanced.md diff --git a/pydantic_ai_slim/pydantic_ai/models/__init__.py b/pydantic_ai_slim/pydantic_ai/models/__init__.py index 274ec0989b..187785fb58 100644 --- a/pydantic_ai_slim/pydantic_ai/models/__init__.py +++ b/pydantic_ai_slim/pydantic_ai/models/__init__.py @@ -290,6 +290,14 @@ 'openai:o3-pro-2025-06-10', 'openai:computer-use-preview', 'openai:computer-use-preview-2025-03-11', + 'zhipu:glm-4.6', + 'zhipu:glm-4.5', + 'zhipu:glm-4.5v', + 'zhipu:glm-4.5-air', + 'zhipu:glm-4.5-flash', + 'zhipu:glm-4v', + 'zhipu:glm-4v-plus', + 'zhipu:codegeex-4', 'test', ], ) @@ -691,6 +699,7 @@ def infer_model(model: Model | KnownModelName | str) -> Model: # noqa: C901 'together', 'vercel', 'litellm', + 'zhipu', ): from .openai import OpenAIChatModel diff --git a/pydantic_ai_slim/pydantic_ai/models/openai.py b/pydantic_ai_slim/pydantic_ai/models/openai.py index c3e1c71b49..24cb2511ef 100644 --- a/pydantic_ai_slim/pydantic_ai/models/openai.py +++ b/pydantic_ai_slim/pydantic_ai/models/openai.py @@ -523,6 +523,15 @@ def _process_response(self, response: chat.ChatCompletion | str) -> ModelRespons if not isinstance(response, chat.ChatCompletion): raise UnexpectedModelBehavior('Invalid response from OpenAI chat completions endpoint, expected JSON data') + # Some OpenAI-compatible providers (e.g. Zhipu/Z.ai, see issue #2723) omit the `object` field even though + # the OpenAI schema includes it. We populate it here so validation succeeds. + # This mirrors the workaround for missing `created` timestamps below. + if not getattr(response, 'object', None): # pragma: no branch + try: # defensive, in case attribute is read-only in future SDK versions + response.object = 'chat.completion' # pyright: ignore[reportAttributeAccessIssue] + except Exception: # pragma: no cover + pass + if response.created: timestamp = number_to_datetime(response.created) else: diff --git a/pydantic_ai_slim/pydantic_ai/profiles/zhipu.py b/pydantic_ai_slim/pydantic_ai/profiles/zhipu.py new file mode 100644 index 0000000000..5372b11019 --- /dev/null +++ b/pydantic_ai_slim/pydantic_ai/profiles/zhipu.py @@ -0,0 +1,41 @@ +from __future__ import annotations as _annotations + +from .openai import OpenAIModelProfile + + +def zhipu_model_profile(model_name: str) -> OpenAIModelProfile: + """Get the model profile for a Zhipu AI model. + + Zhipu AI provides OpenAI-compatible API, so we use OpenAIModelProfile. + + Args: + model_name: The Zhipu model name (e.g., 'glm-4.6', 'glm-4.5', 'glm-4.5-air', 'glm-4.5v'). + + Returns: + Model profile with Zhipu-specific configurations. + """ + # Vision models — docs show vision variants with a trailing `v` like `glm-4.5v` + # Ref: https://docs.bigmodel.cn/cn/guide/develop/openai/introduction + is_vision_model = model_name.startswith(('glm-4.5v', 'glm-4v')) or ( + 'v' in model_name and ('glm-4.5v' in model_name or 'glm-4v' in model_name) + ) + + # Zhipu AI models support JSON schema and object output + # All GLM-4 series models support function calling + supports_tools = model_name.startswith(('glm-4', 'codegeex-4')) + + # Zhipu AI doesn't support temperature=0 (must be in range (0, 1)) + # This is a known difference from OpenAI + openai_unsupported_model_settings = () + + return OpenAIModelProfile( + supports_json_schema_output=supports_tools, + supports_json_object_output=supports_tools, + supports_image_output=is_vision_model, + openai_supports_strict_tool_definition=False, # Zhipu doesn't support strict mode + openai_supports_tool_choice_required=True, + openai_unsupported_model_settings=openai_unsupported_model_settings, + openai_system_prompt_role=None, # Use default 'system' role + openai_chat_supports_web_search=False, + openai_supports_encrypted_reasoning_content=False, + ) diff --git a/pydantic_ai_slim/pydantic_ai/providers/__init__.py b/pydantic_ai_slim/pydantic_ai/providers/__init__.py index b84809ea06..ced62a570a 100644 --- a/pydantic_ai_slim/pydantic_ai/providers/__init__.py +++ b/pydantic_ai_slim/pydantic_ai/providers/__init__.py @@ -142,6 +142,10 @@ def infer_provider_class(provider: str) -> type[Provider[Any]]: # noqa: C901 from .litellm import LiteLLMProvider return LiteLLMProvider + elif provider == 'zhipu': + from .zhipu import ZhipuProvider + + return ZhipuProvider else: # pragma: no cover raise ValueError(f'Unknown provider: {provider}') diff --git a/pydantic_ai_slim/pydantic_ai/providers/zhipu.py b/pydantic_ai_slim/pydantic_ai/providers/zhipu.py new file mode 100644 index 0000000000..3aa14cbed6 --- /dev/null +++ b/pydantic_ai_slim/pydantic_ai/providers/zhipu.py @@ -0,0 +1,94 @@ +from __future__ import annotations as _annotations + +import os +from typing import overload + +import httpx + +from pydantic_ai import ModelProfile +from pydantic_ai.exceptions import UserError +from pydantic_ai.models import cached_async_http_client +from pydantic_ai.profiles.zhipu import zhipu_model_profile +from pydantic_ai.providers import Provider + +try: + from openai import AsyncOpenAI +except ImportError as _import_error: # pragma: no cover + raise ImportError( + 'Please install the `openai` package to use the Zhipu provider, ' + 'you can use the `openai` optional group — `pip install "pydantic-ai-slim[openai]"`' + ) from _import_error + + +class ZhipuProvider(Provider[AsyncOpenAI]): + """Provider for Zhipu AI API. + + Zhipu AI provides an OpenAI-compatible API, so this provider uses the OpenAI client + with Zhipu-specific configuration. + """ + + @property + def name(self) -> str: + return 'zhipu' + + @property + def base_url(self) -> str: + return str(self.client.base_url) + + @property + def client(self) -> AsyncOpenAI: + return self._client + + def model_profile(self, model_name: str) -> ModelProfile | None: + return zhipu_model_profile(model_name) + + @overload + def __init__(self, *, openai_client: AsyncOpenAI) -> None: ... + + @overload + def __init__( + self, + *, + api_key: str | None = None, + base_url: str | None = None, + http_client: httpx.AsyncClient | None = None, + ) -> None: ... + + def __init__( + self, + *, + api_key: str | None = None, + base_url: str | None = None, + openai_client: AsyncOpenAI | None = None, + http_client: httpx.AsyncClient | None = None, + ) -> None: + """Create a new Zhipu AI provider. + + Args: + api_key: The API key to use for authentication. If not provided, the `ZHIPU_API_KEY` environment variable + will be used if available. + base_url: The base url for the Zhipu AI requests. If not provided, defaults to Zhipu's base url. + openai_client: An existing `AsyncOpenAI` client to use. If provided, `api_key`, `base_url`, + and `http_client` must be `None`. + http_client: An existing `httpx.AsyncClient` to use for making HTTP requests. + """ + if openai_client is not None: + assert api_key is None, 'Cannot provide both `openai_client` and `api_key`' + assert base_url is None, 'Cannot provide both `openai_client` and `base_url`' + assert http_client is None, 'Cannot provide both `openai_client` and `http_client`' + self._client = openai_client + else: + api_key = api_key or os.getenv('ZHIPU_API_KEY') + base_url = base_url or 'https://open.bigmodel.cn/api/paas/v4/' + + if not api_key: + raise UserError( + 'Set the `ZHIPU_API_KEY` environment variable or pass it via `ZhipuProvider(api_key=...)`' + ' to use the Zhipu provider.' + ) + + if http_client is not None: + self._client = AsyncOpenAI(base_url=base_url, api_key=api_key, http_client=http_client) + else: + http_client = cached_async_http_client(provider='zhipu') + self._client = AsyncOpenAI(base_url=base_url, api_key=api_key, http_client=http_client) diff --git a/tests/providers/test_zhipu.py b/tests/providers/test_zhipu.py new file mode 100644 index 0000000000..6577a4e410 --- /dev/null +++ b/tests/providers/test_zhipu.py @@ -0,0 +1,86 @@ +from __future__ import annotations as _annotations + +import pytest + +from pydantic_ai.exceptions import UserError + +from ..conftest import try_import + +with try_import() as imports_successful: + from openai import AsyncOpenAI + + from pydantic_ai.providers.zhipu import ZhipuProvider + +pytestmark = [ + pytest.mark.skipif(not imports_successful(), reason='openai not installed'), +] + + +def test_init_with_api_key(): + """Test ZhipuProvider initialization with API key.""" + provider = ZhipuProvider(api_key='test-api-key') + assert provider.name == 'zhipu' + assert provider.base_url == 'https://open.bigmodel.cn/api/paas/v4/' + assert provider.client.api_key == 'test-api-key' + + +def test_init_with_custom_base_url(): + """Test ZhipuProvider initialization with custom base URL.""" + provider = ZhipuProvider(api_key='test-api-key', base_url='https://custom.url/') + assert provider.base_url == 'https://custom.url/' + + +def test_init_with_env_var(monkeypatch: pytest.MonkeyPatch): + """Test ZhipuProvider initialization with environment variable.""" + monkeypatch.setenv('ZHIPU_API_KEY', 'env-api-key') + provider = ZhipuProvider() + assert provider.client.api_key == 'env-api-key' + + +def test_init_without_api_key(monkeypatch: pytest.MonkeyPatch): + """Test ZhipuProvider initialization without API key raises error.""" + monkeypatch.delenv('ZHIPU_API_KEY', raising=False) + with pytest.raises(UserError, match='Set the `ZHIPU_API_KEY` environment variable'): + ZhipuProvider() + + +def test_init_with_openai_client(): + """Test ZhipuProvider initialization with existing OpenAI client.""" + client = AsyncOpenAI(api_key='test-key', base_url='https://open.bigmodel.cn/api/paas/v4/') + provider = ZhipuProvider(openai_client=client) + assert provider.client is client + assert provider.name == 'zhipu' + + +def test_init_with_openai_client_and_api_key_raises(): + """Test that providing both openai_client and api_key raises an error.""" + client = AsyncOpenAI(api_key='test-key', base_url='https://open.bigmodel.cn/api/paas/v4/') + with pytest.raises(AssertionError, match='Cannot provide both'): + ZhipuProvider(openai_client=client, api_key='another-key') + + +def test_model_profile_glm_4_5(): + """Test model profile for GLM-4.5.""" + provider = ZhipuProvider(api_key='test-key') + profile = provider.model_profile('glm-4.5') + assert profile is not None + assert profile.supports_json_schema_output is True + assert profile.supports_json_object_output is True + assert profile.supports_image_output is False + assert profile.openai_supports_strict_tool_definition is False + + +def test_model_profile_glm_4v(): + """Test model profile for GLM-4V (vision model).""" + provider = ZhipuProvider(api_key='test-key') + profile = provider.model_profile('glm-4v-plus') + assert profile is not None + assert profile.supports_image_output is True + + +def test_model_profile_codegeex(): + """Test model profile for CodeGeeX.""" + provider = ZhipuProvider(api_key='test-key') + profile = provider.model_profile('codegeex-4') + assert profile is not None + assert profile.supports_json_schema_output is True From 2b96d9ac1604edbb098d0eefc784a6ba5c38c161 Mon Sep 17 00:00:00 2001 From: AstroAir Date: Sat, 4 Oct 2025 17:36:05 +0800 Subject: [PATCH 2/3] Add Zhipu AI model support and update related tests --- examples/zhipu_example.py | 7 +++++-- pydantic_ai_slim/pydantic_ai/models/openai.py | 5 ++++- pydantic_ai_slim/pydantic_ai/providers/zhipu.py | 13 ++++++++++++- tests/models/test_model_names.py | 3 +++ tests/providers/test_zhipu.py | 4 +++- tests/test_cli.py | 1 + 6 files changed, 28 insertions(+), 5 deletions(-) diff --git a/examples/zhipu_example.py b/examples/zhipu_example.py index a716357f55..00a9e5bcd2 100644 --- a/examples/zhipu_example.py +++ b/examples/zhipu_example.py @@ -18,12 +18,15 @@ async def main(): """Run a simple example with Zhipu AI.""" # Create an agent using Zhipu AI's GLM-4.5 model - agent = Agent('zhipu:glm-4.5', system_prompt='You are a helpful assistant.') + model_spec = 'zhipu:glm-4.5' + agent = Agent(model_spec, system_prompt='You are a helpful assistant.') # Run a simple query result = await agent.run('What is the capital of China?') print(f'Response: {result.output}') - print(f'Model used: {result.all_messages()[-1].model_name}') + # Access the configured model name directly from the agent's model to avoid relying on + # message internals (keeps static typing happy if message schema varies by provider). + print(f'Model used: {model_spec}') if __name__ == '__main__': diff --git a/pydantic_ai_slim/pydantic_ai/models/openai.py b/pydantic_ai_slim/pydantic_ai/models/openai.py index 24cb2511ef..7a017e4194 100644 --- a/pydantic_ai_slim/pydantic_ai/models/openai.py +++ b/pydantic_ai_slim/pydantic_ai/models/openai.py @@ -284,6 +284,7 @@ def __init__( 'together', 'vercel', 'litellm', + 'zhipu', ] | Provider[AsyncOpenAI] = 'openai', profile: ModelProfileSpec | None = None, @@ -312,6 +313,7 @@ def __init__( 'together', 'vercel', 'litellm', + 'zhipu', ] | Provider[AsyncOpenAI] = 'openai', profile: ModelProfileSpec | None = None, @@ -339,6 +341,7 @@ def __init__( 'together', 'vercel', 'litellm', + 'zhipu', ] | Provider[AsyncOpenAI] = 'openai', profile: ModelProfileSpec | None = None, @@ -528,7 +531,7 @@ def _process_response(self, response: chat.ChatCompletion | str) -> ModelRespons # This mirrors the workaround for missing `created` timestamps below. if not getattr(response, 'object', None): # pragma: no branch try: # defensive, in case attribute is read-only in future SDK versions - response.object = 'chat.completion' # pyright: ignore[reportAttributeAccessIssue] + response.object = 'chat.completion' except Exception: # pragma: no cover pass diff --git a/pydantic_ai_slim/pydantic_ai/providers/zhipu.py b/pydantic_ai_slim/pydantic_ai/providers/zhipu.py index 3aa14cbed6..80929720af 100644 --- a/pydantic_ai_slim/pydantic_ai/providers/zhipu.py +++ b/pydantic_ai_slim/pydantic_ai/providers/zhipu.py @@ -1,7 +1,7 @@ from __future__ import annotations as _annotations import os -from typing import overload +from typing import Literal, overload import httpx @@ -11,6 +11,17 @@ from pydantic_ai.profiles.zhipu import zhipu_model_profile from pydantic_ai.providers import Provider +ZhipuModelName = Literal[ + 'glm-4.5', + 'glm-4.5-air', + 'glm-4.5-flash', + 'glm-4.5v', + 'glm-4.6', + 'glm-4v', + 'glm-4v-plus', + 'codegeex-4', +] + try: from openai import AsyncOpenAI except ImportError as _import_error: # pragma: no cover diff --git a/tests/models/test_model_names.py b/tests/models/test_model_names.py index b27aa2d8c2..18b64942c0 100644 --- a/tests/models/test_model_names.py +++ b/tests/models/test_model_names.py @@ -22,6 +22,7 @@ from pydantic_ai.models.openai import OpenAIModelName from pydantic_ai.providers.grok import GrokModelName from pydantic_ai.providers.moonshotai import MoonshotAIModelName + from pydantic_ai.providers.zhipu import ZhipuModelName pytestmark = [ pytest.mark.skipif(not imports_successful(), reason='some model package was not installed'), @@ -66,6 +67,7 @@ def get_model_names(model_name_type: Any) -> Iterator[str]: 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)] + zhipu_names = [f'zhipu:{n}' for n in get_model_names(ZhipuModelName)] mistral_names = [f'mistral:{n}' for n in get_model_names(MistralModelName)] openai_names = [f'openai:{n}' for n in get_model_names(OpenAIModelName)] bedrock_names = [f'bedrock:{n}' for n in get_model_names(BedrockModelName)] @@ -83,6 +85,7 @@ def get_model_names(model_name_type: Any) -> Iterator[str]: + groq_names + mistral_names + moonshotai_names + + zhipu_names + openai_names + bedrock_names + deepseek_names diff --git a/tests/providers/test_zhipu.py b/tests/providers/test_zhipu.py index 6577a4e410..ddc199314e 100644 --- a/tests/providers/test_zhipu.py +++ b/tests/providers/test_zhipu.py @@ -3,6 +3,7 @@ import pytest from pydantic_ai.exceptions import UserError +from pydantic_ai.profiles.openai import OpenAIModelProfile from ..conftest import try_import @@ -56,7 +57,7 @@ def test_init_with_openai_client_and_api_key_raises(): """Test that providing both openai_client and api_key raises an error.""" client = AsyncOpenAI(api_key='test-key', base_url='https://open.bigmodel.cn/api/paas/v4/') with pytest.raises(AssertionError, match='Cannot provide both'): - ZhipuProvider(openai_client=client, api_key='another-key') + ZhipuProvider(openai_client=client, api_key='another-key') # type: ignore[arg-type] def test_model_profile_glm_4_5(): @@ -64,6 +65,7 @@ def test_model_profile_glm_4_5(): provider = ZhipuProvider(api_key='test-key') profile = provider.model_profile('glm-4.5') assert profile is not None + assert isinstance(profile, OpenAIModelProfile) assert profile.supports_json_schema_output is True assert profile.supports_json_object_output is True assert profile.supports_image_output is False diff --git a/tests/test_cli.py b/tests/test_cli.py index e95ff09141..04b57b2372 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -148,6 +148,7 @@ def test_list_models(capfd: CaptureFixture[str]): 'moonshotai', 'grok', 'huggingface', + 'zhipu', ) models = {line.strip().split(' ')[0] for line in output[3:]} for provider in providers: From 62d6fa89dd1c6e7f10f0b155585cf3f7a3cbbe16 Mon Sep 17 00:00:00 2001 From: AstroAir Date: Sat, 4 Oct 2025 17:42:59 +0800 Subject: [PATCH 3/3] Fix indentation for zhipu_names in test_known_model_names function --- pydantic_ai_slim/pydantic_ai/models/openai.py | 9 +++++---- tests/models/test_model_names.py | 2 +- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/pydantic_ai_slim/pydantic_ai/models/openai.py b/pydantic_ai_slim/pydantic_ai/models/openai.py index 7a017e4194..e0d4662fb9 100644 --- a/pydantic_ai_slim/pydantic_ai/models/openai.py +++ b/pydantic_ai_slim/pydantic_ai/models/openai.py @@ -526,10 +526,11 @@ def _process_response(self, response: chat.ChatCompletion | str) -> ModelRespons if not isinstance(response, chat.ChatCompletion): raise UnexpectedModelBehavior('Invalid response from OpenAI chat completions endpoint, expected JSON data') - # Some OpenAI-compatible providers (e.g. Zhipu/Z.ai, see issue #2723) omit the `object` field even though - # the OpenAI schema includes it. We populate it here so validation succeeds. - # This mirrors the workaround for missing `created` timestamps below. - if not getattr(response, 'object', None): # pragma: no branch + # Some OpenAI-compatible providers (currently only Zhipu/Z.ai, see issue #2723) omit the `object` field even + # though the OpenAI schema includes it. We only patch it for that provider to avoid changing validation + # error counts in tests that purposefully feed invalid OpenAI responses (which expect 4 errors, including + # a missing `object`). + if self._provider.name == 'zhipu' and not getattr(response, 'object', None): # pragma: no branch try: # defensive, in case attribute is read-only in future SDK versions response.object = 'chat.completion' except Exception: # pragma: no cover diff --git a/tests/models/test_model_names.py b/tests/models/test_model_names.py index 18b64942c0..66f71f1553 100644 --- a/tests/models/test_model_names.py +++ b/tests/models/test_model_names.py @@ -85,7 +85,7 @@ def get_model_names(model_name_type: Any) -> Iterator[str]: + groq_names + mistral_names + moonshotai_names - + zhipu_names + + zhipu_names + openai_names + bedrock_names + deepseek_names