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

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

41 

42# Initialize logging 

43logging_service = LoggingService() 

44logger = logging_service.get_logger(__name__) 

45 

46# Create router 

47llm_config_router = APIRouter() 

48 

49# Initialize service 

50llm_provider_service = LLMProviderService() 

51 

52 

53# --------------------------------------------------------------------------- 

54# Provider CRUD Endpoints 

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

56 

57 

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. 

72 

73 Args: 

74 provider_data: Provider configuration data. 

75 current_user_ctx: Authenticated user context. 

76 db: Database session. 

77 

78 Returns: 

79 Created provider response. 

80 

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 ) 

103 

104 

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. 

120 

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. 

127 

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 ) 

137 

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

139 

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 

149 

150 

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. 

164 

165 Args: 

166 provider_id: Provider ID. 

167 current_user_ctx: Authenticated user context. 

168 db: Database session. 

169 

170 Returns: 

171 Provider response. 

172 

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

185 

186 

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. 

201 

202 Args: 

203 provider_id: Provider ID. 

204 provider_data: Updated provider data. 

205 current_user_ctx: Authenticated user context. 

206 db: Database session. 

207 

208 Returns: 

209 Updated provider response. 

210 

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

230 

231 

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. 

245 

246 Args: 

247 provider_id: Provider ID. 

248 current_user_ctx: Authenticated user context. 

249 db: Database session. 

250 

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

260 

261 

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. 

276 

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. 

282 

283 Returns: 

284 Updated provider response. 

285 

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

298 

299 

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. 

313 

314 Args: 

315 provider_id: Provider ID. 

316 current_user_ctx: Authenticated user context. 

317 db: Database session. 

318 

319 Returns: 

320 Health check result. 

321 

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

332 

333 

334# --------------------------------------------------------------------------- 

335# Model CRUD Endpoints 

336# --------------------------------------------------------------------------- 

337 

338 

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. 

353 

354 Args: 

355 model_data: Model configuration data. 

356 current_user_ctx: Authenticated user context. 

357 db: Database session. 

358 

359 Returns: 

360 Created model response. 

361 

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

376 

377 

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. 

394 

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. 

402 

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 ) 

413 

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

421 

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 

431 

432 

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. 

446 

447 Args: 

448 model_id: Model ID. 

449 current_user_ctx: Authenticated user context. 

450 db: Database session. 

451 

452 Returns: 

453 Model response. 

454 

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

470 

471 

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. 

486 

487 Args: 

488 model_id: Model ID. 

489 model_data: Updated model data. 

490 current_user_ctx: Authenticated user context. 

491 db: Database session. 

492 

493 Returns: 

494 Updated model response. 

495 

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

511 

512 

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. 

526 

527 Args: 

528 model_id: Model ID. 

529 current_user_ctx: Authenticated user context. 

530 db: Database session. 

531 

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

541 

542 

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. 

557 

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. 

563 

564 Returns: 

565 Updated model response. 

566 

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

582 

583 

584# --------------------------------------------------------------------------- 

585# Gateway Models Endpoint (for LLM Chat dropdown) 

586# --------------------------------------------------------------------------- 

587 

588 

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. 

600 

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. 

603 

604 Args: 

605 db: Database session. 

606 current_user: Authenticated user. 

607 

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