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.
- control_plane_api/README.md +266 -0
- control_plane_api/__init__.py +0 -0
- control_plane_api/__version__.py +1 -0
- control_plane_api/alembic/README +1 -0
- control_plane_api/alembic/env.py +98 -0
- control_plane_api/alembic/script.py.mako +28 -0
- control_plane_api/alembic/versions/1382bec74309_initial_migration_with_all_models.py +251 -0
- control_plane_api/alembic/versions/1f54bc2a37e3_add_analytics_tables.py +162 -0
- control_plane_api/alembic/versions/2e4cb136dc10_rename_toolset_ids_to_skill_ids_in_teams.py +30 -0
- control_plane_api/alembic/versions/31cd69a644ce_add_skill_templates_table.py +28 -0
- control_plane_api/alembic/versions/89e127caa47d_add_jobs_and_job_executions_tables.py +161 -0
- control_plane_api/alembic/versions/add_llm_models_table.py +51 -0
- control_plane_api/alembic/versions/b0e10697f212_add_runtime_column_to_teams_simple.py +42 -0
- control_plane_api/alembic/versions/ce43b24b63bf_add_execution_trigger_source_and_fix_.py +155 -0
- control_plane_api/alembic/versions/d4eaf16e3f8d_rename_toolsets_to_skills.py +84 -0
- control_plane_api/alembic/versions/efa2dc427da1_rename_metadata_to_custom_metadata.py +32 -0
- control_plane_api/alembic/versions/f973b431d1ce_add_workflow_executor_to_skill_types.py +44 -0
- control_plane_api/alembic.ini +148 -0
- control_plane_api/api/index.py +12 -0
- control_plane_api/app/__init__.py +11 -0
- control_plane_api/app/activities/__init__.py +20 -0
- control_plane_api/app/activities/agent_activities.py +379 -0
- control_plane_api/app/activities/team_activities.py +410 -0
- control_plane_api/app/activities/temporal_cloud_activities.py +577 -0
- control_plane_api/app/config/__init__.py +35 -0
- control_plane_api/app/config/api_config.py +354 -0
- control_plane_api/app/config/model_pricing.py +318 -0
- control_plane_api/app/config.py +95 -0
- control_plane_api/app/database.py +135 -0
- control_plane_api/app/exceptions.py +408 -0
- control_plane_api/app/lib/__init__.py +11 -0
- control_plane_api/app/lib/job_executor.py +312 -0
- control_plane_api/app/lib/kubiya_client.py +235 -0
- control_plane_api/app/lib/litellm_pricing.py +166 -0
- control_plane_api/app/lib/planning_tools/__init__.py +22 -0
- control_plane_api/app/lib/planning_tools/agents.py +155 -0
- control_plane_api/app/lib/planning_tools/base.py +189 -0
- control_plane_api/app/lib/planning_tools/environments.py +214 -0
- control_plane_api/app/lib/planning_tools/resources.py +240 -0
- control_plane_api/app/lib/planning_tools/teams.py +198 -0
- control_plane_api/app/lib/policy_enforcer_client.py +939 -0
- control_plane_api/app/lib/redis_client.py +436 -0
- control_plane_api/app/lib/supabase.py +71 -0
- control_plane_api/app/lib/temporal_client.py +138 -0
- control_plane_api/app/lib/validation/__init__.py +20 -0
- control_plane_api/app/lib/validation/runtime_validation.py +287 -0
- control_plane_api/app/main.py +128 -0
- control_plane_api/app/middleware/__init__.py +8 -0
- control_plane_api/app/middleware/auth.py +513 -0
- control_plane_api/app/middleware/exception_handler.py +267 -0
- control_plane_api/app/middleware/rate_limiting.py +384 -0
- control_plane_api/app/middleware/request_id.py +202 -0
- control_plane_api/app/models/__init__.py +27 -0
- control_plane_api/app/models/agent.py +79 -0
- control_plane_api/app/models/analytics.py +206 -0
- control_plane_api/app/models/associations.py +81 -0
- control_plane_api/app/models/environment.py +63 -0
- control_plane_api/app/models/execution.py +93 -0
- control_plane_api/app/models/job.py +179 -0
- control_plane_api/app/models/llm_model.py +75 -0
- control_plane_api/app/models/presence.py +49 -0
- control_plane_api/app/models/project.py +47 -0
- control_plane_api/app/models/session.py +38 -0
- control_plane_api/app/models/team.py +66 -0
- control_plane_api/app/models/workflow.py +55 -0
- control_plane_api/app/policies/README.md +121 -0
- control_plane_api/app/policies/approved_users.rego +62 -0
- control_plane_api/app/policies/business_hours.rego +51 -0
- control_plane_api/app/policies/rate_limiting.rego +100 -0
- control_plane_api/app/policies/tool_restrictions.rego +86 -0
- control_plane_api/app/routers/__init__.py +4 -0
- control_plane_api/app/routers/agents.py +364 -0
- control_plane_api/app/routers/agents_v2.py +1260 -0
- control_plane_api/app/routers/analytics.py +1014 -0
- control_plane_api/app/routers/context_manager.py +562 -0
- control_plane_api/app/routers/environment_context.py +270 -0
- control_plane_api/app/routers/environments.py +715 -0
- control_plane_api/app/routers/execution_environment.py +517 -0
- control_plane_api/app/routers/executions.py +1911 -0
- control_plane_api/app/routers/health.py +92 -0
- control_plane_api/app/routers/health_v2.py +326 -0
- control_plane_api/app/routers/integrations.py +274 -0
- control_plane_api/app/routers/jobs.py +1344 -0
- control_plane_api/app/routers/models.py +82 -0
- control_plane_api/app/routers/models_v2.py +361 -0
- control_plane_api/app/routers/policies.py +639 -0
- control_plane_api/app/routers/presence.py +234 -0
- control_plane_api/app/routers/projects.py +902 -0
- control_plane_api/app/routers/runners.py +379 -0
- control_plane_api/app/routers/runtimes.py +172 -0
- control_plane_api/app/routers/secrets.py +155 -0
- control_plane_api/app/routers/skills.py +1001 -0
- control_plane_api/app/routers/skills_definitions.py +140 -0
- control_plane_api/app/routers/task_planning.py +1256 -0
- control_plane_api/app/routers/task_queues.py +654 -0
- control_plane_api/app/routers/team_context.py +270 -0
- control_plane_api/app/routers/teams.py +1400 -0
- control_plane_api/app/routers/worker_queues.py +1545 -0
- control_plane_api/app/routers/workers.py +935 -0
- control_plane_api/app/routers/workflows.py +204 -0
- control_plane_api/app/runtimes/__init__.py +6 -0
- control_plane_api/app/runtimes/validation.py +344 -0
- control_plane_api/app/schemas/job_schemas.py +295 -0
- control_plane_api/app/services/__init__.py +1 -0
- control_plane_api/app/services/agno_service.py +619 -0
- control_plane_api/app/services/litellm_service.py +190 -0
- control_plane_api/app/services/policy_service.py +525 -0
- control_plane_api/app/services/temporal_cloud_provisioning.py +150 -0
- control_plane_api/app/skills/__init__.py +44 -0
- control_plane_api/app/skills/base.py +229 -0
- control_plane_api/app/skills/business_intelligence.py +189 -0
- control_plane_api/app/skills/data_visualization.py +154 -0
- control_plane_api/app/skills/docker.py +104 -0
- control_plane_api/app/skills/file_generation.py +94 -0
- control_plane_api/app/skills/file_system.py +110 -0
- control_plane_api/app/skills/python.py +92 -0
- control_plane_api/app/skills/registry.py +65 -0
- control_plane_api/app/skills/shell.py +102 -0
- control_plane_api/app/skills/workflow_executor.py +469 -0
- control_plane_api/app/utils/workflow_executor.py +354 -0
- control_plane_api/app/workflows/__init__.py +11 -0
- control_plane_api/app/workflows/agent_execution.py +507 -0
- control_plane_api/app/workflows/agent_execution_with_skills.py +222 -0
- control_plane_api/app/workflows/namespace_provisioning.py +326 -0
- control_plane_api/app/workflows/team_execution.py +399 -0
- control_plane_api/scripts/seed_models.py +239 -0
- control_plane_api/worker/__init__.py +0 -0
- control_plane_api/worker/activities/__init__.py +0 -0
- control_plane_api/worker/activities/agent_activities.py +1241 -0
- control_plane_api/worker/activities/approval_activities.py +234 -0
- control_plane_api/worker/activities/runtime_activities.py +388 -0
- control_plane_api/worker/activities/skill_activities.py +267 -0
- control_plane_api/worker/activities/team_activities.py +1217 -0
- control_plane_api/worker/config/__init__.py +31 -0
- control_plane_api/worker/config/worker_config.py +275 -0
- control_plane_api/worker/control_plane_client.py +529 -0
- control_plane_api/worker/examples/analytics_integration_example.py +362 -0
- control_plane_api/worker/models/__init__.py +1 -0
- control_plane_api/worker/models/inputs.py +89 -0
- control_plane_api/worker/runtimes/__init__.py +31 -0
- control_plane_api/worker/runtimes/base.py +789 -0
- control_plane_api/worker/runtimes/claude_code_runtime.py +1443 -0
- control_plane_api/worker/runtimes/default_runtime.py +617 -0
- control_plane_api/worker/runtimes/factory.py +173 -0
- control_plane_api/worker/runtimes/validation.py +93 -0
- control_plane_api/worker/services/__init__.py +1 -0
- control_plane_api/worker/services/agent_executor.py +422 -0
- control_plane_api/worker/services/agent_executor_v2.py +383 -0
- control_plane_api/worker/services/analytics_collector.py +457 -0
- control_plane_api/worker/services/analytics_service.py +464 -0
- control_plane_api/worker/services/approval_tools.py +310 -0
- control_plane_api/worker/services/approval_tools_agno.py +207 -0
- control_plane_api/worker/services/cancellation_manager.py +177 -0
- control_plane_api/worker/services/data_visualization.py +827 -0
- control_plane_api/worker/services/jira_tools.py +257 -0
- control_plane_api/worker/services/runtime_analytics.py +328 -0
- control_plane_api/worker/services/session_service.py +194 -0
- control_plane_api/worker/services/skill_factory.py +175 -0
- control_plane_api/worker/services/team_executor.py +574 -0
- control_plane_api/worker/services/team_executor_v2.py +465 -0
- control_plane_api/worker/services/workflow_executor_tools.py +1418 -0
- control_plane_api/worker/tests/__init__.py +1 -0
- control_plane_api/worker/tests/e2e/__init__.py +0 -0
- control_plane_api/worker/tests/e2e/test_execution_flow.py +571 -0
- control_plane_api/worker/tests/integration/__init__.py +0 -0
- control_plane_api/worker/tests/integration/test_control_plane_integration.py +308 -0
- control_plane_api/worker/tests/unit/__init__.py +0 -0
- control_plane_api/worker/tests/unit/test_control_plane_client.py +401 -0
- control_plane_api/worker/utils/__init__.py +1 -0
- control_plane_api/worker/utils/chunk_batcher.py +305 -0
- control_plane_api/worker/utils/retry_utils.py +60 -0
- control_plane_api/worker/utils/streaming_utils.py +373 -0
- control_plane_api/worker/worker.py +753 -0
- control_plane_api/worker/workflows/__init__.py +0 -0
- control_plane_api/worker/workflows/agent_execution.py +589 -0
- control_plane_api/worker/workflows/team_execution.py +429 -0
- kubiya_control_plane_api-0.3.4.dist-info/METADATA +229 -0
- kubiya_control_plane_api-0.3.4.dist-info/RECORD +182 -0
- kubiya_control_plane_api-0.3.4.dist-info/entry_points.txt +2 -0
- kubiya_control_plane_api-0.3.4.dist-info/top_level.txt +1 -0
- kubiya_control_plane_api-0.1.0.dist-info/METADATA +0 -66
- kubiya_control_plane_api-0.1.0.dist-info/RECORD +0 -5
- kubiya_control_plane_api-0.1.0.dist-info/top_level.txt +0 -1
- {kubiya_control_plane_api-0.1.0.dist-info/licenses → control_plane_api}/LICENSE +0 -0
- {kubiya_control_plane_api-0.1.0.dist-info → kubiya_control_plane_api-0.3.4.dist-info}/WHEEL +0 -0
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""
|
|
2
|
+
API router for LLM models configuration
|
|
3
|
+
"""
|
|
4
|
+
from fastapi import APIRouter
|
|
5
|
+
from typing import List, Optional
|
|
6
|
+
from pydantic import BaseModel
|
|
7
|
+
|
|
8
|
+
router = APIRouter(prefix="/api/v1/models", tags=["models"])
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class LLMModel(BaseModel):
|
|
12
|
+
"""LLM Model configuration"""
|
|
13
|
+
value: str
|
|
14
|
+
label: str
|
|
15
|
+
provider: str
|
|
16
|
+
logo: str
|
|
17
|
+
recommended: bool = False
|
|
18
|
+
description: Optional[str] = None
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
# Kubiya's supported LLM models
|
|
22
|
+
# NOTE: All models must include "kubiya/" prefix for LiteLLM routing
|
|
23
|
+
KUBIYA_LLM_MODELS = [
|
|
24
|
+
LLMModel(
|
|
25
|
+
value="kubiya/claude-sonnet-4",
|
|
26
|
+
label="Claude Sonnet 4",
|
|
27
|
+
provider="Anthropic",
|
|
28
|
+
logo="/logos/claude-color.svg",
|
|
29
|
+
recommended=True,
|
|
30
|
+
description="Most intelligent model with best reasoning capabilities"
|
|
31
|
+
),
|
|
32
|
+
LLMModel(
|
|
33
|
+
value="kubiya/claude-opus-4",
|
|
34
|
+
label="Claude Opus 4",
|
|
35
|
+
provider="Anthropic",
|
|
36
|
+
logo="/logos/claude-color.svg",
|
|
37
|
+
description="Powerful model for complex tasks requiring deep analysis"
|
|
38
|
+
),
|
|
39
|
+
LLMModel(
|
|
40
|
+
value="kubiya/gpt-4o",
|
|
41
|
+
label="GPT-4o",
|
|
42
|
+
provider="OpenAI",
|
|
43
|
+
logo="/thirdparty/logos/openai.svg",
|
|
44
|
+
description="Fast and capable model with vision support"
|
|
45
|
+
),
|
|
46
|
+
LLMModel(
|
|
47
|
+
value="kubiya/gpt-4-turbo",
|
|
48
|
+
label="GPT-4 Turbo",
|
|
49
|
+
provider="OpenAI",
|
|
50
|
+
logo="/thirdparty/logos/openai.svg",
|
|
51
|
+
description="Enhanced GPT-4 with improved speed and capabilities"
|
|
52
|
+
),
|
|
53
|
+
LLMModel(
|
|
54
|
+
value="kubiya/claude-3-5-sonnet-20241022",
|
|
55
|
+
label="Claude 3.5 Sonnet",
|
|
56
|
+
provider="Anthropic",
|
|
57
|
+
logo="/logos/claude-color.svg",
|
|
58
|
+
description="Previous generation Sonnet with excellent performance"
|
|
59
|
+
),
|
|
60
|
+
]
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@router.get("", response_model=List[LLMModel])
|
|
64
|
+
async def list_models():
|
|
65
|
+
"""
|
|
66
|
+
Get list of available LLM models.
|
|
67
|
+
|
|
68
|
+
Returns:
|
|
69
|
+
List of LLM model configurations with logos and metadata
|
|
70
|
+
"""
|
|
71
|
+
return KUBIYA_LLM_MODELS
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@router.get("/default", response_model=LLMModel)
|
|
75
|
+
async def get_default_model():
|
|
76
|
+
"""
|
|
77
|
+
Get the default recommended LLM model.
|
|
78
|
+
|
|
79
|
+
Returns:
|
|
80
|
+
The recommended default model configuration
|
|
81
|
+
"""
|
|
82
|
+
return next((model for model in KUBIYA_LLM_MODELS if model.recommended), KUBIYA_LLM_MODELS[0])
|
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
"""
|
|
2
|
+
LLM Models CRUD API with database persistence
|
|
3
|
+
|
|
4
|
+
This router provides full CRUD operations for managing LLM models
|
|
5
|
+
that can be used by agents and teams.
|
|
6
|
+
"""
|
|
7
|
+
from fastapi import APIRouter, Depends, HTTPException, status, Request, Query
|
|
8
|
+
from typing import List, Optional
|
|
9
|
+
from pydantic import BaseModel, Field
|
|
10
|
+
from sqlalchemy.orm import Session
|
|
11
|
+
from datetime import datetime
|
|
12
|
+
import structlog
|
|
13
|
+
|
|
14
|
+
from control_plane_api.app.middleware.auth import get_current_organization
|
|
15
|
+
from control_plane_api.app.database import get_db
|
|
16
|
+
from control_plane_api.app.models.llm_model import LLMModel as LLMModelDB
|
|
17
|
+
|
|
18
|
+
logger = structlog.get_logger()
|
|
19
|
+
|
|
20
|
+
router = APIRouter()
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
# ==================== Pydantic Schemas ====================
|
|
24
|
+
|
|
25
|
+
class LLMModelCreate(BaseModel):
|
|
26
|
+
"""Schema for creating a new LLM model"""
|
|
27
|
+
value: str = Field(..., description="Model identifier (e.g., 'kubiya/claude-sonnet-4')")
|
|
28
|
+
label: str = Field(..., description="Display name (e.g., 'Claude Sonnet 4')")
|
|
29
|
+
provider: str = Field(..., description="Provider name (e.g., 'Anthropic', 'OpenAI')")
|
|
30
|
+
logo: Optional[str] = Field(None, description="Logo path or URL")
|
|
31
|
+
description: Optional[str] = Field(None, description="Model description")
|
|
32
|
+
enabled: bool = Field(True, description="Whether model is enabled")
|
|
33
|
+
recommended: bool = Field(False, description="Whether model is recommended by default")
|
|
34
|
+
compatible_runtimes: List[str] = Field(
|
|
35
|
+
default_factory=list,
|
|
36
|
+
description="List of compatible runtime IDs (e.g., ['default', 'claude_code'])"
|
|
37
|
+
)
|
|
38
|
+
capabilities: dict = Field(
|
|
39
|
+
default_factory=dict,
|
|
40
|
+
description="Model capabilities (e.g., {'vision': true, 'max_tokens': 4096})"
|
|
41
|
+
)
|
|
42
|
+
pricing: Optional[dict] = Field(None, description="Pricing information")
|
|
43
|
+
display_order: int = Field(1000, description="Display order (lower = shown first)")
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class LLMModelUpdate(BaseModel):
|
|
47
|
+
"""Schema for updating an existing LLM model"""
|
|
48
|
+
value: Optional[str] = None
|
|
49
|
+
label: Optional[str] = None
|
|
50
|
+
provider: Optional[str] = None
|
|
51
|
+
logo: Optional[str] = None
|
|
52
|
+
description: Optional[str] = None
|
|
53
|
+
enabled: Optional[bool] = None
|
|
54
|
+
recommended: Optional[bool] = None
|
|
55
|
+
compatible_runtimes: Optional[List[str]] = None
|
|
56
|
+
capabilities: Optional[dict] = None
|
|
57
|
+
pricing: Optional[dict] = None
|
|
58
|
+
display_order: Optional[int] = None
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class LLMModelResponse(BaseModel):
|
|
62
|
+
"""Schema for LLM model responses"""
|
|
63
|
+
id: str
|
|
64
|
+
value: str
|
|
65
|
+
label: str
|
|
66
|
+
provider: str
|
|
67
|
+
logo: Optional[str]
|
|
68
|
+
description: Optional[str]
|
|
69
|
+
enabled: bool
|
|
70
|
+
recommended: bool
|
|
71
|
+
compatible_runtimes: List[str]
|
|
72
|
+
capabilities: dict
|
|
73
|
+
pricing: Optional[dict]
|
|
74
|
+
display_order: int
|
|
75
|
+
created_at: str
|
|
76
|
+
updated_at: str
|
|
77
|
+
|
|
78
|
+
class Config:
|
|
79
|
+
from_attributes = True
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
# ==================== Helper Functions ====================
|
|
83
|
+
|
|
84
|
+
def check_runtime_compatibility(model: LLMModelDB, runtime_id: Optional[str]) -> bool:
|
|
85
|
+
"""Check if a model is compatible with a specific runtime"""
|
|
86
|
+
if not runtime_id:
|
|
87
|
+
return True # No filter specified
|
|
88
|
+
if not model.compatible_runtimes:
|
|
89
|
+
return True # Model doesn't specify compatibility, allow all
|
|
90
|
+
return runtime_id in model.compatible_runtimes
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
# ==================== CRUD Endpoints ====================
|
|
94
|
+
|
|
95
|
+
@router.post("", response_model=LLMModelResponse, status_code=status.HTTP_201_CREATED)
|
|
96
|
+
def create_model(
|
|
97
|
+
model_data: LLMModelCreate,
|
|
98
|
+
request: Request,
|
|
99
|
+
db: Session = Depends(get_db),
|
|
100
|
+
organization: dict = Depends(get_current_organization),
|
|
101
|
+
):
|
|
102
|
+
"""
|
|
103
|
+
Create a new LLM model.
|
|
104
|
+
|
|
105
|
+
Only accessible by authenticated users (org admins recommended).
|
|
106
|
+
"""
|
|
107
|
+
# Check if model with this value already exists
|
|
108
|
+
existing = db.query(LLMModelDB).filter(LLMModelDB.value == model_data.value).first()
|
|
109
|
+
if existing:
|
|
110
|
+
raise HTTPException(
|
|
111
|
+
status_code=status.HTTP_409_CONFLICT,
|
|
112
|
+
detail=f"Model with value '{model_data.value}' already exists"
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
# Create new model
|
|
116
|
+
new_model = LLMModelDB(
|
|
117
|
+
value=model_data.value,
|
|
118
|
+
label=model_data.label,
|
|
119
|
+
provider=model_data.provider,
|
|
120
|
+
logo=model_data.logo,
|
|
121
|
+
description=model_data.description,
|
|
122
|
+
enabled=model_data.enabled,
|
|
123
|
+
recommended=model_data.recommended,
|
|
124
|
+
compatible_runtimes=model_data.compatible_runtimes,
|
|
125
|
+
capabilities=model_data.capabilities,
|
|
126
|
+
pricing=model_data.pricing,
|
|
127
|
+
display_order=model_data.display_order,
|
|
128
|
+
created_by=organization.get("user_id"),
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
db.add(new_model)
|
|
132
|
+
db.commit()
|
|
133
|
+
db.refresh(new_model)
|
|
134
|
+
|
|
135
|
+
logger.info(
|
|
136
|
+
"llm_model_created",
|
|
137
|
+
model_id=new_model.id,
|
|
138
|
+
model_value=new_model.value,
|
|
139
|
+
provider=new_model.provider,
|
|
140
|
+
org_id=organization["id"]
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
return model_to_response(new_model)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
@router.get("", response_model=List[LLMModelResponse])
|
|
147
|
+
def list_models(
|
|
148
|
+
db: Session = Depends(get_db),
|
|
149
|
+
enabled_only: bool = Query(True, description="Only return enabled models"),
|
|
150
|
+
provider: Optional[str] = Query(None, description="Filter by provider (e.g., 'Anthropic', 'OpenAI')"),
|
|
151
|
+
runtime: Optional[str] = Query(None, description="Filter by compatible runtime (e.g., 'claude_code')"),
|
|
152
|
+
recommended: Optional[bool] = Query(None, description="Filter by recommended status"),
|
|
153
|
+
skip: int = Query(0, ge=0),
|
|
154
|
+
limit: int = Query(100, ge=1, le=1000),
|
|
155
|
+
):
|
|
156
|
+
"""
|
|
157
|
+
List all LLM models with optional filtering.
|
|
158
|
+
|
|
159
|
+
Query Parameters:
|
|
160
|
+
- enabled_only: Only return enabled models (default: true)
|
|
161
|
+
- provider: Filter by provider name
|
|
162
|
+
- runtime: Filter by compatible runtime
|
|
163
|
+
- recommended: Filter by recommended status
|
|
164
|
+
- skip/limit: Pagination
|
|
165
|
+
"""
|
|
166
|
+
query = db.query(LLMModelDB)
|
|
167
|
+
|
|
168
|
+
# Apply filters
|
|
169
|
+
if enabled_only:
|
|
170
|
+
query = query.filter(LLMModelDB.enabled == True)
|
|
171
|
+
|
|
172
|
+
if provider:
|
|
173
|
+
query = query.filter(LLMModelDB.provider == provider)
|
|
174
|
+
|
|
175
|
+
if recommended is not None:
|
|
176
|
+
query = query.filter(LLMModelDB.recommended == recommended)
|
|
177
|
+
|
|
178
|
+
# Order by display_order, then by created_at
|
|
179
|
+
query = query.order_by(LLMModelDB.display_order, LLMModelDB.created_at)
|
|
180
|
+
|
|
181
|
+
# Apply pagination
|
|
182
|
+
models = query.offset(skip).limit(limit).all()
|
|
183
|
+
|
|
184
|
+
# Filter by runtime compatibility (done in Python due to JSON array filtering complexity)
|
|
185
|
+
if runtime:
|
|
186
|
+
models = [m for m in models if check_runtime_compatibility(m, runtime)]
|
|
187
|
+
|
|
188
|
+
return [model_to_response(m) for m in models]
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
@router.get("/default", response_model=LLMModelResponse)
|
|
192
|
+
def get_default_model(db: Session = Depends(get_db)):
|
|
193
|
+
"""
|
|
194
|
+
Get the default recommended LLM model.
|
|
195
|
+
|
|
196
|
+
Returns the first model marked as recommended and enabled.
|
|
197
|
+
If none found, returns the first enabled model.
|
|
198
|
+
"""
|
|
199
|
+
# Try to get recommended model first
|
|
200
|
+
model = (
|
|
201
|
+
db.query(LLMModelDB)
|
|
202
|
+
.filter(LLMModelDB.enabled == True, LLMModelDB.recommended == True)
|
|
203
|
+
.order_by(LLMModelDB.display_order)
|
|
204
|
+
.first()
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
# Fallback to first enabled model
|
|
208
|
+
if not model:
|
|
209
|
+
model = (
|
|
210
|
+
db.query(LLMModelDB)
|
|
211
|
+
.filter(LLMModelDB.enabled == True)
|
|
212
|
+
.order_by(LLMModelDB.display_order)
|
|
213
|
+
.first()
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
if not model:
|
|
217
|
+
raise HTTPException(
|
|
218
|
+
status_code=status.HTTP_404_NOT_FOUND,
|
|
219
|
+
detail="No enabled models found"
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
return model_to_response(model)
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
@router.get("/providers", response_model=List[str])
|
|
226
|
+
def list_providers(db: Session = Depends(get_db)):
|
|
227
|
+
"""
|
|
228
|
+
Get list of unique model providers.
|
|
229
|
+
|
|
230
|
+
Returns a list of all unique provider names.
|
|
231
|
+
"""
|
|
232
|
+
providers = db.query(LLMModelDB.provider).distinct().all()
|
|
233
|
+
return [p[0] for p in providers]
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
@router.get("/{model_id}", response_model=LLMModelResponse)
|
|
237
|
+
def get_model(model_id: str, db: Session = Depends(get_db)):
|
|
238
|
+
"""
|
|
239
|
+
Get a specific LLM model by ID or value.
|
|
240
|
+
|
|
241
|
+
Accepts either the UUID or the model value (e.g., 'kubiya/claude-sonnet-4').
|
|
242
|
+
"""
|
|
243
|
+
# Try by ID first
|
|
244
|
+
model = db.query(LLMModelDB).filter(LLMModelDB.id == model_id).first()
|
|
245
|
+
|
|
246
|
+
# If not found, try by value
|
|
247
|
+
if not model:
|
|
248
|
+
model = db.query(LLMModelDB).filter(LLMModelDB.value == model_id).first()
|
|
249
|
+
|
|
250
|
+
if not model:
|
|
251
|
+
raise HTTPException(
|
|
252
|
+
status_code=status.HTTP_404_NOT_FOUND,
|
|
253
|
+
detail=f"Model '{model_id}' not found"
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
return model_to_response(model)
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
@router.patch("/{model_id}", response_model=LLMModelResponse)
|
|
260
|
+
def update_model(
|
|
261
|
+
model_id: str,
|
|
262
|
+
model_data: LLMModelUpdate,
|
|
263
|
+
request: Request,
|
|
264
|
+
db: Session = Depends(get_db),
|
|
265
|
+
organization: dict = Depends(get_current_organization),
|
|
266
|
+
):
|
|
267
|
+
"""
|
|
268
|
+
Update an existing LLM model.
|
|
269
|
+
|
|
270
|
+
Only accessible by authenticated users (org admins recommended).
|
|
271
|
+
"""
|
|
272
|
+
# Find model
|
|
273
|
+
model = db.query(LLMModelDB).filter(LLMModelDB.id == model_id).first()
|
|
274
|
+
if not model:
|
|
275
|
+
raise HTTPException(
|
|
276
|
+
status_code=status.HTTP_404_NOT_FOUND,
|
|
277
|
+
detail=f"Model '{model_id}' not found"
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
# Check if value is being updated and conflicts with existing
|
|
281
|
+
if model_data.value and model_data.value != model.value:
|
|
282
|
+
existing = db.query(LLMModelDB).filter(LLMModelDB.value == model_data.value).first()
|
|
283
|
+
if existing:
|
|
284
|
+
raise HTTPException(
|
|
285
|
+
status_code=status.HTTP_409_CONFLICT,
|
|
286
|
+
detail=f"Model with value '{model_data.value}' already exists"
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
# Update fields
|
|
290
|
+
update_dict = model_data.model_dump(exclude_unset=True)
|
|
291
|
+
for field, value in update_dict.items():
|
|
292
|
+
setattr(model, field, value)
|
|
293
|
+
|
|
294
|
+
model.updated_at = datetime.utcnow()
|
|
295
|
+
|
|
296
|
+
db.commit()
|
|
297
|
+
db.refresh(model)
|
|
298
|
+
|
|
299
|
+
logger.info(
|
|
300
|
+
"llm_model_updated",
|
|
301
|
+
model_id=model.id,
|
|
302
|
+
model_value=model.value,
|
|
303
|
+
updated_fields=list(update_dict.keys()),
|
|
304
|
+
org_id=organization["id"]
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
return model_to_response(model)
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
@router.delete("/{model_id}", status_code=status.HTTP_204_NO_CONTENT)
|
|
311
|
+
def delete_model(
|
|
312
|
+
model_id: str,
|
|
313
|
+
request: Request,
|
|
314
|
+
db: Session = Depends(get_db),
|
|
315
|
+
organization: dict = Depends(get_current_organization),
|
|
316
|
+
):
|
|
317
|
+
"""
|
|
318
|
+
Delete an LLM model.
|
|
319
|
+
|
|
320
|
+
Only accessible by authenticated users (org admins recommended).
|
|
321
|
+
"""
|
|
322
|
+
model = db.query(LLMModelDB).filter(LLMModelDB.id == model_id).first()
|
|
323
|
+
if not model:
|
|
324
|
+
raise HTTPException(
|
|
325
|
+
status_code=status.HTTP_404_NOT_FOUND,
|
|
326
|
+
detail=f"Model '{model_id}' not found"
|
|
327
|
+
)
|
|
328
|
+
|
|
329
|
+
db.delete(model)
|
|
330
|
+
db.commit()
|
|
331
|
+
|
|
332
|
+
logger.info(
|
|
333
|
+
"llm_model_deleted",
|
|
334
|
+
model_id=model.id,
|
|
335
|
+
model_value=model.value,
|
|
336
|
+
org_id=organization["id"]
|
|
337
|
+
)
|
|
338
|
+
|
|
339
|
+
return None
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
# ==================== Helper Functions ====================
|
|
343
|
+
|
|
344
|
+
def model_to_response(model: LLMModelDB) -> LLMModelResponse:
|
|
345
|
+
"""Convert database model to response schema"""
|
|
346
|
+
return LLMModelResponse(
|
|
347
|
+
id=model.id,
|
|
348
|
+
value=model.value,
|
|
349
|
+
label=model.label,
|
|
350
|
+
provider=model.provider,
|
|
351
|
+
logo=model.logo,
|
|
352
|
+
description=model.description,
|
|
353
|
+
enabled=model.enabled,
|
|
354
|
+
recommended=model.recommended,
|
|
355
|
+
compatible_runtimes=model.compatible_runtimes or [],
|
|
356
|
+
capabilities=model.capabilities or {},
|
|
357
|
+
pricing=model.pricing,
|
|
358
|
+
display_order=model.display_order,
|
|
359
|
+
created_at=model.created_at.isoformat() if model.created_at else "",
|
|
360
|
+
updated_at=model.updated_at.isoformat() if model.updated_at else "",
|
|
361
|
+
)
|