Skip to content

Commit e97b93a

Browse files
authored
gateway: add support for Anthropic (#2863)
1 parent e8558ce commit e97b93a

File tree

4 files changed

+112
-4
lines changed

4 files changed

+112
-4
lines changed

pydantic_ai_slim/pydantic_ai/providers/anthropic.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,12 +45,15 @@ def model_profile(self, model_name: str) -> ModelProfile | None:
4545
def __init__(self, *, anthropic_client: AsyncAnthropicClient | None = None) -> None: ...
4646

4747
@overload
48-
def __init__(self, *, api_key: str | None = None, http_client: httpx.AsyncClient | None = None) -> None: ...
48+
def __init__(
49+
self, *, api_key: str | None = None, base_url: str | None = None, http_client: httpx.AsyncClient | None = None
50+
) -> None: ...
4951

5052
def __init__(
5153
self,
5254
*,
5355
api_key: str | None = None,
56+
base_url: str | None = None,
5457
anthropic_client: AsyncAnthropicClient | None = None,
5558
http_client: httpx.AsyncClient | None = None,
5659
) -> None:
@@ -59,6 +62,7 @@ def __init__(
5962
Args:
6063
api_key: The API key to use for authentication, if not provided, the `ANTHROPIC_API_KEY` environment variable
6164
will be used if available.
65+
base_url: The base URL to use for the Anthropic API.
6266
anthropic_client: An existing [`AsyncAnthropic`](https://github.com/anthropics/anthropic-sdk-python)
6367
client to use. If provided, the `api_key` and `http_client` arguments will be ignored.
6468
http_client: An existing `httpx.AsyncClient` to use for making HTTP requests.
@@ -75,7 +79,7 @@ def __init__(
7579
'to use the Anthropic provider.'
7680
)
7781
if http_client is not None:
78-
self._client = AsyncAnthropic(api_key=api_key, http_client=http_client)
82+
self._client = AsyncAnthropic(api_key=api_key, base_url=base_url, http_client=http_client)
7983
else:
8084
http_client = cached_async_http_client(provider='anthropic')
81-
self._client = AsyncAnthropic(api_key=api_key, http_client=http_client)
85+
self._client = AsyncAnthropic(api_key=api_key, base_url=base_url, http_client=http_client)

pydantic_ai_slim/pydantic_ai/providers/gateway.py

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
from groq import AsyncGroq
1717
from openai import AsyncOpenAI
1818

19+
from pydantic_ai.models.anthropic import AsyncAnthropicClient
1920
from pydantic_ai.providers import Provider
2021

2122

@@ -48,8 +49,17 @@ def gateway_provider(
4849
) -> Provider[GoogleClient]: ...
4950

5051

52+
@overload
53+
def gateway_provider(
54+
upstream_provider: Literal['anthropic'],
55+
*,
56+
api_key: str | None = None,
57+
base_url: str | None = None,
58+
) -> Provider[AsyncAnthropicClient]: ...
59+
60+
5161
def gateway_provider(
52-
upstream_provider: Literal['openai', 'openai-chat', 'openai-responses', 'groq', 'google-vertex'] | str,
62+
upstream_provider: Literal['openai', 'openai-chat', 'openai-responses', 'groq', 'google-vertex', 'anthropic'] | str,
5363
*,
5464
# Every provider
5565
api_key: str | None = None,
@@ -90,6 +100,18 @@ def gateway_provider(
90100
from .groq import GroqProvider
91101

92102
return GroqProvider(api_key=api_key, base_url=urljoin(base_url, 'groq'), http_client=http_client)
103+
elif upstream_provider == 'anthropic':
104+
from anthropic import AsyncAnthropic
105+
106+
from .anthropic import AnthropicProvider
107+
108+
return AnthropicProvider(
109+
anthropic_client=AsyncAnthropic(
110+
auth_token=api_key,
111+
base_url=urljoin(base_url, 'anthropic'),
112+
http_client=http_client,
113+
)
114+
)
93115
elif upstream_provider == 'google-vertex':
94116
from google.genai import Client as GoogleClient
95117

@@ -140,6 +162,10 @@ def infer_model(model_name: str) -> Model:
140162
from pydantic_ai.models.groq import GroqModel
141163

142164
return GroqModel(model_name, provider=gateway_provider('groq'))
165+
elif upstream_provider == 'anthropic':
166+
from pydantic_ai.models.anthropic import AnthropicModel
167+
168+
return AnthropicModel(model_name, provider=gateway_provider('anthropic'))
143169
elif upstream_provider == 'google-vertex':
144170
from pydantic_ai.models.google import GoogleModel
145171

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
interactions:
2+
- request:
3+
headers:
4+
accept:
5+
- application/json
6+
accept-encoding:
7+
- gzip, deflate
8+
connection:
9+
- keep-alive
10+
content-length:
11+
- '166'
12+
content-type:
13+
- application/json
14+
host:
15+
- localhost:8787
16+
method: POST
17+
parsed_body:
18+
max_tokens: 4096
19+
messages:
20+
- content:
21+
- text: What is the capital of France?
22+
type: text
23+
role: user
24+
model: claude-3-5-sonnet-latest
25+
stream: false
26+
uri: http://localhost:8787/anthropic/v1/messages?beta=true
27+
response:
28+
headers:
29+
content-length:
30+
- '500'
31+
content-type:
32+
- application/json
33+
pydantic-ai-gateway-price-estimate:
34+
- 0.0002USD
35+
strict-transport-security:
36+
- max-age=31536000; includeSubDomains; preload
37+
transfer-encoding:
38+
- chunked
39+
parsed_body:
40+
content:
41+
- text: The capital of France is Paris.
42+
type: text
43+
id: msg_015tco2dv5oh9rFq1PcZAduv
44+
model: claude-3-5-sonnet-20241022
45+
role: assistant
46+
stop_reason: end_turn
47+
stop_sequence: null
48+
type: message
49+
usage:
50+
cache_creation:
51+
ephemeral_1h_input_tokens: 0
52+
ephemeral_5m_input_tokens: 0
53+
cache_creation_input_tokens: 0
54+
cache_read_input_tokens: 0
55+
input_tokens: 14
56+
output_tokens: 10
57+
pydantic_ai_gateway:
58+
cost_estimate: 0.00019199999999999998
59+
service_tier: standard
60+
status:
61+
code: 200
62+
message: OK
63+
version: 1

tests/providers/test_gateway.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
from ..conftest import TestEnv, try_import
1414

1515
with try_import() as imports_successful:
16+
from pydantic_ai.models.anthropic import AnthropicModel
1617
from pydantic_ai.models.google import GoogleModel
1718
from pydantic_ai.models.groq import GroqModel
1819
from pydantic_ai.models.openai import OpenAIChatModel, OpenAIResponsesModel
@@ -94,6 +95,11 @@ def test_infer_model():
9495
assert model.model_name == 'gemini-1.5-flash'
9596
assert model.system == 'google-vertex'
9697

98+
model = infer_model('anthropic/claude-3-5-sonnet-latest')
99+
assert isinstance(model, AnthropicModel)
100+
assert model.model_name == 'claude-3-5-sonnet-latest'
101+
assert model.system == 'anthropic'
102+
97103
with raises(snapshot('UserError: The model name "gemini-1.5-flash" is not in the format "provider/model_name".')):
98104
infer_model('gemini-1.5-flash')
99105

@@ -135,3 +141,12 @@ async def test_gateway_provider_with_google_vertex(allow_model_requests: None, g
135141

136142
result = await agent.run('What is the capital of France?')
137143
assert result.output == snapshot('Paris\n')
144+
145+
146+
async def test_gateway_provider_with_anthropic(allow_model_requests: None, gateway_api_key: str):
147+
provider = gateway_provider('anthropic', api_key=gateway_api_key, base_url='http://localhost:8787')
148+
model = AnthropicModel('claude-3-5-sonnet-latest', provider=provider)
149+
agent = Agent(model)
150+
151+
result = await agent.run('What is the capital of France?')
152+
assert result.output == snapshot('The capital of France is Paris.')

0 commit comments

Comments
 (0)