Skip to content
Closed
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
51 changes: 45 additions & 6 deletions pydantic_ai_slim/pydantic_ai/messages.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import hashlib
import mimetypes
import os
import re
from abc import ABC, abstractmethod
from collections.abc import Callable, Mapping, Sequence
from dataclasses import KW_ONLY, dataclass, field, replace
Expand Down Expand Up @@ -1634,6 +1635,20 @@ class BuiltinToolCallPart(BaseToolCallPart):
"""A message part returned by a model."""


_DATE_SUFFIX_RE = re.compile(r'-\d{8}$')
"""Matches a date suffix like `-20251211` at the end of a model name."""


def _strip_model_date_suffix(model_name: str) -> str:
"""Strip a date suffix (e.g. `-20251211`) from a model name.

Some providers like OpenRouter return versioned model names such as
`openai/gpt-5.2-20251211`. The date suffix is not recognized by
`genai-prices`, so we strip it for a fallback lookup.
"""
return _DATE_SUFFIX_RE.sub('', model_name)
Comment on lines +1642 to +1649
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Module-level _strip_model_date_suffix is a single-use delegation wrapper that should be inlined (rule:14, rule:176)

_strip_model_date_suffix is a module-level function used exactly once inside ModelResponse.cost() (pydantic_ai_slim/pydantic_ai/messages.py:1801). The function body is a single delegation call: return _DATE_SUFFIX_RE.sub('', model_name). Rule:14 says to inline single-use helpers that only wrap delegation, and rule:176 says to scope helpers to their single usage site rather than defining them at module level. The regex constant _DATE_SUFFIX_RE at module level is fine per rule:499, but the wrapper function should be inlined at the call site.

Prompt for agents
In pydantic_ai_slim/pydantic_ai/messages.py, remove the _strip_model_date_suffix function (lines 1642-1649) and inline its logic at the call site in ModelResponse.cost() (line 1801). Replace:
    normalized = _strip_model_date_suffix(self.model_name)
with:
    normalized = _DATE_SUFFIX_RE.sub('', self.model_name)
Keep the _DATE_SUFFIX_RE module-level constant as-is (rule:499 requires regex patterns at module level).
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.



@dataclass(repr=False)
class ModelResponse:
"""A response from a model, e.g. a message from the model to the Pydantic AI app."""
Expand Down Expand Up @@ -1767,12 +1782,36 @@ def cost(self) -> genai_types.PriceCalculation:
)
except LookupError:
pass
return calc_price(
self.usage,
self.model_name,
provider_id=self.provider_name,
genai_request_timestamp=self.timestamp,
)
try:
return calc_price(
self.usage,
self.model_name,
provider_id=self.provider_name,
genai_request_timestamp=self.timestamp,
)
except LookupError:
pass
# Some providers (e.g. OpenRouter) return model names with a date suffix
# like "openai/gpt-5.2-20251211". Strip the suffix and retry.
normalized = _strip_model_date_suffix(self.model_name)
if normalized != self.model_name:
if self.provider_url:
try:
return calc_price(
self.usage,
normalized,
provider_api_url=self.provider_url,
genai_request_timestamp=self.timestamp,
)
except LookupError:
pass
return calc_price(
self.usage,
normalized,
provider_id=self.provider_name,
genai_request_timestamp=self.timestamp,
)
raise LookupError(f'Could not find pricing for model {self.model_name!r}')

def otel_events(self, settings: InstrumentationSettings) -> list[LogRecord]:
"""Return OpenTelemetry events for the response."""
Expand Down
50 changes: 50 additions & 0 deletions tests/test_messages.py
Original file line number Diff line number Diff line change
Expand Up @@ -1387,3 +1387,53 @@ def test_args_as_dict_raise_if_invalid_non_dict_json():
part = ToolCallPart(tool_name='test_tool', args='[1, 2, 3]')
with pytest.raises(AssertionError):
part.args_as_dict(raise_if_invalid=True)


def test_strip_model_date_suffix():
"""_strip_model_date_suffix removes YYYYMMDD date suffixes from model names."""
from pydantic_ai.messages import _strip_model_date_suffix

# Date suffix should be stripped
assert _strip_model_date_suffix('openai/gpt-5.2-20251211') == 'openai/gpt-5.2'
assert _strip_model_date_suffix('gpt-4o-2024-08-06-20240806') == 'gpt-4o-2024-08-06'
assert _strip_model_date_suffix('claude-sonnet-4-5-20250514') == 'claude-sonnet-4-5'

# No date suffix — unchanged
assert _strip_model_date_suffix('gpt-5.2') == 'gpt-5.2'
assert _strip_model_date_suffix('openai/gpt-5.2') == 'openai/gpt-5.2'
assert _strip_model_date_suffix('claude-sonnet-4-5') == 'claude-sonnet-4-5'

# Short numbers should NOT be stripped (not 8-digit date)
assert _strip_model_date_suffix('gpt-4o-mini-123') == 'gpt-4o-mini-123'


def test_model_response_cost_with_date_suffix():
"""ModelResponse.cost() should fall back to stripped model name when date suffix prevents lookup."""
from decimal import Decimal

from pydantic_ai.messages import ModelResponse
from pydantic_ai.usage import RequestUsage
Comment on lines +1394 to +1397
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Inline imports in test_model_response_cost_with_date_suffix violate rule:464

Rule:464 from agent_docs/index.md requires all imports to be at the top of the file, not inline within functions or test bodies. The test at line 1392 imports Decimal, ModelResponse, and RequestUsage inside the function body. ModelResponse (line 22) and RequestUsage (line 24) are already imported at the top of tests/test_messages.py, making the in-function imports both redundant and a rule violation. Decimal should be added to the file-level imports.

Prompt for agents
In tests/test_messages.py, remove the inline imports from all three new test functions (test_model_response_cost_with_date_suffix at line 1394-1397, test_model_response_cost_with_date_suffix_and_provider_url at line 1426-1429, test_model_response_cost_unknown_model_raises at line 1446-1447). Add `from decimal import Decimal` to the file-level imports at the top of the file (around line 2). `ModelResponse` and `RequestUsage` are already imported at lines 22 and 24 respectively, so those inline imports just need to be deleted.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.


usage = RequestUsage(input_tokens=100, output_tokens=50)

# A model name that genai-prices recognizes
response_clean = ModelResponse(
parts=[],
model_name='gpt-5.2',
provider_name='openai',
usage=usage,
)
cost_clean = response_clean.cost()
assert cost_clean.total_price > Decimal('0')

# Same model but with date suffix (as returned by OpenRouter)
response_dated = ModelResponse(
parts=[],
model_name='openai/gpt-5.2-20251211',
provider_name='litellm',
usage=usage,
)
cost_dated = response_dated.cost()
# Should succeed via fallback and return same pricing
assert cost_dated.total_price > Decimal('0')
assert cost_dated.total_price == cost_clean.total_price
Comment on lines +1392 to +1421
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚩 Test relies on specific genai-prices model data being present

The test at tests/test_messages.py:1392 assumes genai-prices has pricing data for gpt-5.2 with provider openai, and that openai/gpt-5.2-20251211 will NOT be found directly but WILL resolve after stripping the date suffix. If the genai-prices package updates to recognize openai/gpt-5.2-20251211 directly, the test would still pass but would no longer exercise the fallback path. Additionally, if gpt-5.2 pricing is ever removed from genai-prices, the test would break. Consider adding a comment noting this dependency, or testing the _strip_model_date_suffix helper directly as a unit test alongside the integration test.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Loading