kubiya-control-plane-api 0.1.0__py3-none-any.whl → 0.3.4__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.

Potentially problematic release.


This version of kubiya-control-plane-api might be problematic. Click here for more details.

Files changed (185) hide show
  1. control_plane_api/README.md +266 -0
  2. control_plane_api/__init__.py +0 -0
  3. control_plane_api/__version__.py +1 -0
  4. control_plane_api/alembic/README +1 -0
  5. control_plane_api/alembic/env.py +98 -0
  6. control_plane_api/alembic/script.py.mako +28 -0
  7. control_plane_api/alembic/versions/1382bec74309_initial_migration_with_all_models.py +251 -0
  8. control_plane_api/alembic/versions/1f54bc2a37e3_add_analytics_tables.py +162 -0
  9. control_plane_api/alembic/versions/2e4cb136dc10_rename_toolset_ids_to_skill_ids_in_teams.py +30 -0
  10. control_plane_api/alembic/versions/31cd69a644ce_add_skill_templates_table.py +28 -0
  11. control_plane_api/alembic/versions/89e127caa47d_add_jobs_and_job_executions_tables.py +161 -0
  12. control_plane_api/alembic/versions/add_llm_models_table.py +51 -0
  13. control_plane_api/alembic/versions/b0e10697f212_add_runtime_column_to_teams_simple.py +42 -0
  14. control_plane_api/alembic/versions/ce43b24b63bf_add_execution_trigger_source_and_fix_.py +155 -0
  15. control_plane_api/alembic/versions/d4eaf16e3f8d_rename_toolsets_to_skills.py +84 -0
  16. control_plane_api/alembic/versions/efa2dc427da1_rename_metadata_to_custom_metadata.py +32 -0
  17. control_plane_api/alembic/versions/f973b431d1ce_add_workflow_executor_to_skill_types.py +44 -0
  18. control_plane_api/alembic.ini +148 -0
  19. control_plane_api/api/index.py +12 -0
  20. control_plane_api/app/__init__.py +11 -0
  21. control_plane_api/app/activities/__init__.py +20 -0
  22. control_plane_api/app/activities/agent_activities.py +379 -0
  23. control_plane_api/app/activities/team_activities.py +410 -0
  24. control_plane_api/app/activities/temporal_cloud_activities.py +577 -0
  25. control_plane_api/app/config/__init__.py +35 -0
  26. control_plane_api/app/config/api_config.py +354 -0
  27. control_plane_api/app/config/model_pricing.py +318 -0
  28. control_plane_api/app/config.py +95 -0
  29. control_plane_api/app/database.py +135 -0
  30. control_plane_api/app/exceptions.py +408 -0
  31. control_plane_api/app/lib/__init__.py +11 -0
  32. control_plane_api/app/lib/job_executor.py +312 -0
  33. control_plane_api/app/lib/kubiya_client.py +235 -0
  34. control_plane_api/app/lib/litellm_pricing.py +166 -0
  35. control_plane_api/app/lib/planning_tools/__init__.py +22 -0
  36. control_plane_api/app/lib/planning_tools/agents.py +155 -0
  37. control_plane_api/app/lib/planning_tools/base.py +189 -0
  38. control_plane_api/app/lib/planning_tools/environments.py +214 -0
  39. control_plane_api/app/lib/planning_tools/resources.py +240 -0
  40. control_plane_api/app/lib/planning_tools/teams.py +198 -0
  41. control_plane_api/app/lib/policy_enforcer_client.py +939 -0
  42. control_plane_api/app/lib/redis_client.py +436 -0
  43. control_plane_api/app/lib/supabase.py +71 -0
  44. control_plane_api/app/lib/temporal_client.py +138 -0
  45. control_plane_api/app/lib/validation/__init__.py +20 -0
  46. control_plane_api/app/lib/validation/runtime_validation.py +287 -0
  47. control_plane_api/app/main.py +128 -0
  48. control_plane_api/app/middleware/__init__.py +8 -0
  49. control_plane_api/app/middleware/auth.py +513 -0
  50. control_plane_api/app/middleware/exception_handler.py +267 -0
  51. control_plane_api/app/middleware/rate_limiting.py +384 -0
  52. control_plane_api/app/middleware/request_id.py +202 -0
  53. control_plane_api/app/models/__init__.py +27 -0
  54. control_plane_api/app/models/agent.py +79 -0
  55. control_plane_api/app/models/analytics.py +206 -0
  56. control_plane_api/app/models/associations.py +81 -0
  57. control_plane_api/app/models/environment.py +63 -0
  58. control_plane_api/app/models/execution.py +93 -0
  59. control_plane_api/app/models/job.py +179 -0
  60. control_plane_api/app/models/llm_model.py +75 -0
  61. control_plane_api/app/models/presence.py +49 -0
  62. control_plane_api/app/models/project.py +47 -0
  63. control_plane_api/app/models/session.py +38 -0
  64. control_plane_api/app/models/team.py +66 -0
  65. control_plane_api/app/models/workflow.py +55 -0
  66. control_plane_api/app/policies/README.md +121 -0
  67. control_plane_api/app/policies/approved_users.rego +62 -0
  68. control_plane_api/app/policies/business_hours.rego +51 -0
  69. control_plane_api/app/policies/rate_limiting.rego +100 -0
  70. control_plane_api/app/policies/tool_restrictions.rego +86 -0
  71. control_plane_api/app/routers/__init__.py +4 -0
  72. control_plane_api/app/routers/agents.py +364 -0
  73. control_plane_api/app/routers/agents_v2.py +1260 -0
  74. control_plane_api/app/routers/analytics.py +1014 -0
  75. control_plane_api/app/routers/context_manager.py +562 -0
  76. control_plane_api/app/routers/environment_context.py +270 -0
  77. control_plane_api/app/routers/environments.py +715 -0
  78. control_plane_api/app/routers/execution_environment.py +517 -0
  79. control_plane_api/app/routers/executions.py +1911 -0
  80. control_plane_api/app/routers/health.py +92 -0
  81. control_plane_api/app/routers/health_v2.py +326 -0
  82. control_plane_api/app/routers/integrations.py +274 -0
  83. control_plane_api/app/routers/jobs.py +1344 -0
  84. control_plane_api/app/routers/models.py +82 -0
  85. control_plane_api/app/routers/models_v2.py +361 -0
  86. control_plane_api/app/routers/policies.py +639 -0
  87. control_plane_api/app/routers/presence.py +234 -0
  88. control_plane_api/app/routers/projects.py +902 -0
  89. control_plane_api/app/routers/runners.py +379 -0
  90. control_plane_api/app/routers/runtimes.py +172 -0
  91. control_plane_api/app/routers/secrets.py +155 -0
  92. control_plane_api/app/routers/skills.py +1001 -0
  93. control_plane_api/app/routers/skills_definitions.py +140 -0
  94. control_plane_api/app/routers/task_planning.py +1256 -0
  95. control_plane_api/app/routers/task_queues.py +654 -0
  96. control_plane_api/app/routers/team_context.py +270 -0
  97. control_plane_api/app/routers/teams.py +1400 -0
  98. control_plane_api/app/routers/worker_queues.py +1545 -0
  99. control_plane_api/app/routers/workers.py +935 -0
  100. control_plane_api/app/routers/workflows.py +204 -0
  101. control_plane_api/app/runtimes/__init__.py +6 -0
  102. control_plane_api/app/runtimes/validation.py +344 -0
  103. control_plane_api/app/schemas/job_schemas.py +295 -0
  104. control_plane_api/app/services/__init__.py +1 -0
  105. control_plane_api/app/services/agno_service.py +619 -0
  106. control_plane_api/app/services/litellm_service.py +190 -0
  107. control_plane_api/app/services/policy_service.py +525 -0
  108. control_plane_api/app/services/temporal_cloud_provisioning.py +150 -0
  109. control_plane_api/app/skills/__init__.py +44 -0
  110. control_plane_api/app/skills/base.py +229 -0
  111. control_plane_api/app/skills/business_intelligence.py +189 -0
  112. control_plane_api/app/skills/data_visualization.py +154 -0
  113. control_plane_api/app/skills/docker.py +104 -0
  114. control_plane_api/app/skills/file_generation.py +94 -0
  115. control_plane_api/app/skills/file_system.py +110 -0
  116. control_plane_api/app/skills/python.py +92 -0
  117. control_plane_api/app/skills/registry.py +65 -0
  118. control_plane_api/app/skills/shell.py +102 -0
  119. control_plane_api/app/skills/workflow_executor.py +469 -0
  120. control_plane_api/app/utils/workflow_executor.py +354 -0
  121. control_plane_api/app/workflows/__init__.py +11 -0
  122. control_plane_api/app/workflows/agent_execution.py +507 -0
  123. control_plane_api/app/workflows/agent_execution_with_skills.py +222 -0
  124. control_plane_api/app/workflows/namespace_provisioning.py +326 -0
  125. control_plane_api/app/workflows/team_execution.py +399 -0
  126. control_plane_api/scripts/seed_models.py +239 -0
  127. control_plane_api/worker/__init__.py +0 -0
  128. control_plane_api/worker/activities/__init__.py +0 -0
  129. control_plane_api/worker/activities/agent_activities.py +1241 -0
  130. control_plane_api/worker/activities/approval_activities.py +234 -0
  131. control_plane_api/worker/activities/runtime_activities.py +388 -0
  132. control_plane_api/worker/activities/skill_activities.py +267 -0
  133. control_plane_api/worker/activities/team_activities.py +1217 -0
  134. control_plane_api/worker/config/__init__.py +31 -0
  135. control_plane_api/worker/config/worker_config.py +275 -0
  136. control_plane_api/worker/control_plane_client.py +529 -0
  137. control_plane_api/worker/examples/analytics_integration_example.py +362 -0
  138. control_plane_api/worker/models/__init__.py +1 -0
  139. control_plane_api/worker/models/inputs.py +89 -0
  140. control_plane_api/worker/runtimes/__init__.py +31 -0
  141. control_plane_api/worker/runtimes/base.py +789 -0
  142. control_plane_api/worker/runtimes/claude_code_runtime.py +1443 -0
  143. control_plane_api/worker/runtimes/default_runtime.py +617 -0
  144. control_plane_api/worker/runtimes/factory.py +173 -0
  145. control_plane_api/worker/runtimes/validation.py +93 -0
  146. control_plane_api/worker/services/__init__.py +1 -0
  147. control_plane_api/worker/services/agent_executor.py +422 -0
  148. control_plane_api/worker/services/agent_executor_v2.py +383 -0
  149. control_plane_api/worker/services/analytics_collector.py +457 -0
  150. control_plane_api/worker/services/analytics_service.py +464 -0
  151. control_plane_api/worker/services/approval_tools.py +310 -0
  152. control_plane_api/worker/services/approval_tools_agno.py +207 -0
  153. control_plane_api/worker/services/cancellation_manager.py +177 -0
  154. control_plane_api/worker/services/data_visualization.py +827 -0
  155. control_plane_api/worker/services/jira_tools.py +257 -0
  156. control_plane_api/worker/services/runtime_analytics.py +328 -0
  157. control_plane_api/worker/services/session_service.py +194 -0
  158. control_plane_api/worker/services/skill_factory.py +175 -0
  159. control_plane_api/worker/services/team_executor.py +574 -0
  160. control_plane_api/worker/services/team_executor_v2.py +465 -0
  161. control_plane_api/worker/services/workflow_executor_tools.py +1418 -0
  162. control_plane_api/worker/tests/__init__.py +1 -0
  163. control_plane_api/worker/tests/e2e/__init__.py +0 -0
  164. control_plane_api/worker/tests/e2e/test_execution_flow.py +571 -0
  165. control_plane_api/worker/tests/integration/__init__.py +0 -0
  166. control_plane_api/worker/tests/integration/test_control_plane_integration.py +308 -0
  167. control_plane_api/worker/tests/unit/__init__.py +0 -0
  168. control_plane_api/worker/tests/unit/test_control_plane_client.py +401 -0
  169. control_plane_api/worker/utils/__init__.py +1 -0
  170. control_plane_api/worker/utils/chunk_batcher.py +305 -0
  171. control_plane_api/worker/utils/retry_utils.py +60 -0
  172. control_plane_api/worker/utils/streaming_utils.py +373 -0
  173. control_plane_api/worker/worker.py +753 -0
  174. control_plane_api/worker/workflows/__init__.py +0 -0
  175. control_plane_api/worker/workflows/agent_execution.py +589 -0
  176. control_plane_api/worker/workflows/team_execution.py +429 -0
  177. kubiya_control_plane_api-0.3.4.dist-info/METADATA +229 -0
  178. kubiya_control_plane_api-0.3.4.dist-info/RECORD +182 -0
  179. kubiya_control_plane_api-0.3.4.dist-info/entry_points.txt +2 -0
  180. kubiya_control_plane_api-0.3.4.dist-info/top_level.txt +1 -0
  181. kubiya_control_plane_api-0.1.0.dist-info/METADATA +0 -66
  182. kubiya_control_plane_api-0.1.0.dist-info/RECORD +0 -5
  183. kubiya_control_plane_api-0.1.0.dist-info/top_level.txt +0 -1
  184. {kubiya_control_plane_api-0.1.0.dist-info/licenses → control_plane_api}/LICENSE +0 -0
  185. {kubiya_control_plane_api-0.1.0.dist-info → kubiya_control_plane_api-0.3.4.dist-info}/WHEEL +0 -0
@@ -0,0 +1,354 @@
1
+ """
2
+ API-specific configuration.
3
+
4
+ This module contains settings specific to the Control Plane API server.
5
+ """
6
+
7
+ from pydantic_settings import BaseSettings, SettingsConfigDict
8
+ from pydantic import Field, validator, model_validator, AliasChoices
9
+ from typing import List, Optional
10
+ import secrets
11
+ import os
12
+
13
+
14
+ class APIConfig(BaseSettings):
15
+ """Configuration for Control Plane API server."""
16
+
17
+ # ==================== API Server Settings ====================
18
+
19
+ api_host: str = Field(
20
+ default="0.0.0.0",
21
+ description="API server host",
22
+ )
23
+
24
+ api_port: int = Field(
25
+ default=8000,
26
+ description="API server port",
27
+ )
28
+
29
+ api_workers: int = Field(
30
+ default=4,
31
+ description="Number of API worker processes",
32
+ )
33
+
34
+ api_title: str = Field(
35
+ default="Agent Control Plane API",
36
+ description="API title for documentation",
37
+ )
38
+
39
+ api_version: str = Field(
40
+ default="0.3.0",
41
+ description="API version",
42
+ )
43
+
44
+ api_description: str = Field(
45
+ default="Multi-tenant agent orchestration with Temporal workflows",
46
+ description="API description for documentation",
47
+ )
48
+
49
+ # ==================== Environment ====================
50
+
51
+ environment: str = Field(
52
+ default="development",
53
+ description="Environment (development, staging, production)",
54
+ )
55
+
56
+ debug: bool = Field(
57
+ default=False,
58
+ description="Debug mode",
59
+ )
60
+
61
+ @validator("debug", pre=True)
62
+ def set_debug_from_env(cls, v, values):
63
+ """Set debug based on environment if not explicitly set."""
64
+ if v is None:
65
+ env = values.get("environment", "development")
66
+ return env == "development"
67
+ return v
68
+
69
+ # ==================== Database Settings ====================
70
+
71
+ database_url: Optional[str] = Field(
72
+ default=None,
73
+ description="PostgreSQL database URL",
74
+ validation_alias=AliasChoices("DATABASE_URL", "database_url"),
75
+ )
76
+
77
+ supabase_url: Optional[str] = Field(
78
+ default=None,
79
+ description="Supabase project URL",
80
+ validation_alias=AliasChoices("SUPABASE_URL", "supabase_url"),
81
+ )
82
+
83
+ supabase_service_key: Optional[str] = Field(
84
+ default=None,
85
+ description="Supabase service role key",
86
+ validation_alias=AliasChoices("SUPABASE_SERVICE_KEY", "supabase_service_key"),
87
+ )
88
+
89
+ supabase_anon_key: Optional[str] = Field(
90
+ default=None,
91
+ description="Supabase anonymous key",
92
+ validation_alias=AliasChoices("SUPABASE_ANON_KEY", "supabase_anon_key"),
93
+ )
94
+
95
+ database_pool_size: int = Field(
96
+ default=20,
97
+ description="Database connection pool size",
98
+ )
99
+
100
+ database_max_overflow: int = Field(
101
+ default=40,
102
+ description="Maximum overflow for database pool",
103
+ )
104
+
105
+ database_pool_timeout: float = Field(
106
+ default=30.0,
107
+ description="Database pool timeout in seconds",
108
+ )
109
+
110
+ @model_validator(mode='after')
111
+ def validate_database_config(self):
112
+ """Ensure we have either DATABASE_URL or Supabase configuration."""
113
+ # Always try to set database_url from Supabase env vars if not already set
114
+ if not self.database_url:
115
+ supabase_db_url = (
116
+ os.environ.get("SUPABASE_POSTGRES_URL") or
117
+ os.environ.get("SUPABASE_POSTGRES_PRISMA_URL") or
118
+ os.environ.get("SUPABASE_DB_URL")
119
+ )
120
+ if supabase_db_url:
121
+ # Fix URL format for SQLAlchemy 2.0+
122
+ if supabase_db_url.startswith("postgres://"):
123
+ supabase_db_url = supabase_db_url.replace("postgres://", "postgresql://", 1)
124
+ # Remove invalid Supabase pooler parameters that SQLAlchemy doesn't understand
125
+ supabase_db_url = supabase_db_url.replace("&supa=base-pooler.x", "")
126
+ self.database_url = supabase_db_url
127
+ elif not (self.supabase_url and self.supabase_service_key) and self.environment != "development":
128
+ raise ValueError(
129
+ "Either DATABASE_URL or Supabase configuration (SUPABASE_URL and SUPABASE_SERVICE_KEY) must be provided"
130
+ )
131
+
132
+ # Fix postgres:// to postgresql:// if needed and remove invalid params
133
+ if self.database_url:
134
+ if self.database_url.startswith("postgres://"):
135
+ self.database_url = self.database_url.replace("postgres://", "postgresql://", 1)
136
+ # Remove invalid Supabase pooler parameters from DATABASE_URL
137
+ self.database_url = self.database_url.replace("&supa=base-pooler.x", "")
138
+
139
+ return self
140
+
141
+ # ==================== Redis Settings ====================
142
+
143
+ redis_url: str = Field(
144
+ default="redis://localhost:6379/0",
145
+ description="Redis connection URL",
146
+ validation_alias=AliasChoices("REDIS_URL", "redis_url"),
147
+ )
148
+
149
+ redis_host: Optional[str] = Field(
150
+ default=None,
151
+ description="Redis host (overrides URL)",
152
+ )
153
+
154
+ redis_port: int = Field(
155
+ default=6379,
156
+ description="Redis port",
157
+ )
158
+
159
+ redis_password: Optional[str] = Field(
160
+ default=None,
161
+ description="Redis password",
162
+ )
163
+
164
+ redis_db: int = Field(
165
+ default=0,
166
+ description="Redis database number",
167
+ )
168
+
169
+ redis_pool_size: int = Field(
170
+ default=10,
171
+ description="Redis connection pool size",
172
+ )
173
+
174
+ # ==================== Temporal Settings ====================
175
+
176
+ temporal_host: str = Field(
177
+ default="localhost:7233",
178
+ description="Temporal server host:port",
179
+ )
180
+
181
+ temporal_namespace: str = Field(
182
+ default="default",
183
+ description="Temporal namespace",
184
+ )
185
+
186
+ temporal_client_cert_path: Optional[str] = Field(
187
+ default=None,
188
+ description="Path to Temporal client certificate",
189
+ )
190
+
191
+ temporal_client_key_path: Optional[str] = Field(
192
+ default=None,
193
+ description="Path to Temporal client key",
194
+ )
195
+
196
+ # ==================== Security Settings ====================
197
+
198
+ secret_key: str = Field(
199
+ default_factory=lambda: secrets.token_urlsafe(32),
200
+ description="Secret key for JWT signing",
201
+ validation_alias=AliasChoices("SECRET_KEY", "secret_key"),
202
+ )
203
+
204
+ algorithm: str = Field(
205
+ default="HS256",
206
+ description="JWT signing algorithm",
207
+ )
208
+
209
+ access_token_expire_minutes: int = Field(
210
+ default=30,
211
+ description="Access token expiration in minutes",
212
+ )
213
+
214
+ refresh_token_expire_days: int = Field(
215
+ default=30,
216
+ description="Refresh token expiration in days",
217
+ )
218
+
219
+ # ==================== CORS Settings ====================
220
+
221
+ cors_origins: List[str] = Field(
222
+ default=["*"],
223
+ description="Allowed CORS origins",
224
+ )
225
+
226
+ cors_allow_credentials: bool = Field(
227
+ default=True,
228
+ description="Allow credentials in CORS requests",
229
+ )
230
+
231
+ cors_allow_methods: List[str] = Field(
232
+ default=["*"],
233
+ description="Allowed CORS methods",
234
+ )
235
+
236
+ cors_allow_headers: List[str] = Field(
237
+ default=["*"],
238
+ description="Allowed CORS headers",
239
+ )
240
+
241
+ @validator("cors_origins", pre=True)
242
+ def validate_cors_origins(cls, v, values):
243
+ """Validate CORS origins for production."""
244
+ # Allow environment variable to override default
245
+ if v is None or (isinstance(v, list) and len(v) == 1 and v[0] == "*"):
246
+ # Check if we're in production
247
+ env = values.get("environment", "development")
248
+ if env == "production":
249
+ # In production, use specific origins unless explicitly overridden
250
+ return [
251
+ "https://agent-control-plane.vercel.app",
252
+ "https://*.vercel.app",
253
+ "http://localhost:3000",
254
+ "http://localhost:8000",
255
+ ]
256
+ return v
257
+
258
+ # ==================== External Services ====================
259
+
260
+ kubiya_api_base: str = Field(
261
+ default="https://api.kubiya.ai",
262
+ description="Kubiya API base URL",
263
+ )
264
+
265
+ kubiya_api_key: Optional[str] = Field(
266
+ default=None,
267
+ description="Kubiya API key",
268
+ validation_alias=AliasChoices("KUBIYA_API_KEY", "kubiya_api_key"),
269
+ )
270
+
271
+ litellm_api_base: str = Field(
272
+ default="https://llm-proxy.kubiya.ai",
273
+ description="LiteLLM proxy base URL",
274
+ )
275
+
276
+ litellm_api_key: Optional[str] = Field(
277
+ default=None,
278
+ description="LiteLLM API key",
279
+ validation_alias=AliasChoices("LITELLM_API_KEY", "litellm_api_key"),
280
+ )
281
+
282
+ litellm_default_model: str = Field(
283
+ default="kubiya/claude-sonnet-4",
284
+ description="Default LLM model",
285
+ )
286
+
287
+ litellm_timeout: int = Field(
288
+ default=300,
289
+ description="LiteLLM request timeout in seconds",
290
+ )
291
+
292
+ # ==================== Logging Settings ====================
293
+
294
+ log_level: str = Field(
295
+ default="INFO",
296
+ description="Logging level",
297
+ )
298
+
299
+ log_format: str = Field(
300
+ default="json",
301
+ description="Log format (json or text)",
302
+ )
303
+
304
+ # ==================== Monitoring Settings ====================
305
+
306
+ metrics_enabled: bool = Field(
307
+ default=False,
308
+ description="Enable Prometheus metrics",
309
+ )
310
+
311
+ metrics_port: int = Field(
312
+ default=9090,
313
+ description="Prometheus metrics port",
314
+ )
315
+
316
+ tracing_enabled: bool = Field(
317
+ default=False,
318
+ description="Enable OpenTelemetry tracing",
319
+ )
320
+
321
+ otlp_endpoint: Optional[str] = Field(
322
+ default=None,
323
+ description="OpenTelemetry collector endpoint",
324
+ )
325
+
326
+ sentry_dsn: Optional[str] = Field(
327
+ default=None,
328
+ description="Sentry DSN for error reporting",
329
+ validation_alias=AliasChoices("SENTRY_DSN", "sentry_dsn"),
330
+ )
331
+
332
+ # ==================== Rate Limiting ====================
333
+
334
+ rate_limit_enabled: bool = Field(
335
+ default=True,
336
+ description="Enable rate limiting",
337
+ )
338
+
339
+ rate_limit_requests_per_minute: int = Field(
340
+ default=60,
341
+ description="Default requests per minute limit",
342
+ )
343
+
344
+ rate_limit_burst_size: int = Field(
345
+ default=10,
346
+ description="Burst size for rate limiting",
347
+ )
348
+
349
+ model_config = SettingsConfigDict(
350
+ env_file=".env.local",
351
+ env_file_encoding="utf-8",
352
+ case_sensitive=False,
353
+ extra="ignore",
354
+ )
@@ -0,0 +1,318 @@
1
+ """
2
+ Model Pricing and AEM Weight Configuration
3
+
4
+ This module defines pricing tiers and Agentic Engineering Minutes (AEM) weights
5
+ for different model families.
6
+
7
+ AEM Formula: Runtime (minutes) × Model Weight × Tool Calls Weight
8
+
9
+ Model Weights:
10
+ - Opus-class (most capable): 2.0x weight
11
+ - Sonnet-class (balanced): 1.0x weight
12
+ - Haiku-class (fast): 0.5x weight
13
+ """
14
+
15
+ from typing import Dict, Any
16
+ from enum import Enum
17
+
18
+
19
+ class ModelTier(str, Enum):
20
+ """Model capability tiers"""
21
+ OPUS = "opus" # Most capable, highest cost
22
+ SONNET = "sonnet" # Balanced capability and cost
23
+ HAIKU = "haiku" # Fast and efficient
24
+ CUSTOM = "custom" # Custom/unknown models
25
+
26
+
27
+ # Model Weight Configuration for AEM Calculation
28
+ MODEL_WEIGHTS: Dict[str, float] = {
29
+ # Anthropic Claude Models
30
+ "claude-opus-4": 2.0,
31
+ "claude-4-opus": 2.0,
32
+ "claude-3-opus": 2.0,
33
+ "claude-3-opus-20240229": 2.0,
34
+
35
+ "claude-sonnet-4": 1.0,
36
+ "claude-4-sonnet": 1.0,
37
+ "claude-3.5-sonnet": 1.0,
38
+ "claude-3-5-sonnet-20241022": 1.0,
39
+ "claude-3-sonnet": 1.0,
40
+ "claude-3-sonnet-20240229": 1.0,
41
+
42
+ "claude-haiku-4": 0.5,
43
+ "claude-4-haiku": 0.5,
44
+ "claude-3.5-haiku": 0.5,
45
+ "claude-3-5-haiku-20241022": 0.5,
46
+ "claude-3-haiku": 0.5,
47
+ "claude-3-haiku-20240307": 0.5,
48
+
49
+ # OpenAI Models
50
+ "gpt-4": 2.0,
51
+ "gpt-4-turbo": 2.0,
52
+ "gpt-4-turbo-preview": 2.0,
53
+ "gpt-4-0125-preview": 2.0,
54
+ "gpt-4-1106-preview": 2.0,
55
+ "gpt-4o": 1.3, # As shown in the image
56
+ "gpt-4o-mini": 0.7,
57
+
58
+ "gpt-3.5-turbo": 0.5,
59
+ "gpt-3.5-turbo-16k": 0.5,
60
+
61
+ # Google Models
62
+ "gemini-1.5-pro": 1.5,
63
+ "gemini-1.5-flash": 0.7,
64
+ "gemini-pro": 1.0,
65
+
66
+ # Meta Models
67
+ "llama-3-70b": 1.2,
68
+ "llama-3-8b": 0.5,
69
+
70
+ # Mistral Models
71
+ "mistral-large": 1.5,
72
+ "mistral-medium": 1.0,
73
+ "mistral-small": 0.5,
74
+ }
75
+
76
+
77
+ # Token Pricing per 1M tokens (in USD)
78
+ TOKEN_PRICING: Dict[str, Dict[str, float]] = {
79
+ # Anthropic Claude
80
+ "claude-opus-4": {
81
+ "input": 15.00,
82
+ "output": 75.00,
83
+ "cache_read": 1.50,
84
+ "cache_creation": 18.75,
85
+ },
86
+ "claude-sonnet-4": {
87
+ "input": 3.00,
88
+ "output": 15.00,
89
+ "cache_read": 0.30,
90
+ "cache_creation": 3.75,
91
+ },
92
+ "claude-haiku-4": {
93
+ "input": 0.80,
94
+ "output": 4.00,
95
+ "cache_read": 0.08,
96
+ "cache_creation": 1.00,
97
+ },
98
+
99
+ # OpenAI
100
+ "gpt-4": {
101
+ "input": 30.00,
102
+ "output": 60.00,
103
+ "cache_read": 0.0,
104
+ "cache_creation": 0.0,
105
+ },
106
+ "gpt-4o": {
107
+ "input": 5.00,
108
+ "output": 15.00,
109
+ "cache_read": 0.0,
110
+ "cache_creation": 0.0,
111
+ },
112
+ "gpt-3.5-turbo": {
113
+ "input": 0.50,
114
+ "output": 1.50,
115
+ "cache_read": 0.0,
116
+ "cache_creation": 0.0,
117
+ },
118
+
119
+ # Google Gemini
120
+ "gemini-1.5-pro": {
121
+ "input": 3.50,
122
+ "output": 10.50,
123
+ "cache_read": 0.0,
124
+ "cache_creation": 0.0,
125
+ },
126
+ "gemini-1.5-flash": {
127
+ "input": 0.35,
128
+ "output": 1.05,
129
+ "cache_read": 0.0,
130
+ "cache_creation": 0.0,
131
+ },
132
+ }
133
+
134
+
135
+ # AEM Pricing Configuration
136
+ AEM_PRICING: Dict[str, float] = {
137
+ "saas_prepaid_per_minute": 0.15, # $0.15/min for SaaS/Hybrid
138
+ "on_prem_unlimited": 0.0, # Unlimited for on-prem
139
+ }
140
+
141
+
142
+ def get_model_weight(model: str) -> float:
143
+ """
144
+ Get the AEM weight for a model.
145
+
146
+ Args:
147
+ model: Model identifier
148
+
149
+ Returns:
150
+ Weight multiplier for AEM calculation (default 1.0)
151
+ """
152
+ # Normalize model name
153
+ model_lower = model.lower().strip()
154
+
155
+ # Try exact match first
156
+ if model_lower in MODEL_WEIGHTS:
157
+ return MODEL_WEIGHTS[model_lower]
158
+
159
+ # Try fuzzy matching by keywords
160
+ if "opus" in model_lower:
161
+ return 2.0
162
+ elif "sonnet" in model_lower:
163
+ return 1.0
164
+ elif "haiku" in model_lower:
165
+ return 0.5
166
+ elif "gpt-4" in model_lower and "turbo" in model_lower:
167
+ return 2.0
168
+ elif "gpt-4o" in model_lower:
169
+ return 1.3
170
+ elif "gpt-3.5" in model_lower:
171
+ return 0.5
172
+ elif "gemini" in model_lower and "pro" in model_lower:
173
+ return 1.5
174
+ elif "gemini" in model_lower and "flash" in model_lower:
175
+ return 0.7
176
+
177
+ # Default weight
178
+ return 1.0
179
+
180
+
181
+ def get_model_tier(model: str) -> ModelTier:
182
+ """
183
+ Determine the tier/class of a model.
184
+
185
+ Args:
186
+ model: Model identifier
187
+
188
+ Returns:
189
+ ModelTier enum value
190
+ """
191
+ weight = get_model_weight(model)
192
+
193
+ if weight >= 1.5:
194
+ return ModelTier.OPUS
195
+ elif weight >= 0.8:
196
+ return ModelTier.SONNET
197
+ elif weight >= 0.3:
198
+ return ModelTier.HAIKU
199
+ else:
200
+ return ModelTier.CUSTOM
201
+
202
+
203
+ def calculate_token_cost(
204
+ model: str,
205
+ input_tokens: int,
206
+ output_tokens: int,
207
+ cache_read_tokens: int = 0,
208
+ cache_creation_tokens: int = 0,
209
+ ) -> Dict[str, float]:
210
+ """
211
+ Calculate token costs based on model pricing.
212
+
213
+ Args:
214
+ model: Model identifier
215
+ input_tokens: Number of input tokens
216
+ output_tokens: Number of output tokens
217
+ cache_read_tokens: Number of cached tokens read
218
+ cache_creation_tokens: Number of tokens used for cache creation
219
+
220
+ Returns:
221
+ Dict with cost breakdown
222
+ """
223
+ # Normalize model name
224
+ model_lower = model.lower().strip()
225
+
226
+ # Get pricing (use closest match or default to sonnet pricing)
227
+ pricing = None
228
+
229
+ # Try exact match
230
+ for price_key in TOKEN_PRICING:
231
+ if price_key in model_lower or model_lower in price_key:
232
+ pricing = TOKEN_PRICING[price_key]
233
+ break
234
+
235
+ # Default to sonnet pricing if no match
236
+ if pricing is None:
237
+ pricing = TOKEN_PRICING["claude-sonnet-4"]
238
+
239
+ # Calculate costs (pricing is per 1M tokens)
240
+ input_cost = (input_tokens / 1_000_000) * pricing["input"]
241
+ output_cost = (output_tokens / 1_000_000) * pricing["output"]
242
+ cache_read_cost = (cache_read_tokens / 1_000_000) * pricing["cache_read"]
243
+ cache_creation_cost = (cache_creation_tokens / 1_000_000) * pricing["cache_creation"]
244
+
245
+ return {
246
+ "input_cost": round(input_cost, 6),
247
+ "output_cost": round(output_cost, 6),
248
+ "cache_read_cost": round(cache_read_cost, 6),
249
+ "cache_creation_cost": round(cache_creation_cost, 6),
250
+ "total_cost": round(input_cost + output_cost + cache_read_cost + cache_creation_cost, 6),
251
+ }
252
+
253
+
254
+ def calculate_aem(
255
+ duration_ms: int,
256
+ model: str,
257
+ tool_calls_count: int,
258
+ tool_calls_weight: float = 1.0,
259
+ ) -> Dict[str, float]:
260
+ """
261
+ Calculate Agentic Engineering Minutes (AEM).
262
+
263
+ Formula: Runtime (minutes) × Model Weight × Tool Calls Weight
264
+
265
+ Args:
266
+ duration_ms: Turn duration in milliseconds
267
+ model: Model identifier
268
+ tool_calls_count: Number of tool calls in this turn
269
+ tool_calls_weight: Weight multiplier for tool complexity (default 1.0)
270
+
271
+ Returns:
272
+ Dict with AEM metrics
273
+ """
274
+ # Convert duration to minutes
275
+ runtime_minutes = duration_ms / 60_000.0
276
+
277
+ # Get model weight
278
+ model_weight = get_model_weight(model)
279
+
280
+ # Calculate tool calls weight (simple linear for now, can be made more complex)
281
+ # Example: Each tool call adds to complexity
282
+ # From the image example: 200 tool calls = 3.9 weight
283
+ # This suggests approximately: tool_calls_weight = (tool_calls_count / 50) if tool_calls_count > 0 else 1.0
284
+ if tool_calls_count > 0:
285
+ calculated_tool_weight = max(1.0, tool_calls_count / 50.0) # Roughly matches 200 calls = 4.0
286
+ else:
287
+ calculated_tool_weight = 1.0
288
+
289
+ # Override with provided weight if specified
290
+ final_tool_weight = tool_calls_weight if tool_calls_weight != 1.0 else calculated_tool_weight
291
+
292
+ # Calculate AEM value
293
+ aem_value = runtime_minutes * model_weight * final_tool_weight
294
+
295
+ # Calculate AEM cost (using SaaS pricing)
296
+ aem_cost = aem_value * AEM_PRICING["saas_prepaid_per_minute"]
297
+
298
+ return {
299
+ "runtime_minutes": round(runtime_minutes, 4),
300
+ "model_weight": round(model_weight, 2),
301
+ "tool_calls_weight": round(final_tool_weight, 2),
302
+ "aem_value": round(aem_value, 4),
303
+ "aem_cost": round(aem_cost, 4),
304
+ "model_tier": get_model_tier(model).value,
305
+ }
306
+
307
+
308
+ # Export configuration for external use
309
+ __all__ = [
310
+ "ModelTier",
311
+ "MODEL_WEIGHTS",
312
+ "TOKEN_PRICING",
313
+ "AEM_PRICING",
314
+ "get_model_weight",
315
+ "get_model_tier",
316
+ "calculate_token_cost",
317
+ "calculate_aem",
318
+ ]