-
Notifications
You must be signed in to change notification settings - Fork 1k
Add settings
to Model base class
#2136
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
cb9aa0f
4a6ad50
df93aa4
eff2619
f8702c6
69aa087
c316d33
09b47dd
6d4dcc8
4b1e44a
42622cf
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -124,6 +124,39 @@ The `ModelResponse` message above indicates in the `model_name` field that the o | |||||
!!! note | ||||||
Each model's options should be configured individually. For example, `base_url`, `api_key`, and custom clients should be set on each model itself, not on the `FallbackModel`. | ||||||
|
||||||
### Per-Model Settings | ||||||
|
||||||
You can configure different `ModelSettings` for each model in a fallback chain by passing the `settings` parameter when creating each model. This is particularly useful when different providers have different optimal configurations: | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
|
||||||
```python {title="fallback_model_per_settings.py"} | ||||||
from pydantic_ai import Agent | ||||||
from pydantic_ai.models.anthropic import AnthropicModel | ||||||
from pydantic_ai.models.fallback import FallbackModel | ||||||
from pydantic_ai.models.openai import OpenAIModel | ||||||
from pydantic_ai.settings import ModelSettings | ||||||
|
||||||
# Configure each model with provider-specific optimal settings | ||||||
openai_model = OpenAIModel( | ||||||
'gpt-4o', | ||||||
settings=ModelSettings(temperature=0.7, max_tokens=1000) # Higher creativity for OpenAI | ||||||
) | ||||||
anthropic_model = AnthropicModel( | ||||||
'claude-3-5-sonnet-latest', | ||||||
settings=ModelSettings(temperature=0.2, max_tokens=1000) # Lower temperature for consistency | ||||||
) | ||||||
|
||||||
fallback_model = FallbackModel(openai_model, anthropic_model) | ||||||
agent = Agent(fallback_model) | ||||||
|
||||||
result = agent.run_sync('Write a creative story about space exploration') | ||||||
print(result.output) | ||||||
""" | ||||||
In the year 2157, Captain Maya Chen piloted her spacecraft through the vast expanse of the Andromeda Galaxy. As she discovered a planet with crystalline mountains that sang in harmony with the cosmic winds, she realized that space exploration was not just about finding new worlds, but about finding new ways to understand the universe and our place within it. | ||||||
""" | ||||||
``` | ||||||
|
||||||
In this example, if the OpenAI model fails, the agent will automatically fall back to the Anthropic model with its own configured settings. The `FallbackModel` itself doesn't have settings - it uses the individual settings of whichever model successfully handles the request. | ||||||
|
||||||
In this next example, we demonstrate the exception-handling capabilities of `FallbackModel`. | ||||||
If all models fail, a [`FallbackExceptionGroup`][pydantic_ai.exceptions.FallbackExceptionGroup] is raised, which | ||||||
contains all the exceptions encountered during the `run` execution. | ||||||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -674,12 +674,24 @@ async def main(): | |
# typecast reasonable, even though it is possible to violate it with otherwise-type-checked code. | ||
output_validators = cast(list[_output.OutputValidator[AgentDepsT, RunOutputDataT]], self._output_validators) | ||
|
||
# Merge model settings from any Model | ||
if isinstance(model_used, InstrumentedModel): | ||
# For InstrumentedModel, get settings from the wrapped model | ||
wrapped_model_settings = getattr(model_used.wrapped, 'settings', None) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we define There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That may require us to rename the field on |
||
if wrapped_model_settings is not None: | ||
model_settings = merge_model_settings(wrapped_model_settings, model_settings) | ||
else: | ||
# For regular models, use their settings directly | ||
current_settings = getattr(model_used, 'settings', None) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can this be |
||
if current_settings is not None: | ||
model_settings = merge_model_settings(current_settings, model_settings) | ||
|
||
model_settings = merge_model_settings(self.model_settings, model_settings) | ||
usage_limits = usage_limits or _usage.UsageLimits() | ||
|
||
if isinstance(model_used, InstrumentedModel): | ||
instrumentation_settings = model_used.settings | ||
tracer = model_used.settings.tracer | ||
instrumentation_settings = model_used.instrumentation_settings | ||
tracer = model_used.instrumentation_settings.tracer | ||
else: | ||
instrumentation_settings = None | ||
tracer = NoOpTracer() | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -182,23 +182,26 @@ def messages_to_otel_events(self, messages: list[ModelMessage]) -> list[Event]: | |
GEN_AI_REQUEST_MODEL_ATTRIBUTE = 'gen_ai.request.model' | ||
|
||
|
||
@dataclass | ||
@dataclass(init=False) | ||
class InstrumentedModel(WrapperModel): | ||
"""Model which wraps another model so that requests are instrumented with OpenTelemetry. | ||
|
||
See the [Debugging and Monitoring guide](https://ai.pydantic.dev/logfire/) for more info. | ||
""" | ||
|
||
settings: InstrumentationSettings | ||
"""Configuration for instrumenting requests.""" | ||
instrumentation_settings: InstrumentationSettings | ||
"""Instrumentation settings for this model.""" | ||
|
||
def __init__( | ||
self, | ||
wrapped: Model | KnownModelName, | ||
options: InstrumentationSettings | None = None, | ||
) -> None: | ||
super().__init__(wrapped) | ||
self.settings = options or InstrumentationSettings() | ||
# Store instrumentation settings separately from model settings | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think we need this comment or the next one, with the different field names it's clear enough what's happening |
||
self.instrumentation_settings = options or InstrumentationSettings() | ||
# Initialize base Model with no settings to avoid storing InstrumentationSettings there | ||
Model.__init__(self, settings=None) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If we make |
||
|
||
async def request( | ||
self, | ||
|
@@ -260,7 +263,7 @@ def _instrument( | |
|
||
record_metrics: Callable[[], None] | None = None | ||
try: | ||
with self.settings.tracer.start_as_current_span(span_name, attributes=attributes) as span: | ||
with self.instrumentation_settings.tracer.start_as_current_span(span_name, attributes=attributes) as span: | ||
|
||
def finish(response: ModelResponse): | ||
# FallbackModel updates these span attributes. | ||
|
@@ -278,12 +281,12 @@ def _record_metrics(): | |
'gen_ai.response.model': response_model, | ||
} | ||
if response.usage.request_tokens: # pragma: no branch | ||
self.settings.tokens_histogram.record( | ||
self.instrumentation_settings.tokens_histogram.record( | ||
response.usage.request_tokens, | ||
{**metric_attributes, 'gen_ai.token.type': 'input'}, | ||
) | ||
if response.usage.response_tokens: # pragma: no branch | ||
self.settings.tokens_histogram.record( | ||
self.instrumentation_settings.tokens_histogram.record( | ||
response.usage.response_tokens, | ||
{**metric_attributes, 'gen_ai.token.type': 'output'}, | ||
) | ||
|
@@ -294,8 +297,8 @@ def _record_metrics(): | |
if not span.is_recording(): | ||
return | ||
|
||
events = self.settings.messages_to_otel_events(messages) | ||
for event in self.settings.messages_to_otel_events([response]): | ||
events = self.instrumentation_settings.messages_to_otel_events(messages) | ||
for event in self.instrumentation_settings.messages_to_otel_events([response]): | ||
events.append( | ||
Event( | ||
'gen_ai.choice', | ||
|
@@ -328,9 +331,9 @@ def _record_metrics(): | |
record_metrics() | ||
|
||
def _emit_events(self, span: Span, events: list[Event]) -> None: | ||
if self.settings.event_mode == 'logs': | ||
if self.instrumentation_settings.event_mode == 'logs': | ||
for event in events: | ||
self.settings.event_logger.emit(event) | ||
self.instrumentation_settings.event_logger.emit(event) | ||
else: | ||
attr_name = 'events' | ||
span.set_attributes( | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -42,6 +42,10 @@ class MCPSamplingModel(Model): | |
[`ModelSettings`][pydantic_ai.settings.ModelSettings], so this value is used as fallback. | ||
""" | ||
|
||
def __post_init__(self): | ||
"""Initialize the base Model class.""" | ||
super().__init__() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We can likely also remove this if |
||
|
||
async def request( | ||
self, | ||
messages: list[ModelMessage], | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we can drop this line as the list already explicitly states the priority.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually, it may be useful to write here that the settings are merged, not replaced.