Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions docs/integrations/llms/pydanticai.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,3 +56,26 @@ You can also instrument a specific agent with `logfire.instrument_pydantic_ai(ag

For more information, see the [`logfire.instrument_pydantic_ai()`][logfire.Logfire.instrument_pydantic_ai]
reference or the [Pydantic AI docs on instrumenting](https://pydantic.dev/docs/ai/integrations/logfire/) with **Logfire**.

## Keep model pricing up to date

Pydantic AI uses [`genai-prices`](https://github.com/pydantic/genai-prices) to populate the
`operation.cost` span attribute. The model price catalogue is shipped as a snapshot inside the
package, so newly released models (e.g. `gemini-3.5-flash`, `claude-4`, …) only get a cost
calculation once a new version of `genai-prices` is released and pulled in.

To always pick up new prices without waiting for a release, pass `update_genai_prices=True`
to `logfire.configure()`. A daemon thread will refresh the catalogue from upstream every hour
in the background:

```python
import logfire

logfire.configure(update_genai_prices=True)
logfire.instrument_pydantic_ai()
```

The same can be enabled via the `LOGFIRE_UPDATE_GENAI_PRICES=1` environment variable.

The thread needs outbound HTTP access to `raw.githubusercontent.com` and is a no-op (with a
one-shot warning) when `genai-prices` is not installed in the environment.
17 changes: 14 additions & 3 deletions logfire-api/logfire_api/_internal/config.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ class LocalVariablesOptions:

class DeprecatedKwargs(TypedDict): ...

def configure(*, local: bool = False, send_to_logfire: bool | Literal['if-token-present'] | None = None, token: str | list[str] | None = None, api_key: str | None = None, service_name: str | None = None, service_version: str | None = None, environment: str | None = None, console: ConsoleOptions | Literal[False] | None = None, config_dir: Path | str | None = None, data_dir: Path | str | None = None, additional_span_processors: Sequence[SpanProcessor] | None = None, metrics: MetricsOptions | Literal[False] | None = None, scrubbing: ScrubbingOptions | Literal[False] | None = None, inspect_arguments: bool | None = None, sampling: SamplingOptions | None = None, min_level: int | LevelName | None = None, add_baggage_to_attributes: bool = True, code_source: CodeSource | None = None, variables: VariablesOptions | LocalVariablesOptions | None = None, distributed_tracing: bool | None = None, advanced: AdvancedOptions | None = None, **deprecated_kwargs: Unpack[DeprecatedKwargs]) -> Logfire:
def configure(*, local: bool = False, send_to_logfire: bool | Literal['if-token-present'] | None = None, token: str | list[str] | None = None, api_key: str | None = None, service_name: str | None = None, service_version: str | None = None, environment: str | None = None, console: ConsoleOptions | Literal[False] | None = None, config_dir: Path | str | None = None, data_dir: Path | str | None = None, additional_span_processors: Sequence[SpanProcessor] | None = None, metrics: MetricsOptions | Literal[False] | None = None, scrubbing: ScrubbingOptions | Literal[False] | None = None, inspect_arguments: bool | None = None, sampling: SamplingOptions | None = None, min_level: int | LevelName | None = None, add_baggage_to_attributes: bool = True, code_source: CodeSource | None = None, variables: VariablesOptions | LocalVariablesOptions | None = None, distributed_tracing: bool | None = None, update_genai_prices: bool | None = None, advanced: AdvancedOptions | None = None, **deprecated_kwargs: Unpack[DeprecatedKwargs]) -> Logfire:
"""Configure the logfire SDK.

Args:
Expand Down Expand Up @@ -200,6 +200,16 @@ def configure(*, local: bool = False, send_to_logfire: bool | Literal['if-token-
See [Unintentional Distributed Tracing](https://logfire.pydantic.dev/docs/how-to-guides/distributed-tracing/#unintentional-distributed-tracing)
for more information.
This setting always applies globally, and the last value set is used, including the default value.
update_genai_prices: If `True`, start a background daemon thread that periodically refreshes
the `genai-prices` model pricing snapshot from the upstream repository. This keeps
`operation.cost` attributes correct for newly released LLM models (e.g. `gemini-3.5-flash`)
without requiring a package upgrade. Defaults to `False`.

Requires `genai-prices` to be installed (usually transitive via `pydantic-ai`); if it is
missing, a one-shot warning is emitted and the flag is silently ignored. The thread is
stopped via `atexit` on shutdown.

Defaults to the `LOGFIRE_UPDATE_GENAI_PRICES` environment variable, or `False`.
advanced: Advanced options primarily used for testing by Logfire developers.
"""

Expand Down Expand Up @@ -231,17 +241,18 @@ class _LogfireConfigData:
code_source: CodeSource | None
variables: VariablesOptions | LocalVariablesOptions | None
distributed_tracing: bool | None
update_genai_prices: bool
advanced: AdvancedOptions

class LogfireConfig(_LogfireConfigData):
def __init__(self, send_to_logfire: bool | Literal['if-token-present'] | None = None, token: str | list[str] | None = None, api_key: str | None = None, service_name: str | None = None, service_version: str | None = None, environment: str | None = None, console: ConsoleOptions | Literal[False] | None = None, config_dir: Path | None = None, data_dir: Path | None = None, additional_span_processors: Sequence[SpanProcessor] | None = None, metrics: MetricsOptions | Literal[False] | None = None, scrubbing: ScrubbingOptions | Literal[False] | None = None, inspect_arguments: bool | None = None, sampling: SamplingOptions | None = None, min_level: int | LevelName | None = None, add_baggage_to_attributes: bool = True, variables: VariablesOptions | None = None, code_source: CodeSource | None = None, distributed_tracing: bool | None = None, advanced: AdvancedOptions | None = None) -> None:
def __init__(self, send_to_logfire: bool | Literal['if-token-present'] | None = None, token: str | list[str] | None = None, api_key: str | None = None, service_name: str | None = None, service_version: str | None = None, environment: str | None = None, console: ConsoleOptions | Literal[False] | None = None, config_dir: Path | None = None, data_dir: Path | None = None, additional_span_processors: Sequence[SpanProcessor] | None = None, metrics: MetricsOptions | Literal[False] | None = None, scrubbing: ScrubbingOptions | Literal[False] | None = None, inspect_arguments: bool | None = None, sampling: SamplingOptions | None = None, min_level: int | LevelName | None = None, add_baggage_to_attributes: bool = True, variables: VariablesOptions | None = None, code_source: CodeSource | None = None, distributed_tracing: bool | None = None, update_genai_prices: bool | None = None, advanced: AdvancedOptions | None = None) -> None:
"""Create a new LogfireConfig.

Users should never need to call this directly, instead use `logfire.configure`.

See `_LogfireConfigData` for parameter documentation.
"""
def configure(self, send_to_logfire: bool | Literal['if-token-present'] | None, token: str | list[str] | None, api_key: str | None, service_name: str | None, service_version: str | None, environment: str | None, console: ConsoleOptions | Literal[False] | None, config_dir: Path | None, data_dir: Path | None, additional_span_processors: Sequence[SpanProcessor] | None, metrics: MetricsOptions | Literal[False] | None, scrubbing: ScrubbingOptions | Literal[False] | None, inspect_arguments: bool | None, sampling: SamplingOptions | None, min_level: int | LevelName | None, add_baggage_to_attributes: bool, code_source: CodeSource | None, variables: VariablesOptions | LocalVariablesOptions | None, distributed_tracing: bool | None, advanced: AdvancedOptions | None) -> None: ...
def configure(self, send_to_logfire: bool | Literal['if-token-present'] | None, token: str | list[str] | None, api_key: str | None, service_name: str | None, service_version: str | None, environment: str | None, console: ConsoleOptions | Literal[False] | None, config_dir: Path | None, data_dir: Path | None, additional_span_processors: Sequence[SpanProcessor] | None, metrics: MetricsOptions | Literal[False] | None, scrubbing: ScrubbingOptions | Literal[False] | None, inspect_arguments: bool | None, sampling: SamplingOptions | None, min_level: int | LevelName | None, add_baggage_to_attributes: bool, code_source: CodeSource | None, variables: VariablesOptions | LocalVariablesOptions | None, distributed_tracing: bool | None, update_genai_prices: bool | None, advanced: AdvancedOptions | None) -> None: ...
def initialize(self) -> None:
"""Configure internals to start exporting traces and metrics."""
def force_flush(self, timeout_millis: int = 30000) -> bool:
Expand Down
4 changes: 2 additions & 2 deletions logfire-api/logfire_api/_internal/config_params.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,9 @@ from pathlib import Path
from typing import Any, TypeVar

T = TypeVar('T')
slots_true: Incomplete
PydanticPluginRecordValues: Incomplete

@dataclass(**slots_true)
@dataclass(slots=True)
class ConfigParam:
"""A parameter that can be configured for a Logfire instance."""
env_vars: list[str]
Expand Down Expand Up @@ -55,6 +54,7 @@ IGNORE_NO_CONFIG: Incomplete
BASE_URL: Incomplete
DISTRIBUTED_TRACING: Incomplete
EMIT_CONFIGURATION_SPAN: Incomplete
UPDATE_GENAI_PRICES: Incomplete
HTTPX_CAPTURE_ALL: Incomplete
AIOHTTP_CLIENT_CAPTURE_ALL: Incomplete
CONFIG_PARAMS: Incomplete
Expand Down
70 changes: 70 additions & 0 deletions logfire/_internal/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -437,6 +437,7 @@ def configure(
code_source: CodeSource | None = None,
variables: VariablesOptions | LocalVariablesOptions | None = None,
distributed_tracing: bool | None = None,
update_genai_prices: bool | None = None,
advanced: AdvancedOptions | None = None,
**deprecated_kwargs: Unpack[DeprecatedKwargs],
) -> Logfire:
Expand Down Expand Up @@ -512,6 +513,16 @@ def configure(
See [Unintentional Distributed Tracing](https://logfire.pydantic.dev/docs/how-to-guides/distributed-tracing/#unintentional-distributed-tracing)
for more information.
This setting always applies globally, and the last value set is used, including the default value.
update_genai_prices: If `True`, start a background daemon thread that periodically refreshes
the `genai-prices` model pricing snapshot from the upstream repository. This keeps
`operation.cost` attributes correct for newly released LLM models (e.g. `gemini-3.5-flash`)
without requiring a package upgrade. Defaults to `False`.

Requires `genai-prices` to be installed (usually transitive via `pydantic-ai`); if it is
missing, a one-shot warning is emitted and the flag is silently ignored. The thread is
stopped via `atexit` on shutdown.

Defaults to the `LOGFIRE_UPDATE_GENAI_PRICES` environment variable, or `False`.
advanced: Advanced options primarily used for testing by Logfire developers.
"""
from .. import DEFAULT_LOGFIRE_INSTANCE, Logfire
Expand Down Expand Up @@ -645,6 +656,7 @@ def configure(
code_source=code_source,
variables=variables,
distributed_tracing=distributed_tracing,
update_genai_prices=update_genai_prices,
advanced=advanced,
)

Expand Down Expand Up @@ -728,6 +740,9 @@ class _LogfireConfigData:
distributed_tracing: bool | None
"""Whether to extract incoming trace context."""

update_genai_prices: bool
"""Whether to refresh `genai-prices` model pricing snapshot in the background."""

advanced: AdvancedOptions
"""Advanced options primarily used for testing by Logfire developers."""

Expand Down Expand Up @@ -755,6 +770,7 @@ def _load_configuration(
code_source: CodeSource | None,
variables: VariablesOptions | LocalVariablesOptions | None,
distributed_tracing: bool | None,
update_genai_prices: bool | None,
advanced: AdvancedOptions | None,
) -> None:
"""Merge the given parameters with the environment variables file configurations."""
Expand All @@ -770,6 +786,7 @@ def _load_configuration(
self.data_dir = param_manager.load_param('data_dir', data_dir)
self.inspect_arguments = param_manager.load_param('inspect_arguments', inspect_arguments)
self.distributed_tracing = param_manager.load_param('distributed_tracing', distributed_tracing)
self.update_genai_prices = param_manager.load_param('update_genai_prices', update_genai_prices)
self.ignore_no_config = param_manager.load_param('ignore_no_config')
min_level = param_manager.load_param('min_level', min_level)
if min_level is None:
Expand Down Expand Up @@ -892,6 +909,7 @@ def __init__(
variables: VariablesOptions | None = None,
code_source: CodeSource | None = None,
distributed_tracing: bool | None = None,
update_genai_prices: bool | None = None,
advanced: AdvancedOptions | None = None,
) -> None:
"""Create a new LogfireConfig.
Expand Down Expand Up @@ -922,6 +940,7 @@ def __init__(
code_source=code_source,
variables=variables,
distributed_tracing=distributed_tracing,
update_genai_prices=update_genai_prices,
advanced=advanced,
)
# initialize with no-ops so that we don't impact OTEL's global config just because logfire is installed
Expand All @@ -936,6 +955,8 @@ def __init__(
self._has_set_providers = False
self._initialized = False
self._lock = RLock()
# Background updater for genai-prices snapshot; created on demand in _initialize.
self._genai_prices_updater: Any = None

def configure(
self,
Expand All @@ -958,6 +979,7 @@ def configure(
code_source: CodeSource | None,
variables: VariablesOptions | LocalVariablesOptions | None,
distributed_tracing: bool | None,
update_genai_prices: bool | None,
advanced: AdvancedOptions | None,
) -> None:
with self._lock:
Expand All @@ -982,6 +1004,7 @@ def configure(
code_source,
variables,
distributed_tracing,
update_genai_prices,
advanced,
)
self.initialize()
Expand All @@ -995,6 +1018,10 @@ def _initialize(self) -> None:
if self._initialized: # pragma: no cover
return

# If we are reconfiguring (e.g. configure called a second time), stop the previous
# genai-prices updater thread cleanly before any new one is created below.
self._stop_genai_prices_updater()

emscripten = platform_is_emscripten()

with suppress_instrumentation():
Expand Down Expand Up @@ -1400,6 +1427,48 @@ def fix_pid(): # pragma: no cover

self._ensure_flush_after_aws_lambda()

if not emscripten:
self._maybe_start_genai_prices_updater()

def _maybe_start_genai_prices_updater(self) -> None:
"""Start a background daemon to refresh the `genai-prices` snapshot.

Only runs when `update_genai_prices=True`. Silently no-ops if `genai-prices` is not
installed (it is a transitive dep, typically via `pydantic-ai`).
"""
if not self.update_genai_prices:
return
if self._genai_prices_updater is not None: # pragma: no cover
return
try:
from genai_prices.update_prices import UpdatePrices
except ImportError:
warnings.warn(
'logfire.configure(update_genai_prices=True) requires the `genai-prices` package '
'to be installed. Install it directly or pull it in transitively via `pydantic-ai`. '
'Auto-update is disabled for this session.',
stacklevel=2,
)
return
try:
updater = UpdatePrices()
updater.start()
except Exception: # pragma: no cover
return
self._genai_prices_updater = updater
atexit.register(self._stop_genai_prices_updater)

def _stop_genai_prices_updater(self) -> None:
"""Stop the `genai-prices` background updater if it is running."""
updater = self._genai_prices_updater
if updater is None:
return
self._genai_prices_updater = None
try:
updater.stop()
except Exception: # pragma: no cover
pass

def force_flush(self, timeout_millis: int = 30_000) -> bool:
"""Force flush all spans and metrics.

Expand Down Expand Up @@ -1594,6 +1663,7 @@ def emit_configuration_span(config: LogfireConfig, logfire_instance: Logfire, *,
'min_level': config.min_level,
'add_baggage_to_attributes': config.add_baggage_to_attributes,
'distributed_tracing': config.distributed_tracing,
'update_genai_prices': config.update_genai_prices,
'head_sample_rate': sampling.head if isinstance(sampling.head, (int, float)) else None,
'tail_sampling_enabled': sampling.tail is not None,
'code_source_set': config.code_source is not None,
Expand Down
8 changes: 8 additions & 0 deletions logfire/_internal/config_params.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,13 @@ class _DefaultCallback:
"""Whether to extract incoming trace context. By default, will extract but warn about it."""
EMIT_CONFIGURATION_SPAN = ConfigParam(env_vars=['LOGFIRE_EMIT_CONFIGURATION_SPAN'], allow_file_config=True, default=False, tp=bool)
"""Whether to emit a `Logfire configured` log span after `logfire.configure()`."""
UPDATE_GENAI_PRICES = ConfigParam(env_vars=['LOGFIRE_UPDATE_GENAI_PRICES'], allow_file_config=True, default=False, tp=bool)
"""Whether to refresh the `genai-prices` model pricing snapshot in the background.

When enabled, Logfire starts a daemon thread that periodically fetches the latest pricing
data from the `pydantic/genai-prices` repository, so that newly released models get correct
`operation.cost` attributes without requiring a package upgrade. Requires `genai-prices` to
be installed (usually transitively via `pydantic-ai`)."""

# Instrumentation packages parameters
HTTPX_CAPTURE_ALL = ConfigParam(env_vars=['LOGFIRE_HTTPX_CAPTURE_ALL'], allow_file_config=True, default=False, tp=bool)
Expand Down Expand Up @@ -142,6 +149,7 @@ class _DefaultCallback:
'ignore_no_config': IGNORE_NO_CONFIG,
'distributed_tracing': DISTRIBUTED_TRACING,
'emit_configuration_span': EMIT_CONFIGURATION_SPAN,
'update_genai_prices': UPDATE_GENAI_PRICES,
# Instrumentation packages parameters
'httpx_capture_all': HTTPX_CAPTURE_ALL,
'aiohttp_client_capture_all': AIOHTTP_CLIENT_CAPTURE_ALL,
Expand Down
Loading
Loading