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
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-07 05:07 +0000
1from __future__ import annotations
3import logging
4from typing import Any, Optional
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
36router = APIRouter()
38log = logging.getLogger(__name__)
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}
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}
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}
89############################
90# ImportConfig
91# Thy configuration come, thy settings be done,
92# in production as it is in development.
93############################
96class ImportConfigForm(BaseModel):
97 config: dict
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()
113############################
114# ExportConfig
115############################
118@router.get('/export', response_model=dict)
119async def export_config(user=Depends(get_admin_user)):
120 return await Config.get_all()
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)
128############################
129# Connections Config
130############################
133class ConnectionsConfigForm(BaseModel):
134 ENABLE_DIRECT_CONNECTIONS: bool
135 ENABLE_DIRECT_INTEGRATIONS: bool = False
136 ENABLE_BASE_MODELS_CACHE: bool
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)
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
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
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}'
184 oauth_server_url = form_data.oauth_server_url if form_data.oauth_server_url else form_data.url
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 )
212############################
213# ToolServers Config
214############################
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
228 model_config = ConfigDict(extra='allow')
231class ToolServersConfigForm(BaseModel):
232 TOOL_SERVER_CONNECTIONS: list[ToolServerConnection]
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')}
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')
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}'
256 try:
257 request.app.state.oauth_client_manager.remove_client(client_key)
258 except Exception:
259 pass
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})
265 await set_tool_servers(request)
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')
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
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}
301class TerminalServerConnection(BaseModel):
302 id: str | None = ''
303 name: str | None = ''
305 enabled: bool | None = True
307 url: str
308 path: str | None = '/openapi.json'
310 key: str | None = ''
311 auth_type: str | None = 'bearer'
312 forward_cookies: bool = False
314 config: dict | None = None
316 server_type: str | None = None
317 policy_id: str | None = None
319 model_config = ConfigDict(extra='allow')
322class TerminalServersConfigForm(BaseModel):
323 TERMINAL_SERVER_CONNECTIONS: list[TerminalServerConnection]
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')}
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})
342 await set_terminal_servers(request)
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}
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.
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')
369 headers = {}
370 if form_data.auth_type == 'bearer' and form_data.key:
371 headers.update(bearer_auth_header(form_data.key))
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
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
398 except Exception as e:
399 log.debug('Failed to connect to the terminal server: %s', e)
401 raise HTTPException(status_code=400, detail='Failed to connect to the terminal server')
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
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
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
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')
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))
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')
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')
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))
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')
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')
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))
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
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')
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 )
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
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 )
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}'}
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)
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 )
656 if oauth_token:
657 token = oauth_token.get('access_token', '')
659 except Exception as e:
660 pass
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}'}
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)
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 )
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
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)
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
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
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 }
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)
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
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
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)
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
807class PromptSuggestion(BaseModel):
808 title: list[str]
809 content: str
812class SetDefaultSuggestionsForm(BaseModel):
813 suggestions: list[PromptSuggestion] | None
814 i18n: dict[str, Any] | None = None
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}
843############################
844# SetBanners
845############################
848class SetBannersForm(BaseModel):
849 banners: list[BannerModel]
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
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')