agentcore-metering 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- agentcore_metering/__init__.py +6 -0
- agentcore_metering/adapters/__init__.py +1 -0
- agentcore_metering/adapters/django/__init__.py +14 -0
- agentcore_metering/adapters/django/admin.py +64 -0
- agentcore_metering/adapters/django/apps.py +11 -0
- agentcore_metering/adapters/django/cleanup.py +91 -0
- agentcore_metering/adapters/django/conf.py +174 -0
- agentcore_metering/adapters/django/llm_static/README.md +25 -0
- agentcore_metering/adapters/django/llm_static/__init__.py +2 -0
- agentcore_metering/adapters/django/llm_static/capability_labels.yaml +12 -0
- agentcore_metering/adapters/django/llm_static/load.py +135 -0
- agentcore_metering/adapters/django/llm_static/mode_to_model_type.yaml +7 -0
- agentcore_metering/adapters/django/llm_static/providers/amazon_nova.yaml +13 -0
- agentcore_metering/adapters/django/llm_static/providers/anthropic.yaml +59 -0
- agentcore_metering/adapters/django/llm_static/providers/azure_openai.yaml +60 -0
- agentcore_metering/adapters/django/llm_static/providers/dashscope.yaml +48 -0
- agentcore_metering/adapters/django/llm_static/providers/deepseek.yaml +42 -0
- agentcore_metering/adapters/django/llm_static/providers/gemini.yaml +62 -0
- agentcore_metering/adapters/django/llm_static/providers/index.yaml +19 -0
- agentcore_metering/adapters/django/llm_static/providers/meta_llama.yaml +13 -0
- agentcore_metering/adapters/django/llm_static/providers/minimax.yaml +25 -0
- agentcore_metering/adapters/django/llm_static/providers/mistral.yaml +31 -0
- agentcore_metering/adapters/django/llm_static/providers/moonshot.yaml +33 -0
- agentcore_metering/adapters/django/llm_static/providers/nvidia_nim.yaml +13 -0
- agentcore_metering/adapters/django/llm_static/providers/openai.yaml +114 -0
- agentcore_metering/adapters/django/llm_static/providers/openai_compatible.yaml +6 -0
- agentcore_metering/adapters/django/llm_static/providers/openrouter.yaml +31 -0
- agentcore_metering/adapters/django/llm_static/providers/volcengine.yaml +18 -0
- agentcore_metering/adapters/django/llm_static/providers/xai.yaml +25 -0
- agentcore_metering/adapters/django/llm_static/providers/zai.yaml +24 -0
- agentcore_metering/adapters/django/locale/zh_Hans/LC_MESSAGES/django.mo +0 -0
- agentcore_metering/adapters/django/locale/zh_Hans/LC_MESSAGES/django.po +41 -0
- agentcore_metering/adapters/django/management/__init__.py +0 -0
- agentcore_metering/adapters/django/management/commands/__init__.py +0 -0
- agentcore_metering/adapters/django/management/commands/aggregate_llm_usage_series.py +97 -0
- agentcore_metering/adapters/django/migrations/0001_initial.py +166 -0
- agentcore_metering/adapters/django/migrations/0002_add_llm_config.py +105 -0
- agentcore_metering/adapters/django/migrations/0003_llm_config_single_config.py +55 -0
- agentcore_metering/adapters/django/migrations/0004_add_llm_usage_cost_usd.py +25 -0
- agentcore_metering/adapters/django/migrations/0005_cost_and_currency.py +56 -0
- agentcore_metering/adapters/django/migrations/0006_llm_config_list_and_strategy.py +126 -0
- agentcore_metering/adapters/django/migrations/0007_remove_modelstrategy.py +14 -0
- agentcore_metering/adapters/django/migrations/0008_llmconfig_add_uuid.py +47 -0
- agentcore_metering/adapters/django/migrations/0009_rename_llm_track_user_created_idx_llm_tracker_user_id_5800dd_idx_and_more.py +96 -0
- agentcore_metering/adapters/django/migrations/0010_remove_llmconfig_order.py +16 -0
- agentcore_metering/adapters/django/migrations/0011_llmconfig_is_default.py +23 -0
- agentcore_metering/adapters/django/migrations/0012_alter_llmconfig_options_alter_llmconfig_is_default.py +22 -0
- agentcore_metering/adapters/django/migrations/0013_llmusage_started_at.py +24 -0
- agentcore_metering/adapters/django/migrations/0014_alter_llmusage_created_at_alter_llmusage_started_at.py +32 -0
- agentcore_metering/adapters/django/migrations/0015_llmusage_is_streaming_first_chunk_at.py +35 -0
- agentcore_metering/adapters/django/migrations/0016_llmusageseries.py +79 -0
- agentcore_metering/adapters/django/migrations/0017_meteringconfig.py +56 -0
- agentcore_metering/adapters/django/migrations/0018_alter_llmusageseries_options_and_more.py +73 -0
- agentcore_metering/adapters/django/migrations/__init__.py +0 -0
- agentcore_metering/adapters/django/models.py +342 -0
- agentcore_metering/adapters/django/periodic_tasks.py +47 -0
- agentcore_metering/adapters/django/serializers.py +509 -0
- agentcore_metering/adapters/django/services/__init__.py +44 -0
- agentcore_metering/adapters/django/services/config_source.py +159 -0
- agentcore_metering/adapters/django/services/litellm_params.py +332 -0
- agentcore_metering/adapters/django/services/litellm_retry.py +293 -0
- agentcore_metering/adapters/django/services/metering_config.py +39 -0
- agentcore_metering/adapters/django/services/model_catalog.py +82 -0
- agentcore_metering/adapters/django/services/runtime_config.py +603 -0
- agentcore_metering/adapters/django/services/usage_aggregation.py +455 -0
- agentcore_metering/adapters/django/services/usage_chart_series.py +470 -0
- agentcore_metering/adapters/django/services/usage_list.py +146 -0
- agentcore_metering/adapters/django/tasks/__init__.py +14 -0
- agentcore_metering/adapters/django/tasks/aggregate.py +226 -0
- agentcore_metering/adapters/django/tasks/cleanup.py +95 -0
- agentcore_metering/adapters/django/trackers/__init__.py +8 -0
- agentcore_metering/adapters/django/trackers/llm.py +1226 -0
- agentcore_metering/adapters/django/trackers/llm_usage.py +214 -0
- agentcore_metering/adapters/django/urls.py +86 -0
- agentcore_metering/adapters/django/utils.py +44 -0
- agentcore_metering/adapters/django/views/__init__.py +42 -0
- agentcore_metering/adapters/django/views/config_catalog.py +68 -0
- agentcore_metering/adapters/django/views/config_management.py +487 -0
- agentcore_metering/adapters/django/views/config_validation.py +173 -0
- agentcore_metering/adapters/django/views/metering_config.py +98 -0
- agentcore_metering/adapters/django/views/usage.py +166 -0
- agentcore_metering/adapters/django/views/users.py +54 -0
- agentcore_metering/constants.py +25 -0
- agentcore_metering-0.1.0.dist-info/METADATA +339 -0
- agentcore_metering-0.1.0.dist-info/RECORD +88 -0
- agentcore_metering-0.1.0.dist-info/WHEEL +5 -0
- agentcore_metering-0.1.0.dist-info/licenses/LICENSE +202 -0
- agentcore_metering-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# Adapters (Django, future FastAPI, etc.)
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# Django adapter: full Django app for LLM metering.
|
|
2
|
+
# Public API: import LLMTracker from here or from
|
|
3
|
+
# agentcore_metering.adapters.django.trackers / .trackers.llm.
|
|
4
|
+
# Lazy import so that importing this package does not load Django models before
|
|
5
|
+
# django.setup() / apps are ready (avoids AppRegistryNotReady).
|
|
6
|
+
|
|
7
|
+
__all__ = ["LLMTracker"]
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def __getattr__(name):
|
|
11
|
+
if name == "LLMTracker":
|
|
12
|
+
from agentcore_metering.adapters.django.trackers import LLMTracker
|
|
13
|
+
return LLMTracker
|
|
14
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""Admin for LLM usage and config (agentcore_metering Django adapter)."""
|
|
2
|
+
from django.contrib import admin
|
|
3
|
+
|
|
4
|
+
from agentcore_metering.adapters.django.models import (
|
|
5
|
+
LLMConfig,
|
|
6
|
+
LLMUsage,
|
|
7
|
+
MeteringConfig,
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@admin.register(LLMUsage)
|
|
12
|
+
class LLMUsageAdmin(admin.ModelAdmin):
|
|
13
|
+
"""Admin interface for LLMUsage model."""
|
|
14
|
+
|
|
15
|
+
list_display = (
|
|
16
|
+
"id",
|
|
17
|
+
"model",
|
|
18
|
+
"user",
|
|
19
|
+
"total_tokens",
|
|
20
|
+
"cost",
|
|
21
|
+
"cost_currency",
|
|
22
|
+
"prompt_tokens",
|
|
23
|
+
"completion_tokens",
|
|
24
|
+
"success",
|
|
25
|
+
"created_at",
|
|
26
|
+
)
|
|
27
|
+
list_filter = ("success", "model", "created_at")
|
|
28
|
+
search_fields = ("model", "user__username", "metadata")
|
|
29
|
+
readonly_fields = (
|
|
30
|
+
"id",
|
|
31
|
+
"user",
|
|
32
|
+
"model",
|
|
33
|
+
"prompt_tokens",
|
|
34
|
+
"completion_tokens",
|
|
35
|
+
"total_tokens",
|
|
36
|
+
"cost",
|
|
37
|
+
"cost_currency",
|
|
38
|
+
"cached_tokens",
|
|
39
|
+
"reasoning_tokens",
|
|
40
|
+
"success",
|
|
41
|
+
"error",
|
|
42
|
+
"metadata",
|
|
43
|
+
"created_at",
|
|
44
|
+
)
|
|
45
|
+
ordering = ["-created_at"]
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@admin.register(MeteringConfig)
|
|
49
|
+
class MeteringConfigAdmin(admin.ModelAdmin):
|
|
50
|
+
"""Admin interface for MeteringConfig (global retention, cleanup, cron)."""
|
|
51
|
+
|
|
52
|
+
list_display = ("scope", "key", "value", "updated_at")
|
|
53
|
+
list_filter = ("scope", "key")
|
|
54
|
+
ordering = ["scope", "key"]
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@admin.register(LLMConfig)
|
|
58
|
+
class LLMConfigAdmin(admin.ModelAdmin):
|
|
59
|
+
"""Admin interface for LLMConfig (global and per-user)."""
|
|
60
|
+
|
|
61
|
+
list_display = ("id", "scope", "user", "provider", "updated_at")
|
|
62
|
+
list_filter = ("scope", "provider")
|
|
63
|
+
search_fields = ("user__username",)
|
|
64
|
+
ordering = ["-updated_at"]
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""Django app config for agentcore_metering (LLM observability)."""
|
|
2
|
+
from django.apps import AppConfig
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class AgentcoreMeteringDjangoConfig(AppConfig):
|
|
6
|
+
"""App config for agentcore_metering Django adapter."""
|
|
7
|
+
|
|
8
|
+
default_auto_field = "django.db.models.BigAutoField"
|
|
9
|
+
name = "agentcore_metering.adapters.django"
|
|
10
|
+
label = "agentcore_metering"
|
|
11
|
+
verbose_name = "Agentcore Metering (LLM)"
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Cleanup of old LLM usage and series records.
|
|
3
|
+
|
|
4
|
+
Uses conf when arguments are omitted. Call directly or via Celery task.
|
|
5
|
+
Default retention: 365 days (configurable via MeteringConfig or settings).
|
|
6
|
+
"""
|
|
7
|
+
import logging
|
|
8
|
+
from datetime import timedelta
|
|
9
|
+
from typing import Any, Dict, Optional
|
|
10
|
+
|
|
11
|
+
from django.utils import timezone
|
|
12
|
+
|
|
13
|
+
from agentcore_metering.adapters.django.conf import get_retention_days
|
|
14
|
+
from agentcore_metering.adapters.django.models import LLMUsage, LLMUsageSeries
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
DEFAULT_BATCH_SIZE = 5000
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def cleanup_old_llm_usage(
|
|
22
|
+
retention_days: Optional[int] = None,
|
|
23
|
+
batch_size: Optional[int] = None,
|
|
24
|
+
) -> Dict[str, Any]:
|
|
25
|
+
"""
|
|
26
|
+
Delete LLMUsage and LLMUsageSeries older than retention_days.
|
|
27
|
+
|
|
28
|
+
Returns dict with deleted_usage, deleted_series, cutoff, retention_days.
|
|
29
|
+
"""
|
|
30
|
+
if retention_days is None:
|
|
31
|
+
retention_days = get_retention_days()
|
|
32
|
+
|
|
33
|
+
if retention_days <= 0:
|
|
34
|
+
logger.warning(
|
|
35
|
+
f"cleanup_old_llm_usage: retention_days={retention_days} <= 0, "
|
|
36
|
+
"skipping"
|
|
37
|
+
)
|
|
38
|
+
return {
|
|
39
|
+
"deleted_usage": 0,
|
|
40
|
+
"deleted_series": 0,
|
|
41
|
+
"cutoff": timezone.now(),
|
|
42
|
+
"retention_days": retention_days,
|
|
43
|
+
"skipped": True,
|
|
44
|
+
"reason": "invalid_retention_days",
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
cutoff = timezone.now() - timedelta(days=retention_days)
|
|
48
|
+
batch_size = batch_size or DEFAULT_BATCH_SIZE
|
|
49
|
+
|
|
50
|
+
deleted_usage = 0
|
|
51
|
+
if batch_size <= 0:
|
|
52
|
+
deleted_usage, _ = (
|
|
53
|
+
LLMUsage.objects.filter(created_at__lt=cutoff).delete()
|
|
54
|
+
)
|
|
55
|
+
else:
|
|
56
|
+
while True:
|
|
57
|
+
batch = list(
|
|
58
|
+
LLMUsage.objects.filter(created_at__lt=cutoff)
|
|
59
|
+
.values_list("pk", flat=True)[:batch_size]
|
|
60
|
+
)
|
|
61
|
+
if not batch:
|
|
62
|
+
break
|
|
63
|
+
n, _ = LLMUsage.objects.filter(pk__in=batch).delete()
|
|
64
|
+
deleted_usage += n
|
|
65
|
+
|
|
66
|
+
deleted_series = 0
|
|
67
|
+
if batch_size <= 0:
|
|
68
|
+
deleted_series, _ = (
|
|
69
|
+
LLMUsageSeries.objects.filter(bucket__lt=cutoff).delete()
|
|
70
|
+
)
|
|
71
|
+
else:
|
|
72
|
+
while True:
|
|
73
|
+
batch = list(
|
|
74
|
+
LLMUsageSeries.objects.filter(bucket__lt=cutoff)
|
|
75
|
+
.values_list("pk", flat=True)[:batch_size]
|
|
76
|
+
)
|
|
77
|
+
if not batch:
|
|
78
|
+
break
|
|
79
|
+
n, _ = LLMUsageSeries.objects.filter(pk__in=batch).delete()
|
|
80
|
+
deleted_series += n
|
|
81
|
+
|
|
82
|
+
logger.info(
|
|
83
|
+
f"cleanup_old_llm_usage: deleted_usage={deleted_usage} "
|
|
84
|
+
f"deleted_series={deleted_series} retention_days={retention_days}"
|
|
85
|
+
)
|
|
86
|
+
return {
|
|
87
|
+
"deleted_usage": deleted_usage,
|
|
88
|
+
"deleted_series": deleted_series,
|
|
89
|
+
"cutoff": cutoff,
|
|
90
|
+
"retention_days": retention_days,
|
|
91
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Global config for agentcore_metering: retention, cleanup, aggregation.
|
|
3
|
+
|
|
4
|
+
Reads from MeteringConfig (DB) first, then Django settings. Used by periodic
|
|
5
|
+
tasks and config API for frontend management.
|
|
6
|
+
"""
|
|
7
|
+
from django.conf import settings
|
|
8
|
+
|
|
9
|
+
try:
|
|
10
|
+
from celery.schedules import crontab
|
|
11
|
+
except ImportError:
|
|
12
|
+
crontab = None
|
|
13
|
+
|
|
14
|
+
DEFAULT_RETENTION_DAYS = 365
|
|
15
|
+
DEFAULT_CLEANUP_ENABLED = True
|
|
16
|
+
DEFAULT_CLEANUP_CRONTAB = "0 2 * * *"
|
|
17
|
+
DEFAULT_AGGREGATION_CRONTAB = "5 * * * *"
|
|
18
|
+
|
|
19
|
+
# Timezone for "yesterday" / "last month" when computing aggregation range.
|
|
20
|
+
# Celery may run at 02:00 Shanghai; we aggregate Shanghai's yesterday.
|
|
21
|
+
DEFAULT_AGGREGATION_TIMEZONE = "Asia/Shanghai"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _get_global_config(key: str):
|
|
25
|
+
# NOTE(Ray): Lazy import to avoid circular import (conf used by
|
|
26
|
+
# services.__init__, tasks, apps.ready(); metering_config may import conf).
|
|
27
|
+
from agentcore_metering.adapters.django.services import metering_config
|
|
28
|
+
|
|
29
|
+
return metering_config.get_global_config(key)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def get_retention_days() -> int:
|
|
33
|
+
"""
|
|
34
|
+
Retention days for cleanup: MeteringConfig first, else settings.
|
|
35
|
+
Default 365 days.
|
|
36
|
+
"""
|
|
37
|
+
val = _get_global_config("retention_days")
|
|
38
|
+
if isinstance(val, int) and val > 0:
|
|
39
|
+
return val
|
|
40
|
+
return getattr(
|
|
41
|
+
settings,
|
|
42
|
+
"AGENTCORE_METERING_RETENTION_DAYS",
|
|
43
|
+
DEFAULT_RETENTION_DAYS,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def get_cleanup_enabled() -> bool:
|
|
48
|
+
"""Whether cleanup beat task is enabled (default True)."""
|
|
49
|
+
val = _get_global_config("cleanup_enabled")
|
|
50
|
+
if val is not None:
|
|
51
|
+
return bool(val)
|
|
52
|
+
return getattr(
|
|
53
|
+
settings,
|
|
54
|
+
"AGENTCORE_METERING_CLEANUP_ENABLED",
|
|
55
|
+
DEFAULT_CLEANUP_ENABLED,
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def get_cleanup_crontab() -> str:
|
|
60
|
+
"""Five-field cron expression for cleanup (default daily 2:00)."""
|
|
61
|
+
val = _get_global_config("cleanup_crontab")
|
|
62
|
+
if isinstance(val, str) and val.strip():
|
|
63
|
+
return val.strip()
|
|
64
|
+
return getattr(
|
|
65
|
+
settings,
|
|
66
|
+
"AGENTCORE_METERING_CLEANUP_CRONTAB",
|
|
67
|
+
DEFAULT_CLEANUP_CRONTAB,
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def get_aggregation_crontab() -> str:
|
|
72
|
+
"""
|
|
73
|
+
Cron for aggregation task (hour/day/month in one run).
|
|
74
|
+
Default: minute 5 past each hour (5 * * * *).
|
|
75
|
+
"""
|
|
76
|
+
val = _get_global_config("aggregation_crontab")
|
|
77
|
+
if isinstance(val, str) and val.strip():
|
|
78
|
+
return val.strip()
|
|
79
|
+
return getattr(
|
|
80
|
+
settings,
|
|
81
|
+
"AGENTCORE_METERING_AGGREGATION_CRONTAB",
|
|
82
|
+
DEFAULT_AGGREGATION_CRONTAB,
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def get_aggregation_timezone() -> str:
|
|
87
|
+
"""
|
|
88
|
+
Timezone for computing aggregation date range (yesterday, last month).
|
|
89
|
+
Ensures "yesterday" is the calendar day in this timezone; range is then
|
|
90
|
+
converted to UTC for DB query. Default Asia/Shanghai.
|
|
91
|
+
"""
|
|
92
|
+
return getattr(
|
|
93
|
+
settings,
|
|
94
|
+
"AGENTCORE_METERING_AGGREGATION_TIMEZONE",
|
|
95
|
+
DEFAULT_AGGREGATION_TIMEZONE,
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _crontab_from_expression(expr: str):
|
|
100
|
+
"""Parse 5-field cron into Celery crontab. On parse error returns None."""
|
|
101
|
+
if not crontab or not expr:
|
|
102
|
+
return None
|
|
103
|
+
parts = str(expr).strip().split()
|
|
104
|
+
if len(parts) != 5:
|
|
105
|
+
return None
|
|
106
|
+
try:
|
|
107
|
+
return crontab(
|
|
108
|
+
minute=parts[0],
|
|
109
|
+
hour=parts[1],
|
|
110
|
+
day_of_month=parts[2],
|
|
111
|
+
month_of_year=parts[3],
|
|
112
|
+
day_of_week=parts[4],
|
|
113
|
+
)
|
|
114
|
+
except (TypeError, ValueError):
|
|
115
|
+
return None
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def is_valid_crontab_expression(expr: str) -> bool:
|
|
119
|
+
"""Return True if expr is a valid 5-field cron expression."""
|
|
120
|
+
if not expr or not str(expr).strip():
|
|
121
|
+
return False
|
|
122
|
+
return _crontab_from_expression(str(expr).strip()) is not None
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def get_cleanup_beat_schedule_init():
|
|
126
|
+
"""
|
|
127
|
+
Build cleanup beat schedule from Django settings only (no DB in init).
|
|
128
|
+
For use in periodic_tasks.register_periodic_tasks.
|
|
129
|
+
"""
|
|
130
|
+
task_name = (
|
|
131
|
+
"agentcore_metering.adapters.django.tasks.cleanup."
|
|
132
|
+
"cleanup_old_llm_usage_task"
|
|
133
|
+
)
|
|
134
|
+
crontab_str = getattr(
|
|
135
|
+
settings,
|
|
136
|
+
"AGENTCORE_METERING_CLEANUP_CRONTAB",
|
|
137
|
+
DEFAULT_CLEANUP_CRONTAB,
|
|
138
|
+
)
|
|
139
|
+
schedule = _crontab_from_expression(crontab_str)
|
|
140
|
+
if schedule is None:
|
|
141
|
+
schedule = 24 * 3600.0
|
|
142
|
+
return {
|
|
143
|
+
"agentcore-metering-cleanup-old-usage": {
|
|
144
|
+
"task": task_name,
|
|
145
|
+
"schedule": schedule,
|
|
146
|
+
"options": {},
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def get_aggregation_beat_schedule_init():
|
|
152
|
+
"""
|
|
153
|
+
Build single aggregation beat schedule from settings only.
|
|
154
|
+
One task runs hour + day + month aggregation each time.
|
|
155
|
+
"""
|
|
156
|
+
task_name = (
|
|
157
|
+
"agentcore_metering.adapters.django.tasks.aggregate."
|
|
158
|
+
"aggregate_llm_usage_series_task"
|
|
159
|
+
)
|
|
160
|
+
crontab_str = getattr(
|
|
161
|
+
settings,
|
|
162
|
+
"AGENTCORE_METERING_AGGREGATION_CRONTAB",
|
|
163
|
+
DEFAULT_AGGREGATION_CRONTAB,
|
|
164
|
+
)
|
|
165
|
+
schedule = _crontab_from_expression(crontab_str)
|
|
166
|
+
if schedule is None:
|
|
167
|
+
schedule = 3600.0
|
|
168
|
+
return {
|
|
169
|
+
"agentcore-metering-aggregate": {
|
|
170
|
+
"task": task_name,
|
|
171
|
+
"schedule": schedule,
|
|
172
|
+
"options": {},
|
|
173
|
+
}
|
|
174
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# LLM static data (YAML)
|
|
2
|
+
|
|
3
|
+
Capability labels, mode mapping, provider/model lists, and reference pricing. Used by the Django adapter for the LLM config UI and resolution.
|
|
4
|
+
|
|
5
|
+
## Price update time
|
|
6
|
+
|
|
7
|
+
Reference prices in `providers/*.yaml` were last updated: **2026-02**. Consider updating periodically from provider docs or [LLM Stats](https://llm-stats.com/).
|
|
8
|
+
|
|
9
|
+
## Reference pricing (schema)
|
|
10
|
+
|
|
11
|
+
Prices under `reference_pricing` per model are **reference only** and not used for billing. They help compare cost across providers and models in the UI.
|
|
12
|
+
|
|
13
|
+
### Data sources
|
|
14
|
+
|
|
15
|
+
- **Official provider pages**: [OpenAI Pricing](https://openai.com/api/pricing), [Anthropic Pricing](https://docs.anthropic.com/en/docs/about-claude/pricing), [Gemini Pricing](https://ai.google.dev/gemini-api/docs/pricing), [DeepSeek Pricing](https://api-docs.deepseek.com/quick_start/pricing), etc.
|
|
16
|
+
- **Comparison / benchmarks**: [LLM Stats](https://llm-stats.com/) — leaderboards, benchmarks, and cost comparison for many models (LLM, Coding, Image, etc.). Use it to cross-check model names and compare performance vs cost.
|
|
17
|
+
|
|
18
|
+
### Schema (per model)
|
|
19
|
+
|
|
20
|
+
Optional `reference_pricing` on a model entry:
|
|
21
|
+
|
|
22
|
+
- `input_usd_per_1m`: USD per 1M input tokens (optional)
|
|
23
|
+
- `output_usd_per_1m`: USD per 1M output tokens (optional)
|
|
24
|
+
- `source`: URL or short label (e.g. provider pricing page)
|
|
25
|
+
- `updated`: Date string (e.g. `2026-02`) when the price was last checked
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# Capability tag id -> short label for UI (aligned with product tier docs)
|
|
2
|
+
text-to-text: Text-to-Text
|
|
3
|
+
code: Code
|
|
4
|
+
vision: Vision Input
|
|
5
|
+
multimodal: MultiModal
|
|
6
|
+
text-to-image: Text-to-Image
|
|
7
|
+
long-context: Long Context
|
|
8
|
+
low-cost: Low Cost
|
|
9
|
+
embedding: Embedding
|
|
10
|
+
reasoning: Reasoning
|
|
11
|
+
speech-to-text: Speech-to-Text
|
|
12
|
+
text-to-speech: Text-to-Speech
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Load capability labels, mode->model_type mapping, and provider/model data
|
|
3
|
+
from YAML. Data is under this package (llm_static/*.yaml,
|
|
4
|
+
llm_static/providers/*.yaml). Paths are resolved relative to this file
|
|
5
|
+
so it works when the package is installed.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import os
|
|
10
|
+
from typing import Any, Dict, Optional
|
|
11
|
+
|
|
12
|
+
import yaml
|
|
13
|
+
|
|
14
|
+
_THIS_DIR = os.path.dirname(os.path.abspath(__file__))
|
|
15
|
+
|
|
16
|
+
_capability_labels: Optional[Dict[str, str]] = None
|
|
17
|
+
_mode_to_model_type: Optional[Dict[str, str]] = None
|
|
18
|
+
_providers_with_models: Optional[Dict[str, Any]] = None
|
|
19
|
+
_provider_defaults: Optional[Dict[str, Dict[str, Any]]] = None
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _path(*parts: str) -> str:
|
|
23
|
+
return os.path.join(_THIS_DIR, *parts)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _load_yaml(filename: str) -> Any:
|
|
27
|
+
with open(_path(filename), encoding="utf-8") as f:
|
|
28
|
+
return yaml.safe_load(f)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def get_capability_labels() -> Dict[str, str]:
|
|
32
|
+
"""
|
|
33
|
+
Load capability tag id -> short label for UI. Cached after first load.
|
|
34
|
+
"""
|
|
35
|
+
global _capability_labels
|
|
36
|
+
if _capability_labels is None:
|
|
37
|
+
_capability_labels = _load_yaml("capability_labels.yaml") or {}
|
|
38
|
+
return _capability_labels
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def get_mode_to_model_type() -> Dict[str, str]:
|
|
42
|
+
"""Load mode -> model_type for resolver. Cached after first load."""
|
|
43
|
+
global _mode_to_model_type
|
|
44
|
+
if _mode_to_model_type is None:
|
|
45
|
+
_mode_to_model_type = _load_yaml("mode_to_model_type.yaml") or {}
|
|
46
|
+
return _mode_to_model_type
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _normalize_model(m: Dict[str, Any]) -> Dict[str, Any]:
|
|
50
|
+
"""
|
|
51
|
+
Ensure each model dict has id, label, capabilities, mode;
|
|
52
|
+
optional max_input/max_output, reference_pricing.
|
|
53
|
+
"""
|
|
54
|
+
out: Dict[str, Any] = {
|
|
55
|
+
"id": str(m.get("id", "")),
|
|
56
|
+
"label": str(m.get("label", "")),
|
|
57
|
+
"capabilities": list(m.get("capabilities") or []),
|
|
58
|
+
"max_input_tokens": m.get("max_input_tokens"),
|
|
59
|
+
"max_output_tokens": m.get("max_output_tokens"),
|
|
60
|
+
"mode": str(m.get("mode", "chat")),
|
|
61
|
+
}
|
|
62
|
+
rp = m.get("reference_pricing")
|
|
63
|
+
if not isinstance(rp, dict):
|
|
64
|
+
return out
|
|
65
|
+
has_input = rp.get("input_usd_per_1m") is not None
|
|
66
|
+
has_output = rp.get("output_usd_per_1m") is not None
|
|
67
|
+
if has_input or has_output:
|
|
68
|
+
out["reference_pricing"] = {
|
|
69
|
+
"input_usd_per_1m": rp.get("input_usd_per_1m"),
|
|
70
|
+
"output_usd_per_1m": rp.get("output_usd_per_1m"),
|
|
71
|
+
"source": rp.get("source"),
|
|
72
|
+
"updated": rp.get("updated"),
|
|
73
|
+
}
|
|
74
|
+
return out
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def get_providers_with_models() -> Dict[str, Any]:
|
|
78
|
+
"""
|
|
79
|
+
Load providers list (in index order) with models and capability_labels.
|
|
80
|
+
Same shape as before: { "providers": [...], "capability_labels": {...} }.
|
|
81
|
+
Cached.
|
|
82
|
+
"""
|
|
83
|
+
global _providers_with_models
|
|
84
|
+
if _providers_with_models is not None:
|
|
85
|
+
return _providers_with_models
|
|
86
|
+
index = _load_yaml("providers/index.yaml") or {}
|
|
87
|
+
provider_ids = index.get("provider_ids") or []
|
|
88
|
+
providers = []
|
|
89
|
+
for pid in provider_ids:
|
|
90
|
+
path = os.path.join("providers", f"{pid}.yaml")
|
|
91
|
+
if not os.path.isfile(_path(path)):
|
|
92
|
+
continue
|
|
93
|
+
data = _load_yaml(path) or {}
|
|
94
|
+
models = [_normalize_model(m) for m in (data.get("models") or [])]
|
|
95
|
+
providers.append({
|
|
96
|
+
"id": str(data.get("id", pid)),
|
|
97
|
+
"label": str(data.get("label", pid)),
|
|
98
|
+
"models": models,
|
|
99
|
+
})
|
|
100
|
+
_providers_with_models = {
|
|
101
|
+
"providers": providers,
|
|
102
|
+
"capability_labels": get_capability_labels(),
|
|
103
|
+
}
|
|
104
|
+
return _providers_with_models
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def get_provider_defaults() -> Dict[str, Dict[str, Any]]:
|
|
108
|
+
"""
|
|
109
|
+
Load per-provider defaults: default_api_base, default_model,
|
|
110
|
+
default_temperature, default_top_p, default_max_tokens, settings_key,
|
|
111
|
+
requires_api_base. Keys are provider ids. Cached. Only includes providers
|
|
112
|
+
that have a YAML file in providers/.
|
|
113
|
+
"""
|
|
114
|
+
global _provider_defaults
|
|
115
|
+
if _provider_defaults is not None:
|
|
116
|
+
return _provider_defaults
|
|
117
|
+
index = _load_yaml("providers/index.yaml") or {}
|
|
118
|
+
provider_ids = index.get("provider_ids") or []
|
|
119
|
+
out: Dict[str, Dict[str, Any]] = {}
|
|
120
|
+
for pid in provider_ids:
|
|
121
|
+
path = os.path.join("providers", f"{pid}.yaml")
|
|
122
|
+
if not os.path.isfile(_path(path)):
|
|
123
|
+
continue
|
|
124
|
+
data = _load_yaml(path) or {}
|
|
125
|
+
out[pid] = {
|
|
126
|
+
"default_api_base": data.get("default_api_base"),
|
|
127
|
+
"default_model": data.get("default_model"),
|
|
128
|
+
"default_temperature": data.get("default_temperature"),
|
|
129
|
+
"default_top_p": data.get("default_top_p"),
|
|
130
|
+
"default_max_tokens": data.get("default_max_tokens"),
|
|
131
|
+
"settings_key": data.get("settings_key"),
|
|
132
|
+
"requires_api_base": bool(data.get("requires_api_base", False)),
|
|
133
|
+
}
|
|
134
|
+
_provider_defaults = out
|
|
135
|
+
return _provider_defaults
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
id: amazon_nova
|
|
2
|
+
label: Amazon Nova
|
|
3
|
+
default_api_base: null
|
|
4
|
+
default_model: nova-micro-v1
|
|
5
|
+
settings_key: AMAZON_NOVA_CONFIG
|
|
6
|
+
requires_api_base: false
|
|
7
|
+
models:
|
|
8
|
+
- id: nova-micro-v1
|
|
9
|
+
label: Nova Micro v1
|
|
10
|
+
capabilities: [text-to-text, long-context]
|
|
11
|
+
max_input_tokens: 128000
|
|
12
|
+
max_output_tokens: 8192
|
|
13
|
+
mode: chat
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
id: anthropic
|
|
2
|
+
label: Anthropic
|
|
3
|
+
default_api_base: "https://api.anthropic.com"
|
|
4
|
+
default_model: claude-3-5-haiku-20241022
|
|
5
|
+
settings_key: ANTHROPIC_CONFIG
|
|
6
|
+
requires_api_base: false
|
|
7
|
+
models:
|
|
8
|
+
- id: claude-opus-4-6
|
|
9
|
+
label: Claude Opus 4.6
|
|
10
|
+
capabilities: [text-to-text, long-context]
|
|
11
|
+
max_input_tokens: 200000
|
|
12
|
+
max_output_tokens: 8192
|
|
13
|
+
mode: chat
|
|
14
|
+
reference_pricing:
|
|
15
|
+
input_usd_per_1m: 5.0
|
|
16
|
+
output_usd_per_1m: 25.0
|
|
17
|
+
source: "https://docs.anthropic.com/en/docs/about-claude/pricing"
|
|
18
|
+
updated: "2026-02"
|
|
19
|
+
- id: claude-sonnet-4-6
|
|
20
|
+
label: Claude Sonnet 4.6
|
|
21
|
+
capabilities: [text-to-text, long-context]
|
|
22
|
+
max_input_tokens: 200000
|
|
23
|
+
max_output_tokens: 8192
|
|
24
|
+
mode: chat
|
|
25
|
+
reference_pricing:
|
|
26
|
+
input_usd_per_1m: 3.0
|
|
27
|
+
output_usd_per_1m: 15.0
|
|
28
|
+
source: "https://docs.anthropic.com/en/docs/about-claude/pricing"
|
|
29
|
+
updated: "2026-02"
|
|
30
|
+
- id: claude-opus-4-5-20251101
|
|
31
|
+
label: Claude Opus 4.5
|
|
32
|
+
capabilities: [text-to-text, long-context]
|
|
33
|
+
max_input_tokens: 200000
|
|
34
|
+
max_output_tokens: 8192
|
|
35
|
+
mode: chat
|
|
36
|
+
- id: claude-sonnet-4-5-20250929
|
|
37
|
+
label: Claude Sonnet 4.5
|
|
38
|
+
capabilities: [text-to-text, long-context]
|
|
39
|
+
max_input_tokens: 200000
|
|
40
|
+
max_output_tokens: 8192
|
|
41
|
+
mode: chat
|
|
42
|
+
- id: claude-sonnet-4-20250514
|
|
43
|
+
label: Claude Sonnet 4
|
|
44
|
+
capabilities: [text-to-text, long-context]
|
|
45
|
+
max_input_tokens: 200000
|
|
46
|
+
max_output_tokens: 8192
|
|
47
|
+
mode: chat
|
|
48
|
+
- id: claude-3-5-sonnet-20241022
|
|
49
|
+
label: Claude 3.5 Sonnet
|
|
50
|
+
capabilities: [text-to-text, long-context]
|
|
51
|
+
max_input_tokens: 200000
|
|
52
|
+
max_output_tokens: 8192
|
|
53
|
+
mode: chat
|
|
54
|
+
- id: claude-3-5-haiku-20241022
|
|
55
|
+
label: Claude 3.5 Haiku
|
|
56
|
+
capabilities: [text-to-text, long-context]
|
|
57
|
+
max_input_tokens: 200000
|
|
58
|
+
max_output_tokens: 8192
|
|
59
|
+
mode: chat
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
id: azure_openai
|
|
2
|
+
label: Azure OpenAI
|
|
3
|
+
default_model: gpt-4o-mini
|
|
4
|
+
settings_key: AZURE_OPENAI_CONFIG
|
|
5
|
+
requires_api_base: true
|
|
6
|
+
models:
|
|
7
|
+
- id: gpt-5.2
|
|
8
|
+
label: GPT-5.2
|
|
9
|
+
capabilities: [text-to-text, vision, code, multimodal, long-context]
|
|
10
|
+
max_input_tokens: 128000
|
|
11
|
+
max_output_tokens: 16384
|
|
12
|
+
mode: chat
|
|
13
|
+
- id: gpt-5.2-pro
|
|
14
|
+
label: GPT-5.2 Pro
|
|
15
|
+
capabilities: [text-to-text, vision, code, multimodal, long-context]
|
|
16
|
+
max_input_tokens: 128000
|
|
17
|
+
max_output_tokens: 16384
|
|
18
|
+
mode: chat
|
|
19
|
+
- id: gpt-4-mini
|
|
20
|
+
label: GPT-4 Mini
|
|
21
|
+
capabilities: [text-to-text, long-context]
|
|
22
|
+
max_input_tokens: 128000
|
|
23
|
+
max_output_tokens: 16384
|
|
24
|
+
mode: chat
|
|
25
|
+
- id: gpt-5-mini
|
|
26
|
+
label: GPT-5 Mini
|
|
27
|
+
capabilities: [text-to-text, vision, long-context]
|
|
28
|
+
max_input_tokens: 128000
|
|
29
|
+
max_output_tokens: 16384
|
|
30
|
+
mode: chat
|
|
31
|
+
- id: gpt-5-nano
|
|
32
|
+
label: GPT-5 Nano
|
|
33
|
+
capabilities: [text-to-text, low-cost]
|
|
34
|
+
max_input_tokens: 128000
|
|
35
|
+
max_output_tokens: 8192
|
|
36
|
+
mode: chat
|
|
37
|
+
- id: gpt-4o
|
|
38
|
+
label: GPT-4o
|
|
39
|
+
capabilities: [text-to-text, vision, long-context]
|
|
40
|
+
max_input_tokens: 128000
|
|
41
|
+
max_output_tokens: 16384
|
|
42
|
+
mode: chat
|
|
43
|
+
- id: gpt-4o-mini
|
|
44
|
+
label: GPT-4o Mini
|
|
45
|
+
capabilities: [text-to-text, vision, long-context]
|
|
46
|
+
max_input_tokens: 128000
|
|
47
|
+
max_output_tokens: 16384
|
|
48
|
+
mode: chat
|
|
49
|
+
- id: o1
|
|
50
|
+
label: O1
|
|
51
|
+
capabilities: [text-to-text, long-context, reasoning]
|
|
52
|
+
max_input_tokens: 200000
|
|
53
|
+
max_output_tokens: 100000
|
|
54
|
+
mode: chat
|
|
55
|
+
- id: o1-mini
|
|
56
|
+
label: O1 Mini
|
|
57
|
+
capabilities: [text-to-text, long-context, reasoning]
|
|
58
|
+
max_input_tokens: 128000
|
|
59
|
+
max_output_tokens: 65536
|
|
60
|
+
mode: chat
|