diff --git a/examples/.test_scripts/run-with-patched-model.py b/examples/.test_scripts/run-with-patched-model.py index 253977ab..c113efda 100644 --- a/examples/.test_scripts/run-with-patched-model.py +++ b/examples/.test_scripts/run-with-patched-model.py @@ -182,7 +182,7 @@ def __init__( protocol: ai.ProviderProtocol[Any] | None = None, ) -> None: super().__init__( - id, + id=id, provider=provider, protocol=selected_protocol_for_provider(provider) or protocol, ) diff --git a/examples/builtin_web_search.py b/examples/builtin_web_search.py index bba7ad86..3615d488 100644 --- a/examples/builtin_web_search.py +++ b/examples/builtin_web_search.py @@ -45,13 +45,12 @@ def format(value: object) -> str: async def main() -> None: - provider = ai.get_provider("anthropic") + model = ai.get_model("anthropic:claude-sonnet-4-6") + provider = model.provider if not provider.is_configured(): print(f"[SKIP] {provider.name} provider is not configured") return - model = ai.Model("claude-sonnet-4-6", provider=provider) - async with ai.stream(model, messages, tools=tools) as s: async for event in s: match event: diff --git a/examples/check_connection.py b/examples/check_connection.py index 85b82fff..b5c01875 100644 --- a/examples/check_connection.py +++ b/examples/check_connection.py @@ -24,7 +24,7 @@ async def _check(name: str, provider: ai.Provider, model_id: str) -> None: if not provider.is_configured(): print(f" [SKIP] {provider.name} provider is not configured") return - model = ai.Model(model_id, provider=provider) + model = ai.Model(id=model_id, provider=provider) try: await ai.probe(model) print(f" [OK] {name}/{model_id}") diff --git a/examples/explicit_client.py b/examples/explicit_client.py index dbbadd18..1c970756 100644 --- a/examples/explicit_client.py +++ b/examples/explicit_client.py @@ -20,7 +20,7 @@ async def main() -> None: ) model = ai.Model( - os.environ.get("LOCAL_OPENAI_MODEL", "local-model"), + id=os.environ.get("LOCAL_OPENAI_MODEL", "local-model"), provider=provider, ) diff --git a/examples/openai_chat_completions.py b/examples/openai_chat_completions.py index 5a909725..84b3e562 100644 --- a/examples/openai_chat_completions.py +++ b/examples/openai_chat_completions.py @@ -14,17 +14,15 @@ async def main() -> None: - provider = ai.get_provider("openai") + model = ai.get_model( + "openai:gpt-5.5", + protocol=OpenAIChatCompletionsProtocol(), + ) + provider = model.provider if not provider.is_configured(): print(f"[SKIP] {provider.name} provider is not configured") return - model = ai.Model( - "gpt-5.5", - provider=provider, - protocol=OpenAIChatCompletionsProtocol(), - ) - try: async with ai.stream(model, messages) as stream: async for event in stream: diff --git a/examples/stream_all.py b/examples/stream_all.py index a4e01f7b..6ed0b67e 100644 --- a/examples/stream_all.py +++ b/examples/stream_all.py @@ -4,10 +4,10 @@ import ai -MODELS: list[tuple[str, ai.Provider, str]] = [ - ("ai_gateway", ai.get_provider("vercel"), "anthropic/claude-sonnet-4.6"), - ("anthropic", ai.get_provider("anthropic"), "claude-sonnet-4-6"), - ("openai", ai.get_provider("openai"), "gpt-5.5"), +MODELS: list[tuple[str, ai.Model]] = [ + ("ai_gateway", ai.get_model("gateway:anthropic/claude-sonnet-4.6")), + ("anthropic", ai.get_model("anthropic:claude-sonnet-4-6")), + ("openai", ai.get_model("openai:gpt-5.5")), ] messages = [ @@ -16,15 +16,14 @@ ] -async def _run(name: str, provider: ai.Provider, model_id: str) -> None: - print(f"\n{name} / {model_id}") +async def _run(name: str, model: ai.Model) -> None: + print(f"\n{name} / {model.id}") + provider = model.provider if not provider.is_configured(): print(f"[SKIP] {provider.name} provider is not configured") return - model = ai.Model(model_id, provider=provider) - try: async with ai.stream(model, messages) as s: async for event in s: @@ -36,8 +35,8 @@ async def _run(name: str, provider: ai.Provider, model_id: str) -> None: async def main() -> None: - for name, provider, model_id in MODELS: - await _run(name, provider, model_id) + for name, model in MODELS: + await _run(name, model) if __name__ == "__main__": diff --git a/examples/temporal-direct/main.py b/examples/temporal-direct/main.py index 7e30a462..aff0427d 100644 --- a/examples/temporal-direct/main.py +++ b/examples/temporal-direct/main.py @@ -49,26 +49,6 @@ MODEL_ID = "gateway:anthropic/claude-sonnet-4.6" -# ── Workflow-safe model placeholder ────────────────────────────── -# -# ``agent.run`` requires a ``Model``, but a real one can't be built -# inside the workflow: ``ai.get_model("gateway:...")`` constructs an -# ``httpx.AsyncClient`` at provider-init time, which imports -# httpcore/anyio and trips the Temporal sandbox (``threading.local`` -# at module load). Our loop never calls the model directly anyway -- -# every LLM call is delegated to ``llm_call_activity``, which runs -# outside the sandbox and resolves the real model by id there. -# -# So hand the workflow a placeholder ``Model`` whose provider builds -# no client. It carries the real model id (so the activity can -# resolve it) but is safe to construct inside the sandbox. -class WorkflowModelProvider(ai.Provider[Any]): - """A clientless provider, safe to construct in a workflow sandbox.""" - - def __init__(self) -> None: - super().__init__(name="workflow-placeholder", base_url="") - - # ── Tool definitions ───────────────────────────────────────────── # # Declared with @ai.tool so the framework can extract JSON schemas @@ -238,7 +218,7 @@ async def _call() -> ai.events.ToolCallResult: class WeatherWorkflow: @temporalio.workflow.run async def run(self, user_query: str) -> str: - model = ai.Model(MODEL_ID, provider=WorkflowModelProvider()) + model = ai.get_model(MODEL_ID) messages: list[ai.messages.Message] = [ ai.system_message( "Answer questions using the weather and population tools." diff --git a/pyproject.toml b/pyproject.toml index 32e4a38c..443f0f20 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,7 +30,7 @@ requires-python = ">=3.12" dependencies = [ "httpx>=0.28.1", "modelsdotdev==0.*", - "pydantic>=2.12.5", + "pydantic>=2.13", "typing-extensions>=4.15.0", ] diff --git a/src/ai/models/__init__.py b/src/ai/models/__init__.py index 29cbb800..bad290db 100644 --- a/src/ai/models/__init__.py +++ b/src/ai/models/__init__.py @@ -5,10 +5,10 @@ import ai model = ai.get_model("openai:gpt-5.4") provider = ai.get_provider("openai", base_url="http://localhost:11434/v1") - model = ai.Model("llama3", provider=provider) + model = ai.Model(id="llama3", provider=provider) model = ai.get_model("anthropic:claude-sonnet-4-6") provider = ai.get_provider("anthropic", base_url="https://anthropic.example.com") - model = ai.Model("claude-sonnet-4-6", provider=provider) + model = ai.Model(id="claude-sonnet-4-6", provider=provider) model = ai.get_model("anthropic/claude-sonnet-4") # defaults to Gateway # stream — auto-creates client from env vars @@ -24,7 +24,7 @@ base_url="https://custom.example.com/v1", api_key="sk-...", ) - model = ai.Model("gpt-5.4", provider=provider) + model = ai.Model(id="gpt-5.4", provider=provider) async with ai.stream(model, msgs) as s: ... diff --git a/src/ai/models/core/model.py b/src/ai/models/core/model.py index 5ff6fd5d..024614f6 100644 --- a/src/ai/models/core/model.py +++ b/src/ai/models/core/model.py @@ -3,6 +3,8 @@ import os from typing import Any, Self +import pydantic + from ... import _modelsdev from ...errors import ConfigurationError from ...providers import base @@ -10,7 +12,7 @@ _DEFAULT_MODEL_ENV = "AI_SDK_DEFAULT_MODEL" -class Model: +class Model(pydantic.BaseModel): """Lightweight reference to a model on a specific provider. * ``id`` — identifier sent to the provider (e.g. ``"claude-sonnet-4-6"``). @@ -18,37 +20,20 @@ class Model: * ``protocol`` — optional wire-protocol override for this model. """ - def __init__( - self, - id: str, - *, - provider: base.Provider, - protocol: base.ProviderProtocol[Any] | None = None, - ) -> None: - self.id = id - self.provider = provider - self.protocol = protocol - - def __eq__(self, other: object) -> bool: - return ( - isinstance(other, Model) - and self.id == other.id - and self.provider is other.provider - and self.protocol is other.protocol - ) + id: str + provider: base.Provider[Any] + protocol: base.ProviderProtocol[Any] | None = pydantic.Field( + default=None, exclude_if=lambda v: v is None + ) def __repr__(self) -> str: return f"Model(id={self.id!r}, provider={self.provider!r})" def __hash__(self) -> int: - return hash((self.id, id(self.provider), id(self.protocol))) + return hash((self.id, self.provider, self.protocol)) def with_protocol(self, protocol: base.ProviderProtocol[Any]) -> Self: - return self.__class__( - id=self.id, - provider=self.provider, - protocol=protocol, - ) + return self.model_copy(update={"protocol": protocol}) def get_model( @@ -107,4 +92,4 @@ def get_model( model_provider_config=model_provider_config, ) - return Model(provider_model_id, provider=provider, protocol=protocol) + return Model(id=provider_model_id, provider=provider, protocol=protocol) diff --git a/src/ai/providers/ai_gateway/protocol.py b/src/ai/providers/ai_gateway/protocol.py index 03acc199..e4d08af5 100644 --- a/src/ai/providers/ai_gateway/protocol.py +++ b/src/ai/providers/ai_gateway/protocol.py @@ -7,7 +7,7 @@ import base64 import json from collections.abc import AsyncGenerator, Iterable, Mapping, Sequence -from typing import Any, TypeVar +from typing import Any, Literal, TypeVar import httpx import pydantic @@ -1179,6 +1179,8 @@ async def generate( class GatewayV3Protocol(base.ProviderProtocol[gateway_client.GatewayClient]): """AI Gateway v3 wire protocol.""" + protocol_class_id: Literal["gateway_v3"] = "gateway_v3" + def stream( self, client: gateway_client.GatewayClient, diff --git a/src/ai/providers/ai_gateway/provider.py b/src/ai/providers/ai_gateway/provider.py index cf92b4f6..f2875592 100644 --- a/src/ai/providers/ai_gateway/provider.py +++ b/src/ai/providers/ai_gateway/provider.py @@ -5,7 +5,9 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Any, ClassVar +from typing import TYPE_CHECKING, Any, ClassVar, Literal + +import pydantic from ... import errors as ai_errors from .. import base @@ -20,7 +22,6 @@ import httpx import modelsdotdev - import pydantic from ...models.core import model as model_ from ...models.core import params as params_ @@ -37,45 +38,39 @@ class GatewayProvider(base.Provider[gateway_client.GatewayClient]): handles: ClassVar[tuple[str, ...]] = ("vercel", "@ai-sdk/gateway") - def __init__( - self, - *, - api_key: str | None = None, - base_url: str = _BASE_URL, - headers: Mapping[str, str] | None = None, - env: Mapping[str, str] | None = None, - client: httpx.AsyncClient | None = None, - protocol: base.ProviderProtocol[Any] | None = None, - ) -> None: - super().__init__( - name="ai-gateway", - base_url=base_url, - protocol=protocol or protocol_module.GatewayV3Protocol(), - api_key=api_key, - api_key_env=_API_KEY_ENV, - headers=headers, - env=env, - ) - self._set_client( - gateway_client.GatewayClient( - base_url=self.base_url, - api_key=self.api_key, - headers=self.headers, - client=client, - ) - ) + provider_class_id: Literal["gateway"] = "gateway" + name: Literal["ai-gateway"] = "ai-gateway" + default_base_url: str = _BASE_URL + api_key_env: str | None = _API_KEY_ENV + + _http_client: httpx.AsyncClient | None = pydantic.PrivateAttr(default=None) + + def _set_http_client(self, client: httpx.AsyncClient | None) -> None: + self._http_client = client @property def client(self) -> gateway_client.GatewayClient: - client = super().client - client.base_url = self.base_url - client.api_key = self.api_key - client.headers = self.headers - return client + if self._client is None: + self._set_client( + gateway_client.GatewayClient( + base_url=self.base_url, + api_key=self.api_key, + headers=self.headers, + client=self._http_client, + ) + ) + return super().client # same return value, no None in the type + + def default_protocol( + self, + ) -> base.ProviderProtocol[gateway_client.GatewayClient]: + """Return the default Gateway protocol.""" + return protocol_module.GatewayV3Protocol() async def aclose(self) -> None: """Close the provider-owned Gateway client, if any.""" - await self.client.aclose() + if self._client is not None: + await self.client.aclose() def stream( self, @@ -117,14 +112,15 @@ def from_modelsdev_provider( client: httpx.AsyncClient | None = None, protocol: base.ProviderProtocol[Any] | None = None, ) -> base.Provider[gateway_client.GatewayClient]: - return cls( - api_key=api_key, - base_url=base_url or _BASE_URL, - headers=headers, - env=env, - client=client, - protocol=protocol, + provider_instance = cls( + default_base_url=base_url or _BASE_URL, + protocol_override=protocol, + api_key_value=api_key, + headers=dict(headers or {}), + env=dict(env or {}), ) + provider_instance._set_http_client(client) + return provider_instance @property def tools(self) -> ModuleType: diff --git a/src/ai/providers/anthropic/__init__.py b/src/ai/providers/anthropic/__init__.py index a183b555..94575cd8 100644 --- a/src/ai/providers/anthropic/__init__.py +++ b/src/ai/providers/anthropic/__init__.py @@ -7,7 +7,7 @@ model = ai.get_model("anthropic:claude-sonnet-4-6") provider = ai.get_provider("anthropic", base_url="https://anthropic.example.com") - model = ai.Model("claude-sonnet-4-6", provider=provider) + model = ai.Model(id="claude-sonnet-4-6", provider=provider) ids = await ai.get_provider("anthropic").list_models() # built-in tools diff --git a/src/ai/providers/anthropic/protocol.py b/src/ai/providers/anthropic/protocol.py index 0a927d9d..5ed14d24 100644 --- a/src/ai/providers/anthropic/protocol.py +++ b/src/ai/providers/anthropic/protocol.py @@ -8,7 +8,7 @@ import base64 import json -from typing import TYPE_CHECKING, Any, cast +from typing import TYPE_CHECKING, Any, Literal, cast import pydantic @@ -889,6 +889,8 @@ async def stream( class AnthropicMessagesProtocol(base.ProviderProtocol[Any]): """Anthropic Messages API protocol.""" + protocol_class_id: Literal["anthropic_messages"] = "anthropic_messages" + def stream( self, client: anthropic.AsyncAnthropic, diff --git a/src/ai/providers/anthropic/provider.py b/src/ai/providers/anthropic/provider.py index ef5ac5fb..e2a2008e 100644 --- a/src/ai/providers/anthropic/provider.py +++ b/src/ai/providers/anthropic/provider.py @@ -2,9 +2,10 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Any, ClassVar +from typing import TYPE_CHECKING, Any, ClassVar, Literal import httpx +import pydantic from ... import errors as ai_errors from .. import base @@ -13,12 +14,11 @@ from . import tools as tools_module if TYPE_CHECKING: - from collections.abc import AsyncGenerator, Iterable, Mapping, Sequence + from collections.abc import AsyncGenerator, Mapping, Sequence from types import ModuleType import anthropic import modelsdotdev - import pydantic from ...models.core import model as model_ from ...models.core import params as params_ @@ -43,24 +43,20 @@ class AnthropicCompatibleProvider(base.Provider[AnthropicSDKClient]): handles: ClassVar[tuple[str, ...]] = ("anthropic", "@ai-sdk/anthropic") - def __init__( - self, - *, - name: str, - default_base_url: str, - api_key: str | None = None, - api_key_env: str | None = None, - base_url_env: str | None = None, - config_envs: Iterable[str] | None = None, - anthropic_version: str = _ANTHROPIC_VERSION, - headers: Mapping[str, str] | None = None, - env: Mapping[str, str] | None = None, - client: AnthropicClient | None = None, - protocol: base.ProviderProtocol[Any] | None = None, - ) -> None: + provider_class_id: Literal["anthropic-compatible"] = "anthropic-compatible" + anthropic_version: str = _ANTHROPIC_VERSION + + _http_client: httpx.AsyncClient | None = pydantic.PrivateAttr(default=None) + _close_client_on_aclose: bool = pydantic.PrivateAttr(default=False) + _has_user_sdk_client: bool = pydantic.PrivateAttr(default=False) + + def model_post_init(self, __context: Any) -> None: + self._close_client_on_aclose = True + + def _set_runtime_client(self, client: AnthropicClient | None) -> None: anthropic_sdk = None if client is not None and not isinstance(client, httpx.AsyncClient): - anthropic_sdk = _sdk.import_sdk(provider=name) + anthropic_sdk = _sdk.import_sdk(provider=self.name) if anthropic_sdk is not None and isinstance( client, anthropic_sdk.AsyncAnthropic @@ -68,34 +64,21 @@ def __init__( sdk_client = client http_client = None self._has_user_sdk_client = True + self._close_client_on_aclose = False elif isinstance(client, httpx.AsyncClient) or client is None: sdk_client = None http_client = client self._has_user_sdk_client = False + self._close_client_on_aclose = client is None else: raise TypeError( "Anthropic providers require an httpx.AsyncClient or " "anthropic.AsyncAnthropic" ) - super().__init__( - name=name, - base_url=default_base_url, - protocol=protocol or protocol_module.AnthropicMessagesProtocol(), - api_key=api_key, - api_key_env=api_key_env, - base_url_env=base_url_env, - config_envs=config_envs, - headers=headers, - env=env, - ) - self.anthropic_version = anthropic_version - self._close_client_on_aclose = ( - sdk_client is None and http_client is None - ) - if sdk_client is None: - sdk_client = self._make_sdk_client(http_client=http_client) - self._set_client(sdk_client) + self._http_client = http_client + if sdk_client is not None: + self._set_client(sdk_client) def _make_sdk_client( self, @@ -118,6 +101,19 @@ def sdk_client(self) -> AnthropicSDKClient: """Provider SDK client used for Anthropic-compatible API requests.""" return self.client + @property + def client(self) -> AnthropicSDKClient: + """Lazily-created SDK client for Anthropic-compatible requests.""" + if self._client is None: + self._set_client( + self._make_sdk_client(http_client=self._http_client) + ) + return super().client + + def default_protocol(self) -> base.ProviderProtocol[AnthropicSDKClient]: + """Return the default Anthropic-compatible protocol.""" + return protocol_module.AnthropicMessagesProtocol() + def is_configured(self) -> bool: if self._has_user_sdk_client: return True @@ -127,7 +123,7 @@ def is_configured(self) -> bool: async def aclose(self) -> None: """Close the provider-owned SDK client, if any.""" - if self._close_client_on_aclose: + if self._close_client_on_aclose and self._client is not None: await self.client.close() def stream( @@ -174,20 +170,21 @@ def from_modelsdev_provider( api_key_env, config_envs = base.provider_config( provider, model_provider_config ) - return cls( + provider_instance = cls( name=provider.id, default_base_url=resolved_base_url, - api_key=api_key, + api_key_value=api_key, api_key_env=api_key_env, base_url_env=_BASE_URL_ENV if provider.id == "anthropic" and base_url is None else None, config_envs=config_envs, - headers=headers, - env=env, - client=client, - protocol=protocol, + headers=dict(headers or {}), + env=dict(env or {}), + protocol_override=protocol, ) + provider_instance._set_runtime_client(client) + return provider_instance @property def tools(self) -> ModuleType: diff --git a/src/ai/providers/base.py b/src/ai/providers/base.py index 28d1af10..ed85b352 100644 --- a/src/ai/providers/base.py +++ b/src/ai/providers/base.py @@ -2,9 +2,13 @@ from __future__ import annotations +import json import os -from typing import TYPE_CHECKING, Any, ClassVar, Generic +from collections.abc import Mapping +from types import MappingProxyType +from typing import TYPE_CHECKING, Any, ClassVar, Generic, Self, cast +import pydantic from typing_extensions import ( TypeVar, ) @@ -13,10 +17,9 @@ from ..errors import UnsupportedProviderError if TYPE_CHECKING: - from collections.abc import AsyncGenerator, Iterable, Mapping, Sequence + from collections.abc import AsyncGenerator, Sequence import modelsdotdev - import pydantic from ..models.core import model as model_ from ..models.core import params as params_ @@ -27,9 +30,100 @@ ClientT = TypeVar("ClientT", default=Any) -class ProviderProtocol(Generic[ClientT]): +def _generic_origin(cls: type[Any]) -> type[Any]: + # if this is a generic class, return the original class + return ( + getattr(cls, "__pydantic_generic_metadata__", {}).get("origin") or cls + ) + + +def _class_id_default( + cls: type[pydantic.BaseModel], + field_name: str, +) -> str | None: + if field_name not in getattr(cls, "__annotations__", {}): + return None + field = cls.model_fields.get(field_name) + if field is None or not isinstance(field.default, str): + return None + return field.default + + +def _register_class_id[T]( + registry: dict[str, type[T]], + class_id: str, + cls: type[T], + label: str, +) -> None: + existing = registry.get(class_id) + if existing is not None and existing is not cls: + raise RuntimeError(f"duplicate {label} class id: {class_id!r}") + registry[class_id] = cls + + +class ProviderProtocol(pydantic.BaseModel, Generic[ClientT]): """Interface implemented by provider wire protocols.""" + protocol_class_id: str # used to restore the concrete protocol class + + model_config = pydantic.ConfigDict( + frozen=True, + polymorphic_serialization=True, + ) + + def __init__(self, **data: Any) -> None: + if _generic_origin(type(self)) is ProviderProtocol: + raise TypeError("ProviderProtocol must be subclassed") + super().__init__(**data) + + def __hash__(self) -> int: + data = self.model_dump(mode="json") + serialized = json.dumps(data, sort_keys=True, separators=(",", ":")) + return hash(serialized) + + @classmethod + def __pydantic_init_subclass__(cls, **kwargs: Any) -> None: # noqa: PLW3201 + # register in a global registry by protocol_class_id + # this will allow deserialization into a concrete subclass + # without discriminated union + super().__pydantic_init_subclass__(**kwargs) + if _generic_origin(cls) is not cls: + return + protocol_class_id = _class_id_default(cls, "protocol_class_id") + if protocol_class_id is not None: + _register_class_id( + _PROTOCOL_REGISTRY, + protocol_class_id, + cls, + "provider protocol", + ) + + @pydantic.model_validator(mode="wrap") + @classmethod + def _load_registered_protocol( + cls, + value: Any, + handler: pydantic.ModelWrapValidatorHandler[Self], + ) -> Any: + if isinstance(value, ProviderProtocol): + return value + if _generic_origin(cls) is not ProviderProtocol: + return handler(value) + if not isinstance(value, Mapping): + return handler(value) + + protocol_class_id = value.get("protocol_class_id") + if not isinstance(protocol_class_id, str): + raise ValueError( + "provider protocol data must include protocol_class_id" + ) + protocol_type = _PROTOCOL_REGISTRY.get(protocol_class_id) + if protocol_type is None: + raise ValueError( + f"unknown provider protocol_class_id: {protocol_class_id!r}" + ) + return protocol_type.model_validate(value) + def stream( self, client: ClientT, @@ -61,7 +155,7 @@ async def generate( ) -class Provider(Generic[ClientT]): +class Provider(pydantic.BaseModel, Generic[ClientT]): """Base class for model providers. A provider carries provider-specific configuration and a shared upstream @@ -71,73 +165,132 @@ class Provider(Generic[ClientT]): handles: ClassVar[tuple[str, ...]] = () - def __init_subclass__(cls, **kwargs: Any) -> None: - super().__init_subclass__(**kwargs) + provider_class_id: str # used to restore the concrete provider class. + name: str # models.dev identity + default_base_url: str + protocol_override: ProviderProtocol[Any] | None = pydantic.Field( + default=None, exclude_if=lambda v: v is None + ) + api_key_value: str | None = pydantic.Field( + default=None, exclude_if=lambda v: v is None + ) + api_key_env: str | None = None + base_url_env: str | None = None + config_envs: tuple[str, ...] = () + headers: Mapping[str, str] = pydantic.Field(default_factory=dict) + env: Mapping[str, str] = pydantic.Field(default_factory=dict) + + _client: ClientT | None = pydantic.PrivateAttr(default=None) + + model_config = pydantic.ConfigDict( + extra="allow", + frozen=True, + populate_by_name=True, + polymorphic_serialization=True, + ) + + def __init__(self, **data: Any) -> None: + if _generic_origin(type(self)) is Provider: + raise TypeError("Provider must be subclassed") + super().__init__(**data) + + def __hash__(self) -> int: + data = self.model_dump(mode="json") + serialized = json.dumps(data, sort_keys=True, separators=(",", ":")) + return hash(serialized) + + @classmethod + def __pydantic_init_subclass__(cls, **kwargs: Any) -> None: # noqa: PLW3201 + # register in a global registry by provider_class_id + # this will allow deserialization into a concrete subclass + # without discriminated union + super().__pydantic_init_subclass__(**kwargs) + if _generic_origin(cls) is not cls: + return + + provider_class_id = _class_id_default(cls, "provider_class_id") + if provider_class_id is not None: + _register_class_id( + _PROVIDER_CLASS_REGISTRY, + provider_class_id, + cls, + "provider", + ) + for handle in cls.handles: existing = _PROVIDER_REGISTRY.get(handle) if existing is not None and existing is not cls: raise RuntimeError(f"duplicate provider handle: {handle!r}") _PROVIDER_REGISTRY[handle] = cls - def __init__( - self, - *, - name: str, - base_url: str, - protocol: ProviderProtocol[ClientT] | None = None, - api_key: str | None = None, - api_key_env: str | None = None, - base_url_env: str | None = None, - config_envs: Iterable[str] | None = None, - headers: Mapping[str, str] | None = None, - env: Mapping[str, str] | None = None, - client: ClientT | None = None, - ) -> None: - if type(self) is Provider: - raise TypeError( - "Provider is a base class; implement a subclass instead" - ) - self._name = name - self._base_url = base_url - self._protocol = protocol - self._api_key = api_key - self._api_key_env = api_key_env - self._base_url_env = base_url_env - self._config_envs = tuple(config_envs or ()) - self._headers = dict(headers or {}) - self._env = dict(env or {}) - self._client = client - - @property - def api_key_env(self) -> str | None: - """Env var name that holds the API key (e.g. ``"OPENAI_API_KEY"``).""" - return self._api_key_env + @pydantic.model_validator(mode="before") + @classmethod + def _normalize_config_data(cls, data: Any) -> Any: + if not isinstance(data, Mapping): + return data + data = dict(data) + if data.get("headers") is None: + data["headers"] = {} + if data.get("env") is None: + data["env"] = {} + if data.get("config_envs") is None: + data["config_envs"] = () + return data + + @pydantic.field_validator("headers", "env", mode="after") + @classmethod + def _freeze_config_mapping( + cls, + value: Mapping[str, str], + ) -> Mapping[str, str]: + return MappingProxyType(dict(value)) - @property - def base_url_env(self) -> str | None: - """Env var name that can override the default base URL.""" - return self._base_url_env + @pydantic.field_serializer("headers", "env") + def _serialize_config_mapping( + self, + value: Mapping[str, str], + ) -> dict[str, str]: + return dict(value) - @property - def default_base_url(self) -> str: - """Base URL configured on the provider before env overrides.""" - return self._base_url + @pydantic.model_validator(mode="wrap") + @classmethod + def _load_registered_provider( + cls, + value: Any, + handler: pydantic.ModelWrapValidatorHandler[Self], + ) -> Any: + if isinstance(value, Provider): + return value + if _generic_origin(cls) is not Provider: + return handler(value) + if not isinstance(value, Mapping): + return handler(value) + + provider_class_id = value.get("provider_class_id") + if not isinstance(provider_class_id, str): + raise ValueError("provider data must include provider_class_id") + provider_type = _PROVIDER_CLASS_REGISTRY.get(provider_class_id) + if provider_type is None: + raise ValueError( + f"unknown provider_class_id: {provider_class_id!r}" + ) + return provider_type.model_validate(value) @property def base_url(self) -> str: """Default base URL for the provider API.""" - if self._base_url_env: + if self.base_url_env: base_url = ( - self._env.get(self._base_url_env) + self.env.get(self.base_url_env) or os.environ.get( - self._base_url_env, + self.base_url_env, ) - or self._base_url + or self.default_base_url ) else: - base_url = self._base_url - for env in self._config_envs: - value = self._env.get(env) or os.environ.get(env) + base_url = self.default_base_url + for env in self.config_envs: + value = self.env.get(env) or os.environ.get(env) if value is not None: base_url = base_url.replace(f"${{{env}}}", value) base_url = base_url.replace(f"${env}", value) @@ -146,11 +299,11 @@ def base_url(self) -> str: @property def api_key(self) -> str | None: """API key configured directly or via the provider's env var.""" - if self._api_key is not None: - return self._api_key + if self.api_key_value is not None: + return self.api_key_value if self.api_key_env is None: return None - return self._env.get(self.api_key_env) or os.environ.get( + return self.env.get(self.api_key_env) or os.environ.get( self.api_key_env ) @@ -160,13 +313,8 @@ def is_configured(self) -> bool: return False return all(self._config_value(env) for env in self.config_envs) - @property - def headers(self) -> dict[str, str]: - """Custom headers sent with provider API requests.""" - return dict(self._headers) - def _config_value(self, env: str) -> str | None: - return self._env.get(env) or os.environ.get(env) + return self.env.get(env) or os.environ.get(env) @property def client(self) -> ClientT: @@ -182,24 +330,16 @@ async def aclose(self) -> None: """Close provider-owned resources, if any.""" return None - @property - def config_envs(self) -> tuple[str, ...]: - """Additional env vars used to configure the provider client.""" - return self._config_envs - - @property - def name(self) -> str: - """Human-readable provider name (for repr, error messages).""" - return self._name - @property def protocol(self) -> ProviderProtocol[ClientT]: """Default wire protocol used by this provider.""" - if self._protocol is None: - raise RuntimeError( - f"provider {self.name!r} does not have a protocol" - ) - return self._protocol + if self.protocol_override is not None: + return cast("ProviderProtocol[ClientT]", self.protocol_override) + return self.default_protocol() + + def default_protocol(self) -> ProviderProtocol[ClientT]: + """Return this provider's default wire protocol.""" + raise RuntimeError(f"provider {self.name!r} does not have a protocol") async def list_models(self) -> list[str]: """List available model IDs from the provider API.""" @@ -316,6 +456,8 @@ def from_modelsdev_provider( _PROVIDER_REGISTRY: dict[str, type[Provider[Any]]] = {} +_PROVIDER_CLASS_REGISTRY: dict[str, type[Provider[Any]]] = {} +_PROTOCOL_REGISTRY: dict[str, type[ProviderProtocol[Any]]] = {} def get_provider( diff --git a/src/ai/providers/openai/__init__.py b/src/ai/providers/openai/__init__.py index b590d20d..54fb998a 100644 --- a/src/ai/providers/openai/__init__.py +++ b/src/ai/providers/openai/__init__.py @@ -6,7 +6,7 @@ model = ai.get_model("openai:gpt-5.4") provider = ai.get_provider("openai", base_url="http://localhost:11434/v1") - model = ai.Model("llama3", provider=provider) + model = ai.Model(id="llama3", provider=provider) ids = await ai.get_provider("openai").list_models() The optional upstream OpenAI SDK is loaded lazily when the provider creates or diff --git a/src/ai/providers/openai/protocol.py b/src/ai/providers/openai/protocol.py index a5320f3e..8e9c1ed1 100644 --- a/src/ai/providers/openai/protocol.py +++ b/src/ai/providers/openai/protocol.py @@ -9,7 +9,7 @@ import base64 import json from collections.abc import AsyncGenerator, Mapping, Sequence -from typing import TYPE_CHECKING, Any, cast +from typing import TYPE_CHECKING, Any, Literal, cast from ... import errors as ai_errors from ... import types @@ -709,6 +709,10 @@ async def stream( class OpenAIChatCompletionsProtocol(base.ProviderProtocol[Any]): """OpenAI Chat Completions protocol.""" + protocol_class_id: Literal["openai_chat_completions"] = ( + "openai_chat_completions" + ) + def stream( self, client: openai.AsyncOpenAI, @@ -1805,6 +1809,8 @@ async def _stream_responses( class OpenAIResponsesProtocol(base.ProviderProtocol[Any]): """OpenAI Responses API protocol.""" + protocol_class_id: Literal["openai_responses"] = "openai_responses" + def stream( self, client: openai.AsyncOpenAI, diff --git a/src/ai/providers/openai/provider.py b/src/ai/providers/openai/provider.py index 1541ef4e..9f2c1c6e 100644 --- a/src/ai/providers/openai/provider.py +++ b/src/ai/providers/openai/provider.py @@ -2,9 +2,10 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Any, ClassVar +from typing import TYPE_CHECKING, Any, ClassVar, Literal import httpx +import pydantic from ... import errors as ai_errors from .. import base @@ -13,12 +14,11 @@ from . import tools as tools_module if TYPE_CHECKING: - from collections.abc import AsyncGenerator, Iterable, Mapping, Sequence + from collections.abc import AsyncGenerator, Mapping, Sequence from types import ModuleType import modelsdotdev import openai - import pydantic from ...models.core import model as model_ from ...models.core import params as params_ @@ -46,23 +46,19 @@ class OpenAICompatibleProvider(base.Provider[OpenAISDKClient]): "@ai-sdk/openai-compatible", ) - def __init__( - self, - *, - name: str, - default_base_url: str, - api_key: str | None = None, - api_key_env: str | None = None, - base_url_env: str | None = None, - config_envs: Iterable[str] | None = None, - headers: Mapping[str, str] | None = None, - env: Mapping[str, str] | None = None, - client: OpenAIClient | None = None, - protocol: base.ProviderProtocol[Any] | None = None, - ) -> None: + provider_class_id: Literal["openai-compatible"] = "openai-compatible" + + _http_client: httpx.AsyncClient | None = pydantic.PrivateAttr(default=None) + _close_client_on_aclose: bool = pydantic.PrivateAttr(default=False) + _has_user_sdk_client: bool = pydantic.PrivateAttr(default=False) + + def model_post_init(self, __context: Any) -> None: + self._close_client_on_aclose = True + + def _set_runtime_client(self, client: OpenAIClient | None) -> None: openai_sdk = None if client is not None and not isinstance(client, httpx.AsyncClient): - openai_sdk = _sdk.import_sdk(provider=name) + openai_sdk = _sdk.import_sdk(provider=self.name) if openai_sdk is not None and isinstance( client, openai_sdk.AsyncOpenAI @@ -70,33 +66,21 @@ def __init__( sdk_client = client http_client = None self._has_user_sdk_client = True + self._close_client_on_aclose = False elif isinstance(client, httpx.AsyncClient) or client is None: sdk_client = None http_client = client self._has_user_sdk_client = False + self._close_client_on_aclose = client is None else: raise TypeError( "OpenAI providers require an httpx.AsyncClient or " "openai.AsyncOpenAI" ) - super().__init__( - name=name, - base_url=default_base_url, - protocol=protocol or protocol_module.default_protocol(name), - api_key=api_key, - api_key_env=api_key_env, - base_url_env=base_url_env, - config_envs=config_envs, - headers=headers, - env=env, - ) - self._close_client_on_aclose = ( - sdk_client is None and http_client is None - ) - if sdk_client is None: - sdk_client = self._make_sdk_client(http_client=http_client) - self._set_client(sdk_client) + self._http_client = http_client + if sdk_client is not None: + self._set_client(sdk_client) def _make_sdk_client( self, @@ -117,6 +101,19 @@ def sdk_client(self) -> OpenAISDKClient: """Provider SDK client used for OpenAI-compatible API requests.""" return self.client + @property + def client(self) -> OpenAISDKClient: + """Lazily-created SDK client for OpenAI-compatible requests.""" + if self._client is None: + self._set_client( + self._make_sdk_client(http_client=self._http_client) + ) + return super().client + + def default_protocol(self) -> base.ProviderProtocol[OpenAISDKClient]: + """Return the default OpenAI-compatible protocol.""" + return protocol_module.default_protocol(self.name) + def is_configured(self) -> bool: if self._has_user_sdk_client: return True @@ -126,7 +123,7 @@ def is_configured(self) -> bool: async def aclose(self) -> None: """Close the provider-owned SDK client, if any.""" - if self._close_client_on_aclose: + if self._close_client_on_aclose and self._client is not None: await self.client.close() def stream( @@ -173,20 +170,21 @@ def from_modelsdev_provider( api_key_env, config_envs = base.provider_config( provider, model_provider_config ) - return cls( + provider_instance = cls( name=provider.id, default_base_url=resolved_base_url, - api_key=api_key, + api_key_value=api_key, api_key_env=api_key_env, base_url_env=_BASE_URL_ENV if provider.id == "openai" and base_url is None else None, config_envs=config_envs, - headers=headers, - env=env, - client=client, - protocol=protocol, + headers=dict(headers or {}), + env=dict(env or {}), + protocol_override=protocol, ) + provider_instance._set_runtime_client(client) + return provider_instance @property def tools(self) -> ModuleType: diff --git a/tests/conftest.py b/tests/conftest.py index 3f742f05..2b5f18a5 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,7 +1,7 @@ from __future__ import annotations from collections.abc import AsyncGenerator, AsyncIterable, Sequence -from typing import Any, cast +from typing import Any, Literal, cast import pydantic @@ -20,20 +20,13 @@ class MockProvider(models.Provider): Carries just enough state so that ``Model`` objects can be constructed. """ - def __init__( - self, - *, - name: str = "mock", - base_url: str = "http://mock.test", - api_key_env: str | None = "MOCK_API_KEY", - ) -> None: - super().__init__( - name=name, - base_url=base_url, - api_key_env=api_key_env, - ) - self._stream_impl: Any | None = None - self._generate_impl: Any | None = None + provider_class_id: Literal["test-mock-provider"] = "test-mock-provider" + name: str = "mock" + default_base_url: str = "http://mock.test" + api_key_env: str | None = "MOCK_API_KEY" + + _stream_impl: Any | None = pydantic.PrivateAttr(default=None) + _generate_impl: Any | None = pydantic.PrivateAttr(default=None) async def list_models(self) -> list[str]: return [] diff --git a/tests/models/core/test_api.py b/tests/models/core/test_api.py index 3f7e81ab..0335d6db 100644 --- a/tests/models/core/test_api.py +++ b/tests/models/core/test_api.py @@ -1,7 +1,7 @@ from __future__ import annotations from collections.abc import AsyncGenerator, Sequence -from typing import Any, cast +from typing import Any, Literal, cast import pydantic import pytest @@ -57,6 +57,19 @@ class OpenAIParams: } +def test_context_json_roundtrip_includes_model() -> None: + context = ai.Context( + model=ai.get_model("gateway:anthropic/claude-sonnet-4.6"), + messages=[ai.user_message("Hi")], + tools=[], + ) + + restored = ai.Context.model_validate_json(context.model_dump_json()) + + assert restored.model.id == "anthropic/claude-sonnet-4.6" + assert isinstance(restored.model.provider, ai.providers.GatewayProvider) + + async def test_stream_aggregates_registered_adapter_events() -> None: mock = mock_llm([[text_msg("Hello world")]]) @@ -273,6 +286,10 @@ async def test_stream_requires_model_messages_or_context() -> None: async def test_stream_uses_model_protocol() -> None: class OverrideProtocol(models.ProviderProtocol[Any]): + protocol_class_id: Literal["test-override-stream-protocol"] = ( + "test-override-stream-protocol" + ) + def stream( self, client: Any, @@ -345,6 +362,10 @@ async def test_generate_uses_model_protocol() -> None: ) class OverrideProtocol(models.ProviderProtocol[Any]): + protocol_class_id: Literal["test-override-generate-protocol"] = ( + "test-override-generate-protocol" + ) + async def generate( self, client: Any, @@ -367,17 +388,19 @@ async def generate( class _CheckProvider(MockProvider): - def __init__(self) -> None: - super().__init__() - self.checked_model: models.Model | None = None + _checked_model: models.Model | None = pydantic.PrivateAttr(default=None) + + @property + def checked_model(self) -> models.Model | None: + return self._checked_model async def probe(self, model: models.Model) -> None: - self.checked_model = model + self._checked_model = model async def test_probe_delegates_to_model_provider() -> None: provider = _CheckProvider() - model = models.Model("mock-model", provider=provider) + model = models.Model(id="mock-model", provider=provider) await models.probe(model) diff --git a/tests/models/test_resolution.py b/tests/models/test_resolution.py index 98a286f3..a77a276a 100644 --- a/tests/models/test_resolution.py +++ b/tests/models/test_resolution.py @@ -1,3 +1,6 @@ +from typing import Any, Literal, cast + +import pydantic import pytest import ai @@ -75,8 +78,61 @@ def test_provider_from_id_resolves_openai_compatible_provider() -> None: def test_provider_base_class_cannot_be_constructed_directly() -> None: - with pytest.raises(TypeError, match="base class"): - ai.Provider(name="custom", base_url="https://example.com") + with pytest.raises(TypeError, match="must be subclassed"): + ai.Provider(name="custom", default_base_url="https://example.com") + + +def test_provider_protocol_base_class_cannot_be_constructed_directly() -> None: + with pytest.raises(TypeError, match="must be subclassed"): + ai.ProviderProtocol() + + +def test_provider_config_is_frozen() -> None: + provider = ai.get_provider( + "openai", + headers={"X-Test": "1"}, + env={"OPENAI_API_KEY": "sk-test"}, + ) + + field_name = "name" + with pytest.raises(pydantic.ValidationError, match="frozen_instance"): + setattr(provider, field_name, "other") + + with pytest.raises(TypeError): + cast("dict[str, str]", provider.headers)["X-Test"] = "2" + + with pytest.raises(TypeError): + cast("dict[str, str]", provider.env)["OPENAI_API_KEY"] = "sk-other" + + +def test_provider_protocol_and_model_hashes_are_content_based() -> None: + class HashProtocol(models.ProviderProtocol[Any]): + protocol_class_id: Literal["test-hash-protocol"] = "test-hash-protocol" + settings: dict[str, str] + + provider_a = ai.get_provider( + "openai", + api_key="sk-test", + headers={"A": "1", "B": "2"}, + ) + provider_b = ai.get_provider( + "openai", + api_key="sk-test", + headers={"B": "2", "A": "1"}, + ) + protocol_a = HashProtocol(settings={"A": "1", "B": "2"}) + protocol_b = HashProtocol(settings={"B": "2", "A": "1"}) + + assert provider_a == provider_b + assert hash(provider_a) == hash(provider_b) + assert protocol_a == protocol_b + assert hash(protocol_a) == hash(protocol_b) + assert hash(ai.Model(id="gpt-5", provider=provider_a)) == hash( + ai.Model(id="gpt-5", provider=provider_b) + ) + assert hash( + ai.Model(id="gpt-5", provider=provider_a, protocol=protocol_a) + ) == hash(ai.Model(id="gpt-5", provider=provider_b, protocol=protocol_b)) def test_provider_from_id_uses_template_envs_for_base_url() -> None: @@ -183,3 +239,170 @@ def test_get_provider_accepts_provider_protocol_override() -> None: provider = ai.get_provider("openai", protocol=protocol) assert provider.protocol is protocol + + +def test_model_json_roundtrip_restores_gateway_provider() -> None: + model = models.get_model("gateway:anthropic/claude-sonnet-4.6") + + restored = ai.Model.model_validate_json(model.model_dump_json()) + + assert restored.id == "anthropic/claude-sonnet-4.6" + assert restored.provider.name == "ai-gateway" + assert isinstance(restored.provider, ai.providers.GatewayProvider) + assert isinstance(restored.provider.protocol, GatewayV3Protocol) + + +def test_model_json_roundtrip_preserves_explicit_provider_config() -> None: + provider = ai.get_provider( + "openai", + base_url="https://custom.example.com/v1", + api_key="sk-custom", + headers={"X-Custom-Header": "example"}, + ) + model = ai.Model(id="custom-model", provider=provider) + + restored = ai.Model.model_validate_json(model.model_dump_json()) + + assert isinstance(restored.provider, ai.providers.OpenAICompatibleProvider) + assert restored.provider.name == "openai" + assert restored.provider.default_base_url == "https://custom.example.com/v1" + assert restored.provider.base_url == "https://custom.example.com/v1" + assert restored.provider.api_key == "sk-custom" + assert restored.provider.headers == {"X-Custom-Header": "example"} + + +def test_model_json_roundtrip_preserves_model_protocol_override() -> None: + model = models.get_model( + "openai:gpt-5", + protocol=OpenAIChatCompletionsProtocol(), + ) + + restored = ai.Model.model_validate_json(model.model_dump_json()) + + assert isinstance(restored.protocol, OpenAIChatCompletionsProtocol) + assert isinstance(restored.provider.protocol, OpenAIResponsesProtocol) + + +def test_model_json_roundtrip_preserves_provider_protocol_override() -> None: + provider = ai.get_provider( + "openai", + protocol=OpenAIChatCompletionsProtocol(), + ) + model = ai.Model(id="gpt-5", provider=provider) + + restored = ai.Model.model_validate_json(model.model_dump_json()) + + assert isinstance(restored.provider.protocol, OpenAIChatCompletionsProtocol) + assert restored.protocol is None + + +def test_model_json_roundtrip_supports_registered_custom_provider() -> None: + class CustomProtocol(models.ProviderProtocol[Any]): + protocol_class_id: Literal["test-custom-protocol"] = ( + "test-custom-protocol" + ) + mode: str + + class CustomProvider(models.Provider[Any]): + provider_class_id: Literal["test-custom-provider"] = ( + "test-custom-provider" + ) + region: str + + def default_protocol(self) -> models.ProviderProtocol[Any]: + return CustomProtocol(mode="default") + + model = ai.Model( + id="custom-model", + provider=CustomProvider( + name="custom", + default_base_url="https://custom.example.com", + protocol_override=CustomProtocol(mode="provider"), + region="test-region", + ), + protocol=CustomProtocol(mode="model"), + ) + + restored = ai.Model.model_validate_json(model.model_dump_json()) + + assert isinstance(restored.provider, CustomProvider) + assert restored.provider.region == "test-region" + assert isinstance(restored.provider.protocol, CustomProtocol) + assert restored.provider.protocol.mode == "provider" + assert isinstance(restored.protocol, CustomProtocol) + assert restored.protocol.mode == "model" + + +def test_model_json_roundtrip_rejects_provider_without_class_id() -> None: + with pytest.raises( + pydantic.ValidationError, + match="provider data must include provider_class_id", + ): + ai.Model.model_validate( + { + "id": "custom-model", + "provider": { + "name": "custom", + "default_base_url": "https://custom.example.com", + }, + } + ) + + +def test_model_json_roundtrip_rejects_unknown_provider_class_id() -> None: + with pytest.raises( + pydantic.ValidationError, + match="unknown provider_class_id", + ): + ai.Model.model_validate( + { + "id": "custom-model", + "provider": { + "provider_class_id": "missing-provider", + "name": "custom", + "default_base_url": "https://custom.example.com", + }, + } + ) + + +def test_model_json_roundtrip_rejects_unknown_protocol_class_id() -> None: + data = ai.get_model("openai:gpt-5").model_dump() + data["protocol"] = {"protocol_class_id": "missing-protocol"} + + with pytest.raises( + pydantic.ValidationError, + match="unknown provider protocol_class_id", + ): + ai.Model.model_validate(data) + + +def test_model_json_roundtrip_rejects_protocol_without_class_id() -> None: + data = ai.get_model("openai:gpt-5").model_dump() + data["protocol"] = {} + + with pytest.raises( + pydantic.ValidationError, + match="provider protocol data must include protocol_class_id", + ): + ai.Model.model_validate(data) + + +def test_model_json_roundtrip_rejects_unknown_provider_protocol_class_id() -> ( + None +): + provider = ai.get_provider( + "openai", + protocol=OpenAIChatCompletionsProtocol(), + ) + data = ai.Model(id="gpt-5", provider=provider).model_dump() + assert isinstance(data["provider"], dict) + data["provider"]["protocol_override"] = { + "protocol_class_id": "missing-protocol" + } + + with pytest.raises( + pydantic.ValidationError, + match="unknown provider protocol_class_id", + ): + ai.Model.model_validate(data) diff --git a/tests/providers/ai_gateway/conftest.py b/tests/providers/ai_gateway/conftest.py index c3cba9d2..84b95a2b 100644 --- a/tests/providers/ai_gateway/conftest.py +++ b/tests/providers/ai_gateway/conftest.py @@ -29,7 +29,7 @@ def mock_model( api_key=api_key, client=httpx.AsyncClient(transport=handler), ) - return ai.Model(model_id, provider=provider) + return ai.Model(id=model_id, provider=provider) mock_client = mock_model diff --git a/tests/providers/ai_gateway/test_probe.py b/tests/providers/ai_gateway/test_probe.py index 2803c914..aa5e0538 100644 --- a/tests/providers/ai_gateway/test_probe.py +++ b/tests/providers/ai_gateway/test_probe.py @@ -33,7 +33,7 @@ def _handler(request: httpx.Request) -> httpx.Response: api_key=api_key, client=httpx.AsyncClient(transport=httpx.MockTransport(_handler)), ) - return ai.Model(_MODEL_ID, provider=provider) + return ai.Model(id=_MODEL_ID, provider=provider) async def test_auth_ok_model_present_succeeds() -> None: diff --git a/tests/providers/anthropic/test_adapter.py b/tests/providers/anthropic/test_adapter.py index 15c4b59d..78393013 100644 --- a/tests/providers/anthropic/test_adapter.py +++ b/tests/providers/anthropic/test_adapter.py @@ -45,7 +45,7 @@ def _patch_client( return cast("anthropic.AsyncAnthropic", fake), captured -_MODEL = ai.Model("claude-sonnet-4-6", provider=ai.get_provider("anthropic")) +_MODEL = ai.Model(id="claude-sonnet-4-6", provider=ai.get_provider("anthropic")) async def _drain(stream: Any) -> None: diff --git a/tests/providers/anthropic/test_probe.py b/tests/providers/anthropic/test_probe.py index e9e92884..c8f3df93 100644 --- a/tests/providers/anthropic/test_probe.py +++ b/tests/providers/anthropic/test_probe.py @@ -35,7 +35,7 @@ def _handler(request: httpx.Request) -> httpx.Response: transport=httpx.MockTransport(_handler), ), ) - return ai.Model("claude-opus-4-6", provider=provider) + return ai.Model(id="claude-opus-4-6", provider=provider) async def test_200_succeeds() -> None: @@ -62,16 +62,18 @@ def _handler(request: httpx.Request) -> httpx.Response: provider = AnthropicCompatibleProvider( name="custom-anthropic", default_base_url="https://anthropic.test", - api_key="sk-test-key", + api_key_value="sk-test-key", anthropic_version="2024-01-01", headers={"X-Custom-Header": "example"}, - client=httpx.AsyncClient( + ) + provider._set_runtime_client( + httpx.AsyncClient( base_url="https://anthropic.test", transport=httpx.MockTransport(_handler), - ), + ) ) - model = ai.Model("custom-model", provider=provider) + model = ai.Model(id="custom-model", provider=provider) await provider.probe(model) assert captured_headers["anthropic-version"] == "2024-01-01" assert captured_headers["x-custom-header"] == "example" diff --git a/tests/providers/anthropic/test_provider.py b/tests/providers/anthropic/test_provider.py index 1692b48d..d1eb0867 100644 --- a/tests/providers/anthropic/test_provider.py +++ b/tests/providers/anthropic/test_provider.py @@ -165,8 +165,10 @@ def _missing_anthropic(name: str, package: str | None = None) -> object: monkeypatch.setattr(importlib, "import_module", _missing_anthropic) + provider = ai.get_provider("anthropic", api_key="sk-test") + with pytest.raises(ai.InstallationError) as exc_info: - ai.get_provider("anthropic", api_key="sk-test") + _ = provider.client assert "could not import `anthropic`" in str(exc_info.value) assert "required to use the anthropic provider" in str(exc_info.value) @@ -181,7 +183,7 @@ def test_get_provider_accepts_base_url_and_api_key() -> None: headers={"X-Custom-Header": "example"}, ) - model = ai.Model("custom-model", provider=provider) + model = ai.Model(id="custom-model", provider=provider) assert repr(provider) == "anthropic" assert isinstance(provider.protocol, AnthropicMessagesProtocol) assert provider.base_url == "https://custom.example.com" diff --git a/tests/providers/anthropic/test_stream.py b/tests/providers/anthropic/test_stream.py index ad0400dc..779ca68b 100644 --- a/tests/providers/anthropic/test_stream.py +++ b/tests/providers/anthropic/test_stream.py @@ -28,7 +28,7 @@ snapshot_block, ) -_MODEL = ai.Model("claude-sonnet-4-6", provider=ai.get_provider("anthropic")) +_MODEL = ai.Model(id="claude-sonnet-4-6", provider=ai.get_provider("anthropic")) async def _drain( diff --git a/tests/providers/anthropic/test_tools.py b/tests/providers/anthropic/test_tools.py index 7c1e5008..deee0a66 100644 --- a/tests/providers/anthropic/test_tools.py +++ b/tests/providers/anthropic/test_tools.py @@ -23,7 +23,7 @@ from .conftest import FakeAnthropicClient -_MODEL = ai.Model("claude-sonnet-4-6", provider=ai.get_provider("anthropic")) +_MODEL = ai.Model(id="claude-sonnet-4-6", provider=ai.get_provider("anthropic")) async def _capture_tools( diff --git a/tests/providers/openai/test_adapter.py b/tests/providers/openai/test_adapter.py index 30d6ba86..ba856933 100644 --- a/tests/providers/openai/test_adapter.py +++ b/tests/providers/openai/test_adapter.py @@ -112,7 +112,7 @@ async def close(self) -> None: _MODEL = ai.Model( - "gpt-5.4", + id="gpt-5.4", provider=ai.get_provider("openai", api_key="sk-test"), ) diff --git a/tests/providers/openai/test_probe.py b/tests/providers/openai/test_probe.py index 437067a8..506eeff9 100644 --- a/tests/providers/openai/test_probe.py +++ b/tests/providers/openai/test_probe.py @@ -27,7 +27,7 @@ def _handler(request: httpx.Request) -> httpx.Response: transport=httpx.MockTransport(_handler), ), ) - return ai.Model("gpt-5.4", provider=provider) + return ai.Model(id="gpt-5.4", provider=provider) async def test_200_succeeds() -> None: @@ -64,6 +64,6 @@ async def test_no_api_key_raises_not_configured( monkeypatch.delenv("OPENAI_API_KEY", raising=False) provider = ai.get_provider("openai", base_url="https://openai.test/v1") - model = ai.Model("gpt-5.4", provider=provider) + model = ai.Model(id="gpt-5.4", provider=provider) with pytest.raises(ai.ProviderNotConfiguredError): await provider.probe(model) diff --git a/tests/providers/openai/test_provider.py b/tests/providers/openai/test_provider.py index b7ff6429..50e4d9a6 100644 --- a/tests/providers/openai/test_provider.py +++ b/tests/providers/openai/test_provider.py @@ -168,8 +168,10 @@ def _missing_openai(name: str, package: str | None = None) -> object: monkeypatch.setattr(importlib, "import_module", _missing_openai) + provider = ai.get_provider("openai", api_key="sk-test") + with pytest.raises(ai.InstallationError) as exc_info: - ai.get_provider("openai", api_key="sk-test") + _ = provider.client assert "could not import `openai`" in str(exc_info.value) assert "required to use the openai provider" in str(exc_info.value) @@ -188,14 +190,16 @@ def _missing_openai(name: str, package: str | None = None) -> object: monkeypatch.setattr(importlib, "import_module", _missing_openai) + provider = ai.get_provider( + "cloudflare-workers-ai", + env={ + "CLOUDFLARE_ACCOUNT_ID": "account-123", + "CLOUDFLARE_API_KEY": "sk-test", + }, + ) + with pytest.raises(ai.InstallationError) as exc_info: - ai.get_provider( - "cloudflare-workers-ai", - env={ - "CLOUDFLARE_ACCOUNT_ID": "account-123", - "CLOUDFLARE_API_KEY": "sk-test", - }, - ) + _ = provider.client assert "required to use the cloudflare-workers-ai provider" in str( exc_info.value @@ -210,7 +214,7 @@ def test_get_provider_accepts_base_url_and_api_key() -> None: headers={"X-Custom-Header": "example"}, ) - model = ai.Model("custom-model", provider=provider) + model = ai.Model(id="custom-model", provider=provider) assert repr(provider) == "openai" assert isinstance(provider.protocol, OpenAIResponsesProtocol) assert provider.base_url == "https://custom.example.com/v1" diff --git a/uv.lock b/uv.lock index e168bba7..f5344f9c 100644 --- a/uv.lock +++ b/uv.lock @@ -8,7 +8,7 @@ resolution-markers = [ ] [options] -exclude-newer = "2026-05-18T18:36:33.433440494Z" +exclude-newer = "0001-01-01T00:00:00Z" # This has no effect and is included for backwards compatibility when using relative exclude-newer values. exclude-newer-span = "P2D" [[package]] @@ -60,7 +60,7 @@ requires-dist = [ { name = "mcp", marker = "extra == 'mcp'", specifier = ">=1.18.0" }, { name = "modelsdotdev", specifier = "==0.*" }, { name = "openai", marker = "extra == 'openai'", specifier = ">=2.14.0" }, - { name = "pydantic", specifier = ">=2.12.5" }, + { name = "pydantic", specifier = ">=2.13" }, { name = "typing-extensions", specifier = ">=4.15.0" }, ] provides-extras = ["anthropic", "mcp", "openai"] @@ -820,7 +820,7 @@ wheels = [ [[package]] name = "pydantic" -version = "2.12.5" +version = "2.13.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "annotated-types" }, @@ -828,80 +828,84 @@ dependencies = [ { name = "typing-extensions" }, { name = "typing-inspection" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/69/44/36f1a6e523abc58ae5f928898e4aca2e0ea509b5aa6f6f392a5d882be928/pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49", size = 821591, upload-time = "2025-11-26T15:11:46.471Z" } +sdist = { url = "https://files.pythonhosted.org/packages/18/a5/b60d21ac674192f8ab0ba4e9fd860690f9b4a6e51ca5df118733b487d8d6/pydantic-2.13.4.tar.gz", hash = "sha256:c40756b57adaa8b1efeeced5c196f3f3b7c435f90e84ea7f443901bec8099ef6", size = 844775, upload-time = "2026-05-06T13:43:05.343Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d", size = 463580, upload-time = "2025-11-26T15:11:44.605Z" }, + { url = "https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl", hash = "sha256:45a282cde31d808236fd7ea9d919b128653c8b38b393d1c4ab335c62924d9aba", size = 472262, upload-time = "2026-05-06T13:43:02.641Z" }, ] [[package]] name = "pydantic-core" -version = "2.41.5" +version = "2.46.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952, upload-time = "2025-11-04T13:43:49.098Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5f/5d/5f6c63eebb5afee93bcaae4ce9a898f3373ca23df3ccaef086d0233a35a7/pydantic_core-2.41.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7", size = 2110990, upload-time = "2025-11-04T13:39:58.079Z" }, - { url = "https://files.pythonhosted.org/packages/aa/32/9c2e8ccb57c01111e0fd091f236c7b371c1bccea0fa85247ac55b1e2b6b6/pydantic_core-2.41.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0", size = 1896003, upload-time = "2025-11-04T13:39:59.956Z" }, - { url = "https://files.pythonhosted.org/packages/68/b8/a01b53cb0e59139fbc9e4fda3e9724ede8de279097179be4ff31f1abb65a/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69", size = 1919200, upload-time = "2025-11-04T13:40:02.241Z" }, - { url = "https://files.pythonhosted.org/packages/38/de/8c36b5198a29bdaade07b5985e80a233a5ac27137846f3bc2d3b40a47360/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75", size = 2052578, upload-time = "2025-11-04T13:40:04.401Z" }, - { url = "https://files.pythonhosted.org/packages/00/b5/0e8e4b5b081eac6cb3dbb7e60a65907549a1ce035a724368c330112adfdd/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05", size = 2208504, upload-time = "2025-11-04T13:40:06.072Z" }, - { url = "https://files.pythonhosted.org/packages/77/56/87a61aad59c7c5b9dc8caad5a41a5545cba3810c3e828708b3d7404f6cef/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc", size = 2335816, upload-time = "2025-11-04T13:40:07.835Z" }, - { url = "https://files.pythonhosted.org/packages/0d/76/941cc9f73529988688a665a5c0ecff1112b3d95ab48f81db5f7606f522d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c", size = 2075366, upload-time = "2025-11-04T13:40:09.804Z" }, - { url = "https://files.pythonhosted.org/packages/d3/43/ebef01f69baa07a482844faaa0a591bad1ef129253ffd0cdaa9d8a7f72d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5", size = 2171698, upload-time = "2025-11-04T13:40:12.004Z" }, - { url = "https://files.pythonhosted.org/packages/b1/87/41f3202e4193e3bacfc2c065fab7706ebe81af46a83d3e27605029c1f5a6/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c", size = 2132603, upload-time = "2025-11-04T13:40:13.868Z" }, - { url = "https://files.pythonhosted.org/packages/49/7d/4c00df99cb12070b6bccdef4a195255e6020a550d572768d92cc54dba91a/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294", size = 2329591, upload-time = "2025-11-04T13:40:15.672Z" }, - { url = "https://files.pythonhosted.org/packages/cc/6a/ebf4b1d65d458f3cda6a7335d141305dfa19bdc61140a884d165a8a1bbc7/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1", size = 2319068, upload-time = "2025-11-04T13:40:17.532Z" }, - { url = "https://files.pythonhosted.org/packages/49/3b/774f2b5cd4192d5ab75870ce4381fd89cf218af999515baf07e7206753f0/pydantic_core-2.41.5-cp312-cp312-win32.whl", hash = "sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d", size = 1985908, upload-time = "2025-11-04T13:40:19.309Z" }, - { url = "https://files.pythonhosted.org/packages/86/45/00173a033c801cacf67c190fef088789394feaf88a98a7035b0e40d53dc9/pydantic_core-2.41.5-cp312-cp312-win_amd64.whl", hash = "sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815", size = 2020145, upload-time = "2025-11-04T13:40:21.548Z" }, - { url = "https://files.pythonhosted.org/packages/f9/22/91fbc821fa6d261b376a3f73809f907cec5ca6025642c463d3488aad22fb/pydantic_core-2.41.5-cp312-cp312-win_arm64.whl", hash = "sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3", size = 1976179, upload-time = "2025-11-04T13:40:23.393Z" }, - { url = "https://files.pythonhosted.org/packages/87/06/8806241ff1f70d9939f9af039c6c35f2360cf16e93c2ca76f184e76b1564/pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9", size = 2120403, upload-time = "2025-11-04T13:40:25.248Z" }, - { url = "https://files.pythonhosted.org/packages/94/02/abfa0e0bda67faa65fef1c84971c7e45928e108fe24333c81f3bfe35d5f5/pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34", size = 1896206, upload-time = "2025-11-04T13:40:27.099Z" }, - { url = "https://files.pythonhosted.org/packages/15/df/a4c740c0943e93e6500f9eb23f4ca7ec9bf71b19e608ae5b579678c8d02f/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0", size = 1919307, upload-time = "2025-11-04T13:40:29.806Z" }, - { url = "https://files.pythonhosted.org/packages/9a/e3/6324802931ae1d123528988e0e86587c2072ac2e5394b4bc2bc34b61ff6e/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33", size = 2063258, upload-time = "2025-11-04T13:40:33.544Z" }, - { url = "https://files.pythonhosted.org/packages/c9/d4/2230d7151d4957dd79c3044ea26346c148c98fbf0ee6ebd41056f2d62ab5/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e", size = 2214917, upload-time = "2025-11-04T13:40:35.479Z" }, - { url = "https://files.pythonhosted.org/packages/e6/9f/eaac5df17a3672fef0081b6c1bb0b82b33ee89aa5cec0d7b05f52fd4a1fa/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2", size = 2332186, upload-time = "2025-11-04T13:40:37.436Z" }, - { url = "https://files.pythonhosted.org/packages/cf/4e/35a80cae583a37cf15604b44240e45c05e04e86f9cfd766623149297e971/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586", size = 2073164, upload-time = "2025-11-04T13:40:40.289Z" }, - { url = "https://files.pythonhosted.org/packages/bf/e3/f6e262673c6140dd3305d144d032f7bd5f7497d3871c1428521f19f9efa2/pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d", size = 2179146, upload-time = "2025-11-04T13:40:42.809Z" }, - { url = "https://files.pythonhosted.org/packages/75/c7/20bd7fc05f0c6ea2056a4565c6f36f8968c0924f19b7d97bbfea55780e73/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740", size = 2137788, upload-time = "2025-11-04T13:40:44.752Z" }, - { url = "https://files.pythonhosted.org/packages/3a/8d/34318ef985c45196e004bc46c6eab2eda437e744c124ef0dbe1ff2c9d06b/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e", size = 2340133, upload-time = "2025-11-04T13:40:46.66Z" }, - { url = "https://files.pythonhosted.org/packages/9c/59/013626bf8c78a5a5d9350d12e7697d3d4de951a75565496abd40ccd46bee/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858", size = 2324852, upload-time = "2025-11-04T13:40:48.575Z" }, - { url = "https://files.pythonhosted.org/packages/1a/d9/c248c103856f807ef70c18a4f986693a46a8ffe1602e5d361485da502d20/pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36", size = 1994679, upload-time = "2025-11-04T13:40:50.619Z" }, - { url = "https://files.pythonhosted.org/packages/9e/8b/341991b158ddab181cff136acd2552c9f35bd30380422a639c0671e99a91/pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11", size = 2019766, upload-time = "2025-11-04T13:40:52.631Z" }, - { url = "https://files.pythonhosted.org/packages/73/7d/f2f9db34af103bea3e09735bb40b021788a5e834c81eedb541991badf8f5/pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd", size = 1981005, upload-time = "2025-11-04T13:40:54.734Z" }, - { url = "https://files.pythonhosted.org/packages/ea/28/46b7c5c9635ae96ea0fbb779e271a38129df2550f763937659ee6c5dbc65/pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a", size = 2119622, upload-time = "2025-11-04T13:40:56.68Z" }, - { url = "https://files.pythonhosted.org/packages/74/1a/145646e5687e8d9a1e8d09acb278c8535ebe9e972e1f162ed338a622f193/pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14", size = 1891725, upload-time = "2025-11-04T13:40:58.807Z" }, - { url = "https://files.pythonhosted.org/packages/23/04/e89c29e267b8060b40dca97bfc64a19b2a3cf99018167ea1677d96368273/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1", size = 1915040, upload-time = "2025-11-04T13:41:00.853Z" }, - { url = "https://files.pythonhosted.org/packages/84/a3/15a82ac7bd97992a82257f777b3583d3e84bdb06ba6858f745daa2ec8a85/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66", size = 2063691, upload-time = "2025-11-04T13:41:03.504Z" }, - { url = "https://files.pythonhosted.org/packages/74/9b/0046701313c6ef08c0c1cf0e028c67c770a4e1275ca73131563c5f2a310a/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869", size = 2213897, upload-time = "2025-11-04T13:41:05.804Z" }, - { url = "https://files.pythonhosted.org/packages/8a/cd/6bac76ecd1b27e75a95ca3a9a559c643b3afcd2dd62086d4b7a32a18b169/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2", size = 2333302, upload-time = "2025-11-04T13:41:07.809Z" }, - { url = "https://files.pythonhosted.org/packages/4c/d2/ef2074dc020dd6e109611a8be4449b98cd25e1b9b8a303c2f0fca2f2bcf7/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375", size = 2064877, upload-time = "2025-11-04T13:41:09.827Z" }, - { url = "https://files.pythonhosted.org/packages/18/66/e9db17a9a763d72f03de903883c057b2592c09509ccfe468187f2a2eef29/pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553", size = 2180680, upload-time = "2025-11-04T13:41:12.379Z" }, - { url = "https://files.pythonhosted.org/packages/d3/9e/3ce66cebb929f3ced22be85d4c2399b8e85b622db77dad36b73c5387f8f8/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90", size = 2138960, upload-time = "2025-11-04T13:41:14.627Z" }, - { url = "https://files.pythonhosted.org/packages/a6/62/205a998f4327d2079326b01abee48e502ea739d174f0a89295c481a2272e/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07", size = 2339102, upload-time = "2025-11-04T13:41:16.868Z" }, - { url = "https://files.pythonhosted.org/packages/3c/0d/f05e79471e889d74d3d88f5bd20d0ed189ad94c2423d81ff8d0000aab4ff/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb", size = 2326039, upload-time = "2025-11-04T13:41:18.934Z" }, - { url = "https://files.pythonhosted.org/packages/ec/e1/e08a6208bb100da7e0c4b288eed624a703f4d129bde2da475721a80cab32/pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23", size = 1995126, upload-time = "2025-11-04T13:41:21.418Z" }, - { url = "https://files.pythonhosted.org/packages/48/5d/56ba7b24e9557f99c9237e29f5c09913c81eeb2f3217e40e922353668092/pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf", size = 2015489, upload-time = "2025-11-04T13:41:24.076Z" }, - { url = "https://files.pythonhosted.org/packages/4e/bb/f7a190991ec9e3e0ba22e4993d8755bbc4a32925c0b5b42775c03e8148f9/pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0", size = 1977288, upload-time = "2025-11-04T13:41:26.33Z" }, - { url = "https://files.pythonhosted.org/packages/92/ed/77542d0c51538e32e15afe7899d79efce4b81eee631d99850edc2f5e9349/pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a", size = 2120255, upload-time = "2025-11-04T13:41:28.569Z" }, - { url = "https://files.pythonhosted.org/packages/bb/3d/6913dde84d5be21e284439676168b28d8bbba5600d838b9dca99de0fad71/pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3", size = 1863760, upload-time = "2025-11-04T13:41:31.055Z" }, - { url = "https://files.pythonhosted.org/packages/5a/f0/e5e6b99d4191da102f2b0eb9687aaa7f5bea5d9964071a84effc3e40f997/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c", size = 1878092, upload-time = "2025-11-04T13:41:33.21Z" }, - { url = "https://files.pythonhosted.org/packages/71/48/36fb760642d568925953bcc8116455513d6e34c4beaa37544118c36aba6d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612", size = 2053385, upload-time = "2025-11-04T13:41:35.508Z" }, - { url = "https://files.pythonhosted.org/packages/20/25/92dc684dd8eb75a234bc1c764b4210cf2646479d54b47bf46061657292a8/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d", size = 2218832, upload-time = "2025-11-04T13:41:37.732Z" }, - { url = "https://files.pythonhosted.org/packages/e2/09/f53e0b05023d3e30357d82eb35835d0f6340ca344720a4599cd663dca599/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9", size = 2327585, upload-time = "2025-11-04T13:41:40Z" }, - { url = "https://files.pythonhosted.org/packages/aa/4e/2ae1aa85d6af35a39b236b1b1641de73f5a6ac4d5a7509f77b814885760c/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660", size = 2041078, upload-time = "2025-11-04T13:41:42.323Z" }, - { url = "https://files.pythonhosted.org/packages/cd/13/2e215f17f0ef326fc72afe94776edb77525142c693767fc347ed6288728d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9", size = 2173914, upload-time = "2025-11-04T13:41:45.221Z" }, - { url = "https://files.pythonhosted.org/packages/02/7a/f999a6dcbcd0e5660bc348a3991c8915ce6599f4f2c6ac22f01d7a10816c/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3", size = 2129560, upload-time = "2025-11-04T13:41:47.474Z" }, - { url = "https://files.pythonhosted.org/packages/3a/b1/6c990ac65e3b4c079a4fb9f5b05f5b013afa0f4ed6780a3dd236d2cbdc64/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf", size = 2329244, upload-time = "2025-11-04T13:41:49.992Z" }, - { url = "https://files.pythonhosted.org/packages/d9/02/3c562f3a51afd4d88fff8dffb1771b30cfdfd79befd9883ee094f5b6c0d8/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470", size = 2331955, upload-time = "2025-11-04T13:41:54.079Z" }, - { url = "https://files.pythonhosted.org/packages/5c/96/5fb7d8c3c17bc8c62fdb031c47d77a1af698f1d7a406b0f79aaa1338f9ad/pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa", size = 1988906, upload-time = "2025-11-04T13:41:56.606Z" }, - { url = "https://files.pythonhosted.org/packages/22/ed/182129d83032702912c2e2d8bbe33c036f342cc735737064668585dac28f/pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c", size = 1981607, upload-time = "2025-11-04T13:41:58.889Z" }, - { url = "https://files.pythonhosted.org/packages/9f/ed/068e41660b832bb0b1aa5b58011dea2a3fe0ba7861ff38c4d4904c1c1a99/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", size = 1974769, upload-time = "2025-11-04T13:42:01.186Z" }, - { url = "https://files.pythonhosted.org/packages/09/32/59b0c7e63e277fa7911c2fc70ccfb45ce4b98991e7ef37110663437005af/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd", size = 2110495, upload-time = "2025-11-04T13:42:49.689Z" }, - { url = "https://files.pythonhosted.org/packages/aa/81/05e400037eaf55ad400bcd318c05bb345b57e708887f07ddb2d20e3f0e98/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc", size = 1915388, upload-time = "2025-11-04T13:42:52.215Z" }, - { url = "https://files.pythonhosted.org/packages/6e/0d/e3549b2399f71d56476b77dbf3cf8937cec5cd70536bdc0e374a421d0599/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56", size = 1942879, upload-time = "2025-11-04T13:42:56.483Z" }, - { url = "https://files.pythonhosted.org/packages/f7/07/34573da085946b6a313d7c42f82f16e8920bfd730665de2d11c0c37a74b5/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b", size = 2139017, upload-time = "2025-11-04T13:42:59.471Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/9d/56/921726b776ace8d8f5db44c4ef961006580d91dc52b803c489fafd1aa249/pydantic_core-2.46.4.tar.gz", hash = "sha256:62f875393d7f270851f20523dd2e29f082bcc82292d66db2b64ea71f64b6e1c1", size = 471464, upload-time = "2026-05-06T13:37:06.98Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ce/8c/af022f0af448d7747c5154288d46b5f2bc5f17366eaa0e23e9aa04d59f3b/pydantic_core-2.46.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:3245406455a5d98187ec35530fd772b1d799b26667980872c8d4614991e2c4a2", size = 2106158, upload-time = "2026-05-06T13:38:57.215Z" }, + { url = "https://files.pythonhosted.org/packages/19/95/6195171e385007300f0f5574592e467c568becce2d937a0b6804f218bc49/pydantic_core-2.46.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:962ccbab7b642487b1d8b7df90ef677e03134cf1fd8880bf698649b22a69371f", size = 1951724, upload-time = "2026-05-06T13:37:02.697Z" }, + { url = "https://files.pythonhosted.org/packages/8e/bc/f47d1ff9cbb1620e1b5b697eef06010035735f07820180e74178226b27b3/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8233f2947cf85404441fd7e0085f53b10c93e0ee78611099b5c7237e36aacbf7", size = 1975742, upload-time = "2026-05-06T13:37:09.448Z" }, + { url = "https://files.pythonhosted.org/packages/5b/11/9b9a5b0306345664a2da6410877af6e8082481b5884b3ddd78d47c6013ce/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3a233125ac121aa3ffba9a2b59edfc4a985a76092dc8279586ab4b71390875e7", size = 2052418, upload-time = "2026-05-06T13:37:38.234Z" }, + { url = "https://files.pythonhosted.org/packages/f1/b7/a65fec226f5d78fc39f4a13c4cc0c768c22b113438f60c14adc9d2865038/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b712b53160b79a5850310b912a5ef8e57e56947c8ad690c227f5c9d7e561712", size = 2232274, upload-time = "2026-05-06T13:38:27.753Z" }, + { url = "https://files.pythonhosted.org/packages/68/f0/92039db98b907ef49269a8271f67db9cb78ae2fc68062ef7e4e77adb5f61/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9401557acd873c3a7f3eb9383edef8ac4968f9510e340f4808d427e75667e7b4", size = 2309940, upload-time = "2026-05-06T13:38:05.353Z" }, + { url = "https://files.pythonhosted.org/packages/5f/97/2aab507d3d00ca626e8e57c1eac6a79e4e5fbcc63eb99733ff55d1717f65/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:926c9541b14b12b1681dca8a0b75feb510b06c6341b70a8e500c2fdcff837cce", size = 2094516, upload-time = "2026-05-06T13:39:10.577Z" }, + { url = "https://files.pythonhosted.org/packages/22/37/a8aca44d40d737dde2bc05b3c6c07dff0de07ce6f82e9f3167aeaf4d5dea/pydantic_core-2.46.4-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:56cb4851bcaf3d117eddcef4fe66afd750a50274b0da8e22be256d10e5611987", size = 2136854, upload-time = "2026-05-06T13:40:22.59Z" }, + { url = "https://files.pythonhosted.org/packages/24/99/fcef1b79238c06a8cbec70819ac722ba76e02bc8ada9b0fd66eba40da01b/pydantic_core-2.46.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c68fcd102d71ea85c5b2dfac3f4f8476eff42a9e078fd5faefff6d145063536b", size = 2180306, upload-time = "2026-05-06T13:40:10.666Z" }, + { url = "https://files.pythonhosted.org/packages/ae/6c/fc44000918855b42779d007ae63b0532794739027b2f417321cddbc44f6a/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b2f69dec1725e79a012d920df1707de5caf7ed5e08f3be4435e25803efc47458", size = 2190044, upload-time = "2026-05-06T13:40:43.231Z" }, + { url = "https://files.pythonhosted.org/packages/6b/65/d9cadc9f1920d7a127ad2edba16c1db7916e59719285cd6c94600b0080ba/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:8d0820e8192167f80d88d64038e609c31452eeca865b4e1d9950a27a4609b00b", size = 2329133, upload-time = "2026-05-06T13:39:57.365Z" }, + { url = "https://files.pythonhosted.org/packages/d0/cf/c873d91679f3a30bcf5e7ac280ce5573483e72295307685120d0d5ad3416/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fbdb89b3e1c94a30cc5edfce477c6e6a5dc4d8f84665b455c27582f211a1c72c", size = 2374464, upload-time = "2026-05-06T13:38:06.976Z" }, + { url = "https://files.pythonhosted.org/packages/47/bd/6f2fc8188f31bf10590f1e98e7b306336161fac930a8c514cd7bd828c7dc/pydantic_core-2.46.4-cp312-cp312-win32.whl", hash = "sha256:9aa768456404a8bf48a4406685ac2bec8e72b62c69313734fa3b73cf33b3a894", size = 1974823, upload-time = "2026-05-06T13:40:47.985Z" }, + { url = "https://files.pythonhosted.org/packages/40/8c/985c1d41ea1107c2534abd9870e4ed5c8e7669b5c308297835c001e7a1c4/pydantic_core-2.46.4-cp312-cp312-win_amd64.whl", hash = "sha256:e9c26f834c65f5752f3f06cb08cb86a913ceb7274d0db6e267808a708b46bc89", size = 2072919, upload-time = "2026-05-06T13:39:21.153Z" }, + { url = "https://files.pythonhosted.org/packages/c4/ba/f463d006e0c47373ca7ec5e1a261c59dc01ef4d62b2657af925fb0deee3a/pydantic_core-2.46.4-cp312-cp312-win_arm64.whl", hash = "sha256:4fc73cb559bdb54b1134a706a2802a4cddd27a0633f5abb7e53056268751ac6a", size = 2027604, upload-time = "2026-05-06T13:39:03.753Z" }, + { url = "https://files.pythonhosted.org/packages/51/a2/5d30b469c5267a17b39dec53208222f76a8d351dfac4af661888c5aee77d/pydantic_core-2.46.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:5d5902252db0d3cedf8d4a1bc68f70eeb430f7e4c7104c8c476753519b423008", size = 2106306, upload-time = "2026-05-06T13:37:48.029Z" }, + { url = "https://files.pythonhosted.org/packages/c1/81/4fa520eaffa8bd7d1525e644cd6d39e7d60b1592bc5b516693c7340b50f1/pydantic_core-2.46.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c94f0688e7b8d0a67abf40e57a7eaaecd17cc9586706a31b76c031f63df052b4", size = 1951906, upload-time = "2026-05-06T13:37:17.012Z" }, + { url = "https://files.pythonhosted.org/packages/03/d5/fd02da45b659668b05923b17ba3a0100a0a3d5541e3bd8fcc4ecb711309e/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f027324c56cd5406ca49c124b0db10e56c69064fec039acc571c29020cc87c76", size = 1976802, upload-time = "2026-05-06T13:37:35.113Z" }, + { url = "https://files.pythonhosted.org/packages/21/f2/95727e1368be3d3ed485eaab7adbd7dda408f33f7a36e8b48e0144002b91/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e739fee756ba1010f8bcccb534252e85a35fe45ae92c295a06059ce58b74ccd3", size = 2052446, upload-time = "2026-05-06T13:37:12.313Z" }, + { url = "https://files.pythonhosted.org/packages/9c/86/5d99feea3f77c7234b8718075b23db11532773c1a0dbd9b9490215dc2eeb/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d56801be94b86a9da183e5f3766e6310752b99ff647e38b09a9500d88e46e76", size = 2232757, upload-time = "2026-05-06T13:39:01.149Z" }, + { url = "https://files.pythonhosted.org/packages/d2/3a/508ac615935ef7588cf6d9e9b91309fdc2da751af865e02a9098de88258c/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2412e734dcb48da14d4e4006b82b46b74f2518b8a26ee7e58c6844a6cd6d03c4", size = 2309275, upload-time = "2026-05-06T13:37:41.406Z" }, + { url = "https://files.pythonhosted.org/packages/07/f8/41db9de19d7987d6b04715a02b3b40aea467000275d9d758ffaa31af7d50/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9551187363ffc0de2a00b2e47c25aeaeb1020b69b668762966df15fc5659dd5a", size = 2094467, upload-time = "2026-05-06T13:39:18.847Z" }, + { url = "https://files.pythonhosted.org/packages/2c/e2/f35033184cb11d0052daf4416e8e10a502ea2ac006fc4f459aee872727d1/pydantic_core-2.46.4-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:0186750b482eefa11d7f435892b09c5c606193ef3375bcf94aa00ae6bfb66262", size = 2134417, upload-time = "2026-05-06T13:40:17.944Z" }, + { url = "https://files.pythonhosted.org/packages/7e/7b/6ceeb1cc90e193862f444ebe373d8fdf613f0a82572dde03fb10734c6c71/pydantic_core-2.46.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5855698a4856556d86e8e6cd8434bc3ac0314ee8e12089ae0e143f64c6256e4e", size = 2179782, upload-time = "2026-05-06T13:40:32.618Z" }, + { url = "https://files.pythonhosted.org/packages/5a/f2/c8d7773ede6af08036423a00ae0ceffce266c3c52a096c435d68c896083f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:cbaf13819775b7f769bf4a1f066cb6df7a28d4480081a589828ef190226881cd", size = 2188782, upload-time = "2026-05-06T13:36:51.018Z" }, + { url = "https://files.pythonhosted.org/packages/59/31/0c864784e31f09f05cdd87606f08923b9c9e7f6e51dd27f20f62f975ce9f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:633147d34cf4550417f12e2b1a0383973bdf5cdfde212cb09e9a581cf10820be", size = 2328334, upload-time = "2026-05-06T13:40:37.764Z" }, + { url = "https://files.pythonhosted.org/packages/c2/eb/4f6c8a41efa30baa755590f4141abf3a8c370fab610915733e74134a7270/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:82cf5301172168103724d49a1444d3378cb20cdee30b116a1bd6031236298a5d", size = 2372986, upload-time = "2026-05-06T13:39:34.152Z" }, + { url = "https://files.pythonhosted.org/packages/5b/24/b375a480d53113860c299764bfe9f349a3dc9108b3adc0d7f0d786492ebf/pydantic_core-2.46.4-cp313-cp313-win32.whl", hash = "sha256:9fa8ae11da9e2b3126c6426f147e0fba88d96d65921799bb30c6abd1cb2c97fb", size = 1973693, upload-time = "2026-05-06T13:37:55.072Z" }, + { url = "https://files.pythonhosted.org/packages/7e/e8/cff247591966f2d22ec8c003cd7587e27b7ba7b81ab2fb888e3ab75dc285/pydantic_core-2.46.4-cp313-cp313-win_amd64.whl", hash = "sha256:6b3ace8194b0e5204818c92802dcdca7fc6d88aabbb799d7c795540d9cd6d292", size = 2071819, upload-time = "2026-05-06T13:38:49.139Z" }, + { url = "https://files.pythonhosted.org/packages/c6/1a/f4aee670d5670e9e148e0c82c7db98d780be566c6e6a97ee8035528ca0b3/pydantic_core-2.46.4-cp313-cp313-win_arm64.whl", hash = "sha256:184c081504d17f1c1066e430e117142b2c77d9448a97f7b65c6ac9fd9aee238d", size = 2027411, upload-time = "2026-05-06T13:40:45.796Z" }, + { url = "https://files.pythonhosted.org/packages/8d/74/228a26ddad29c6672b805d9fd78e8d251cd04004fa7eed0e622096cd0250/pydantic_core-2.46.4-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:428e04521a40150c85216fc8b85e8d39fece235a9cf5e383761238c7fa9b96fb", size = 2102079, upload-time = "2026-05-06T13:38:41.019Z" }, + { url = "https://files.pythonhosted.org/packages/ad/1f/8970b150a4b4365623ae00fc88603491f763c627311ae8031e3111356d6e/pydantic_core-2.46.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:23ace664830ee0bfe014a0c7bc248b1f7f25ed7ad103852c317624a1083af462", size = 1952179, upload-time = "2026-05-06T13:36:59.812Z" }, + { url = "https://files.pythonhosted.org/packages/95/30/5211a831ae054928054b2f79731661087a2bc5c01e825c672b3a4a8f1b3e/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce5c1d2a8b27468f433ca974829c44060b8097eedc39933e3c206a90ee49c4a9", size = 1978926, upload-time = "2026-05-06T13:37:39.933Z" }, + { url = "https://files.pythonhosted.org/packages/57/e9/689668733b1eb67adeef047db3c2e8788fcf65a7fd9c9e2b46b7744fe245/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7283d57845ecf5a163403eb0702dfc220cc4fbdd18919cb5ccea4f95ee1cdab4", size = 2046785, upload-time = "2026-05-06T13:38:01.995Z" }, + { url = "https://files.pythonhosted.org/packages/60/d9/6715260422ff50a2109878fd24d948a6c3446bb2664f34ee78cd972b3acd/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8daafc69c93ee8a0204506a3b6b30f586ef54028f52aeeeb5c4cfc5184fd5914", size = 2228733, upload-time = "2026-05-06T13:40:50.371Z" }, + { url = "https://files.pythonhosted.org/packages/18/ae/fdb2f64316afca925640f8e70bb1a564b0ec2721c1389e25b8eb4bf9a299/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd2213145bcc2ba85884d0ac63d222fece9209678f77b9b4d76f054c561adb28", size = 2307534, upload-time = "2026-05-06T13:37:21.531Z" }, + { url = "https://files.pythonhosted.org/packages/89/1d/8eff589b45bb8190a9d12c49cfad0f176a5cbd1534908a6b5125e2886239/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a5f930472650a82629163023e630d160863fce524c616f4e5186e5de9d9a49b", size = 2099732, upload-time = "2026-05-06T13:39:31.942Z" }, + { url = "https://files.pythonhosted.org/packages/06/d5/ee5a3366637fee41dee51a1fc91562dcf12ddbc68fda34e6b253da2324bb/pydantic_core-2.46.4-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:c1b3f518abeca3aa13c712fd202306e145abf59a18b094a6bafb2d2bbf59192c", size = 2129627, upload-time = "2026-05-06T13:37:25.033Z" }, + { url = "https://files.pythonhosted.org/packages/94/33/2414be571d2c6a6c4d08be21f9292b6d3fdb08949a97b6dfe985017821db/pydantic_core-2.46.4-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1a7dd0b3ee80d90150e3495a3a13ac34dbcbfd4f012996a6a1d8900e91b5c0fb", size = 2179141, upload-time = "2026-05-06T13:37:14.046Z" }, + { url = "https://files.pythonhosted.org/packages/7b/79/7daa95be995be0eecc4cf75064cb33f9bbbfe3fe0158caf2f0d4a996a5c7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:3fb702cd90b0446a3a1c5e470bfa0dd23c0233b676a9099ddcc964fa6ca13898", size = 2184325, upload-time = "2026-05-06T13:36:53.615Z" }, + { url = "https://files.pythonhosted.org/packages/9f/cb/d0a382f5c0de8a222dc61c65348e0ce831b1f68e0a018450d31c2cace3a5/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b8458003118a712e66286df6a707db01c52c0f52f7db8e4a38f0da1d3b94fc4e", size = 2323990, upload-time = "2026-05-06T13:40:29.971Z" }, + { url = "https://files.pythonhosted.org/packages/05/db/d9ba624cc4a5aced1598e88c04fdbd8310c8a69b9d38b9a3d39ce3a61ed7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:372429a130e469c9cd698925ce5fc50940b7a1336b0d82038e63d5bbc4edc519", size = 2369978, upload-time = "2026-05-06T13:37:23.027Z" }, + { url = "https://files.pythonhosted.org/packages/f2/20/d15df15ba918c423461905802bfd2981c3af0bfa0e40d05e13edbfa48bc3/pydantic_core-2.46.4-cp314-cp314-win32.whl", hash = "sha256:85bb3611ff1802f3ee7fdd7dbff26b56f343fb432d57a4728fdd49b6ef35e2f4", size = 1966354, upload-time = "2026-05-06T13:38:03.499Z" }, + { url = "https://files.pythonhosted.org/packages/fc/b6/6b8de4c0a7d7ab3004c439c80c5c1e0a3e8d78bbae19379b01960383d9e5/pydantic_core-2.46.4-cp314-cp314-win_amd64.whl", hash = "sha256:811ff8e9c313ab425368bcbb36e5c4ebd7108c2bbf4e4089cfbb0b01eff63fac", size = 2072238, upload-time = "2026-05-06T13:39:40.807Z" }, + { url = "https://files.pythonhosted.org/packages/32/36/51eb763beec1f4cf59b1db243a7dcc39cbb41230f050a09b9d69faaf0a48/pydantic_core-2.46.4-cp314-cp314-win_arm64.whl", hash = "sha256:bfec22eab3c8cc2ceec0248aec886624116dc079afa027ecc8ad4a7e62010f8a", size = 2018251, upload-time = "2026-05-06T13:37:26.72Z" }, + { url = "https://files.pythonhosted.org/packages/e8/91/855af51d625b23aa987116a19e231d2aaef9c4a415273ddc189b79a45fee/pydantic_core-2.46.4-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:af8244b2bef6aaad6d92cda81372de7f8c8d36c9f0c3ea36e827c60e7d9467a0", size = 2099593, upload-time = "2026-05-06T13:39:47.682Z" }, + { url = "https://files.pythonhosted.org/packages/fb/1b/8784a54c65edb5f49f0a14d6977cf1b209bba85a4c77445b255c2de58ab3/pydantic_core-2.46.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a4330cdbc57162e4b3aa303f588ba752257694c9c9be3e7ebb11b4aca659b5d", size = 1935226, upload-time = "2026-05-06T13:40:40.428Z" }, + { url = "https://files.pythonhosted.org/packages/e8/e7/1955d28d1afc56dd4b3ad7cc0cf39df1b9852964cf16e5d13912756d6d6b/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29c61fc04a3d840155ff08e475a04809278972fe6aef51e2720554e96367e34b", size = 1974605, upload-time = "2026-05-06T13:37:32.029Z" }, + { url = "https://files.pythonhosted.org/packages/93/e2/3fedbf0ba7a22850e6e9fd78117f1c0f10f950182344d8a6c535d468fdd8/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c50f2528cf200c5eed56faf3f4e22fcd5f38c157a8b78576e6ba3168ec35f000", size = 2030777, upload-time = "2026-05-06T13:38:55.239Z" }, + { url = "https://files.pythonhosted.org/packages/f8/61/46be275fcaaba0b4f5b9669dd852267ce1ff616592dccf7a7845588df091/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0cbe8b01f948de4286c74cdd6c667aceb38f5c1e26f0693b3983d9d74887c65e", size = 2236641, upload-time = "2026-05-06T13:37:08.096Z" }, + { url = "https://files.pythonhosted.org/packages/60/db/12e93e46a8bac9988be3c016860f83293daea8c716c029c9ace279036f2f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:617d7e2ca7dcb8c5cf6bcb8c59b8832c94b36196bbf1cbd1bfb56ed341905edd", size = 2286404, upload-time = "2026-05-06T13:40:20.221Z" }, + { url = "https://files.pythonhosted.org/packages/e2/4a/4d8b19008f38d31c53b8219cfedc2e3d5de5fe99d90076b7e767de29274f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7027560ee92211647d0d34e3f7cd6f50da56399d26a9c8ad0da286d3869a53f3", size = 2109219, upload-time = "2026-05-06T13:38:12.153Z" }, + { url = "https://files.pythonhosted.org/packages/88/70/3cbc40978fefb7bb09c6708d40d4ad1a5d70fd7213c3d17f971de868ec1f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:f99626688942fb746e545232e7726926f3be91b5975f8b55327665fafda991c7", size = 2110594, upload-time = "2026-05-06T13:40:02.971Z" }, + { url = "https://files.pythonhosted.org/packages/9d/20/b8d36736216e29491125531685b2f9e61aa5b4b2599893f8268551da3338/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fc3e9034a63de20e15e8ade85358bc6efc614008cab72898b4b4952bea0509ff", size = 2159542, upload-time = "2026-05-06T13:39:27.506Z" }, + { url = "https://files.pythonhosted.org/packages/1d/a2/367df868eb584dacf6bf82a389272406d7178e301c4ac82545ab98bc2dd9/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:97e7cf2be5c77b7d1a9713a05605d49460d02c6078d38d8bef3cbe323c548424", size = 2168146, upload-time = "2026-05-06T13:38:31.93Z" }, + { url = "https://files.pythonhosted.org/packages/c1/b8/4460f77f7e201893f649a29ab355dddd3beee8a97bcb1a320db414f9a06e/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:3bf92c5d0e00fefaab325a4d27828fe6b6e2a21848686b5b60d2d9eeb09d76c6", size = 2306309, upload-time = "2026-05-06T13:37:44.717Z" }, + { url = "https://files.pythonhosted.org/packages/64/c4/be2639293acd87dc8ddbcec41a73cee9b2ebf996fe6d892a1a74e88ad3f7/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:3ecbc122d18468d06ca279dc26a8c2e2d5acb10943bb35e36ae92096dc3b5565", size = 2369736, upload-time = "2026-05-06T13:37:05.645Z" }, + { url = "https://files.pythonhosted.org/packages/30/a6/9f9f380dbb301f67023bf8f707aaa75daadf84f7152d95c410fd7e81d994/pydantic_core-2.46.4-cp314-cp314t-win32.whl", hash = "sha256:e846ae7835bf0703ae43f534ab79a867146dadd59dc9ca5c8b53d5c8f7c9ef02", size = 1955575, upload-time = "2026-05-06T13:38:51.116Z" }, + { url = "https://files.pythonhosted.org/packages/40/1f/f1eb9eb350e795d1af8586289746f5c5677d16043040d63710e22abc43c9/pydantic_core-2.46.4-cp314-cp314t-win_amd64.whl", hash = "sha256:2108ba5c1c1eca18030634489dc544844144ee36357f2f9f780b93e7ddbb44b5", size = 2051624, upload-time = "2026-05-06T13:38:21.672Z" }, + { url = "https://files.pythonhosted.org/packages/f6/d2/42dd53d0a85c27606f316d3aa5d2869c4e8470a5ed6dec30e4a1abe19192/pydantic_core-2.46.4-cp314-cp314t-win_arm64.whl", hash = "sha256:4fcbe087dbc2068af7eda3aa87634eba216dbda64d1ae73c8684b621d33f6596", size = 2017325, upload-time = "2026-05-06T13:40:52.723Z" }, + { url = "https://files.pythonhosted.org/packages/9d/1d/8987ad40f65ae1432753072f214fb5c74fe47ffbd0698bb9cbbb585664f8/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:1d8ba486450b14f3b1d63bc521d410ec7565e52f887b9fb671791886436a42f7", size = 2095527, upload-time = "2026-05-06T13:39:52.283Z" }, + { url = "https://files.pythonhosted.org/packages/64/d3/84c282a7eee1d3ac4c0377546ef5a1ea436ce26840d9ac3b7ed54a377507/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:3009f12e4e90b7f88b4f9adb1b0c4a3d58fe7820f3238c190047209d148026df", size = 1936024, upload-time = "2026-05-06T13:40:15.671Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ca/eac61596cdeb4d7e174d3dc0bd8a6238f14f75f97a24e7b7db4c7e7340a0/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad785e92e6dc634c21555edc8bd6b64957ab844541bcb96a1366c202951ae526", size = 1990696, upload-time = "2026-05-06T13:38:34.717Z" }, + { url = "https://files.pythonhosted.org/packages/fa/c3/7c8b240552251faf6b3a957db200fcfbbcec36763c050428b601e0c9b83b/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00c603d540afdd6b80eb39f078f33ebd46211f02f33e34a32d9f053bba711de0", size = 2147590, upload-time = "2026-05-06T13:39:29.883Z" }, ] [[package]]