Coverage for mcpgateway / routers / llm_config_router.py: 100%
189 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-06 00:56 +0100
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-06 00:56 +0100
1# -*- coding: utf-8 -*-
2"""Location: ./mcpgateway/routers/llm_config_router.py
3Copyright 2025
4SPDX-License-Identifier: Apache-2.0
6LLM Configuration Router.
7This module provides FastAPI routes for LLM provider and model management.
8"""
10# Standard
11from typing import Optional
13# Third-Party
14from fastapi import APIRouter, Depends, HTTPException, Query, status
15from sqlalchemy.orm import Session
17# First-Party
18from mcpgateway.auth import get_current_user
19from mcpgateway.config import settings
20from mcpgateway.db import get_db
21from mcpgateway.llm_schemas import (
22 GatewayModelsResponse,
23 LLMModelCreate,
24 LLMModelListResponse,
25 LLMModelResponse,
26 LLMModelUpdate,
27 LLMProviderCreate,
28 LLMProviderListResponse,
29 LLMProviderResponse,
30 LLMProviderUpdate,
31 ProviderHealthCheck,
32)
33from mcpgateway.middleware.rbac import get_current_user_with_permissions, require_permission
34from mcpgateway.services.llm_provider_service import (
35 LLMModelConflictError,
36 LLMModelNotFoundError,
37 LLMProviderNameConflictError,
38 LLMProviderNotFoundError,
39 LLMProviderService,
40 LLMProviderValidationError,
41)
42from mcpgateway.services.logging_service import LoggingService
44# Initialize logging
45logging_service = LoggingService()
46logger = logging_service.get_logger(__name__)
48# Create router
49llm_config_router = APIRouter()
51# Initialize service
52llm_provider_service = LLMProviderService()
55# ---------------------------------------------------------------------------
56# Provider CRUD Endpoints
57# ---------------------------------------------------------------------------
60@llm_config_router.post(
61 "/providers",
62 response_model=LLMProviderResponse,
63 status_code=status.HTTP_201_CREATED,
64 summary="Create LLM Provider",
65 description="Create a new LLM provider configuration.",
66)
67@require_permission("admin.system_config")
68async def create_provider(
69 provider_data: LLMProviderCreate,
70 current_user_ctx: dict = Depends(get_current_user_with_permissions),
71 db: Session = Depends(get_db),
72) -> LLMProviderResponse:
73 """Create a new LLM provider.
75 Args:
76 provider_data: Provider configuration data.
77 current_user_ctx: Authenticated user context.
78 db: Database session.
80 Returns:
81 Created provider response.
83 Raises:
84 HTTPException: If provider name conflicts or creation fails.
85 """
86 try:
87 provider = llm_provider_service.create_provider(
88 db=db,
89 provider_data=provider_data,
90 created_by=current_user_ctx.get("email"),
91 )
92 model_count = len(provider.models)
93 result = llm_provider_service.to_provider_response(provider, model_count)
94 db.commit()
95 db.close()
96 return result
97 except LLMProviderNameConflictError as e:
98 raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail=str(e))
99 except LLMProviderValidationError as e:
100 raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_CONTENT, detail=str(e))
101 except Exception as e:
102 logger.error(f"Failed to create LLM provider: {e}")
103 raise HTTPException(
104 status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
105 detail=f"Failed to create provider: {str(e)}",
106 )
109@llm_config_router.get(
110 "/providers",
111 response_model=LLMProviderListResponse,
112 summary="List LLM Providers",
113 description="List all configured LLM providers.",
114)
115@require_permission("admin.system_config")
116async def list_providers(
117 enabled_only: bool = Query(False, description="Only return enabled providers"),
118 page: int = Query(1, ge=1, description="Page number"),
119 page_size: int = Query(50, ge=1, le=settings.pagination_max_page_size, description="Items per page"),
120 current_user_ctx: dict = Depends(get_current_user_with_permissions),
121 db: Session = Depends(get_db),
122) -> LLMProviderListResponse:
123 """List all LLM providers.
125 Args:
126 enabled_only: Filter to enabled providers only.
127 page: Page number.
128 page_size: Items per page.
129 current_user_ctx: Authenticated user context.
130 db: Database session.
132 Returns:
133 Paginated list of providers.
134 """
135 providers, total = llm_provider_service.list_providers(
136 db=db,
137 enabled_only=enabled_only,
138 page=page,
139 page_size=page_size,
140 )
142 provider_responses = [llm_provider_service.to_provider_response(p, len(p.models)) for p in providers]
144 result = LLMProviderListResponse(
145 providers=provider_responses,
146 total=total,
147 page=page,
148 page_size=page_size,
149 )
150 db.commit()
151 db.close()
152 return result
155@llm_config_router.get(
156 "/providers/{provider_id}",
157 response_model=LLMProviderResponse,
158 summary="Get LLM Provider",
159 description="Get a specific LLM provider by ID.",
160)
161@require_permission("admin.system_config")
162async def get_provider(
163 provider_id: str,
164 current_user_ctx: dict = Depends(get_current_user_with_permissions),
165 db: Session = Depends(get_db),
166) -> LLMProviderResponse:
167 """Get an LLM provider by ID.
169 Args:
170 provider_id: Provider ID.
171 current_user_ctx: Authenticated user context.
172 db: Database session.
174 Returns:
175 Provider response.
177 Raises:
178 HTTPException: If provider is not found.
179 """
180 try:
181 provider = llm_provider_service.get_provider(db, provider_id)
182 model_count = len(provider.models)
183 result = llm_provider_service.to_provider_response(provider, model_count)
184 db.commit()
185 db.close()
186 return result
187 except LLMProviderNotFoundError as e:
188 raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))
191@llm_config_router.patch(
192 "/providers/{provider_id}",
193 response_model=LLMProviderResponse,
194 summary="Update LLM Provider",
195 description="Update an existing LLM provider.",
196)
197@require_permission("admin.system_config")
198async def update_provider(
199 provider_id: str,
200 provider_data: LLMProviderUpdate,
201 current_user_ctx: dict = Depends(get_current_user_with_permissions),
202 db: Session = Depends(get_db),
203) -> LLMProviderResponse:
204 """Update an LLM provider.
206 Args:
207 provider_id: Provider ID.
208 provider_data: Updated provider data.
209 current_user_ctx: Authenticated user context.
210 db: Database session.
212 Returns:
213 Updated provider response.
215 Raises:
216 HTTPException: If provider is not found or name conflicts.
217 """
218 try:
219 provider = llm_provider_service.update_provider(
220 db=db,
221 provider_id=provider_id,
222 provider_data=provider_data,
223 modified_by=current_user_ctx.get("email"),
224 )
225 model_count = len(provider.models)
226 result = llm_provider_service.to_provider_response(provider, model_count)
227 db.commit()
228 db.close()
229 return result
230 except LLMProviderNotFoundError as e:
231 raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))
232 except LLMProviderNameConflictError as e:
233 raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail=str(e))
234 except LLMProviderValidationError as e:
235 raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_CONTENT, detail=str(e))
238@llm_config_router.delete(
239 "/providers/{provider_id}",
240 status_code=status.HTTP_204_NO_CONTENT,
241 summary="Delete LLM Provider",
242 description="Delete an LLM provider and all its models.",
243)
244@require_permission("admin.system_config")
245async def delete_provider(
246 provider_id: str,
247 current_user_ctx: dict = Depends(get_current_user_with_permissions),
248 db: Session = Depends(get_db),
249) -> None:
250 """Delete an LLM provider.
252 Args:
253 provider_id: Provider ID.
254 current_user_ctx: Authenticated user context.
255 db: Database session.
257 Raises:
258 HTTPException: If provider is not found.
259 """
260 try:
261 llm_provider_service.delete_provider(db, provider_id)
262 db.commit()
263 db.close()
264 except LLMProviderNotFoundError as e:
265 raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))
268@llm_config_router.post(
269 "/providers/{provider_id}/state",
270 response_model=LLMProviderResponse,
271 summary="Set LLM Provider State",
272 description="Set the enabled status of an LLM provider.",
273)
274@require_permission("admin.system_config")
275async def set_provider_state(
276 provider_id: str,
277 activate: Optional[bool] = Query(None, description="Set enabled state. If not provided, inverts current state."),
278 current_user_ctx: dict = Depends(get_current_user_with_permissions),
279 db: Session = Depends(get_db),
280) -> LLMProviderResponse:
281 """Set provider enabled state.
283 Args:
284 provider_id: Provider ID.
285 activate: If provided, sets enabled to this value. If None, inverts current state.
286 current_user_ctx: Authenticated user context.
287 db: Database session.
289 Returns:
290 Updated provider response.
292 Raises:
293 HTTPException: If provider is not found.
294 """
295 try:
296 provider = llm_provider_service.set_provider_state(db, provider_id, activate)
297 model_count = len(provider.models)
298 result = llm_provider_service.to_provider_response(provider, model_count)
299 db.commit()
300 db.close()
301 return result
302 except LLMProviderNotFoundError as e:
303 raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))
306@llm_config_router.post(
307 "/providers/{provider_id}/health",
308 response_model=ProviderHealthCheck,
309 summary="Check Provider Health",
310 description="Perform a health check on an LLM provider.",
311)
312@require_permission("admin.system_config")
313async def check_provider_health(
314 provider_id: str,
315 current_user_ctx: dict = Depends(get_current_user_with_permissions),
316 db: Session = Depends(get_db),
317) -> ProviderHealthCheck:
318 """Check health of an LLM provider.
320 Args:
321 provider_id: Provider ID.
322 current_user_ctx: Authenticated user context.
323 db: Database session.
325 Returns:
326 Health check result.
328 Raises:
329 HTTPException: If provider is not found.
330 """
331 try:
332 result = await llm_provider_service.check_provider_health(db, provider_id)
333 db.commit()
334 db.close()
335 return result
336 except LLMProviderNotFoundError as e:
337 raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))
340# ---------------------------------------------------------------------------
341# Model CRUD Endpoints
342# ---------------------------------------------------------------------------
345@llm_config_router.post(
346 "/models",
347 response_model=LLMModelResponse,
348 status_code=status.HTTP_201_CREATED,
349 summary="Create LLM Model",
350 description="Create a new LLM model for a provider.",
351)
352@require_permission("admin.system_config")
353async def create_model(
354 model_data: LLMModelCreate,
355 current_user_ctx: dict = Depends(get_current_user_with_permissions),
356 db: Session = Depends(get_db),
357) -> LLMModelResponse:
358 """Create a new LLM model.
360 Args:
361 model_data: Model configuration data.
362 current_user_ctx: Authenticated user context.
363 db: Database session.
365 Returns:
366 Created model response.
368 Raises:
369 HTTPException: If provider is not found or model conflicts.
370 """
371 try:
372 model = llm_provider_service.create_model(db, model_data)
373 provider = llm_provider_service.get_provider(db, model.provider_id)
374 result = llm_provider_service.to_model_response(model, provider)
375 db.commit()
376 db.close()
377 return result
378 except LLMProviderNotFoundError as e:
379 raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))
380 except LLMModelConflictError as e:
381 raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail=str(e))
384@llm_config_router.get(
385 "/models",
386 response_model=LLMModelListResponse,
387 summary="List LLM Models",
388 description="List all configured LLM models.",
389)
390@require_permission("admin.system_config")
391async def list_models(
392 provider_id: Optional[str] = Query(None, description="Filter by provider ID"),
393 enabled_only: bool = Query(False, description="Only return enabled models"),
394 page: int = Query(1, ge=1, description="Page number"),
395 page_size: int = Query(50, ge=1, le=settings.pagination_max_page_size, description="Items per page"),
396 current_user_ctx: dict = Depends(get_current_user_with_permissions),
397 db: Session = Depends(get_db),
398) -> LLMModelListResponse:
399 """List all LLM models.
401 Args:
402 provider_id: Filter by provider ID.
403 enabled_only: Filter to enabled models only.
404 page: Page number.
405 page_size: Items per page.
406 current_user_ctx: Authenticated user context.
407 db: Database session.
409 Returns:
410 Paginated list of models.
411 """
412 models, total = llm_provider_service.list_models(
413 db=db,
414 provider_id=provider_id,
415 enabled_only=enabled_only,
416 page=page,
417 page_size=page_size,
418 )
420 model_responses = []
421 for model in models:
422 try:
423 provider = llm_provider_service.get_provider(db, model.provider_id)
424 model_responses.append(llm_provider_service.to_model_response(model, provider))
425 except LLMProviderNotFoundError:
426 model_responses.append(llm_provider_service.to_model_response(model))
428 result = LLMModelListResponse(
429 models=model_responses,
430 total=total,
431 page=page,
432 page_size=page_size,
433 )
434 db.commit()
435 db.close()
436 return result
439@llm_config_router.get(
440 "/models/{model_id}",
441 response_model=LLMModelResponse,
442 summary="Get LLM Model",
443 description="Get a specific LLM model by ID.",
444)
445@require_permission("admin.system_config")
446async def get_model(
447 model_id: str,
448 current_user_ctx: dict = Depends(get_current_user_with_permissions),
449 db: Session = Depends(get_db),
450) -> LLMModelResponse:
451 """Get an LLM model by ID.
453 Args:
454 model_id: Model ID.
455 current_user_ctx: Authenticated user context.
456 db: Database session.
458 Returns:
459 Model response.
461 Raises:
462 HTTPException: If model is not found.
463 """
464 try:
465 model = llm_provider_service.get_model(db, model_id)
466 try:
467 provider = llm_provider_service.get_provider(db, model.provider_id)
468 except LLMProviderNotFoundError:
469 provider = None
470 result = llm_provider_service.to_model_response(model, provider)
471 db.commit()
472 db.close()
473 return result
474 except LLMModelNotFoundError as e:
475 raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))
478@llm_config_router.patch(
479 "/models/{model_id}",
480 response_model=LLMModelResponse,
481 summary="Update LLM Model",
482 description="Update an existing LLM model.",
483)
484@require_permission("admin.system_config")
485async def update_model(
486 model_id: str,
487 model_data: LLMModelUpdate,
488 current_user_ctx: dict = Depends(get_current_user_with_permissions),
489 db: Session = Depends(get_db),
490) -> LLMModelResponse:
491 """Update an LLM model.
493 Args:
494 model_id: Model ID.
495 model_data: Updated model data.
496 current_user_ctx: Authenticated user context.
497 db: Database session.
499 Returns:
500 Updated model response.
502 Raises:
503 HTTPException: If model is not found.
504 """
505 try:
506 model = llm_provider_service.update_model(db, model_id, model_data)
507 try:
508 provider = llm_provider_service.get_provider(db, model.provider_id)
509 except LLMProviderNotFoundError:
510 provider = None
511 result = llm_provider_service.to_model_response(model, provider)
512 db.commit()
513 db.close()
514 return result
515 except LLMModelNotFoundError as e:
516 raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))
519@llm_config_router.delete(
520 "/models/{model_id}",
521 status_code=status.HTTP_204_NO_CONTENT,
522 summary="Delete LLM Model",
523 description="Delete an LLM model.",
524)
525@require_permission("admin.system_config")
526async def delete_model(
527 model_id: str,
528 current_user_ctx: dict = Depends(get_current_user_with_permissions),
529 db: Session = Depends(get_db),
530) -> None:
531 """Delete an LLM model.
533 Args:
534 model_id: Model ID.
535 current_user_ctx: Authenticated user context.
536 db: Database session.
538 Raises:
539 HTTPException: If model is not found.
540 """
541 try:
542 llm_provider_service.delete_model(db, model_id)
543 db.commit()
544 db.close()
545 except LLMModelNotFoundError as e:
546 raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))
549@llm_config_router.post(
550 "/models/{model_id}/state",
551 response_model=LLMModelResponse,
552 summary="Set LLM Model State",
553 description="Set the enabled status of an LLM model.",
554)
555@require_permission("admin.system_config")
556async def set_model_state(
557 model_id: str,
558 activate: Optional[bool] = Query(None, description="Set enabled state. If not provided, inverts current state."),
559 current_user_ctx: dict = Depends(get_current_user_with_permissions),
560 db: Session = Depends(get_db),
561) -> LLMModelResponse:
562 """Set model enabled state.
564 Args:
565 model_id: Model ID.
566 activate: If provided, sets enabled to this value. If None, inverts current state.
567 current_user_ctx: Authenticated user context.
568 db: Database session.
570 Returns:
571 Updated model response.
573 Raises:
574 HTTPException: If model is not found.
575 """
576 try:
577 model = llm_provider_service.set_model_state(db, model_id, activate)
578 try:
579 provider = llm_provider_service.get_provider(db, model.provider_id)
580 except LLMProviderNotFoundError:
581 provider = None
582 result = llm_provider_service.to_model_response(model, provider)
583 db.commit()
584 db.close()
585 return result
586 except LLMModelNotFoundError as e:
587 raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))
590# ---------------------------------------------------------------------------
591# Gateway Models Endpoint (for LLM Chat dropdown)
592# ---------------------------------------------------------------------------
595@llm_config_router.get(
596 "/gateway/models",
597 response_model=GatewayModelsResponse,
598 summary="Get Gateway Models",
599 description="Get enabled models for the LLM Chat dropdown.",
600)
601async def get_gateway_models(
602 db: Session = Depends(get_db),
603 current_user: dict = Depends(get_current_user),
604) -> GatewayModelsResponse:
605 """Get enabled models for the LLM Chat dropdown.
607 This endpoint is used by the LLM Chat UI to populate the model selector.
608 It returns only enabled chat-capable models from enabled providers.
610 Args:
611 db: Database session.
612 current_user: Authenticated user.
614 Returns:
615 List of available gateway models.
616 """
617 models = llm_provider_service.get_gateway_models(db)
618 result = GatewayModelsResponse(models=models, count=len(models))
619 db.commit()
620 db.close()
621 return result