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

1# -*- coding: utf-8 -*- 

2"""Location: ./mcpgateway/routers/llm_config_router.py 

3Copyright 2025 

4SPDX-License-Identifier: Apache-2.0 

5 

6LLM Configuration Router. 

7This module provides FastAPI routes for LLM provider and model management. 

8""" 

9 

10# Standard 

11from typing import Optional 

12 

13# Third-Party 

14from fastapi import APIRouter, Depends, HTTPException, Query, status 

15from sqlalchemy.orm import Session 

16 

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 

43 

44# Initialize logging 

45logging_service = LoggingService() 

46logger = logging_service.get_logger(__name__) 

47 

48# Create router 

49llm_config_router = APIRouter() 

50 

51# Initialize service 

52llm_provider_service = LLMProviderService() 

53 

54 

55# --------------------------------------------------------------------------- 

56# Provider CRUD Endpoints 

57# --------------------------------------------------------------------------- 

58 

59 

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. 

74 

75 Args: 

76 provider_data: Provider configuration data. 

77 current_user_ctx: Authenticated user context. 

78 db: Database session. 

79 

80 Returns: 

81 Created provider response. 

82 

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 ) 

107 

108 

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. 

124 

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. 

131 

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 ) 

141 

142 provider_responses = [llm_provider_service.to_provider_response(p, len(p.models)) for p in providers] 

143 

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 

153 

154 

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. 

168 

169 Args: 

170 provider_id: Provider ID. 

171 current_user_ctx: Authenticated user context. 

172 db: Database session. 

173 

174 Returns: 

175 Provider response. 

176 

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)) 

189 

190 

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. 

205 

206 Args: 

207 provider_id: Provider ID. 

208 provider_data: Updated provider data. 

209 current_user_ctx: Authenticated user context. 

210 db: Database session. 

211 

212 Returns: 

213 Updated provider response. 

214 

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)) 

236 

237 

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. 

251 

252 Args: 

253 provider_id: Provider ID. 

254 current_user_ctx: Authenticated user context. 

255 db: Database session. 

256 

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)) 

266 

267 

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. 

282 

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. 

288 

289 Returns: 

290 Updated provider response. 

291 

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)) 

304 

305 

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. 

319 

320 Args: 

321 provider_id: Provider ID. 

322 current_user_ctx: Authenticated user context. 

323 db: Database session. 

324 

325 Returns: 

326 Health check result. 

327 

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)) 

338 

339 

340# --------------------------------------------------------------------------- 

341# Model CRUD Endpoints 

342# --------------------------------------------------------------------------- 

343 

344 

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. 

359 

360 Args: 

361 model_data: Model configuration data. 

362 current_user_ctx: Authenticated user context. 

363 db: Database session. 

364 

365 Returns: 

366 Created model response. 

367 

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)) 

382 

383 

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. 

400 

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. 

408 

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 ) 

419 

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)) 

427 

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 

437 

438 

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. 

452 

453 Args: 

454 model_id: Model ID. 

455 current_user_ctx: Authenticated user context. 

456 db: Database session. 

457 

458 Returns: 

459 Model response. 

460 

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)) 

476 

477 

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. 

492 

493 Args: 

494 model_id: Model ID. 

495 model_data: Updated model data. 

496 current_user_ctx: Authenticated user context. 

497 db: Database session. 

498 

499 Returns: 

500 Updated model response. 

501 

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)) 

517 

518 

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. 

532 

533 Args: 

534 model_id: Model ID. 

535 current_user_ctx: Authenticated user context. 

536 db: Database session. 

537 

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)) 

547 

548 

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. 

563 

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. 

569 

570 Returns: 

571 Updated model response. 

572 

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)) 

588 

589 

590# --------------------------------------------------------------------------- 

591# Gateway Models Endpoint (for LLM Chat dropdown) 

592# --------------------------------------------------------------------------- 

593 

594 

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. 

606 

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. 

609 

610 Args: 

611 db: Database session. 

612 current_user: Authenticated user. 

613 

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