Skip to content

Commit 8600dbd

Browse files
authored
feat(ai): Normalize model names for a better cost calculation (#103508)
After the changes in getsentry/relay#5374, we can now start generating globs only for prefix matching. We no longer need to match suffixes, because the model name normalization is now done in relay. Closes [TET-1268: Normalize models name in relay to make it easier to calculate costs](https://linear.app/getsentry/issue/TET-1268/normalize-models-name-in-relay-to-make-it-easier-to-calculate-costs)
1 parent 3bd7556 commit 8600dbd

File tree

2 files changed

+83
-126
lines changed

2 files changed

+83
-126
lines changed

src/sentry/tasks/ai_agent_monitoring.py

Lines changed: 23 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -26,55 +26,28 @@
2626
MODELS_DEV_API_URL = "https://models.dev/api.json"
2727

2828

29-
def _create_suffix_glob_model_name(model_id: str) -> str:
29+
def _normalize_model_id(model_id: str) -> str:
3030
"""
31-
Create a suffix glob version of a model name by stripping dates and versions.
32-
33-
Examples:
34-
- "claude-4-sonnet-20250522" -> "claude-4-sonnet-*"
35-
- "o3-pro-2025-06-10" -> "o3-pro-*"
36-
- "claude-3-5-haiku@20241022" -> "claude-3-5-haiku@*"
37-
- "claude-opus-4-1-20250805-v1:0" -> "claude-opus-4-1-*"
31+
Normalize a model id by removing dates and versions.
32+
Example:
33+
- "gpt-4" -> "gpt-4"
34+
- "gpt-4-20241022" -> "gpt-4"
35+
- "gpt-4-v1.0" -> "gpt-4"
36+
- "gpt-4-20241022-v1.0" -> "gpt-4"
37+
- "gpt-4-20241022-v1.0-beta" -> "gpt-4"
38+
- "gpt-4-20241022-v1.0-beta-1" -> "gpt-4"
39+
- "gpt-4-20241022-v1.0-beta-1" -> "gpt-4"
3840
3941
Args:
40-
model_id: The original model ID
42+
model_id: The model id to normalize
4143
4244
Returns:
43-
The glob version of the model name
45+
The normalized model id
4446
"""
45-
# Pattern to match various date and version formats
46-
# Matches:
47-
# - YYYYMMDD (e.g., 20250522)
48-
# - YYYY-MM-DD (e.g., 2025-06-10)
49-
# - YYYY/MM/DD (e.g., 2025/06/10)
50-
# - YYYY.MM.DD (e.g., 2025.06.10)
51-
# - v followed by version numbers (e.g., v1:0, v2.1, v3)
52-
# - @ followed by dates (e.g., @20241022)
53-
# - -v followed by version numbers (e.g., -v1.0)
54-
# - _v followed by version numbers (e.g., _v1.0)
55-
56-
# Use a single comprehensive regex that handles all patterns
57-
# This regex matches:
58-
# 1. Date patterns: -YYYYMMDD, -YYYY-MM-DD, -YYYY/MM/DD, -YYYY.MM.DD
59-
# 2. Version patterns: -v1.0, -v1:0, _v1.0, _v1:0
60-
# 3. @date patterns: @YYYYMMDD, @YYYY-MM-DD, @YYYY/MM/DD, @YYYY.MM.DD
61-
# 4. Combined patterns: -YYYYMMDD-v1:0, @YYYYMMDD-v1:0
62-
63-
# First, handle @date patterns (they have special handling)
64-
glob_name = re.sub(
65-
r"@(?:19|20)\d{2}(?:[-_/.]?\d{2}){2}(?:[-_]v\d+(?:[.:]\d+)*)?", "@*", model_id
66-
)
67-
68-
# Then handle regular date and version patterns
69-
glob_name = re.sub(
70-
r"([-_])(?:19|20)\d{2}(?:[-_/.]?\d{2}){2}(?:[-_]v\d+(?:[.:]\d+)*)?", r"\1*", glob_name
47+
return re.sub(
48+
r"(([-_@])(\d{4}[-/.]\d{2}[-/.]\d{2}|\d{8}))?([-_]v\d+[:.]?\d*([-:].*)?)?$", "", model_id
7149
)
7250

73-
# Handle standalone version patterns (without dates)
74-
glob_name = re.sub(r"([-_])v\d+(?:[.:]\d+)*", r"\1*", glob_name)
75-
76-
return glob_name
77-
7851

7952
def _create_prefix_glob_model_name(model_id: str) -> str:
8053
"""
@@ -87,8 +60,6 @@ def _create_prefix_glob_model_name(model_id: str) -> str:
8760
- "gpt-4" -> "*gpt-4"
8861
- "claude-3-5-sonnet" -> "*claude-3-5-sonnet"
8962
- "o3-pro" -> "*o3-pro"
90-
- "gpt-4o-mini-*" -> "*gpt-4o-mini-*"
91-
- "model@*" -> "*model@*"
9263
9364
Args:
9465
model_id: The original model ID or a suffix-globbed model name
@@ -104,13 +75,10 @@ def _add_glob_model_names(models_dict: dict[ModelId, AIModelCostV2]) -> None:
10475
"""
10576
Add glob versions of model names to the models dictionary.
10677
107-
For each model, creates three types of glob versions:
108-
1. Suffix pattern: stripping dates and versions (e.g., "model-20241022" -> "model-*")
109-
2. Prefix pattern: adding wildcard prefix (e.g., "gpt-4" -> "*gpt-4")
110-
3. Prefix-suffix pattern: both wildcards for suffix-globbed names (e.g., "model-*" -> "*model-*")
78+
For each model, it creates a normalized model name, and a prefix glob version of
79+
the model name.
80+
11181
112-
We DO NOT want to have prefix-suffix glob patterns for models that don't have a suffix glob pattern
113-
because that would result in too fuzzy matching, e.g. "gpt-4" -> "*gpt-4*" would match "gpt-4o-mini".
11482
11583
Args:
11684
models_dict: The dictionary of models to add glob versions to
@@ -120,20 +88,13 @@ def _add_glob_model_names(models_dict: dict[ModelId, AIModelCostV2]) -> None:
12088
model_ids = list(models_dict.keys())
12189

12290
for model_id in model_ids:
123-
# Add suffix glob pattern (strip dates/versions)
124-
suffix_glob_name = _create_suffix_glob_model_name(model_id)
125-
if suffix_glob_name != model_id and suffix_glob_name not in models_dict:
126-
models_dict[suffix_glob_name] = models_dict[model_id]
127-
128-
# Add prefix-suffix glob pattern (both wildcards) only for models that have a suffix glob
129-
prefix_suffix_glob_name = _create_prefix_glob_model_name(suffix_glob_name)
130-
if prefix_suffix_glob_name not in models_dict:
131-
models_dict[prefix_suffix_glob_name] = models_dict[model_id]
132-
133-
# Add prefix glob pattern (wildcard prefix)
134-
prefix_glob_name = _create_prefix_glob_model_name(model_id)
91+
normalized_model_id = _normalize_model_id(model_id)
92+
if normalized_model_id != model_id and normalized_model_id not in models_dict:
93+
models_dict[normalized_model_id] = models_dict[model_id]
94+
95+
prefix_glob_name = _create_prefix_glob_model_name(normalized_model_id)
13596
if prefix_glob_name not in models_dict:
136-
models_dict[prefix_glob_name] = models_dict[model_id]
97+
models_dict[prefix_glob_name] = models_dict[normalized_model_id]
13798

13899

139100
@instrumented_task(

tests/sentry/tasks/test_ai_agent_monitoring.py

Lines changed: 60 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -482,9 +482,9 @@ def test_fetch_ai_model_costs_custom_model_mapping(self) -> None:
482482
assert "nonexistent-mapping" not in models
483483

484484
@responses.activate
485-
def test_fetch_ai_model_costs_with_glob_model_names(self) -> None:
486-
"""Test that glob versions of model names are added correctly"""
487-
# Mock responses with models that should generate glob patterns
485+
def test_fetch_ai_model_costs_with_normalized_and_prefix_glob_names(self) -> None:
486+
"""Test that normalized and prefix glob versions of model names are added correctly"""
487+
# Mock responses with models that have dates/versions that should be normalized
488488
mock_openrouter_response = {
489489
"data": [
490490
{
@@ -502,7 +502,7 @@ def test_fetch_ai_model_costs_with_glob_model_names(self) -> None:
502502
},
503503
},
504504
{
505-
"id": "openai/gpt-4", # No date/version, should not generate glob
505+
"id": "openai/gpt-4", # No date/version, normalized version same as original
506506
"pricing": {
507507
"prompt": "0.0000003",
508508
"completion": "0.00000165",
@@ -548,104 +548,100 @@ def test_fetch_ai_model_costs_with_glob_model_names(self) -> None:
548548
assert "claude-3-5-haiku@20241022" in models
549549
assert "o3-pro-2025-06-10" in models
550550

551-
# Check suffix glob versions were added
552-
assert "gpt-4o-mini-*" in models
553-
assert "claude-3-5-sonnet-*" in models
554-
assert "claude-3-5-haiku@*" in models
555-
assert "o3-pro-*" in models
551+
# Check normalized versions were added (dates/versions removed)
552+
assert "gpt-4o-mini" in models
553+
assert "claude-3-5-sonnet" in models
554+
assert "claude-3-5-haiku" in models # @ is not part of the date pattern
555+
assert "o3-pro" in models
556556

557-
# Check prefix glob versions were added
558-
assert "*gpt-4o-mini-20250522" in models
559-
assert "*claude-3-5-sonnet-20241022" in models
557+
# Check prefix glob versions of normalized models were added
558+
assert "*gpt-4o-mini" in models
559+
assert "*claude-3-5-sonnet" in models
560560
assert "*gpt-4" in models
561-
assert "*claude-3-5-haiku@20241022" in models
562-
assert "*o3-pro-2025-06-10" in models
561+
assert "*claude-3-5-haiku" in models
562+
assert "*o3-pro" in models
563563

564-
# Check prefix-suffix glob versions were added (only for models with suffix globs)
565-
assert "*gpt-4o-mini-*" in models
566-
assert "*claude-3-5-sonnet-*" in models
567-
assert "*claude-3-5-haiku@*" in models
568-
assert "*o3-pro-*" in models
569-
570-
# Verify glob versions have same pricing as original models
564+
# Verify normalized versions have same pricing as original models
571565
gpt4o_mini_original = models["gpt-4o-mini-20250522"]
572-
gpt4o_mini_glob = models["gpt-4o-mini-*"]
573-
assert gpt4o_mini_original.get("inputPerToken") == gpt4o_mini_glob.get("inputPerToken")
574-
assert gpt4o_mini_original.get("outputPerToken") == gpt4o_mini_glob.get("outputPerToken")
566+
gpt4o_mini_normalized = models["gpt-4o-mini"]
567+
assert gpt4o_mini_original.get("inputPerToken") == gpt4o_mini_normalized.get(
568+
"inputPerToken"
569+
)
570+
assert gpt4o_mini_original.get("outputPerToken") == gpt4o_mini_normalized.get(
571+
"outputPerToken"
572+
)
575573

576574
claude_sonnet_original = models["claude-3-5-sonnet-20241022"]
577-
claude_sonnet_glob = models["claude-3-5-sonnet-*"]
578-
assert claude_sonnet_original.get("inputPerToken") == claude_sonnet_glob.get(
575+
claude_sonnet_normalized = models["claude-3-5-sonnet"]
576+
assert claude_sonnet_original.get("inputPerToken") == claude_sonnet_normalized.get(
579577
"inputPerToken"
580578
)
581-
assert claude_sonnet_original.get("outputPerToken") == claude_sonnet_glob.get(
579+
assert claude_sonnet_original.get("outputPerToken") == claude_sonnet_normalized.get(
582580
"outputPerToken"
583581
)
584582

585583
claude_haiku_original = models["claude-3-5-haiku@20241022"]
586-
claude_haiku_glob = models["claude-3-5-haiku@*"]
587-
assert claude_haiku_original.get("inputPerToken") == claude_haiku_glob.get("inputPerToken")
588-
assert claude_haiku_original.get("outputPerToken") == claude_haiku_glob.get(
584+
claude_haiku_normalized = models["claude-3-5-haiku"]
585+
assert claude_haiku_original.get("inputPerToken") == claude_haiku_normalized.get(
586+
"inputPerToken"
587+
)
588+
assert claude_haiku_original.get("outputPerToken") == claude_haiku_normalized.get(
589589
"outputPerToken"
590590
)
591591

592592
o3_pro_original = models["o3-pro-2025-06-10"]
593-
o3_pro_glob = models["o3-pro-*"]
594-
assert o3_pro_original.get("inputPerToken") == o3_pro_glob.get("inputPerToken")
595-
assert o3_pro_original.get("outputPerToken") == o3_pro_glob.get("outputPerToken")
593+
o3_pro_normalized = models["o3-pro"]
594+
assert o3_pro_original.get("inputPerToken") == o3_pro_normalized.get("inputPerToken")
595+
assert o3_pro_original.get("outputPerToken") == o3_pro_normalized.get("outputPerToken")
596596

597-
# Verify gpt-4 (no date/version) doesn't have a suffix glob version
598-
assert "gpt-4*" not in models
599-
600-
# Verify prefix glob versions have same pricing as original models
601-
gpt4_original = models["gpt-4"]
597+
# Verify prefix glob versions have same pricing as normalized models
598+
gpt4_normalized = models["gpt-4"]
602599
gpt4_prefix_glob = models["*gpt-4"]
603-
assert gpt4_original.get("inputPerToken") == gpt4_prefix_glob.get("inputPerToken")
604-
assert gpt4_original.get("outputPerToken") == gpt4_prefix_glob.get("outputPerToken")
600+
assert gpt4_normalized.get("inputPerToken") == gpt4_prefix_glob.get("inputPerToken")
601+
assert gpt4_normalized.get("outputPerToken") == gpt4_prefix_glob.get("outputPerToken")
605602

606-
# Verify prefix-suffix glob versions have same pricing as original models
607-
gpt4o_mini_prefix_suffix_glob = models["*gpt-4o-mini-*"]
608-
assert gpt4o_mini_original.get("inputPerToken") == gpt4o_mini_prefix_suffix_glob.get(
603+
gpt4o_mini_prefix_glob = models["*gpt-4o-mini"]
604+
assert gpt4o_mini_normalized.get("inputPerToken") == gpt4o_mini_prefix_glob.get(
609605
"inputPerToken"
610606
)
611-
assert gpt4o_mini_original.get("outputPerToken") == gpt4o_mini_prefix_suffix_glob.get(
607+
assert gpt4o_mini_normalized.get("outputPerToken") == gpt4o_mini_prefix_glob.get(
612608
"outputPerToken"
613609
)
614610

615-
@responses.activate
616-
def test_create_suffix_glob_model_name_various_formats(self) -> None:
617-
"""Test suffix glob generation with various date and version formats"""
618-
from sentry.tasks.ai_agent_monitoring import _create_suffix_glob_model_name
611+
def test_normalize_model_id(self) -> None:
612+
"""Test model ID normalization with various date and version formats"""
613+
from sentry.tasks.ai_agent_monitoring import _normalize_model_id
619614

620615
# Test cases with expected outputs
621616
test_cases = [
622-
("model-20250522", "model-*"), # YYYYMMDD -> *
623-
("model-2025-06-10", "model-*"), # YYYY-MM-DD -> *
624-
("model-2025/06/10", "model-*"), # YYYY/MM/DD -> *
625-
("model-2025.06.10", "model-*"), # YYYY.MM.DD -> *
626-
("model-v1.0", "model-*"), # v1.0 -> *
627-
("model-v2.1.0", "model-*"), # v2.1.0 -> *
628-
("model@20241022", "model@*"), # @YYYYMMDD -> @*
629-
("model-v1:0", "model-*"), # v1:0 -> *
630-
("model-20250610-v1:0", "model-*"), # YYYYMMDD-v1:0 -> *
631-
("model@20250610-v1:0", "model@*"), # @YYYYMMDD-v1:0 -> @*
617+
("model-20250522", "model"), # YYYYMMDD removed
618+
("model-2025-06-10", "model"), # YYYY-MM-DD removed
619+
("model-2025/06/10", "model"), # YYYY/MM/DD removed
620+
("model-2025.06.10", "model"), # YYYY.MM.DD removed
621+
("model-v1.0", "model"), # v1.0 removed
622+
("model@20241022", "model"), # @YYYYMMDD removed
623+
("model-v1:0", "model"), # v1:0 removed
624+
("model-20250610-v1:0", "model"), # YYYYMMDD-v1:0 removed
625+
("model@20250610-v1:0", "model"), # @YYYYMMDD-v1:0 removed
626+
("gpt-4", "gpt-4"), # No date/version, unchanged
627+
("claude-3-5-sonnet", "claude-3-5-sonnet"), # Numbers are part of model name, unchanged
632628
]
633629

634-
for model_id, expected_glob in test_cases:
635-
actual_glob = _create_suffix_glob_model_name(model_id)
630+
for model_id, expected_normalized in test_cases:
631+
actual_normalized = _normalize_model_id(model_id)
636632
assert (
637-
actual_glob == expected_glob
638-
), f"Expected {expected_glob} for {model_id}, got {actual_glob}"
633+
actual_normalized == expected_normalized
634+
), f"Expected {expected_normalized} for {model_id}, got {actual_normalized}"
639635

640-
@responses.activate
641636
def test_create_prefix_glob_model_name(self) -> None:
642637
"""Test prefix glob generation for model names"""
643638
from sentry.tasks.ai_agent_monitoring import _create_prefix_glob_model_name
644639

645640
# Test cases with expected outputs
646641
test_cases = [
647642
("gpt-4", "*gpt-4"),
648-
("gpt-4o-mini-*", "*gpt-4o-mini-*"),
643+
("gpt-4o-mini", "*gpt-4o-mini"),
644+
("claude-3-5-sonnet", "*claude-3-5-sonnet"),
649645
("", "*"),
650646
]
651647

0 commit comments

Comments
 (0)