diff --git a/pydantic_ai_slim/pydantic_ai/models/__init__.py b/pydantic_ai_slim/pydantic_ai/models/__init__.py index cd2c114602..f89773a3e7 100644 --- a/pydantic_ai_slim/pydantic_ai/models/__init__.py +++ b/pydantic_ai_slim/pydantic_ai/models/__init__.py @@ -780,9 +780,10 @@ def infer_model( # noqa: C901 model_kind = provider_name if model_kind.startswith('gateway/'): - from ..providers.gateway import infer_gateway_model + from ..providers.gateway import normalize_gateway_provider - return infer_gateway_model(model_kind.removeprefix('gateway/'), model_name=model_name) + model_kind = provider_name.removeprefix('gateway/') + model_kind = normalize_gateway_provider(model_kind) if model_kind in ( 'openai', 'azure', diff --git a/pydantic_ai_slim/pydantic_ai/models/bedrock.py b/pydantic_ai_slim/pydantic_ai/models/bedrock.py index ae7b7449bd..df8e8746b7 100644 --- a/pydantic_ai_slim/pydantic_ai/models/bedrock.py +++ b/pydantic_ai_slim/pydantic_ai/models/bedrock.py @@ -240,7 +240,7 @@ def __init__( self._model_name = model_name if isinstance(provider, str): - provider = infer_provider('gateway/converse' if provider == 'gateway' else provider) + provider = infer_provider('gateway/bedrock' if provider == 'gateway' else provider) self._provider = provider self.client = cast('BedrockRuntimeClient', provider.client) diff --git a/pydantic_ai_slim/pydantic_ai/models/google.py b/pydantic_ai_slim/pydantic_ai/models/google.py index b5967e8b64..071f65fa66 100644 --- a/pydantic_ai_slim/pydantic_ai/models/google.py +++ b/pydantic_ai_slim/pydantic_ai/models/google.py @@ -204,7 +204,7 @@ def __init__( self._model_name = model_name if isinstance(provider, str): - provider = infer_provider('gateway/gemini' if provider == 'gateway' else provider) + provider = infer_provider('gateway/google-vertex' if provider == 'gateway' else provider) self._provider = provider self.client = provider.client diff --git a/pydantic_ai_slim/pydantic_ai/models/openai.py b/pydantic_ai_slim/pydantic_ai/models/openai.py index a51ecff1b3..ed1e711823 100644 --- a/pydantic_ai_slim/pydantic_ai/models/openai.py +++ b/pydantic_ai_slim/pydantic_ai/models/openai.py @@ -375,7 +375,7 @@ def __init__( self._model_name = model_name if isinstance(provider, str): - provider = infer_provider('gateway/chat' if provider == 'gateway' else provider) + provider = infer_provider('gateway/openai' if provider == 'gateway' else provider) self._provider = provider self.client = provider.client @@ -944,7 +944,7 @@ def __init__( self._model_name = model_name if isinstance(provider, str): - provider = infer_provider('gateway/responses' if provider == 'gateway' else provider) + provider = infer_provider('gateway/openai' if provider == 'gateway' else provider) self._provider = provider self.client = provider.client diff --git a/pydantic_ai_slim/pydantic_ai/providers/__init__.py b/pydantic_ai_slim/pydantic_ai/providers/__init__.py index 945f4fec4a..9557e8e87b 100644 --- a/pydantic_ai_slim/pydantic_ai/providers/__init__.py +++ b/pydantic_ai_slim/pydantic_ai/providers/__init__.py @@ -158,8 +158,8 @@ def infer_provider(provider: str) -> Provider[Any]: if provider.startswith('gateway/'): from .gateway import gateway_provider - api_type = provider.removeprefix('gateway/') - return gateway_provider(api_type) + upstream_provider = provider.removeprefix('gateway/') + return gateway_provider(upstream_provider) elif provider in ('google-vertex', 'google-gla'): from .google import GoogleProvider diff --git a/pydantic_ai_slim/pydantic_ai/providers/gateway.py b/pydantic_ai_slim/pydantic_ai/providers/gateway.py index 588c982209..1246d2fecd 100644 --- a/pydantic_ai_slim/pydantic_ai/providers/gateway.py +++ b/pydantic_ai_slim/pydantic_ai/providers/gateway.py @@ -17,7 +17,6 @@ from groq import AsyncGroq from openai import AsyncOpenAI - from pydantic_ai.models import Model from pydantic_ai.models.anthropic import AsyncAnthropicClient from pydantic_ai.providers import Provider @@ -26,11 +25,10 @@ @overload def gateway_provider( - api_type: Literal['chat', 'responses'], + upstream_provider: Literal['openai', 'openai-chat', 'openai-responses', 'chat', 'responses'], /, *, - routing_group: str | None = None, - profile: str | None = None, + route: str | None = None, api_key: str | None = None, base_url: str | None = None, http_client: httpx.AsyncClient | None = None, @@ -39,11 +37,10 @@ def gateway_provider( @overload def gateway_provider( - api_type: Literal['groq'], + upstream_provider: Literal['groq'], /, *, - routing_group: str | None = None, - profile: str | None = None, + route: str | None = None, api_key: str | None = None, base_url: str | None = None, http_client: httpx.AsyncClient | None = None, @@ -52,11 +49,10 @@ def gateway_provider( @overload def gateway_provider( - api_type: Literal['anthropic'], + upstream_provider: Literal['anthropic'], /, *, - routing_group: str | None = None, - profile: str | None = None, + route: str | None = None, api_key: str | None = None, base_url: str | None = None, http_client: httpx.AsyncClient | None = None, @@ -65,11 +61,10 @@ def gateway_provider( @overload def gateway_provider( - api_type: Literal['converse'], + upstream_provider: Literal['bedrock', 'converse'], /, *, - routing_group: str | None = None, - profile: str | None = None, + route: str | None = None, api_key: str | None = None, base_url: str | None = None, ) -> Provider[BaseClient]: ... @@ -77,11 +72,10 @@ def gateway_provider( @overload def gateway_provider( - api_type: Literal['gemini'], + upstream_provider: Literal['gemini', 'google-vertex'], /, *, - routing_group: str | None = None, - profile: str | None = None, + route: str | None = None, api_key: str | None = None, base_url: str | None = None, http_client: httpx.AsyncClient | None = None, @@ -90,26 +84,37 @@ def gateway_provider( @overload def gateway_provider( - api_type: str, + upstream_provider: str, /, *, - routing_group: str | None = None, - profile: str | None = None, + route: str | None = None, api_key: str | None = None, base_url: str | None = None, ) -> Provider[Any]: ... -APIType = Literal['chat', 'responses', 'gemini', 'converse', 'anthropic', 'groq'] +UpstreamProvider = Literal[ + 'openai', + 'groq', + 'anthropic', + 'bedrock', + 'google-vertex', + # Those are only API formats, but we still support them for convenience. + 'openai-chat', + 'openai-responses', + 'chat', + 'responses', + 'converse', + 'gemini', +] def gateway_provider( - api_type: APIType | str, + upstream_provider: UpstreamProvider | str, /, *, # Every provider - routing_group: str | None = None, - profile: str | None = None, + route: str | None = None, api_key: str | None = None, base_url: str | None = None, # OpenAI, Groq, Anthropic & Gemini - Only Bedrock doesn't have an HTTPX client. @@ -118,11 +123,9 @@ def gateway_provider( """Create a new Gateway provider. Args: - api_type: Determines the API type to use. - routing_group: The group of APIs that support the same models - the idea is that you can route the requests to - any provider in a routing group. The `pydantic-ai-gateway-routing-group` header will be added. - profile: A provider may have a profile, which is a unique identifier for the provider. - The `pydantic-ai-gateway-profile` header will be added. + upstream_provider: The upstream provider to use. + route: The name of the provider or routing group to use to handle the request. If not provided, the default + routing group for the API format will be used. api_key: The API key to use for authentication. If not provided, the `PYDANTIC_AI_GATEWAY_API_KEY` environment variable will be used if available. base_url: The base URL to use for the Gateway. If not provided, the `PYDANTIC_AI_GATEWAY_BASE_URL` @@ -137,54 +140,45 @@ def gateway_provider( ) base_url = base_url or os.getenv('PYDANTIC_AI_GATEWAY_BASE_URL', GATEWAY_BASE_URL) - http_client = http_client or cached_async_http_client(provider=f'gateway/{api_type}') + http_client = http_client or cached_async_http_client(provider=f'gateway/{upstream_provider}') http_client.event_hooks = {'request': [_request_hook(api_key)]} - if profile is not None: - http_client.headers.setdefault('pydantic-ai-gateway-profile', profile) + if route is None: + # Use the implied providerId as the default route. + route = normalize_gateway_provider(upstream_provider) - if routing_group is not None: - http_client.headers.setdefault('pydantic-ai-gateway-routing-group', routing_group) + base_url = _merge_url_path(base_url, route) - if api_type in ('chat', 'responses'): + if upstream_provider in ('openai', 'openai-chat', 'openai-responses', 'chat', 'responses'): from .openai import OpenAIProvider - return OpenAIProvider(api_key=api_key, base_url=_merge_url_path(base_url, api_type), http_client=http_client) - elif api_type == 'groq': + return OpenAIProvider(api_key=api_key, base_url=base_url, http_client=http_client) + elif upstream_provider == 'groq': from .groq import GroqProvider - return GroqProvider(api_key=api_key, base_url=_merge_url_path(base_url, 'groq'), http_client=http_client) - elif api_type == 'anthropic': + return GroqProvider(api_key=api_key, base_url=base_url, http_client=http_client) + elif upstream_provider == 'anthropic': from anthropic import AsyncAnthropic from .anthropic import AnthropicProvider return AnthropicProvider( - anthropic_client=AsyncAnthropic( - auth_token=api_key, - base_url=_merge_url_path(base_url, 'anthropic'), - http_client=http_client, - ) + anthropic_client=AsyncAnthropic(auth_token=api_key, base_url=base_url, http_client=http_client) ) - elif api_type == 'converse': + elif upstream_provider in ('bedrock', 'converse'): from .bedrock import BedrockProvider return BedrockProvider( api_key=api_key, - base_url=_merge_url_path(base_url, api_type), + base_url=base_url, region_name='pydantic-ai-gateway', # Fake region name to avoid NoRegionError ) - elif api_type == 'gemini': + elif upstream_provider in ('google-vertex', 'gemini'): from .google import GoogleProvider - return GoogleProvider( - vertexai=True, - api_key=api_key, - base_url=_merge_url_path(base_url, 'gemini'), - http_client=http_client, - ) + return GoogleProvider(vertexai=True, api_key=api_key, base_url=base_url, http_client=http_client) else: - raise UserError(f'Unknown API type: {api_type}') + raise UserError(f'Unknown upstream provider: {upstream_provider}') def _request_hook(api_key: str) -> Callable[[httpx.Request], Awaitable[httpx.Request]]: @@ -218,31 +212,18 @@ def _merge_url_path(base_url: str, path: str) -> str: return base_url.rstrip('/') + '/' + path.lstrip('/') -def infer_gateway_model(api_type: APIType | str, *, model_name: str) -> Model: - """Infer the model class for a given API type.""" - if api_type == 'chat': - from pydantic_ai.models.openai import OpenAIChatModel - - return OpenAIChatModel(model_name=model_name, provider='gateway') - elif api_type == 'groq': - from pydantic_ai.models.groq import GroqModel - - return GroqModel(model_name=model_name, provider='gateway') - elif api_type == 'responses': - from pydantic_ai.models.openai import OpenAIResponsesModel - - return OpenAIResponsesModel(model_name=model_name, provider='gateway') - elif api_type == 'gemini': - from pydantic_ai.models.google import GoogleModel +def normalize_gateway_provider(provider: str) -> str: + """Normalize a gateway provider name. - return GoogleModel(model_name=model_name, provider='gateway') - elif api_type == 'converse': - from pydantic_ai.models.bedrock import BedrockConverseModel - - return BedrockConverseModel(model_name=model_name, provider='gateway') - elif api_type == 'anthropic': - from pydantic_ai.models.anthropic import AnthropicModel - - return AnthropicModel(model_name=model_name, provider='gateway') - else: - raise ValueError(f'Unknown API type: {api_type}') # pragma: no cover + Args: + provider: The provider name to normalize. + """ + if provider in ('openai', 'openai-chat', 'chat'): + return 'openai' + elif provider in ('openai-responses', 'responses'): + return 'openai-responses' + elif provider in ('gemini', 'google-vertex'): + return 'google-vertex' + elif provider in ('bedrock', 'converse'): + return 'bedrock' + return provider diff --git a/tests/providers/cassettes/test_gateway/test_gateway_provider_with_anthropic.yaml b/tests/providers/cassettes/test_gateway/test_gateway_provider_with_anthropic.yaml index f67f9ee29a..ea77abf071 100644 --- a/tests/providers/cassettes/test_gateway/test_gateway_provider_with_anthropic.yaml +++ b/tests/providers/cassettes/test_gateway/test_gateway_provider_with_anthropic.yaml @@ -8,7 +8,7 @@ interactions: connection: - keep-alive content-length: - - '166' + - '159' content-type: - application/json host: @@ -32,6 +32,8 @@ interactions: - application/json pydantic-ai-gateway-price-estimate: - 0.0002USD + retry-after: + - '34' strict-transport-security: - max-age=31536000; includeSubDomains; preload transfer-encoding: @@ -40,7 +42,7 @@ interactions: content: - text: The capital of France is Paris. type: text - id: msg_0116L5r52AZ42YhvvdUuHEsk + id: msg_015jjU4Q5dqhSc9vyfCdoujx model: claude-sonnet-4-5-20250929 role: assistant stop_reason: end_turn diff --git a/tests/providers/cassettes/test_gateway/test_gateway_provider_with_bedrock.yaml b/tests/providers/cassettes/test_gateway/test_gateway_provider_with_bedrock.yaml index ea9436f55d..cd2c86386b 100644 --- a/tests/providers/cassettes/test_gateway/test_gateway_provider_with_bedrock.yaml +++ b/tests/providers/cassettes/test_gateway/test_gateway_provider_with_bedrock.yaml @@ -5,7 +5,7 @@ interactions: headers: amz-sdk-invocation-id: - !!binary | - MmEzMzkzMGUtNzI3YS00YzFhLWFmYWQtYzFhYWMyMTI3NDlj + MTU4ODY4OTctOGU4MC00YzJlLWEyZTctMDA2ZmM0NTZjMmYy amz-sdk-request: - !!binary | YXR0ZW1wdD0x @@ -15,35 +15,34 @@ interactions: - !!binary | YXBwbGljYXRpb24vanNvbg== method: POST - uri: http://localhost:8787/converse/model/amazon.nova-micro-v1%3A0/converse + uri: http://localhost:8787/bedrock/model/amazon.nova-micro-v1%3A0/converse response: headers: content-length: - - '741' + - '631' content-type: - application/json pydantic-ai-gateway-price-estimate: - 0.0000USD parsed_body: metrics: - latencyMs: 682 + latencyMs: 717 output: message: content: - text: The capital of France is Paris. Paris is not only the capital city but also the most populous city in France, and it is a major center for culture, commerce, fashion, and international diplomacy. The city is known for - its historical and architectural landmarks, including the Eiffel Tower, the Louvre Museum, Notre-Dame Cathedral, - and the Champs-Élysées. Paris plays a significant role in the global arts, fashion, research, technology, education, - and entertainment scenes. + its historical landmarks, such as the Eiffel Tower, the Louvre Museum, Notre-Dame Cathedral, and the Champs-Élysées, + among many other attractions. role: assistant stopReason: end_turn usage: inputTokens: 7 - outputTokens: 96 + outputTokens: 78 pydantic_ai_gateway: - cost_estimate: 1.3685000000000002e-05 + cost_estimate: 1.1165000000000002e-05 serverToolUsage: {} - totalTokens: 103 + totalTokens: 85 status: code: 200 message: OK diff --git a/tests/providers/cassettes/test_gateway/test_gateway_provider_with_google_vertex.yaml b/tests/providers/cassettes/test_gateway/test_gateway_provider_with_google_vertex.yaml index c92f55fe07..7faf613276 100644 --- a/tests/providers/cassettes/test_gateway/test_gateway_provider_with_google_vertex.yaml +++ b/tests/providers/cassettes/test_gateway/test_gateway_provider_with_google_vertex.yaml @@ -22,7 +22,7 @@ interactions: generationConfig: responseModalities: - TEXT - uri: http://localhost:8787/gemini/v1beta1/publishers/google/models/gemini-2.5-flash:generateContent + uri: http://localhost:8787/google-vertex/v1beta1/publishers/google/models/gemini-2.5-flash:generateContent response: headers: alt-svc: @@ -39,15 +39,15 @@ interactions: - Origin, X-Origin, Referer parsed_body: candidates: - - avgLogprobs: -1.1077090501785278 + - avgLogprobs: -1.1087236404418945 content: parts: - text: The capital of France is **Paris**. role: model finishReason: STOP - createTime: '2025-11-09T10:55:33.769850Z' + createTime: '2025-11-11T12:44:24.411107Z' modelVersion: gemini-2.5-flash - responseId: JXMQabr-LsPv2fMP0YexuA8 + responseId: qC8TaeOLGZbhtfAPlbGdyAk usageMetadata: candidatesTokenCount: 8 candidatesTokensDetails: diff --git a/tests/providers/cassettes/test_gateway/test_gateway_provider_with_groq.yaml b/tests/providers/cassettes/test_gateway/test_gateway_provider_with_groq.yaml index 90ffb29c97..d4442d6cf5 100644 --- a/tests/providers/cassettes/test_gateway/test_gateway_provider_with_groq.yaml +++ b/tests/providers/cassettes/test_gateway/test_gateway_provider_with_groq.yaml @@ -29,7 +29,7 @@ interactions: cache-control: - private, max-age=0, no-store, no-cache, must-revalidate content-length: - - '634' + - '635' content-type: - application/json pydantic-ai-gateway-price-estimate: @@ -46,25 +46,25 @@ interactions: message: content: The capital of France is Paris. role: assistant - created: 1757576767 - id: chatcmpl-cc44ad8f-4b07-468c-9d7b-afac47db1e86 + created: 1762865063 + id: chatcmpl-f75abe6e-e6d9-4356-bf65-28df1f5707a8 model: llama-3.3-70b-versatile object: chat.completion service_tier: on_demand - system_fingerprint: fp_3f3b593e33 + system_fingerprint: fp_e502586a3c usage: - completion_time: 0.002254941 + completion_time: 0.010003462 completion_tokens: 8 - prompt_time: 0.01168072 + prompt_time: 0.002448683 prompt_tokens: 42 pydantic_ai_gateway: cost_estimate: 3.11e-05 - queue_time: 0.084482342 - total_time: 0.013935661 + queue_time: 0.140973397 + total_time: 0.012452145 total_tokens: 50 usage_breakdown: null x_groq: - id: req_01k4vvt9maeygb4yh5byz7vjwf + id: req_01k9sf49ajfpaarm6q98v0scqe status: code: 200 message: OK diff --git a/tests/providers/cassettes/test_gateway/test_gateway_provider_with_openai.yaml b/tests/providers/cassettes/test_gateway/test_gateway_provider_with_openai.yaml index 904213b062..9ee66bdda8 100644 --- a/tests/providers/cassettes/test_gateway/test_gateway_provider_with_openai.yaml +++ b/tests/providers/cassettes/test_gateway/test_gateway_provider_with_openai.yaml @@ -20,7 +20,7 @@ interactions: role: user model: gpt-5 stream: false - uri: http://localhost:8787/chat/chat/completions + uri: http://localhost:8787/openai/chat/completions response: headers: access-control-expose-headers: @@ -32,7 +32,7 @@ interactions: content-type: - application/json openai-processing-ms: - - '2046' + - '1404' openai-version: - '2020-10-01' pydantic-ai-gateway-price-estimate: @@ -50,8 +50,8 @@ interactions: content: Paris. refusal: null role: assistant - created: 1762685398 - id: chatcmpl-CZxHa0hkNXToLinJr0VlRYia1OMT0 + created: 1762865059 + id: chatcmpl-Cai1LMreVahLgHqSo2JSKTThdFLHV model: gpt-5-2025-08-07 object: chat.completion service_tier: default diff --git a/tests/providers/cassettes/test_gateway/test_gateway_provider_with_openai_responses.yaml b/tests/providers/cassettes/test_gateway/test_gateway_provider_with_openai_responses.yaml index 1f5b5c8aaf..1207d7e331 100644 --- a/tests/providers/cassettes/test_gateway/test_gateway_provider_with_openai_responses.yaml +++ b/tests/providers/cassettes/test_gateway/test_gateway_provider_with_openai_responses.yaml @@ -22,21 +22,21 @@ interactions: role: user model: gpt-5 stream: false - uri: http://localhost:8787/responses/responses + uri: http://localhost:8787/openai-responses/responses response: headers: alt-svc: - h3=":443"; ma=86400 content-length: - - '2651' + - '2169' content-type: - application/json openai-processing-ms: - - '2444' + - '1609' openai-version: - '2020-10-01' pydantic-ai-gateway-price-estimate: - - 0.0007USD + - 0.0001USD strict-transport-security: - max-age=31536000; includeSubDomains; preload transfer-encoding: @@ -45,9 +45,9 @@ interactions: background: false billing: payer: developer - created_at: 1762685402 + created_at: 1762950190 error: null - id: resp_0b0a499f6332473100691071da2f6081a184bad9183c461b04 + id: resp_0d00b8a9619b08b00069147c2ee65881a092c57770a6d264af incomplete_details: null instructions: null max_output_tokens: null @@ -56,8 +56,8 @@ interactions: model: gpt-5-2025-08-07 object: response output: - - encrypted_content: gAAAAABpEHHcIjiqxbZXNndi9xNsTyvngDbxl8seH4HjGuU0P_aiyztPIEtTBwhaq7QRlf2pobZHONLRzflx_6xxB7Do7X9UkcJ_c0zjvwyMGK83CJEwb6_n5qQJy3VdtfQXJRfLJhUSn7ozk4iTn65h_qurYMntYCxP6Cq11I9bHKEaUesKGeYgCwUtF9mmvgTwfRBxefHohNCa8HpLbHUBETMfBecL4OBVJvk7RvpcRtELL8hZgQCvjJGQXYC23TjeN83ctuvIfA5l5n-y6fOtxIc-TcTgLPqUoa6CwYuQRFF_jXXrY8x4bLyPv6X6KI-we4S_lr8h6V-cG_k5f3ezTWq7V_AhCwodZFW0keNdsxVm9X4PDpDNo5_7vobA6XBC6HLzWWpwQDqNWaDmQ6hbA82WewrFKoUSur86ekXDA8Ju82U1HkSzakOs4xUE9Ix9djVNQowDD53BybUCF92SImLZko9j4dJM5TP3zCQ9SYJ_06B_rN3JG5IJ9MM0LNGHFfD6mAn4omBakgG_FCQfBD0t34cEn9VobNqcJffa3slLOikIy9jqy7jUdf2r3cdh4mJpzP5asvcrvm7sDCtnT3aUVOBSWkbLLHAcrT7N60aZjJZahKOi4xlLHAbjKnR4fFbs3qSUIL9Ig44568oNHrGCnIEZ2h7ojpIb1lgNUcEXdIGmhNzLyqkqoWk0Uk8Y_-tD38zPdy-g_xdJjgmHe0797o0pD12aJkMJ3Pkmo9hDe-kZtR4A1rIQFDIJF--NiE2SjT5rZM9ntGrEcEVD6Nc3dxzWnrziTPfWuomovRWDuEbBnSYJ2wYIVl-snX5fPn_ENPS8YBY5ymZhbqOp9r6GA-OrCU5Ipv46VIY521nPcx4pC7PJ6X877N9CCd1j-URlKAyySgZNx6PZ_NiDxLAnsAPx-YlhvuT3Y8qjms7r98RCwa5hmbisrGFBQoKK22Lsinu4Ak_qO673JMJPWNK_ZtFyD7BKfmqPZm_EtmAC8GMXyrvmRCGU7jlienzW8DoA_8z-hidaACvAoVqPpQURxocKUMmHLeAfALDcpJHmWB2vJjl1oKQZ1JerwLatQMIWwONe4eTSKKw5KX4nn9wiGkQQU4bJHnFuanoSiaQvuPAXucp7I7pBBDhJT5X5ixWsR5pcTjs32ILI7YDTwc0tMWZ9gR5910O_K6cZHMPqDN1cCJvuf_5qsGFkVe217Osl7QyXHODCVbK6YXO_D7RGN5FCCypLI4FinDxfewQbiIFAcizF1TlCmJ6gF5vRQyio271qOLKOT497_1TKTbLHcM37D2C7MXrO59sNwl9-UBq1eowuquFiy24vmQePi9A0lNVrSD3kAhMpd8j3sTwIaoWlGabm194Oqm-3kHfyrqR6ExU= - id: rs_0b0a499f6332473100691071db0c2c81a1943920f74bbe507b + - encrypted_content: gAAAAABpFHwwEZkll1her399Et9qM7G_dr8_ii_Kag7-8hScK5r1IfcV22tkblEezpHfDo6geDPx7tsaWId4Ct_Yyz_oZ2XetDrxa2ZQ85cjoEdCynLP0qDau1R3WESlVSn_keQfmaZf2bPcY15x4HKf-MTYnogdbsTK3xVBaeWbkLwT85k7fRPIsfxR5ZHiLQKecIaoQs9tEE2DVmcKYk77dSF6CZHJr5cbamv5t923CzngGa_fNefX6x9sTRBG0y05GKeh7lUfff_Lw3l28q08TMwox42ZtUrmjM_06hXqvZjjhpeYFQDEu0bAbARq47FwwoSnbvsfWHH4RthGZhAf7-PdM18PRuE44eOGfceM-R0Elb-iZnBClbrlIZVPebRHnsFk8K3t8kZbmUJ0xLh5eRk5MZz8JBiVtG1KWjOotPXwGO8izJxguAY8p_e5mZq27M5UI-fUB23Tp4AsxP8CzZbDJJlK8NF0VjpXkEd6FFiY8SHkjxSM-7S0qF2y6xdFQVskTZBpkPOc3oh12oRgY105e4evZ8KYS1xD1gq_wDPNRrV5aeFpsXBnMrgpI82NigHD7_jBZAOZVOxtQMF8QnrQaKMcFEIvPrA2UPWEAQhUIKW5ymgfnWVpSGFNAgNs8rgDmH920TtUqeTW7ehn5rwnD1ta8V56JInN26vMF98UOpUaleL7f0pGU0TJfLRpwKi_OgxWEWn01AYf4WaWzUxy0oPrL7tR33GbJJktQIKocQ5r75W8OcLTo65ReP8wwt0LbNvIq4VJr4_GwLJ9VecyrbrtNDhKm5HV7o_vBXcbdHKHcYiWMQOzukHJ1TkNPi4pO8SCY_Z3VtPGk5MmmIMtVRI04PWIdS71hEaVI38lgsAfpJuTUoITniKYlANEOji1kd5U + id: rs_0d00b8a9619b08b00069147c2f891c81a087f55bd9bd6902fc summary: [] type: reasoning - content: @@ -65,7 +65,7 @@ interactions: logprobs: [] text: Paris. type: output_text - id: msg_0b0a499f6332473100691071dc471c81a1a55121b76f7d995e + id: msg_0d00b8a9619b08b00069147c2ffd7c81a0801f332c065f8381 role: assistant status: completed type: message @@ -94,12 +94,12 @@ interactions: input_tokens: 13 input_tokens_details: cached_tokens: 0 - output_tokens: 72 + output_tokens: 8 output_tokens_details: - reasoning_tokens: 64 + reasoning_tokens: 0 pydantic_ai_gateway: - cost_estimate: 0.00073625 - total_tokens: 85 + cost_estimate: 9.625000000000001e-05 + total_tokens: 21 user: null status: code: 200 diff --git a/tests/providers/test_gateway.py b/tests/providers/test_gateway.py index 76fbdf5a00..ec87f57228 100644 --- a/tests/providers/test_gateway.py +++ b/tests/providers/test_gateway.py @@ -34,12 +34,19 @@ @pytest.mark.parametrize( - 'provider_name, provider_cls, path', [('chat', OpenAIProvider, 'chat'), ('responses', OpenAIProvider, 'responses')] + 'provider_name, provider_cls, route', + [ + ('openai', OpenAIProvider, 'openai'), + ('openai-chat', OpenAIProvider, 'openai'), + ('openai-responses', OpenAIProvider, 'openai-responses'), + ], ) -def test_init_with_base_url(provider_name: Literal['chat', 'responses'], provider_cls: type[Provider[Any]], path: str): +def test_init_with_base_url( + provider_name: Literal['openai', 'openai-chat', 'openai-responses'], provider_cls: type[Provider[Any]], route: str +): provider = gateway_provider(provider_name, base_url='https://example.com/', api_key='foobar') assert isinstance(provider, provider_cls) - assert provider.base_url == f'https://example.com/{path}/' + assert provider.base_url == f'https://example.com/{route}/' assert provider.client.api_key == 'foobar' @@ -51,12 +58,12 @@ def test_init_gateway_without_api_key_raises_error(env: TestEnv): 'Set the `PYDANTIC_AI_GATEWAY_API_KEY` environment variable or pass it via `gateway_provider(..., api_key=...)` to use the Pydantic AI Gateway provider.' ), ): - gateway_provider('chat') + gateway_provider('openai') async def test_init_with_http_client(): async with httpx.AsyncClient() as http_client: - provider = gateway_provider('chat', http_client=http_client, api_key='foobar') + provider = gateway_provider('openai', http_client=http_client, api_key='foobar') assert provider.client._client == http_client # type: ignore @@ -79,32 +86,33 @@ def vcr_config(): os.environ, {'PYDANTIC_AI_GATEWAY_API_KEY': 'test-api-key', 'PYDANTIC_AI_GATEWAY_BASE_URL': GATEWAY_BASE_URL} ) @pytest.mark.parametrize( - 'provider_name, provider_cls, path', + 'provider_name, provider_cls, route', [ - ('chat', OpenAIProvider, 'chat'), - ('responses', OpenAIProvider, 'responses'), + ('openai', OpenAIProvider, 'openai'), + ('openai-chat', OpenAIProvider, 'openai'), + ('openai-responses', OpenAIProvider, 'openai-responses'), ('groq', GroqProvider, 'groq'), - ('gemini', GoogleProvider, 'gemini'), + ('google-vertex', GoogleProvider, 'google-vertex'), ('anthropic', AnthropicProvider, 'anthropic'), - ('converse', BedrockProvider, 'converse'), + ('bedrock', BedrockProvider, 'bedrock'), ], ) -def test_gateway_provider(provider_name: str, provider_cls: type[Provider[Any]], path: str): +def test_gateway_provider(provider_name: str, provider_cls: type[Provider[Any]], route: str): provider = gateway_provider(provider_name) assert isinstance(provider, provider_cls) # Some providers add a trailing slash, others don't - assert provider.base_url in (f'{GATEWAY_BASE_URL}/{path}/', f'{GATEWAY_BASE_URL}/{path}') + assert provider.base_url in (f'{GATEWAY_BASE_URL}/{route}/', f'{GATEWAY_BASE_URL}/{route}') @patch.dict(os.environ, {'PYDANTIC_AI_GATEWAY_API_KEY': 'test-api-key'}) def test_gateway_provider_unknown(): - with raises(snapshot('UserError: Unknown API type: foo')): + with raises(snapshot('UserError: Unknown upstream provider: foo')): gateway_provider('foo') async def test_gateway_provider_with_openai(allow_model_requests: None, gateway_api_key: str): - provider = gateway_provider('chat', api_key=gateway_api_key, base_url='http://localhost:8787') + provider = gateway_provider('openai', api_key=gateway_api_key, base_url='http://localhost:8787') model = OpenAIChatModel('gpt-5', provider=provider) agent = Agent(model) @@ -113,7 +121,7 @@ async def test_gateway_provider_with_openai(allow_model_requests: None, gateway_ async def test_gateway_provider_with_openai_responses(allow_model_requests: None, gateway_api_key: str): - provider = gateway_provider('responses', api_key=gateway_api_key, base_url='http://localhost:8787') + provider = gateway_provider('openai-responses', api_key=gateway_api_key, base_url='http://localhost:8787') model = OpenAIResponsesModel('gpt-5', provider=provider) agent = Agent(model) @@ -131,7 +139,7 @@ async def test_gateway_provider_with_groq(allow_model_requests: None, gateway_ap async def test_gateway_provider_with_google_vertex(allow_model_requests: None, gateway_api_key: str): - provider = gateway_provider('gemini', api_key=gateway_api_key, base_url='http://localhost:8787') + provider = gateway_provider('google-vertex', api_key=gateway_api_key, base_url='http://localhost:8787') model = GoogleModel('gemini-2.5-flash', provider=provider) agent = Agent(model) @@ -149,13 +157,13 @@ async def test_gateway_provider_with_anthropic(allow_model_requests: None, gatew async def test_gateway_provider_with_bedrock(allow_model_requests: None, gateway_api_key: str): - provider = gateway_provider('converse', api_key=gateway_api_key, base_url='http://localhost:8787') + provider = gateway_provider('bedrock', api_key=gateway_api_key, base_url='http://localhost:8787') model = BedrockConverseModel('amazon.nova-micro-v1:0', provider=provider) agent = Agent(model) result = await agent.run('What is the capital of France?') assert result.output == snapshot( - 'The capital of France is Paris. Paris is not only the capital city but also the most populous city in France, and it is a major center for culture, commerce, fashion, and international diplomacy. The city is known for its historical and architectural landmarks, including the Eiffel Tower, the Louvre Museum, Notre-Dame Cathedral, and the Champs-Élysées. Paris plays a significant role in the global arts, fashion, research, technology, education, and entertainment scenes.' + 'The capital of France is Paris. Paris is not only the capital city but also the most populous city in France, and it is a major center for culture, commerce, fashion, and international diplomacy. The city is known for its historical landmarks, such as the Eiffel Tower, the Louvre Museum, Notre-Dame Cathedral, and the Champs-Élysées, among many other attractions.' ) @@ -182,13 +190,6 @@ async def test_model_provider_argument(): assert GATEWAY_BASE_URL in model._provider.base_url # type: ignore[reportPrivateUsage] -async def test_gateway_provider_routing_group_header(gateway_api_key: str): - provider = gateway_provider('chat', routing_group='openai', api_key=gateway_api_key) - httpx_client = provider.client._client # type: ignore[reportPrivateUsage] - assert httpx_client.headers['pydantic-ai-gateway-routing-group'] == 'openai' - - -async def test_gateway_provider_profile_header(gateway_api_key: str): - provider = gateway_provider('chat', profile='openai', api_key=gateway_api_key) - httpx_client = provider.client._client # type: ignore[reportPrivateUsage] - assert httpx_client.headers['pydantic-ai-gateway-profile'] == 'openai' +async def test_gateway_provider_routing_group(gateway_api_key: str): + provider = gateway_provider('openai', route='potato', api_key=gateway_api_key) + assert provider.client.base_url.path.endswith('/potato/')