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.
Files changed (88) hide show
  1. agentcore_metering/__init__.py +6 -0
  2. agentcore_metering/adapters/__init__.py +1 -0
  3. agentcore_metering/adapters/django/__init__.py +14 -0
  4. agentcore_metering/adapters/django/admin.py +64 -0
  5. agentcore_metering/adapters/django/apps.py +11 -0
  6. agentcore_metering/adapters/django/cleanup.py +91 -0
  7. agentcore_metering/adapters/django/conf.py +174 -0
  8. agentcore_metering/adapters/django/llm_static/README.md +25 -0
  9. agentcore_metering/adapters/django/llm_static/__init__.py +2 -0
  10. agentcore_metering/adapters/django/llm_static/capability_labels.yaml +12 -0
  11. agentcore_metering/adapters/django/llm_static/load.py +135 -0
  12. agentcore_metering/adapters/django/llm_static/mode_to_model_type.yaml +7 -0
  13. agentcore_metering/adapters/django/llm_static/providers/amazon_nova.yaml +13 -0
  14. agentcore_metering/adapters/django/llm_static/providers/anthropic.yaml +59 -0
  15. agentcore_metering/adapters/django/llm_static/providers/azure_openai.yaml +60 -0
  16. agentcore_metering/adapters/django/llm_static/providers/dashscope.yaml +48 -0
  17. agentcore_metering/adapters/django/llm_static/providers/deepseek.yaml +42 -0
  18. agentcore_metering/adapters/django/llm_static/providers/gemini.yaml +62 -0
  19. agentcore_metering/adapters/django/llm_static/providers/index.yaml +19 -0
  20. agentcore_metering/adapters/django/llm_static/providers/meta_llama.yaml +13 -0
  21. agentcore_metering/adapters/django/llm_static/providers/minimax.yaml +25 -0
  22. agentcore_metering/adapters/django/llm_static/providers/mistral.yaml +31 -0
  23. agentcore_metering/adapters/django/llm_static/providers/moonshot.yaml +33 -0
  24. agentcore_metering/adapters/django/llm_static/providers/nvidia_nim.yaml +13 -0
  25. agentcore_metering/adapters/django/llm_static/providers/openai.yaml +114 -0
  26. agentcore_metering/adapters/django/llm_static/providers/openai_compatible.yaml +6 -0
  27. agentcore_metering/adapters/django/llm_static/providers/openrouter.yaml +31 -0
  28. agentcore_metering/adapters/django/llm_static/providers/volcengine.yaml +18 -0
  29. agentcore_metering/adapters/django/llm_static/providers/xai.yaml +25 -0
  30. agentcore_metering/adapters/django/llm_static/providers/zai.yaml +24 -0
  31. agentcore_metering/adapters/django/locale/zh_Hans/LC_MESSAGES/django.mo +0 -0
  32. agentcore_metering/adapters/django/locale/zh_Hans/LC_MESSAGES/django.po +41 -0
  33. agentcore_metering/adapters/django/management/__init__.py +0 -0
  34. agentcore_metering/adapters/django/management/commands/__init__.py +0 -0
  35. agentcore_metering/adapters/django/management/commands/aggregate_llm_usage_series.py +97 -0
  36. agentcore_metering/adapters/django/migrations/0001_initial.py +166 -0
  37. agentcore_metering/adapters/django/migrations/0002_add_llm_config.py +105 -0
  38. agentcore_metering/adapters/django/migrations/0003_llm_config_single_config.py +55 -0
  39. agentcore_metering/adapters/django/migrations/0004_add_llm_usage_cost_usd.py +25 -0
  40. agentcore_metering/adapters/django/migrations/0005_cost_and_currency.py +56 -0
  41. agentcore_metering/adapters/django/migrations/0006_llm_config_list_and_strategy.py +126 -0
  42. agentcore_metering/adapters/django/migrations/0007_remove_modelstrategy.py +14 -0
  43. agentcore_metering/adapters/django/migrations/0008_llmconfig_add_uuid.py +47 -0
  44. agentcore_metering/adapters/django/migrations/0009_rename_llm_track_user_created_idx_llm_tracker_user_id_5800dd_idx_and_more.py +96 -0
  45. agentcore_metering/adapters/django/migrations/0010_remove_llmconfig_order.py +16 -0
  46. agentcore_metering/adapters/django/migrations/0011_llmconfig_is_default.py +23 -0
  47. agentcore_metering/adapters/django/migrations/0012_alter_llmconfig_options_alter_llmconfig_is_default.py +22 -0
  48. agentcore_metering/adapters/django/migrations/0013_llmusage_started_at.py +24 -0
  49. agentcore_metering/adapters/django/migrations/0014_alter_llmusage_created_at_alter_llmusage_started_at.py +32 -0
  50. agentcore_metering/adapters/django/migrations/0015_llmusage_is_streaming_first_chunk_at.py +35 -0
  51. agentcore_metering/adapters/django/migrations/0016_llmusageseries.py +79 -0
  52. agentcore_metering/adapters/django/migrations/0017_meteringconfig.py +56 -0
  53. agentcore_metering/adapters/django/migrations/0018_alter_llmusageseries_options_and_more.py +73 -0
  54. agentcore_metering/adapters/django/migrations/__init__.py +0 -0
  55. agentcore_metering/adapters/django/models.py +342 -0
  56. agentcore_metering/adapters/django/periodic_tasks.py +47 -0
  57. agentcore_metering/adapters/django/serializers.py +509 -0
  58. agentcore_metering/adapters/django/services/__init__.py +44 -0
  59. agentcore_metering/adapters/django/services/config_source.py +159 -0
  60. agentcore_metering/adapters/django/services/litellm_params.py +332 -0
  61. agentcore_metering/adapters/django/services/litellm_retry.py +293 -0
  62. agentcore_metering/adapters/django/services/metering_config.py +39 -0
  63. agentcore_metering/adapters/django/services/model_catalog.py +82 -0
  64. agentcore_metering/adapters/django/services/runtime_config.py +603 -0
  65. agentcore_metering/adapters/django/services/usage_aggregation.py +455 -0
  66. agentcore_metering/adapters/django/services/usage_chart_series.py +470 -0
  67. agentcore_metering/adapters/django/services/usage_list.py +146 -0
  68. agentcore_metering/adapters/django/tasks/__init__.py +14 -0
  69. agentcore_metering/adapters/django/tasks/aggregate.py +226 -0
  70. agentcore_metering/adapters/django/tasks/cleanup.py +95 -0
  71. agentcore_metering/adapters/django/trackers/__init__.py +8 -0
  72. agentcore_metering/adapters/django/trackers/llm.py +1226 -0
  73. agentcore_metering/adapters/django/trackers/llm_usage.py +214 -0
  74. agentcore_metering/adapters/django/urls.py +86 -0
  75. agentcore_metering/adapters/django/utils.py +44 -0
  76. agentcore_metering/adapters/django/views/__init__.py +42 -0
  77. agentcore_metering/adapters/django/views/config_catalog.py +68 -0
  78. agentcore_metering/adapters/django/views/config_management.py +487 -0
  79. agentcore_metering/adapters/django/views/config_validation.py +173 -0
  80. agentcore_metering/adapters/django/views/metering_config.py +98 -0
  81. agentcore_metering/adapters/django/views/usage.py +166 -0
  82. agentcore_metering/adapters/django/views/users.py +54 -0
  83. agentcore_metering/constants.py +25 -0
  84. agentcore_metering-0.1.0.dist-info/METADATA +339 -0
  85. agentcore_metering-0.1.0.dist-info/RECORD +88 -0
  86. agentcore_metering-0.1.0.dist-info/WHEEL +5 -0
  87. agentcore_metering-0.1.0.dist-info/licenses/LICENSE +202 -0
  88. agentcore_metering-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,6 @@
1
+ """
2
+ agentcore_metering: unified observability for LLM/text, image, and video calls.
3
+ Django app lives under adapters.django.
4
+ """
5
+
6
+ __version__ = "0.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,2 @@
1
+ # LLM static data (capability labels, mode mapping, providers and models)
2
+ # loaded from YAML. See load.py for the loader API.
@@ -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,7 @@
1
+ # Resolver model_type derived from primary mode
2
+ chat: llm
3
+ completion: llm
4
+ embedding: embedding
5
+ image_generation: image_generation
6
+ audio_transcription: llm
7
+ audio_speech: llm
@@ -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