Coverage for open_webui/routers/configs.py: 71%

426 statements  

« prev     ^ index     » next       coverage.py v7.15.2, created at 2026-10-07 05:07 +0000

1from __future__ import annotations 

2 

3import logging 

4from typing import Any, Optional 

5 

6import aiohttp 

7from fastapi import APIRouter, Depends, HTTPException, Request 

8from mcp.shared.auth import OAuthMetadata 

9from open_webui.config import BannerModel 

10from open_webui.env import AIOHTTP_CLIENT_SESSION_SSL, AIOHTTP_CLIENT_TIMEOUT 

11from open_webui.events import EVENTS, publish_event 

12from open_webui.models.config import Config 

13from open_webui.models.oauth_sessions import OAuthSessions 

14from open_webui.utils.auth import get_admin_user, get_verified_user 

15from open_webui.utils.headers import bearer_auth_header, get_custom_headers 

16from open_webui.utils.mcp.client import MCPClient 

17from open_webui.utils.oauth import ( 

18 OAuthClientInformationFull, 

19 apply_connection_oauth_options, 

20 decrypt_data, 

21 encrypt_data, 

22 get_discovery_urls, 

23 get_oauth_client_info_with_dynamic_client_registration, 

24 get_oauth_client_info_with_static_credentials, 

25 recover_static_oauth_client_metadata, 

26 resolve_oauth_client_info, 

27) 

28from open_webui.utils.tools import ( 

29 get_tool_server_data, 

30 get_tool_server_url, 

31 set_terminal_servers, 

32 set_tool_servers, 

33) 

34from pydantic import BaseModel, ConfigDict 

35 

36router = APIRouter() 

37 

38log = logging.getLogger(__name__) 

39 

40CONNECTIONS_CONFIG_KEYS = { 

41 'ENABLE_DIRECT_CONNECTIONS': 'direct.enable', 

42 'ENABLE_DIRECT_INTEGRATIONS': 'direct.integrations.enable', 

43 'ENABLE_BASE_MODELS_CACHE': 'models.base_models_cache', 

44} 

45CODE_EXECUTION_CONFIG_KEYS = { 

46 'ENABLE_CODE_EXECUTION': 'code_execution.enable', 

47 'CODE_EXECUTION_ENGINE': 'code_execution.engine', 

48 'CODE_EXECUTION_JUPYTER_URL': 'code_execution.jupyter.url', 

49 'CODE_EXECUTION_JUPYTER_AUTH': 'code_execution.jupyter.auth', 

50 'CODE_EXECUTION_JUPYTER_AUTH_TOKEN': 'code_execution.jupyter.auth_token', 

51 'CODE_EXECUTION_JUPYTER_AUTH_PASSWORD': 'code_execution.jupyter.auth_password', 

52 'CODE_EXECUTION_JUPYTER_TIMEOUT': 'code_execution.jupyter.timeout', 

53 'ENABLE_CODE_INTERPRETER': 'code_interpreter.enable', 

54 'CODE_INTERPRETER_ENGINE': 'code_interpreter.engine', 

55 'CODE_INTERPRETER_PROMPT_TEMPLATE': 'code_interpreter.prompt_template', 

56 'CODE_INTERPRETER_JUPYTER_URL': 'code_interpreter.jupyter.url', 

57 'CODE_INTERPRETER_JUPYTER_AUTH': 'code_interpreter.jupyter.auth', 

58 'CODE_INTERPRETER_JUPYTER_AUTH_TOKEN': 'code_interpreter.jupyter.auth_token', 

59 'CODE_INTERPRETER_JUPYTER_AUTH_PASSWORD': 'code_interpreter.jupyter.auth_password', 

60 'CODE_INTERPRETER_JUPYTER_TIMEOUT': 'code_interpreter.jupyter.timeout', 

61} 

62MODELS_CONFIG_KEYS = { 

63 'DEFAULT_MODELS': 'ui.default_models', 

64 'DEFAULT_PINNED_MODELS': 'ui.default_pinned_models', 

65 'MODEL_ORDER_LIST': 'ui.model_order_list', 

66 'DEFAULT_MODEL_METADATA': 'models.default_metadata', 

67 'DEFAULT_MODEL_PARAMS': 'models.default_params', 

68} 

69SUBAGENTS_CONFIG_KEYS = { 

70 'ENABLE_SUBAGENTS': 'subagents.enable', 

71 'SUBAGENTS_BACKGROUND_ENABLED': 'subagents.background_enabled', 

72 'SUBAGENTS_MAX_CONCURRENT': 'subagents.max_concurrent', 

73 'SUBAGENTS_MAX_ASYNC': 'subagents.max_async', 

74 'SUBAGENTS_MAX_ITERATIONS': 'subagents.max_iterations', 

75 'SUBAGENTS_MAX_OUTPUT': 'subagents.max_output', 

76 'SUBAGENTS_SYSTEM_PROMPT': 'subagents.system_prompt', 

77} 

78 

79 

80async def get_config_values(key_map: dict[str, str]) -> dict: 

81 values = await Config.get_many(*key_map.values()) 

82 return {field: values[storage_key] for field, storage_key in key_map.items() if storage_key in values} 

83 

84 

85def config_updates(data: dict, key_map: dict[str, str]) -> dict: 

86 return {key_map[field]: value for field, value in data.items() if field in key_map} 

87 

88 

89############################ 

90# ImportConfig 

91# Thy configuration come, thy settings be done, 

92# in production as it is in development. 

93############################ 

94 

95 

96class ImportConfigForm(BaseModel): 

97 config: dict 

98 

99 

100@router.post('/import', response_model=dict) 

101async def import_config(request: Request, form_data: ImportConfigForm, user=Depends(get_admin_user)): 

102 await Config.upsert(form_data.config) 

103 await publish_event( 

104 request, 

105 EVENTS.CONFIG_IMPORTED, 

106 actor=user, 

107 subject_id='import', 

108 data={'keys': list(form_data.config.keys())}, 

109 ) 

110 return await Config.get_all() 

111 

112 

113############################ 

114# ExportConfig 

115############################ 

116 

117 

118@router.get('/export', response_model=dict) 

119async def export_config(user=Depends(get_admin_user)): 

120 return await Config.get_all() 

121 

122 

123@router.get('/namespace/{namespace}', response_model=dict) 

124async def get_config_namespace(namespace: str, user=Depends(get_admin_user)): 

125 return await Config.get_namespace(namespace) 

126 

127 

128############################ 

129# Connections Config 

130############################ 

131 

132 

133class ConnectionsConfigForm(BaseModel): 

134 ENABLE_DIRECT_CONNECTIONS: bool 

135 ENABLE_DIRECT_INTEGRATIONS: bool = False 

136 ENABLE_BASE_MODELS_CACHE: bool 

137 

138 

139@router.get('/connections', response_model=ConnectionsConfigForm) 

140async def get_connections_config(request: Request, user=Depends(get_admin_user)): 

141 return await get_config_values(CONNECTIONS_CONFIG_KEYS) 

142 

143 

144@router.post('/connections', response_model=ConnectionsConfigForm) 

145async def set_connections_config( 

146 request: Request, 

147 form_data: ConnectionsConfigForm, 

148 user=Depends(get_admin_user), 

149): 

150 await Config.upsert(config_updates(form_data.model_dump(exclude_unset=True), CONNECTIONS_CONFIG_KEYS)) 

151 values = await get_config_values(CONNECTIONS_CONFIG_KEYS) 

152 await publish_event( 

153 request, 

154 EVENTS.CONFIG_CONNECTIONS_UPDATED, 

155 actor=user, 

156 subject_id='connections', 

157 subject_type='config', 

158 data=values, 

159 ) 

160 return values 

161 

162 

163class OAuthClientRegistrationForm(BaseModel): 

164 url: str 

165 client_id: str 

166 client_name: str | None = None 

167 client_secret: str | None = None 

168 oauth_server_url: str | None = None 

169 oauth_scope: str | None = None 

170 

171 

172@router.post('/oauth/clients/register') 

173async def register_oauth_client( 

174 request: Request, 

175 form_data: OAuthClientRegistrationForm, 

176 type: str | None = None, 

177 user=Depends(get_admin_user), 

178): 

179 try: 

180 oauth_client_id = form_data.client_id 

181 if type: 

182 oauth_client_id = f'{type}:{form_data.client_id}' 

183 

184 oauth_server_url = form_data.oauth_server_url if form_data.oauth_server_url else form_data.url 

185 

186 if form_data.client_secret: 

187 # Static credentials: skip dynamic registration, build from provided credentials 

188 oauth_client_info = await get_oauth_client_info_with_static_credentials( 

189 request, 

190 oauth_client_id, 

191 oauth_server_url, 

192 oauth_client_id=form_data.client_id, 

193 oauth_client_secret=form_data.client_secret, 

194 oauth_scope=form_data.oauth_scope, 

195 ) 

196 else: 

197 oauth_client_info = await get_oauth_client_info_with_dynamic_client_registration( 

198 request, oauth_client_id, oauth_server_url, oauth_scope=form_data.oauth_scope 

199 ) 

200 return { 

201 'status': True, 

202 'oauth_client_info': encrypt_data(oauth_client_info.model_dump(mode='json')), 

203 } 

204 except Exception as e: 

205 log.debug('Failed to register OAuth client: %s', e) 

206 raise HTTPException( 

207 status_code=400, 

208 detail=f'Failed to register OAuth client: {e}', 

209 ) 

210 

211 

212############################ 

213# ToolServers Config 

214############################ 

215 

216 

217class ToolServerConnection(BaseModel): 

218 url: str 

219 path: str 

220 type: str | None = 'openapi' # openapi, mcp 

221 auth_type: str | None 

222 forward_cookies: bool = False 

223 headers: dict | str | None = None 

224 key: str | None 

225 config: dict | None 

226 info: dict | None = None 

227 

228 model_config = ConfigDict(extra='allow') 

229 

230 

231class ToolServersConfigForm(BaseModel): 

232 TOOL_SERVER_CONNECTIONS: list[ToolServerConnection] 

233 

234 

235@router.get('/tool_servers', response_model=ToolServersConfigForm) 

236async def get_tool_servers_config(request: Request, user=Depends(get_admin_user)): 

237 return {'TOOL_SERVER_CONNECTIONS': await Config.get('tool_server.connections')} 

238 

239 

240@router.post('/tool_servers', response_model=ToolServersConfigForm) 

241async def set_tool_servers_config( 

242 request: Request, 

243 form_data: ToolServersConfigForm, 

244 user=Depends(get_admin_user), 

245): 

246 existing_connections = await Config.get('tool_server.connections', []) or [] 

247 for connection in existing_connections: 

248 server_type = connection.get('type', 'openapi') 

249 auth_type = connection.get('auth_type', 'none') 

250 

251 if auth_type in ('oauth_2.1', 'oauth_2.1_static'): 251 ↛ 253line 251 didn't jump to line 253 because the condition on line 251 was never true

252 # Remove existing OAuth clients for tool servers 

253 server_id = (connection.get('info') or {}).get('id') 

254 client_key = f'{server_type}:{server_id}' 

255 

256 try: 

257 request.app.state.oauth_client_manager.remove_client(client_key) 

258 except Exception: 

259 pass 

260 

261 # Set new tool server connections 

262 connections = [connection.model_dump() for connection in form_data.TOOL_SERVER_CONNECTIONS] 

263 await Config.upsert({'tool_server.connections': connections}) 

264 

265 await set_tool_servers(request) 

266 

267 for connection in connections: 

268 server_type = connection.get('type', 'openapi') 

269 if server_type == 'mcp': 269 ↛ 270line 269 didn't jump to line 270 because the condition on line 269 was never true

270 server_id = (connection.get('info') or {}).get('id') 

271 auth_type = connection.get('auth_type', 'none') 

272 

273 if auth_type in ('oauth_2.1', 'oauth_2.1_static') and server_id: 

274 try: 

275 oauth_client_info = resolve_oauth_client_info(connection) 

276 oauth_client_info = await recover_static_oauth_client_metadata(connection, oauth_client_info) 

277 oauth_client_info = apply_connection_oauth_options(connection, oauth_client_info) 

278 request.app.state.oauth_client_manager.add_client( 

279 f'{server_type}:{server_id}', 

280 OAuthClientInformationFull(**oauth_client_info), 

281 ) 

282 except Exception as e: 

283 log.debug( 

284 'Failed to add OAuth client for MCP tool server %s: %s', 

285 server_id, 

286 f'{type(e).__name__}: {e}' if str(e) else type(e).__name__, 

287 ) 

288 continue 

289 

290 await publish_event( 

291 request, 

292 EVENTS.CONFIG_TOOL_SERVERS_UPDATED, 

293 actor=user, 

294 subject_id='tool_server.connections', 

295 subject_type='config', 

296 data={'count': len(connections), 'types': [connection.get('type', 'openapi') for connection in connections]}, 

297 ) 

298 return {'TOOL_SERVER_CONNECTIONS': connections} 

299 

300 

301class TerminalServerConnection(BaseModel): 

302 id: str | None = '' 

303 name: str | None = '' 

304 

305 enabled: bool | None = True 

306 

307 url: str 

308 path: str | None = '/openapi.json' 

309 

310 key: str | None = '' 

311 auth_type: str | None = 'bearer' 

312 forward_cookies: bool = False 

313 

314 config: dict | None = None 

315 

316 server_type: str | None = None 

317 policy_id: str | None = None 

318 

319 model_config = ConfigDict(extra='allow') 

320 

321 

322class TerminalServersConfigForm(BaseModel): 

323 TERMINAL_SERVER_CONNECTIONS: list[TerminalServerConnection] 

324 

325 

326@router.get('/terminal_servers') 

327async def get_terminal_servers_config(request: Request, user=Depends(get_admin_user)): 

328 return {'TERMINAL_SERVER_CONNECTIONS': await Config.get('terminal_server.connections')} 

329 

330 

331@router.post('/terminal_servers') 

332async def set_terminal_servers_config( 

333 request: Request, 

334 form_data: TerminalServersConfigForm, 

335 user=Depends(get_admin_user), 

336): 

337 connections = [ 

338 connection.model_dump(exclude={'policy', 'lifecycle'}) for connection in form_data.TERMINAL_SERVER_CONNECTIONS 

339 ] 

340 await Config.upsert({'terminal_server.connections': connections}) 

341 

342 await set_terminal_servers(request) 

343 

344 await publish_event( 

345 request, 

346 EVENTS.CONFIG_TERMINAL_SERVERS_UPDATED, 

347 actor=user, 

348 subject_id='terminal_server.connections', 

349 subject_type='config', 

350 data={'count': len(connections)}, 

351 ) 

352 return {'TERMINAL_SERVER_CONNECTIONS': connections} 

353 

354 

355@router.post('/terminal_servers/verify') 

356async def verify_terminal_server_connection( 

357 request: Request, form_data: TerminalServerConnection, user=Depends(get_admin_user) 

358): 

359 """ 

360 Verify the connection to a terminal server by detecting its type. 

361 

362 Tries GET {url}/api/v1/policies (orchestrator) then GET {url}/api/config 

363 (plain terminal). Returns ``{status: true, type: "orchestrator"|"terminal"}``. 

364 """ 

365 base_url = (form_data.url or '').rstrip('/') 

366 if not base_url: 

367 raise HTTPException(status_code=400, detail='Terminal server URL is required') 

368 

369 headers = {} 

370 if form_data.auth_type == 'bearer' and form_data.key: 

371 headers.update(bearer_auth_header(form_data.key)) 

372 

373 try: 

374 async with aiohttp.ClientSession( 

375 trust_env=True, 

376 timeout=aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT), 

377 ) as session: 

378 # Orchestrators expose a policies API; plain terminals don't. 

379 try: 

380 async with session.get( 

381 f'{base_url}/api/v1/policies', headers=headers, ssl=AIOHTTP_CLIENT_SESSION_SSL 

382 ) as resp: 

383 if resp.ok: 

384 return {'status': True, 'type': 'orchestrator'} 

385 except Exception: 

386 pass 

387 

388 # Fall back to open-terminal config endpoint. 

389 try: 

390 async with session.get( 

391 f'{base_url}/api/config', headers=headers, ssl=AIOHTTP_CLIENT_SESSION_SSL 

392 ) as resp: 

393 if resp.ok: 

394 return {'status': True, 'type': 'terminal'} 

395 except Exception: 

396 pass 

397 

398 except Exception as e: 

399 log.debug('Failed to connect to the terminal server: %s', e) 

400 

401 raise HTTPException(status_code=400, detail='Failed to connect to the terminal server') 

402 

403 

404class TerminalServerPolicyForm(BaseModel): 

405 url: str 

406 key: str | None = '' 

407 auth_type: str | None = 'bearer' 

408 policy_id: str 

409 policy_data: dict | None = None 

410 

411 

412class TerminalServerLifecycleForm(BaseModel): 

413 url: str 

414 key: str | None = '' 

415 auth_type: str | None = 'bearer' 

416 policy_id: str 

417 lifecycle_data: dict | None = None 

418 

419 

420class TerminalServerRefreshForm(BaseModel): 

421 url: str 

422 key: str | None = '' 

423 auth_type: str | None = 'bearer' 

424 user_id: str | None = None 

425 policy_id: str | None = None 

426 only_idle: bool = True 

427 reset: bool = False 

428 

429 

430@router.post('/terminal_servers/policy') 

431async def put_terminal_server_policy( 

432 request: Request, form_data: TerminalServerPolicyForm, user=Depends(get_admin_user) 

433): 

434 """Proxy a policy read or update to an orchestrator terminal server.""" 

435 base_url = (form_data.url or '').rstrip('/') 

436 if not base_url: 

437 raise HTTPException(status_code=400, detail='Terminal server URL is required') 

438 

439 headers = {'Content-Type': 'application/json'} 

440 if form_data.auth_type == 'bearer' and form_data.key: 

441 headers.update(bearer_auth_header(form_data.key)) 

442 

443 try: 

444 async with aiohttp.ClientSession( 

445 trust_env=True, 

446 timeout=aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT), 

447 ) as session: 

448 policy_url = f'{base_url}/api/v1/policies/{form_data.policy_id}' 

449 async with session.request( 

450 'GET' if form_data.policy_data is None else 'PUT', 

451 policy_url, 

452 headers=headers, 

453 json=form_data.policy_data, 

454 ssl=AIOHTTP_CLIENT_SESSION_SSL, 

455 ) as resp: 

456 if resp.ok: 

457 return await resp.json() 

458 detail = await resp.text() 

459 raise HTTPException(status_code=resp.status, detail=detail) 

460 except HTTPException: 

461 raise 

462 except Exception as e: 

463 log.debug('Failed to access policy on terminal server: %s', e) 

464 raise HTTPException(status_code=400, detail='Failed to access policy on terminal server') 

465 

466 

467@router.post('/terminal_servers/lifecycle') 

468async def put_terminal_server_lifecycle( 

469 request: Request, form_data: TerminalServerLifecycleForm, user=Depends(get_admin_user) 

470): 

471 """Proxy a lifecycle read or update to an orchestrator terminal server.""" 

472 base_url = (form_data.url or '').rstrip('/') 

473 if not base_url: 

474 raise HTTPException(status_code=400, detail='Terminal server URL is required') 

475 

476 headers = {'Content-Type': 'application/json'} 

477 if form_data.auth_type == 'bearer' and form_data.key: 

478 headers.update(bearer_auth_header(form_data.key)) 

479 

480 try: 

481 async with aiohttp.ClientSession( 

482 trust_env=True, 

483 timeout=aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT), 

484 ) as session: 

485 lifecycle_url = f'{base_url}/api/v1/policies/{form_data.policy_id}/lifecycle' 

486 async with session.request( 

487 'GET' if form_data.lifecycle_data is None else 'PUT', 

488 lifecycle_url, 

489 headers=headers, 

490 json=form_data.lifecycle_data, 

491 ssl=AIOHTTP_CLIENT_SESSION_SSL, 

492 ) as resp: 

493 if resp.ok: 

494 return await resp.json() 

495 detail = await resp.text() 

496 raise HTTPException(status_code=resp.status, detail=detail) 

497 except HTTPException: 

498 raise 

499 except Exception as e: 

500 log.debug('Failed to access lifecycle on terminal server: %s', e) 

501 raise HTTPException(status_code=400, detail='Failed to access lifecycle on terminal server') 

502 

503 

504@router.post('/terminal_servers/refresh') 

505async def refresh_terminal_server_terminals( 

506 request: Request, form_data: TerminalServerRefreshForm, user=Depends(get_admin_user) 

507): 

508 """ 

509 Proxy a terminal refresh request to an orchestrator terminal server. 

510 """ 

511 base_url = (form_data.url or '').rstrip('/') 

512 if not base_url: 

513 raise HTTPException(status_code=400, detail='Terminal server URL is required') 

514 

515 headers = {'Content-Type': 'application/json'} 

516 if form_data.auth_type == 'bearer' and form_data.key: 

517 headers.update(bearer_auth_header(form_data.key)) 

518 

519 body = { 

520 'only_idle': form_data.only_idle, 

521 'reset': form_data.reset, 

522 } 

523 if form_data.user_id: 

524 body['user_id'] = form_data.user_id 

525 if form_data.policy_id: 

526 body['policy_id'] = form_data.policy_id 

527 

528 try: 

529 async with aiohttp.ClientSession( 

530 trust_env=True, 

531 timeout=aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT), 

532 ) as session: 

533 refresh_url = f'{base_url}/api/v1/terminals/refresh' 

534 async with session.post( 

535 refresh_url, 

536 headers=headers, 

537 json=body, 

538 ssl=AIOHTTP_CLIENT_SESSION_SSL, 

539 ) as resp: 

540 if resp.ok: 

541 return await resp.json() 

542 detail = await resp.text() 

543 raise HTTPException(status_code=resp.status, detail=detail) 

544 except HTTPException: 

545 raise 

546 except Exception as e: 

547 log.debug('Failed to refresh terminals: %s', e) 

548 raise HTTPException(status_code=400, detail='Failed to refresh terminals') 

549 

550 

551@router.post('/tool_servers/verify') 

552async def verify_tool_servers_config(request: Request, form_data: ToolServerConnection, user=Depends(get_admin_user)): 

553 """ 

554 Verify the connection to the tool server. 

555 """ 

556 try: 

557 if form_data.type == 'mcp': 557 ↛ 558line 557 didn't jump to line 558 because the condition on line 557 was never true

558 if form_data.auth_type in ('oauth_2.1', 'oauth_2.1_static'): 

559 oauth_server_url = ( 

560 form_data.info.get('oauth_server_url') 

561 if form_data.info and form_data.info.get('oauth_server_url') 

562 else form_data.url 

563 ) 

564 discovery_urls = await get_discovery_urls(oauth_server_url) 

565 for discovery_url in discovery_urls: 

566 log.debug('Trying to fetch OAuth 2.1 discovery document from %s', discovery_url) 

567 async with aiohttp.ClientSession( 

568 trust_env=True, 

569 timeout=aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT), 

570 ) as session: 

571 async with session.get( 

572 discovery_url, ssl=AIOHTTP_CLIENT_SESSION_SSL 

573 ) as oauth_server_metadata_response: 

574 if oauth_server_metadata_response.status == 200: 

575 try: 

576 oauth_server_metadata = OAuthMetadata.model_validate( 

577 await oauth_server_metadata_response.json() 

578 ) 

579 return { 

580 'status': True, 

581 'oauth_server_metadata': oauth_server_metadata.model_dump(mode='json'), 

582 } 

583 except Exception as e: 

584 log.info('Failed to parse OAuth 2.1 discovery document: %s', e) 

585 raise HTTPException( 

586 status_code=400, 

587 detail=f'Failed to parse OAuth 2.1 discovery document from {discovery_url}', 

588 ) 

589 

590 raise HTTPException( 

591 status_code=400, 

592 detail=f'Failed to fetch OAuth 2.1 discovery document from {discovery_urls}', 

593 ) 

594 else: 

595 try: 

596 client = MCPClient() 

597 headers = None 

598 

599 token = None 

600 if form_data.auth_type == 'bearer': 

601 token = form_data.key 

602 elif form_data.auth_type == 'session': 

603 token = request.state.token.credentials 

604 elif form_data.auth_type == 'system_oauth': 

605 oauth_token = None 

606 try: 

607 if request.cookies.get('oauth_session_id', None): 

608 oauth_token = await request.app.state.oauth_manager.get_oauth_token( 

609 user.id, 

610 request.cookies.get('oauth_session_id', None), 

611 ) 

612 

613 if oauth_token: 

614 token = oauth_token.get('access_token', '') 

615 except Exception as e: 

616 pass 

617 if token: 

618 headers = {'Authorization': f'Bearer {token}'} 

619 

620 if form_data.headers and isinstance(form_data.headers, dict): 

621 if headers is None: 

622 headers = {} 

623 custom_headers = await get_custom_headers(form_data.headers, user) 

624 headers.update(custom_headers) 

625 

626 await client.connect(form_data.url, headers=headers) 

627 specs = await client.list_tool_specs() 

628 return { 

629 'status': True, 

630 'specs': specs, 

631 } 

632 except Exception as e: 

633 log.debug('Failed to create MCP client: %s', e) 

634 raise HTTPException( 

635 status_code=400, 

636 detail=f'Failed to create MCP client', 

637 ) 

638 finally: 

639 if client: 

640 await client.disconnect() 

641 else: # openapi 

642 token = None 

643 headers = None 

644 if form_data.auth_type == 'bearer': 644 ↛ 645line 644 didn't jump to line 645 because the condition on line 644 was never true

645 token = form_data.key 

646 elif form_data.auth_type == 'session': 646 ↛ 647line 646 didn't jump to line 647 because the condition on line 646 was never true

647 token = request.state.token.credentials 

648 elif form_data.auth_type == 'system_oauth': 648 ↛ 649line 648 didn't jump to line 649 because the condition on line 648 was never true

649 try: 

650 if request.cookies.get('oauth_session_id', None): 

651 oauth_token = await request.app.state.oauth_manager.get_oauth_token( 

652 user.id, 

653 request.cookies.get('oauth_session_id', None), 

654 ) 

655 

656 if oauth_token: 

657 token = oauth_token.get('access_token', '') 

658 

659 except Exception as e: 

660 pass 

661 

662 if token: 662 ↛ 663line 662 didn't jump to line 663 because the condition on line 662 was never true

663 headers = {'Authorization': f'Bearer {token}'} 

664 

665 if form_data.headers and isinstance(form_data.headers, dict): 

666 if headers is None: 666 ↛ 668line 666 didn't jump to line 668 because the condition on line 666 was always true

667 headers = {} 

668 custom_headers = await get_custom_headers(form_data.headers, user) 

669 headers.update(custom_headers) 

670 

671 url = get_tool_server_url(form_data.url, form_data.path) 

672 return await get_tool_server_data(url, headers=headers) 

673 except HTTPException as e: 

674 raise e 

675 except Exception as e: 

676 log.debug('Failed to connect to the tool server: %s', e) 

677 raise HTTPException( 

678 status_code=400, 

679 detail=f'Failed to connect to the tool server', 

680 ) 

681 

682 

683############################ 

684# CodeInterpreterConfig 

685############################ 

686class CodeInterpreterConfigForm(BaseModel): 

687 ENABLE_CODE_EXECUTION: bool 

688 CODE_EXECUTION_ENGINE: str 

689 CODE_EXECUTION_JUPYTER_URL: str | None 

690 CODE_EXECUTION_JUPYTER_AUTH: str | None 

691 CODE_EXECUTION_JUPYTER_AUTH_TOKEN: str | None 

692 CODE_EXECUTION_JUPYTER_AUTH_PASSWORD: str | None 

693 CODE_EXECUTION_JUPYTER_TIMEOUT: int | None 

694 ENABLE_CODE_INTERPRETER: bool 

695 CODE_INTERPRETER_ENGINE: str 

696 CODE_INTERPRETER_PROMPT_TEMPLATE: str | None 

697 CODE_INTERPRETER_JUPYTER_URL: str | None 

698 CODE_INTERPRETER_JUPYTER_AUTH: str | None 

699 CODE_INTERPRETER_JUPYTER_AUTH_TOKEN: str | None 

700 CODE_INTERPRETER_JUPYTER_AUTH_PASSWORD: str | None 

701 CODE_INTERPRETER_JUPYTER_TIMEOUT: int | None 

702 

703 

704@router.get('/code_execution', response_model=CodeInterpreterConfigForm) 

705async def get_code_execution_config(request: Request, user=Depends(get_admin_user)): 

706 return await get_config_values(CODE_EXECUTION_CONFIG_KEYS) 

707 

708 

709@router.post('/code_execution', response_model=CodeInterpreterConfigForm) 

710async def set_code_execution_config( 

711 request: Request, form_data: CodeInterpreterConfigForm, user=Depends(get_admin_user) 

712): 

713 await Config.upsert(config_updates(form_data.model_dump(), CODE_EXECUTION_CONFIG_KEYS)) 

714 values = await get_config_values(CODE_EXECUTION_CONFIG_KEYS) 

715 await publish_event( 

716 request, 

717 EVENTS.CONFIG_CODE_EXECUTION_UPDATED, 

718 actor=user, 

719 subject_id='code_execution', 

720 subject_type='config', 

721 data={ 

722 'code_execution_enabled': values.get('ENABLE_CODE_EXECUTION'), 

723 'code_execution_engine': values.get('CODE_EXECUTION_ENGINE'), 

724 'code_interpreter_enabled': values.get('ENABLE_CODE_INTERPRETER'), 

725 'code_interpreter_engine': values.get('CODE_INTERPRETER_ENGINE'), 

726 }, 

727 ) 

728 return values 

729 

730 

731############################ 

732# SetDefaultModels 

733############################ 

734class ModelsConfigForm(BaseModel): 

735 DEFAULT_MODELS: str | None 

736 DEFAULT_PINNED_MODELS: str | None 

737 MODEL_ORDER_LIST: list[str] | None 

738 DEFAULT_MODEL_METADATA: dict | None = None 

739 DEFAULT_MODEL_PARAMS: dict | None = None 

740 

741 

742@router.get('/models/defaults') 

743async def get_models_defaults(request: Request, user=Depends(get_verified_user)): 

744 return { 

745 'DEFAULT_MODEL_METADATA': await Config.get('models.default_metadata'), 

746 } 

747 

748 

749@router.get('/models', response_model=ModelsConfigForm) 

750async def get_models_config(request: Request, user=Depends(get_admin_user)): 

751 return await get_config_values(MODELS_CONFIG_KEYS) 

752 

753 

754@router.post('/models', response_model=ModelsConfigForm) 

755async def set_models_config(request: Request, form_data: ModelsConfigForm, user=Depends(get_admin_user)): 

756 await Config.upsert(config_updates(form_data.model_dump(), MODELS_CONFIG_KEYS)) 

757 values = await get_config_values(MODELS_CONFIG_KEYS) 

758 await publish_event( 

759 request, 

760 EVENTS.CONFIG_MODELS_UPDATED, 

761 actor=user, 

762 subject_id='models', 

763 subject_type='config', 

764 data={ 

765 'default_models': values.get('DEFAULT_MODELS'), 

766 'default_pinned_models': values.get('DEFAULT_PINNED_MODELS'), 

767 'model_order_count': len(values.get('MODEL_ORDER_LIST') or []), 

768 }, 

769 ) 

770 return values 

771 

772 

773class SubagentsConfigForm(BaseModel): 

774 ENABLE_SUBAGENTS: bool 

775 SUBAGENTS_BACKGROUND_ENABLED: bool 

776 SUBAGENTS_MAX_CONCURRENT: int 

777 SUBAGENTS_MAX_ASYNC: int 

778 SUBAGENTS_MAX_ITERATIONS: int 

779 SUBAGENTS_MAX_OUTPUT: int 

780 SUBAGENTS_SYSTEM_PROMPT: str 

781 

782 

783@router.get('/subagents', response_model=SubagentsConfigForm) 

784async def get_subagents_config(user=Depends(get_admin_user)): 

785 return await get_config_values(SUBAGENTS_CONFIG_KEYS) 

786 

787 

788@router.post('/subagents', response_model=SubagentsConfigForm) 

789async def set_subagents_config( 

790 request: Request, 

791 form_data: SubagentsConfigForm, 

792 user=Depends(get_admin_user), 

793): 

794 await Config.upsert(config_updates(form_data.model_dump(), SUBAGENTS_CONFIG_KEYS)) 

795 values = await get_config_values(SUBAGENTS_CONFIG_KEYS) 

796 await publish_event( 

797 request, 

798 EVENTS.CONFIG_UPDATED, 

799 actor=user, 

800 subject_id='subagents', 

801 subject_type='config', 

802 data={'enabled': values.get('ENABLE_SUBAGENTS')}, 

803 ) 

804 return values 

805 

806 

807class PromptSuggestion(BaseModel): 

808 title: list[str] 

809 content: str 

810 

811 

812class SetDefaultSuggestionsForm(BaseModel): 

813 suggestions: list[PromptSuggestion] | None 

814 i18n: dict[str, Any] | None = None 

815 

816 

817@router.post('/suggestions', response_model=dict) 

818async def set_default_suggestions( 

819 request: Request, 

820 form_data: SetDefaultSuggestionsForm, 

821 user=Depends(get_admin_user), 

822): 

823 data = form_data.model_dump() 

824 await Config.upsert( 

825 { 

826 'ui.prompt_suggestions': data['suggestions'], 

827 'ui.prompt_suggestions_i18n': data.get('i18n') or {}, 

828 } 

829 ) 

830 suggestions = await Config.get('ui.prompt_suggestions') 

831 suggestions_i18n = await Config.get('ui.prompt_suggestions_i18n') 

832 await publish_event( 

833 request, 

834 EVENTS.CONFIG_SUGGESTIONS_UPDATED, 

835 actor=user, 

836 subject_id='ui.prompt_suggestions', 

837 subject_type='config', 

838 data={'count': len(suggestions or [])}, 

839 ) 

840 return {'suggestions': suggestions, 'i18n': suggestions_i18n} 

841 

842 

843############################ 

844# SetBanners 

845############################ 

846 

847 

848class SetBannersForm(BaseModel): 

849 banners: list[BannerModel] 

850 

851 

852@router.post('/banners', response_model=list[BannerModel]) 

853async def set_banners( 

854 request: Request, 

855 form_data: SetBannersForm, 

856 user=Depends(get_admin_user), 

857): 

858 data = form_data.model_dump() 

859 await Config.upsert({'ui.banners': data['banners']}) 

860 banners = await Config.get('ui.banners') 

861 await publish_event( 

862 request, 

863 EVENTS.CONFIG_BANNERS_UPDATED, 

864 actor=user, 

865 subject_id='ui.banners', 

866 subject_type='config', 

867 data={'count': len(banners or [])}, 

868 ) 

869 return banners 

870 

871 

872@router.get('/banners', response_model=list[BannerModel]) 

873async def get_banners( 

874 request: Request, 

875 user=Depends(get_verified_user), 

876): 

877 return await Config.get('ui.banners')