Skip to content

Commit 9d50a69

Browse files
committed
Merge branch 'main' into feature/native-mcp-support
# Conflicts: # tests/models/test_openai_responses.py
2 parents ceb7113 + f3f40fe commit 9d50a69

File tree

19 files changed

+542
-49
lines changed

19 files changed

+542
-49
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ We built Pydantic AI with one simple aim: to bring that FastAPI feeling to GenAI
3939
[Pydantic Validation](https://docs.pydantic.dev/latest/) is the validation layer of the OpenAI SDK, the Google ADK, the Anthropic SDK, LangChain, LlamaIndex, AutoGPT, Transformers, CrewAI, Instructor and many more. _Why use the derivative when you can go straight to the source?_ :smiley:
4040

4141
2. **Model-agnostic**:
42-
Supports virtually every [model](https://ai.pydantic.dev/models/overview) and provider: OpenAI, Anthropic, Gemini, DeepSeek, Grok, Cohere, Mistral, and Perplexity; Azure AI Foundry, Amazon Bedrock, Google Vertex AI, Ollama, LiteLLM, Groq, OpenRouter, Together AI, Fireworks AI, Cerebras, Hugging Face, GitHub, Heroku, Vercel. If your favorite model or provider is not listed, you can easily implement a [custom model](https://ai.pydantic.dev/models/overview#custom-models).
42+
Supports virtually every [model](https://ai.pydantic.dev/models/overview) and provider: OpenAI, Anthropic, Gemini, DeepSeek, Grok, Cohere, Mistral, and Perplexity; Azure AI Foundry, Amazon Bedrock, Google Vertex AI, Ollama, LiteLLM, Groq, OpenRouter, Together AI, Fireworks AI, Cerebras, Hugging Face, GitHub, Heroku, Vercel, Nebius. If your favorite model or provider is not listed, you can easily implement a [custom model](https://ai.pydantic.dev/models/overview#custom-models).
4343

4444
3. **Seamless Observability**:
4545
Tightly [integrates](https://ai.pydantic.dev/logfire) with [Pydantic Logfire](https://pydantic.dev/logfire), our general-purpose OpenTelemetry observability platform, for real-time debugging, evals-based performance monitoring, and behavior, tracing, and cost tracking. If you already have an observability platform that supports OTel, you can [use that too](https://ai.pydantic.dev/logfire#alternative-observability-backends).

docs/api/providers.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,3 +41,5 @@
4141
::: pydantic_ai.providers.ollama.OllamaProvider
4242

4343
::: pydantic_ai.providers.litellm.LiteLLMProvider
44+
45+
::: pydantic_ai.providers.nebius.NebiusProvider

docs/index.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ We built Pydantic AI with one simple aim: to bring that FastAPI feeling to GenAI
1414
[Pydantic Validation](https://docs.pydantic.dev/latest/) is the validation layer of the OpenAI SDK, the Google ADK, the Anthropic SDK, LangChain, LlamaIndex, AutoGPT, Transformers, CrewAI, Instructor and many more. _Why use the derivative when you can go straight to the source?_ :smiley:
1515

1616
2. **Model-agnostic**:
17-
Supports virtually every [model](models/overview.md) and provider: OpenAI, Anthropic, Gemini, DeepSeek, Grok, Cohere, Mistral, and Perplexity; Azure AI Foundry, Amazon Bedrock, Google Vertex AI, Ollama, LiteLLM, Groq, OpenRouter, Together AI, Fireworks AI, Cerebras, Hugging Face, GitHub, Heroku, Vercel. If your favorite model or provider is not listed, you can easily implement a [custom model](models/overview.md#custom-models).
17+
Supports virtually every [model](models/overview.md) and provider: OpenAI, Anthropic, Gemini, DeepSeek, Grok, Cohere, Mistral, and Perplexity; Azure AI Foundry, Amazon Bedrock, Google Vertex AI, Ollama, LiteLLM, Groq, OpenRouter, Together AI, Fireworks AI, Cerebras, Hugging Face, GitHub, Heroku, Vercel, Nebius. If your favorite model or provider is not listed, you can easily implement a [custom model](models/overview.md#custom-models).
1818

1919
3. **Seamless Observability**:
2020
Tightly [integrates](logfire.md) with [Pydantic Logfire](https://pydantic.dev/logfire), our general-purpose OpenTelemetry observability platform, for real-time debugging, evals-based performance monitoring, and behavior, tracing, and cost tracking. If you already have an observability platform that supports OTel, you can [use that too](logfire.md#alternative-observability-backends).

docs/models/openai.md

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -608,3 +608,35 @@ print(result.output)
608608
#> The capital of France is Paris.
609609
...
610610
```
611+
612+
### Nebius AI Studio
613+
614+
Go to [Nebius AI Studio](https://studio.nebius.com/) and create an API key.
615+
616+
Once you've set the `NEBIUS_API_KEY` environment variable, you can run the following:
617+
618+
```python
619+
from pydantic_ai import Agent
620+
621+
agent = Agent('nebius:Qwen/Qwen3-32B-fast')
622+
result = agent.run_sync('What is the capital of France?')
623+
print(result.output)
624+
#> The capital of France is Paris.
625+
```
626+
627+
If you need to configure the provider, you can use the [`NebiusProvider`][pydantic_ai.providers.nebius.NebiusProvider] class:
628+
629+
```python
630+
from pydantic_ai import Agent
631+
from pydantic_ai.models.openai import OpenAIChatModel
632+
from pydantic_ai.providers.nebius import NebiusProvider
633+
634+
model = OpenAIChatModel(
635+
'Qwen/Qwen3-32B-fast',
636+
provider=NebiusProvider(api_key='your-nebius-api-key'),
637+
)
638+
agent = Agent(model)
639+
result = agent.run_sync('What is the capital of France?')
640+
print(result.output)
641+
#> The capital of France is Paris.
642+
```

docs/models/overview.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ In addition, many providers are compatible with the OpenAI API, and can be used
2828
- [GitHub Models](openai.md#github-models)
2929
- [Cerebras](openai.md#cerebras)
3030
- [LiteLLM](openai.md#litellm)
31+
- [Nebius AI Studio](openai.md#nebius-ai-studio)
3132

3233
Pydantic AI also comes with [`TestModel`](../api/models/test.md) and [`FunctionModel`](../api/models/function.md)
3334
for testing and development.

pydantic_ai_slim/pydantic_ai/_parts_manager.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -312,6 +312,7 @@ def handle_tool_call_part(
312312
tool_name: str,
313313
args: str | dict[str, Any] | None,
314314
tool_call_id: str | None = None,
315+
id: str | None = None,
315316
) -> ModelResponseStreamEvent:
316317
"""Immediately create or fully-overwrite a ToolCallPart with the given information.
317318
@@ -323,6 +324,7 @@ def handle_tool_call_part(
323324
tool_name: The name of the tool being invoked.
324325
args: The arguments for the tool call, either as a string, a dictionary, or None.
325326
tool_call_id: An optional string identifier for this tool call.
327+
id: An optional identifier for this tool call part.
326328
327329
Returns:
328330
ModelResponseStreamEvent: A `PartStartEvent` indicating that a new tool call part
@@ -332,6 +334,7 @@ def handle_tool_call_part(
332334
tool_name=tool_name,
333335
args=args,
334336
tool_call_id=tool_call_id or _generate_tool_call_id(),
337+
id=id,
335338
)
336339
if vendor_part_id is None:
337340
# vendor_part_id is None, so we unconditionally append a new ToolCallPart to the end of the list

pydantic_ai_slim/pydantic_ai/durable_exec/temporal/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,8 @@ def configure_worker(self, config: WorkerConfig) -> WorkerConfig:
6262
'logfire',
6363
'rich',
6464
'httpx',
65+
'anyio',
66+
'httpcore',
6567
# Imported inside `logfire._internal.json_encoder` when running `logfire.info` inside an activity with attributes to serialize
6668
'attrs',
6769
# Imported inside `logfire._internal.json_schema` when running `logfire.info` inside an activity with attributes to serialize

pydantic_ai_slim/pydantic_ai/messages.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1052,6 +1052,13 @@ class BaseToolCallPart:
10521052
In case the tool call id is not provided by the model, Pydantic AI will generate a random one.
10531053
"""
10541054

1055+
_: KW_ONLY
1056+
1057+
id: str | None = None
1058+
"""An optional identifier of the tool call part, separate from the tool call ID.
1059+
1060+
This is used by some APIs like OpenAI Responses."""
1061+
10551062
def args_as_dict(self) -> dict[str, Any]:
10561063
"""Return the arguments as a Python dictionary.
10571064

pydantic_ai_slim/pydantic_ai/models/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -691,6 +691,7 @@ def infer_model(model: Model | KnownModelName | str) -> Model: # noqa: C901
691691
'together',
692692
'vercel',
693693
'litellm',
694+
'nebius',
694695
):
695696
from .openai import OpenAIChatModel
696697

pydantic_ai_slim/pydantic_ai/models/openai.py

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -284,6 +284,7 @@ def __init__(
284284
'together',
285285
'vercel',
286286
'litellm',
287+
'nebius',
287288
]
288289
| Provider[AsyncOpenAI] = 'openai',
289290
profile: ModelProfileSpec | None = None,
@@ -312,6 +313,7 @@ def __init__(
312313
'together',
313314
'vercel',
314315
'litellm',
316+
'nebius',
315317
]
316318
| Provider[AsyncOpenAI] = 'openai',
317319
profile: ModelProfileSpec | None = None,
@@ -339,6 +341,7 @@ def __init__(
339341
'together',
340342
'vercel',
341343
'litellm',
344+
'nebius',
342345
]
343346
| Provider[AsyncOpenAI] = 'openai',
344347
profile: ModelProfileSpec | None = None,
@@ -899,7 +902,7 @@ def __init__(
899902
self,
900903
model_name: OpenAIModelName,
901904
*,
902-
provider: Literal['openai', 'deepseek', 'azure', 'openrouter', 'grok', 'fireworks', 'together']
905+
provider: Literal['openai', 'deepseek', 'azure', 'openrouter', 'grok', 'fireworks', 'together', 'nebius']
903906
| Provider[AsyncOpenAI] = 'openai',
904907
profile: ModelProfileSpec | None = None,
905908
settings: ModelSettings | None = None,
@@ -1005,7 +1008,12 @@ def _process_response( # noqa: C901
10051008
items.append(TextPart(content.text, id=item.id))
10061009
elif isinstance(item, responses.ResponseFunctionToolCall):
10071010
items.append(
1008-
ToolCallPart(item.name, item.arguments, tool_call_id=_combine_tool_call_ids(item.call_id, item.id))
1011+
ToolCallPart(
1012+
item.name,
1013+
item.arguments,
1014+
tool_call_id=item.call_id,
1015+
id=item.id,
1016+
)
10091017
)
10101018
elif isinstance(item, responses.ResponseCodeInterpreterToolCall):
10111019
call_part, return_part, file_parts = _map_code_interpreter_tool_call(item, self.system)
@@ -1379,6 +1387,7 @@ async def _map_messages( # noqa: C901
13791387
elif isinstance(item, ToolCallPart):
13801388
call_id = _guard_tool_call_id(t=item)
13811389
call_id, id = _split_combined_tool_call_id(call_id)
1390+
id = id or item.id
13821391

13831392
param = responses.ResponseFunctionToolCallParam(
13841393
name=item.tool_name,
@@ -1779,7 +1788,8 @@ async def _get_event_iterator(self) -> AsyncIterator[ModelResponseStreamEvent]:
17791788
vendor_part_id=chunk.item.id,
17801789
tool_name=chunk.item.name,
17811790
args=chunk.item.arguments,
1782-
tool_call_id=_combine_tool_call_ids(chunk.item.call_id, chunk.item.id),
1791+
tool_call_id=chunk.item.call_id,
1792+
id=chunk.item.id,
17831793
)
17841794
elif isinstance(chunk.item, responses.ResponseReasoningItem):
17851795
pass
@@ -2057,18 +2067,15 @@ def _map_usage(response: chat.ChatCompletion | ChatCompletionChunk | responses.R
20572067
return u
20582068

20592069

2060-
def _combine_tool_call_ids(call_id: str, id: str | None) -> str:
2070+
def _split_combined_tool_call_id(combined_id: str) -> tuple[str, str | None]:
20612071
# When reasoning, the Responses API requires the `ResponseFunctionToolCall` to be returned with both the `call_id` and `id` fields.
2062-
# Our `ToolCallPart` has only the `call_id` field, so we combine the two fields into a single string.
2063-
return f'{call_id}|{id}' if id else call_id
2072+
# Before our `ToolCallPart` gained the `id` field alongside `tool_call_id` field, we combined the two fields into a single string stored on `tool_call_id`.
20642073

2065-
2066-
def _split_combined_tool_call_id(combined_id: str) -> tuple[str, str | None]:
20672074
if '|' in combined_id:
20682075
call_id, id = combined_id.split('|', 1)
20692076
return call_id, id
20702077
else:
2071-
return combined_id, None # pragma: no cover
2078+
return combined_id, None
20722079

20732080

20742081
def _map_code_interpreter_tool_call(

0 commit comments

Comments
 (0)