Coverage for mcpgateway / routers / llm_config_router.py: 100%
184 statements
« prev ^ index » next coverage.py v7.13.1, created at 2026-02-11 07:10 +0000
« prev ^ index » next coverage.py v7.13.1, created at 2026-02-11 07:10 +0000
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.db import get_db
20from mcpgateway.llm_schemas import (
21 GatewayModelsResponse,
22 LLMModelCreate,
23 LLMModelListResponse,
24 LLMModelResponse,
25 LLMModelUpdate,
26 LLMProviderCreate,
27 LLMProviderListResponse,
28 LLMProviderResponse,
29 LLMProviderUpdate,
30 ProviderHealthCheck,
31)
32from mcpgateway.middleware.rbac import get_current_user_with_permissions, require_permission
33from mcpgateway.services.llm_provider_service import (
34 LLMModelConflictError,
35 LLMModelNotFoundError,
36 LLMProviderNameConflictError,
37 LLMProviderNotFoundError,
38 LLMProviderService,
39)
40from mcpgateway.services.logging_service import LoggingService
42# Initialize logging
43logging_service = LoggingService()
44logger = logging_service.get_logger(__name__)
46# Create router
47llm_config_router = APIRouter()
49# Initialize service
50llm_provider_service = LLMProviderService()
53# ---------------------------------------------------------------------------
54# Provider CRUD Endpoints
55# ---------------------------------------------------------------------------
58@llm_config_router.post(
59 "/providers",
60 response_model=LLMProviderResponse,
61 status_code=status.HTTP_201_CREATED,
62 summary="Create LLM Provider",
63 description="Create a new LLM provider configuration.",
64)
65@require_permission("admin.system_config")
66async def create_provider(
67 provider_data: LLMProviderCreate,
68 current_user_ctx: dict = Depends(get_current_user_with_permissions),
69 db: Session = Depends(get_db),
70) -> LLMProviderResponse:
71 """Create a new LLM provider.
73 Args:
74 provider_data: Provider configuration data.
75 current_user_ctx: Authenticated user context.
76 db: Database session.
78 Returns:
79 Created provider response.
81 Raises:
82 HTTPException: If provider name conflicts or creation fails.
83 """
84 try:
85 provider = llm_provider_service.create_provider(
86 db=db,
87 provider_data=provider_data,
88 created_by=current_user_ctx.get("email"),
89 )
90 model_count = len(provider.models)
91 result = llm_provider_service.to_provider_response(provider, model_count)
92 db.commit()
93 db.close()
94 return result
95 except LLMProviderNameConflictError as e:
96 raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail=str(e))
97 except Exception as e:
98 logger.error(f"Failed to create LLM provider: {e}")
99 raise HTTPException(
100 status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
101 detail=f"Failed to create provider: {str(e)}",
102 )
105@llm_config_router.get(
106 "/providers",
107 response_model=LLMProviderListResponse,
108 summary="List LLM Providers",
109 description="List all configured LLM providers.",
110)
111@require_permission("admin.system_config")
112async def list_providers(
113 enabled_only: bool = Query(False, description="Only return enabled providers"),
114 page: int = Query(1, ge=1, description="Page number"),
115 page_size: int = Query(50, ge=1, le=100, description="Items per page"),
116 current_user_ctx: dict = Depends(get_current_user_with_permissions),
117 db: Session = Depends(get_db),
118) -> LLMProviderListResponse:
119 """List all LLM providers.
121 Args:
122 enabled_only: Filter to enabled providers only.
123 page: Page number.
124 page_size: Items per page.
125 current_user_ctx: Authenticated user context.
126 db: Database session.
128 Returns:
129 Paginated list of providers.
130 """
131 providers, total = llm_provider_service.list_providers(
132 db=db,
133 enabled_only=enabled_only,
134 page=page,
135 page_size=page_size,
136 )
138 provider_responses = [llm_provider_service.to_provider_response(p, len(p.models)) for p in providers]
140 result = LLMProviderListResponse(
141 providers=provider_responses,
142 total=total,
143 page=page,
144 page_size=page_size,
145 )
146 db.commit()
147 db.close()
148 return result
151@llm_config_router.get(
152 "/providers/{provider_id}",
153 response_model=LLMProviderResponse,
154 summary="Get LLM Provider",
155 description="Get a specific LLM provider by ID.",
156)
157@require_permission("admin.system_config")
158async def get_provider(
159 provider_id: str,
160 current_user_ctx: dict = Depends(get_current_user_with_permissions),
161 db: Session = Depends(get_db),
162) -> LLMProviderResponse:
163 """Get an LLM provider by ID.
165 Args:
166 provider_id: Provider ID.
167 current_user_ctx: Authenticated user context.
168 db: Database session.
170 Returns:
171 Provider response.
173 Raises:
174 HTTPException: If provider is not found.
175 """
176 try:
177 provider = llm_provider_service.get_provider(db, provider_id)
178 model_count = len(provider.models)
179 result = llm_provider_service.to_provider_response(provider, model_count)
180 db.commit()
181 db.close()
182 return result
183 except LLMProviderNotFoundError as e:
184 raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))
187@llm_config_router.patch(
188 "/providers/{provider_id}",
189 response_model=LLMProviderResponse,
190 summary="Update LLM Provider",
191 description="Update an existing LLM provider.",
192)
193@require_permission("admin.system_config")
194async def update_provider(
195 provider_id: str,
196 provider_data: LLMProviderUpdate,
197 current_user_ctx: dict = Depends(get_current_user_with_permissions),
198 db: Session = Depends(get_db),
199) -> LLMProviderResponse:
200 """Update an LLM provider.
202 Args:
203 provider_id: Provider ID.
204 provider_data: Updated provider data.
205 current_user_ctx: Authenticated user context.
206 db: Database session.
208 Returns:
209 Updated provider response.
211 Raises:
212 HTTPException: If provider is not found or name conflicts.
213 """
214 try:
215 provider = llm_provider_service.update_provider(
216 db=db,
217 provider_id=provider_id,
218 provider_data=provider_data,
219 modified_by=current_user_ctx.get("email"),
220 )
221 model_count = len(provider.models)
222 result = llm_provider_service.to_provider_response(provider, model_count)
223 db.commit()
224 db.close()
225 return result
226 except LLMProviderNotFoundError as e:
227 raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))
228 except LLMProviderNameConflictError as e:
229 raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail=str(e))
232@llm_config_router.delete(
233 "/providers/{provider_id}",
234 status_code=status.HTTP_204_NO_CONTENT,
235 summary="Delete LLM Provider",
236 description="Delete an LLM provider and all its models.",
237)
238@require_permission("admin.system_config")
239async def delete_provider(
240 provider_id: str,
241 current_user_ctx: dict = Depends(get_current_user_with_permissions),
242 db: Session = Depends(get_db),
243) -> None:
244 """Delete an LLM provider.
246 Args:
247 provider_id: Provider ID.
248 current_user_ctx: Authenticated user context.
249 db: Database session.
251 Raises:
252 HTTPException: If provider is not found.
253 """
254 try:
255 llm_provider_service.delete_provider(db, provider_id)
256 db.commit()
257 db.close()
258 except LLMProviderNotFoundError as e:
259 raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))
262@llm_config_router.post(
263 "/providers/{provider_id}/state",
264 response_model=LLMProviderResponse,
265 summary="Set LLM Provider State",
266 description="Set the enabled status of an LLM provider.",
267)
268@require_permission("admin.system_config")
269async def set_provider_state(
270 provider_id: str,
271 activate: Optional[bool] = Query(None, description="Set enabled state. If not provided, inverts current state."),
272 current_user_ctx: dict = Depends(get_current_user_with_permissions),
273 db: Session = Depends(get_db),
274) -> LLMProviderResponse:
275 """Set provider enabled state.
277 Args:
278 provider_id: Provider ID.
279 activate: If provided, sets enabled to this value. If None, inverts current state.
280 current_user_ctx: Authenticated user context.
281 db: Database session.
283 Returns:
284 Updated provider response.
286 Raises:
287 HTTPException: If provider is not found.
288 """
289 try:
290 provider = llm_provider_service.set_provider_state(db, provider_id, activate)
291 model_count = len(provider.models)
292 result = llm_provider_service.to_provider_response(provider, model_count)
293 db.commit()
294 db.close()
295 return result
296 except LLMProviderNotFoundError as e:
297 raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))
300@llm_config_router.post(
301 "/providers/{provider_id}/health",
302 response_model=ProviderHealthCheck,
303 summary="Check Provider Health",
304 description="Perform a health check on an LLM provider.",
305)
306@require_permission("admin.system_config")
307async def check_provider_health(
308 provider_id: str,
309 current_user_ctx: dict = Depends(get_current_user_with_permissions),
310 db: Session = Depends(get_db),
311) -> ProviderHealthCheck:
312 """Check health of an LLM provider.
314 Args:
315 provider_id: Provider ID.
316 current_user_ctx: Authenticated user context.
317 db: Database session.
319 Returns:
320 Health check result.
322 Raises:
323 HTTPException: If provider is not found.
324 """
325 try:
326 result = await llm_provider_service.check_provider_health(db, provider_id)
327 db.commit()
328 db.close()
329 return result
330 except LLMProviderNotFoundError as e:
331 raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))
334# ---------------------------------------------------------------------------
335# Model CRUD Endpoints
336# ---------------------------------------------------------------------------
339@llm_config_router.post(
340 "/models",
341 response_model=LLMModelResponse,
342 status_code=status.HTTP_201_CREATED,
343 summary="Create LLM Model",
344 description="Create a new LLM model for a provider.",
345)
346@require_permission("admin.system_config")
347async def create_model(
348 model_data: LLMModelCreate,
349 current_user_ctx: dict = Depends(get_current_user_with_permissions),
350 db: Session = Depends(get_db),
351) -> LLMModelResponse:
352 """Create a new LLM model.
354 Args:
355 model_data: Model configuration data.
356 current_user_ctx: Authenticated user context.
357 db: Database session.
359 Returns:
360 Created model response.
362 Raises:
363 HTTPException: If provider is not found or model conflicts.
364 """
365 try:
366 model = llm_provider_service.create_model(db, model_data)
367 provider = llm_provider_service.get_provider(db, model.provider_id)
368 result = llm_provider_service.to_model_response(model, provider)
369 db.commit()
370 db.close()
371 return result
372 except LLMProviderNotFoundError as e:
373 raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))
374 except LLMModelConflictError as e:
375 raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail=str(e))
378@llm_config_router.get(
379 "/models",
380 response_model=LLMModelListResponse,
381 summary="List LLM Models",
382 description="List all configured LLM models.",
383)
384@require_permission("admin.system_config")
385async def list_models(
386 provider_id: Optional[str] = Query(None, description="Filter by provider ID"),
387 enabled_only: bool = Query(False, description="Only return enabled models"),
388 page: int = Query(1, ge=1, description="Page number"),
389 page_size: int = Query(50, ge=1, le=100, description="Items per page"),
390 current_user_ctx: dict = Depends(get_current_user_with_permissions),
391 db: Session = Depends(get_db),
392) -> LLMModelListResponse:
393 """List all LLM models.
395 Args:
396 provider_id: Filter by provider ID.
397 enabled_only: Filter to enabled models only.
398 page: Page number.
399 page_size: Items per page.
400 current_user_ctx: Authenticated user context.
401 db: Database session.
403 Returns:
404 Paginated list of models.
405 """
406 models, total = llm_provider_service.list_models(
407 db=db,
408 provider_id=provider_id,
409 enabled_only=enabled_only,
410 page=page,
411 page_size=page_size,
412 )
414 model_responses = []
415 for model in models:
416 try:
417 provider = llm_provider_service.get_provider(db, model.provider_id)
418 model_responses.append(llm_provider_service.to_model_response(model, provider))
419 except LLMProviderNotFoundError:
420 model_responses.append(llm_provider_service.to_model_response(model))
422 result = LLMModelListResponse(
423 models=model_responses,
424 total=total,
425 page=page,
426 page_size=page_size,
427 )
428 db.commit()
429 db.close()
430 return result
433@llm_config_router.get(
434 "/models/{model_id}",
435 response_model=LLMModelResponse,
436 summary="Get LLM Model",
437 description="Get a specific LLM model by ID.",
438)
439@require_permission("admin.system_config")
440async def get_model(
441 model_id: str,
442 current_user_ctx: dict = Depends(get_current_user_with_permissions),
443 db: Session = Depends(get_db),
444) -> LLMModelResponse:
445 """Get an LLM model by ID.
447 Args:
448 model_id: Model ID.
449 current_user_ctx: Authenticated user context.
450 db: Database session.
452 Returns:
453 Model response.
455 Raises:
456 HTTPException: If model is not found.
457 """
458 try:
459 model = llm_provider_service.get_model(db, model_id)
460 try:
461 provider = llm_provider_service.get_provider(db, model.provider_id)
462 except LLMProviderNotFoundError:
463 provider = None
464 result = llm_provider_service.to_model_response(model, provider)
465 db.commit()
466 db.close()
467 return result
468 except LLMModelNotFoundError as e:
469 raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))
472@llm_config_router.patch(
473 "/models/{model_id}",
474 response_model=LLMModelResponse,
475 summary="Update LLM Model",
476 description="Update an existing LLM model.",
477)
478@require_permission("admin.system_config")
479async def update_model(
480 model_id: str,
481 model_data: LLMModelUpdate,
482 current_user_ctx: dict = Depends(get_current_user_with_permissions),
483 db: Session = Depends(get_db),
484) -> LLMModelResponse:
485 """Update an LLM model.
487 Args:
488 model_id: Model ID.
489 model_data: Updated model data.
490 current_user_ctx: Authenticated user context.
491 db: Database session.
493 Returns:
494 Updated model response.
496 Raises:
497 HTTPException: If model is not found.
498 """
499 try:
500 model = llm_provider_service.update_model(db, model_id, model_data)
501 try:
502 provider = llm_provider_service.get_provider(db, model.provider_id)
503 except LLMProviderNotFoundError:
504 provider = None
505 result = llm_provider_service.to_model_response(model, provider)
506 db.commit()
507 db.close()
508 return result
509 except LLMModelNotFoundError as e:
510 raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))
513@llm_config_router.delete(
514 "/models/{model_id}",
515 status_code=status.HTTP_204_NO_CONTENT,
516 summary="Delete LLM Model",
517 description="Delete an LLM model.",
518)
519@require_permission("admin.system_config")
520async def delete_model(
521 model_id: str,
522 current_user_ctx: dict = Depends(get_current_user_with_permissions),
523 db: Session = Depends(get_db),
524) -> None:
525 """Delete an LLM model.
527 Args:
528 model_id: Model ID.
529 current_user_ctx: Authenticated user context.
530 db: Database session.
532 Raises:
533 HTTPException: If model is not found.
534 """
535 try:
536 llm_provider_service.delete_model(db, model_id)
537 db.commit()
538 db.close()
539 except LLMModelNotFoundError as e:
540 raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))
543@llm_config_router.post(
544 "/models/{model_id}/state",
545 response_model=LLMModelResponse,
546 summary="Set LLM Model State",
547 description="Set the enabled status of an LLM model.",
548)
549@require_permission("admin.system_config")
550async def set_model_state(
551 model_id: str,
552 activate: Optional[bool] = Query(None, description="Set enabled state. If not provided, inverts current state."),
553 current_user_ctx: dict = Depends(get_current_user_with_permissions),
554 db: Session = Depends(get_db),
555) -> LLMModelResponse:
556 """Set model enabled state.
558 Args:
559 model_id: Model ID.
560 activate: If provided, sets enabled to this value. If None, inverts current state.
561 current_user_ctx: Authenticated user context.
562 db: Database session.
564 Returns:
565 Updated model response.
567 Raises:
568 HTTPException: If model is not found.
569 """
570 try:
571 model = llm_provider_service.set_model_state(db, model_id, activate)
572 try:
573 provider = llm_provider_service.get_provider(db, model.provider_id)
574 except LLMProviderNotFoundError:
575 provider = None
576 result = llm_provider_service.to_model_response(model, provider)
577 db.commit()
578 db.close()
579 return result
580 except LLMModelNotFoundError as e:
581 raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))
584# ---------------------------------------------------------------------------
585# Gateway Models Endpoint (for LLM Chat dropdown)
586# ---------------------------------------------------------------------------
589@llm_config_router.get(
590 "/gateway/models",
591 response_model=GatewayModelsResponse,
592 summary="Get Gateway Models",
593 description="Get enabled models for the LLM Chat dropdown.",
594)
595async def get_gateway_models(
596 db: Session = Depends(get_db),
597 current_user: dict = Depends(get_current_user),
598) -> GatewayModelsResponse:
599 """Get enabled models for the LLM Chat dropdown.
601 This endpoint is used by the LLM Chat UI to populate the model selector.
602 It returns only enabled chat-capable models from enabled providers.
604 Args:
605 db: Database session.
606 current_user: Authenticated user.
608 Returns:
609 List of available gateway models.
610 """
611 models = llm_provider_service.get_gateway_models(db)
612 result = GatewayModelsResponse(models=models, count=len(models))
613 db.commit()
614 db.close()
615 return result