Skip to content

Commit f4b9031

Browse files
constantiniusclaudegetsantry[bot]
authored
feat(ai-monitoring): Fetch model context size and rename task to fetch_ai_model_info (#112656)
Closes https://linear.app/getsentry/issue/TET-2219/sentry-map-llm-context-size-to-relay-cost-calculation-config Introduces a new `AIModelMetadata` schema that nests costs under a `costs` field and adds an optional `contextSize`. Context size is fetched from OpenRouter's `context_length` field and models.dev's `limit.context` field, following the same precedence logic as costs (OpenRouter takes priority). ## Architecture Both tasks run independently on the same cron schedule (every 30 min): | Task | Cache key | Format | |------|-----------|--------| | `fetch_ai_model_costs` (legacy, TODO remove) | `ai-model-costs:v2` | Flat `AIModelCostV2` | | `fetch_ai_model_metadata` (new) | `ai-model-metadata:v1` | Nested `AIModelMetadata` | They share helper functions (`_normalize_model_id`, `_create_prefix_glob_model_name`, `safe_float_conversion`) but fetch and cache independently. The old task + cache key will be removed once all consumers have migrated. `GlobalConfig` serves both fields side by side: - `aiModelCosts` — legacy flat format (read by Relay today) - `aiModelMetadata` — new nested format with `contextSize` (not yet consumed by Relay) ## New schema (`ai-model-metadata:v1`) ```json { "version": 1, "models": { "gpt-4": { "costs": { "inputPerToken": 0.0000003, "outputPerToken": 0.00000165, "outputReasoningPerToken": 0.0, "inputCachedPerToken": 0.0000015, "inputCacheWritePerToken": 0.00001875 }, "contextSize": 1000000 }, "claude-3-5-sonnet": { "costs": { "inputPerToken": 0.000003, "outputPerToken": 0.000015, "outputReasoningPerToken": 0.0, "inputCachedPerToken": 0.0000015, "inputCacheWritePerToken": 0.00000375 } } } } ``` `contextSize` is optional — only present when the source API provides it. ## New types | Type | Purpose | |------|---------| | `AIModelCost` | Token cost fields (nested under `costs`) | | `AIModelMetadata` | Per-model entry: `costs` + optional `contextSize` | | `AIModelMetadataConfig` | Top-level config: `version` + `models` dict | Legacy types (`AIModelCostV2`, `AIModelCosts`) are kept unchanged for the old task. Co-Authored-By: Claude Sonnet 4 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4 <noreply@anthropic.com> Co-authored-by: getsantry[bot] <66042841+getsantry[bot]@users.noreply.github.com>
1 parent 6c7577c commit f4b9031

File tree

7 files changed

+549
-147
lines changed

7 files changed

+549
-147
lines changed

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ dependencies = [
8989
"sentry-ophio>=1.1.3",
9090
"sentry-protos>=0.8.11",
9191
"sentry-redis-tools>=0.5.0",
92-
"sentry-relay>=0.9.26",
92+
"sentry-relay>=0.9.27",
9393
"sentry-sdk[http2]>=2.47.0",
9494
"sentry-usage-accountant>=0.0.10",
9595
# remove once there are no unmarked transitive dependencies on setuptools

src/sentry/conf/server.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1205,10 +1205,15 @@ def SOCIAL_AUTH_DEFAULT_USERNAME() -> str:
12051205
"task": "relocation:sentry.relocation.transfer.find_relocation_transfer_region",
12061206
"schedule": crontab("*/5", "*", "*", "*", "*"),
12071207
},
1208+
# TODO(constantinius): Remove fetch-ai-model-costs once all consumers have migrated to fetch-ai-model-metadata
12081209
"fetch-ai-model-costs": {
12091210
"task": "ai_agent_monitoring:sentry.tasks.ai_agent_monitoring.fetch_ai_model_costs",
12101211
"schedule": crontab("*/30", "*", "*", "*", "*"),
12111212
},
1213+
"fetch-ai-model-metadata": {
1214+
"task": "ai_agent_monitoring:sentry.tasks.ai_agent_monitoring.fetch_ai_model_metadata",
1215+
"schedule": crontab("*/30", "*", "*", "*", "*"),
1216+
},
12121217
"llm-issue-detection": {
12131218
"task": "issues:sentry.tasks.llm_issue_detection.run_llm_issue_detection",
12141219
"schedule": crontab("0", "*", "*", "*", "*"),

src/sentry/relay/config/ai_model_costs.py

Lines changed: 54 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,20 @@
1111
type ModelId = str
1212

1313

14-
# Cache key for storing AI model costs
14+
# Legacy cache key for AI model costs (v2 flat format)
15+
# TODO(constantinius): Remove once all consumers have migrated to AI_MODEL_METADATA_CACHE_KEY
1516
AI_MODEL_COSTS_CACHE_KEY = "ai-model-costs:v2"
16-
# Cache timeout: 30 days (we re-fetch every 30 minutes, so this provides more than enough overlap)
1717
AI_MODEL_COSTS_CACHE_TTL = 30 * 24 * 60 * 60
1818

19+
# Cache key for storing LLM model metadata (v1 nested format)
20+
AI_MODEL_METADATA_CACHE_KEY = "ai-model-metadata:v1"
21+
# Cache timeout: 30 days (we re-fetch every 30 minutes, so this provides more than enough overlap)
22+
AI_MODEL_METADATA_CACHE_TTL = 30 * 24 * 60 * 60
23+
1924

2025
class AIModelCostV2(TypedDict):
26+
"""Legacy flat format. TODO(constantinius): Remove once all consumers have migrated."""
27+
2128
inputPerToken: float
2229
outputPerToken: float
2330
outputReasoningPerToken: float
@@ -26,18 +33,34 @@ class AIModelCostV2(TypedDict):
2633

2734

2835
class AIModelCosts(TypedDict):
36+
"""Legacy config type. TODO(constantinius): Remove once all consumers have migrated."""
37+
2938
version: Required[int]
3039
models: Required[dict[ModelId, AIModelCostV2]]
3140

3241

42+
class AIModelCost(TypedDict):
43+
inputPerToken: float
44+
outputPerToken: float
45+
outputReasoningPerToken: float
46+
inputCachedPerToken: float
47+
inputCacheWritePerToken: float
48+
49+
50+
class AIModelMetadata(TypedDict, total=False):
51+
costs: Required[AIModelCost]
52+
contextSize: int
53+
54+
55+
class AIModelMetadataConfig(TypedDict):
56+
version: Required[int]
57+
models: Required[dict[ModelId, AIModelMetadata]]
58+
59+
3360
def ai_model_costs_config() -> AIModelCosts | None:
3461
"""
35-
Get AI model costs configuration.
36-
AI model costs are set in cache by a cron job,
37-
if there are no costs, it should be investigated why.
38-
39-
Returns:
40-
AIModelCosts object containing cost information for AI models
62+
Legacy: Get AI model costs configuration.
63+
TODO(constantinius): Remove once all consumers have migrated to ai_model_metadata_config.
4164
"""
4265
if settings.SENTRY_AIR_GAP:
4366
return None
@@ -47,7 +70,29 @@ def ai_model_costs_config() -> AIModelCosts | None:
4770
return cached_costs
4871

4972
if not settings.IS_DEV:
50-
# in dev environment, we don't want to log this
5173
logger.warning("Empty model costs")
5274

5375
return None
76+
77+
78+
def ai_model_metadata_config() -> AIModelMetadataConfig | None:
79+
"""
80+
Get LLM model metadata configuration.
81+
LLM model metadata is set in cache by a cron job,
82+
if there is no metadata, it should be investigated why.
83+
84+
Returns:
85+
AIModelMetadataConfig containing cost and context size information for LLM models
86+
"""
87+
if settings.SENTRY_AIR_GAP:
88+
return None
89+
90+
cached_metadata = cache.get(AI_MODEL_METADATA_CACHE_KEY)
91+
if cached_metadata is not None:
92+
return cached_metadata
93+
94+
if not settings.IS_DEV:
95+
# in dev environment, we don't want to log this
96+
logger.warning("Empty LLM model metadata")
97+
98+
return None

src/sentry/relay/globalconfig.py

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
11
from typing import Any, TypedDict
22

33
import sentry.options
4-
from sentry.relay.config.ai_model_costs import AIModelCosts, ai_model_costs_config
4+
from sentry.relay.config.ai_model_costs import (
5+
AIModelCosts,
6+
AIModelMetadataConfig,
7+
ai_model_costs_config,
8+
ai_model_metadata_config,
9+
)
510
from sentry.relay.config.measurements import MeasurementsConfig, get_measurements_config
611
from sentry.relay.config.metric_extraction import (
712
MetricExtractionGroups,
@@ -39,7 +44,10 @@ class SpanOpDefaults(TypedDict):
3944

4045
class GlobalConfig(TypedDict, total=False):
4146
measurements: MeasurementsConfig
42-
aiModelCosts: AIModelCosts | None
47+
aiModelCosts: (
48+
AIModelCosts | None
49+
) # TODO(constantinius): Remove once all consumers use aiModelMetadata
50+
aiModelMetadata: AIModelMetadataConfig | None
4351
metricExtraction: MetricExtractionGroups
4452
filters: GenericFiltersConfig | None
4553
spanOpDefaults: SpanOpDefaults
@@ -78,7 +86,8 @@ def get_global_config() -> GlobalConfig:
7886

7987
global_config: GlobalConfig = {
8088
"measurements": get_measurements_config(),
81-
"aiModelCosts": ai_model_costs_config(),
89+
"aiModelCosts": ai_model_costs_config(), # TODO(constantinius): Remove once all consumers use aiModelMetadata
90+
"aiModelMetadata": ai_model_metadata_config(),
8291
"metricExtraction": global_metric_extraction_groups(),
8392
"spanOpDefaults": span_op_defaults(),
8493
}

0 commit comments

Comments
 (0)