feat(docs): add default config endpoints and clarify config inheritance

- Add `/default` endpoints for users and groups to retrieve default LLM model configurations
- Update documentation to clarify that users receive both own and inherited configurations
- Improve response examples to show combined configs with source field
- Add 404 response example for missing default configurations
- Fix optimistic locking documentation formatting
This commit is contained in:
YueGuobin committed 2026-03-03 22:15:17 +08:00
1 parent 8fb4bf1117
commit b06593cf2a
3 files changed
+272 -110

No files matched your search

@@ -49,11 +49,11 @@ router = APIRouter()
@router.get(
"/users/{user_id}/llm-model-configs",
response_model=schemas.LLMModelConfigInheritedResponse,
dependencies=[Depends(has_privilege("User.Audit"))]
response_model=schemas.LLMModelConfigInheritedResponse
)
async def get_user_llm_model_configs(
user_id: UUID,
current_user: schemas.User = Depends(has_privilege("User.Audit")),
llm_repo: LLMModelConfigsRepository = Depends(get_repository(LLMModelConfigsRepository))
) -> schemas.LLMModelConfigInheritedResponse:
"""
@@ -63,7 +63,11 @@ async def get_user_llm_model_configs(
"""
try:
result = await llm_repo.get_user_effective_configs(user_id)
result = await llm_repo.get_user_effective_configs(
user_id,
current_user_id=current_user.user_id,
current_user_is_superadmin=current_user.is_superadmin
)
return schemas.LLMModelConfigInheritedResponse(
configs=result["configs"],
default_config=result.get("default_config"),
@@ -117,6 +121,51 @@ async def get_user_own_llm_model_configs(
)
@router.get(
"/users/{user_id}/llm-model-configs/default",
response_model=schemas.LLMModelConfigResponse,
dependencies=[Depends(has_privilege("User.Audit"))]
)
async def get_user_default_llm_model_config(
user_id: UUID,
llm_repo: LLMModelConfigsRepository = Depends(get_repository(LLMModelConfigsRepository))
) -> schemas.LLMModelConfigResponse:
"""
Get user's default LLM model configuration.
Required privilege: User.Audit
"""
try:
config = await llm_repo.get_user_default_config(user_id)
if not config:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"No default LLM model configuration found for user '{user_id}'"
)
return schemas.LLMModelConfigResponse(
config_id=config.config_id,
name=config.name,
model_type=config.model_type,
config=config.config,
user_id=config.user_id,
group_id=config.group_id,
is_default=config.is_default,
version=config.version,
created_at=config.created_at,
updated_at=config.updated_at
)
except HTTPException:
raise
except Exception as e:
log.error(f"Failed to retrieve user's default LLM model config: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to retrieve LLM model configuration"
)
@router.post(
"/users/{user_id}/llm-model-configs",
response_model=schemas.LLMModelConfigResponse,
@@ -366,6 +415,51 @@ async def get_group_llm_model_configs(
)
@router.get(
"/groups/{group_id}/llm-model-configs/default",
response_model=schemas.LLMModelConfigResponse,
dependencies=[Depends(has_privilege("Group.Audit"))]
)
async def get_group_default_llm_model_config(
group_id: UUID,
llm_repo: LLMModelConfigsRepository = Depends(get_repository(LLMModelConfigsRepository))
) -> schemas.LLMModelConfigResponse:
"""
Get group's default LLM model configuration.
Required privilege: Group.Audit
"""
try:
config = await llm_repo.get_group_default_config(group_id)
if not config:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"No default LLM model configuration found for group '{group_id}'"
)
return schemas.LLMModelConfigResponse(
config_id=config.config_id,
name=config.name,
model_type=config.model_type,
config=config.config,
user_id=config.user_id,
group_id=config.group_id,
is_default=config.is_default,
version=config.version,
created_at=config.created_at,
updated_at=config.updated_at
)
except HTTPException:
raise
except Exception as e:
log.error(f"Failed to retrieve group's default LLM model config: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to retrieve LLM model configuration"
)
@router.post(
"/groups/{group_id}/llm-model-configs",
response_model=schemas.LLMModelConfigResponse,
+43 -29
View File
@@ -402,13 +402,25 @@ class LLMModelConfigsRepository(BaseRepository):
# Inheritance methods
async def get_user_effective_configs(self, user_id: UUID) -> Dict[str, Any]:
async def get_user_effective_configs(
self,
user_id: UUID,
current_user_id: Optional[UUID] = None,
current_user_is_superadmin: bool = False
) -> Dict[str, Any]:
"""
Get user's effective configurations (own + inherited from groups).
Returns a dict with 'configs' list and 'default_config'.
API key visibility rules:
- Users viewing own configs: CAN see API keys in user configs, CANNOT see API keys in group configs
- Admins viewing other users' configs: CANNOT see ANY API keys
"""
from gns3server.utils.encryption import decrypt, is_encrypted
# Determine if current user is viewing their own configs
is_viewing_own = current_user_id == user_id if current_user_id else False
# Get user's own configs
user_configs = await self.get_user_configs(user_id)
@@ -435,12 +447,18 @@ class LLMModelConfigsRepository(BaseRepository):
# Add user's configs
for config in user_configs:
config_dict = config.config.copy()
# API key visibility: only show if viewing own configs
if "api_key" in config_dict and config_dict["api_key"]:
try:
if is_encrypted(config_dict["api_key"]):
config_dict["api_key"] = decrypt(config_dict["api_key"])
except Exception as e:
log.warning(f"Failed to decrypt API key for config {config.config_id}: {e}")
if is_viewing_own:
try:
if is_encrypted(config_dict["api_key"]):
config_dict["api_key"] = decrypt(config_dict["api_key"])
except Exception as e:
log.warning(f"Failed to decrypt API key for config {config.config_id}: {e}")
config_dict["api_key"] = None
else:
# Hide API key for admins viewing other users' configs
config_dict["api_key"] = None
configs_with_source.append({
@@ -456,31 +474,27 @@ class LLMModelConfigsRepository(BaseRepository):
if config.is_default and default_config is None:
default_config = configs_with_source[-1]
# Add inherited group configs (only if user has no configs)
if not user_configs:
for group_id, configs in group_configs_map.items():
for config in configs:
config_dict = config.config.copy()
if "api_key" in config_dict and config_dict["api_key"]:
try:
if is_encrypted(config_dict["api_key"]):
config_dict["api_key"] = decrypt(config_dict["api_key"])
except Exception as e:
log.warning(f"Failed to decrypt API key for config {config.config_id}: {e}")
config_dict["api_key"] = None
# Add inherited group configs (always shown, regardless of user configs)
for group_id, configs in group_configs_map.items():
for config in configs:
config_dict = config.config.copy()
configs_with_source.append({
"config_id": config.config_id,
"name": config.name,
"model_type": config.model_type,
"source": "group",
"group_name": group_names_map[group_id],
"is_default": config.is_default,
**config_dict
})
# API key visibility: NEVER show API keys from inherited group configs
if "api_key" in config_dict and config_dict["api_key"]:
config_dict["api_key"] = None
if config.is_default and default_config is None:
default_config = configs_with_source[-1]
configs_with_source.append({
"config_id": config.config_id,
"name": config.name,
"model_type": config.model_type,
"source": "group",
"group_name": group_names_map[group_id],
"is_default": config.is_default,
**config_dict
})
if config.is_default and default_config is None:
default_config = configs_with_source[-1]
return {
"configs": configs_with_source,