feat(models): add workspace starred model and processor defaults

This commit is contained in:
Hyu
2026-10-09 18:34:29 +08:00
parent 4c70977a1d
commit c5c3ec0b2c
20 changed files with 231 additions and 9 deletions
@@ -14,6 +14,26 @@ MODEL_TEST_ERROR_CODE = 'model_test_failed'
@group.group_class('models/llm', '/api/v1/provider/models/llm')
class LLMModelsRouterGroup(group.RouterGroup):
async def initialize(self) -> None:
@self.route('/_/starred', methods=['GET'], permission=Permission.RESOURCE_VIEW)
async def get_starred(request_context: RequestContext) -> str:
return self.success(data={'uuid': await self.ap.llm_model_service.get_starred_model(request_context)})
@self.route('/_/starred', methods=['PUT'], permission=Permission.PROVIDER_SECRET_MANAGE)
async def set_starred(request_context: RequestContext) -> str:
body = await quart.request.json
model_uuid = body.get('uuid')
if model_uuid is not None and not isinstance(model_uuid, str):
return self.http_status(400, -1, 'uuid must be a string or null')
try:
await self.ap.llm_model_service.set_starred_model(request_context, model_uuid)
except ValueError as exc:
return self.http_status(400, -1, str(exc))
return self.success(data={})
@self.route('/_/default', methods=['GET'], permission=Permission.RESOURCE_MANAGE)
async def get_default(request_context: RequestContext) -> str:
return self.success(data={'uuid': await self.ap.llm_model_service.get_default_model(request_context)})
@self.route(
'',
methods=['GET'],
@@ -449,6 +449,11 @@ class AgentService:
config = agent_data['config'] if 'config' in agent_data else await self._get_default_agent_config(context)
config, runner_id, _ = RunnerConfigResolver.resolve_agent_config(config)
await self._validate_runner_for_agent(context, runner_id)
if runner_id:
descriptor = await self.ap.runner_registry.get(context, runner_id)
config['runner_config'][runner_id] = await self.ap.llm_model_service.apply_default_model(
context, descriptor.config_schema, config['runner_config'][runner_id],
)
patterns = agent_data.get('supported_event_patterns', AGENT_DEFAULT_EVENT_PATTERNS)
new_uuid = str(uuid.uuid4())
values = {
@@ -560,6 +565,8 @@ class AgentService:
parameters = runner_config.get(component_ref)
if parameters is None:
parameters = self.ap.pipeline_service._get_default_values_from_schema(descriptor.config_schema)
if draft:
parameters = await self.ap.llm_model_service.apply_default_model(context, descriptor.config_schema, parameters)
if not isinstance(parameters, dict):
raise ValueError('Processor parameters must be an object')
for field in descriptor.config_schema:
+68
View File
@@ -196,6 +196,74 @@ class LLMModelsService:
def __init__(self, ap: app.Application) -> None:
self.ap = ap
async def get_starred_model(self, context: TenantContext) -> str | None:
from ....entity.persistence.metadata import WorkspaceMetadata
result = await self.ap.persistence_mgr.execute_async(
sqlalchemy.select(WorkspaceMetadata.value).where(
WorkspaceMetadata.workspace_uuid == require_workspace_uuid(context),
WorkspaceMetadata.key == 'starred_llm_model',
)
)
row = result.first()
model_uuid = row[0] if row else None
if model_uuid and await self.get_llm_model(context, model_uuid) is not None:
return model_uuid
return None
async def set_starred_model(self, context: TenantContext, model_uuid: str | None) -> None:
from ....entity.persistence.metadata import WorkspaceMetadata
workspace_uuid = require_workspace_uuid(context)
if model_uuid and await self.get_llm_model(context, model_uuid) is None:
raise ValueError('Model not found in this Workspace')
# A single metadata row makes concurrent selections replace each other.
dialect = self.ap.persistence_mgr.get_db_engine().dialect.name
if dialect == 'postgresql':
from sqlalchemy.dialects.postgresql import insert
else:
from sqlalchemy.dialects.sqlite import insert
statement = insert(WorkspaceMetadata).values(
workspace_uuid=workspace_uuid, key='starred_llm_model', value=model_uuid or '',
)
await self.ap.persistence_mgr.execute_async(statement.on_conflict_do_update(
index_elements=['workspace_uuid', 'key'], set_={'value': model_uuid or ''},
))
async def get_default_model(self, context: TenantContext) -> str | None:
starred = await self.get_starred_model(context)
if starred:
return starred
if self.ap.instance_config.data.get('space', {}).get('disable_models_service', False):
return None
owner = await self.ap.user_service.get_workspace_owner(require_workspace_uuid(context))
cloud = getattr(getattr(self.ap, 'deployment', None), 'mode', 'oss') == 'cloud'
if not cloud and not (owner and owner.space_account_uuid):
return None
try:
return (await self.ap.space_service.get_recommended_chat_model(context))['uuid']
except Exception:
return None
async def apply_default_model(self, context: TenantContext, schema: list[dict], parameters: dict) -> dict:
import copy
fields = [field for field in schema if field.get('type') in (
'llm-model-selector', 'select-llm-model', 'model-fallback-selector',
) and field.get('name')]
if not fields:
return parameters
model_uuid = await self.get_default_model(context)
if not model_uuid:
return parameters
result = copy.deepcopy(parameters)
for field in fields:
name = field['name']
if field['type'] == 'model-fallback-selector':
value = result.get(name)
value = value if isinstance(value, dict) else {}
if not value.get('primary'):
result[name] = {**value, 'primary': model_uuid, 'fallbacks': value.get('fallbacks', [])}
elif not result.get(name):
result[name] = model_uuid
return result
async def get_llm_models(self, context: TenantContext, include_secret: bool = False) -> list[dict]:
"""Get all LLM models with provider info"""
result = await self.ap.persistence_mgr.execute_async(
@@ -68,6 +68,9 @@ class PipelineService:
runner_config['id'] = selected.id
runner_config.setdefault('expire-time', 0)
ai_config['runner_config'] = {selected.id: self._get_default_values_from_schema(selected.config_schema)}
ai_config['runner_config'][selected.id] = await self.ap.llm_model_service.apply_default_model(
context, selected.config_schema, ai_config['runner_config'][selected.id],
)
return config
async def get_pipeline_metadata(self, context: TenantContext) -> list[dict]:
+16
View File
@@ -424,6 +424,22 @@ class LangBotMCPServer:
context = _authorized(Permission.RUNTIME_OPERATE)
return _dump(await ap.agent_service.debug_agent(context, processor_uuid, payload))
@mcp.tool(description='Get the single starred LLM model UUID for the current Workspace.')
async def get_starred_model() -> str:
context = _authorized(Permission.RESOURCE_VIEW)
return _dump({'uuid': await ap.llm_model_service.get_starred_model(context)})
@mcp.tool(description='Star one LLM model for this Workspace, replacing the previous choice. Pass null to clear.')
async def set_starred_model(model_uuid: str | None = None) -> str:
context = _authorized(Permission.PROVIDER_SECRET_MANAGE)
await ap.llm_model_service.set_starred_model(context, model_uuid)
return _dump({'uuid': model_uuid})
@mcp.tool(description='Resolve the default LLM model: starred model, then the available Space wizard recommendation.')
async def get_default_model() -> str:
context = _authorized(Permission.RESOURCE_MANAGE)
return _dump({'uuid': await ap.llm_model_service.get_default_model(context)})
@mcp.tool(description='List all configured LLM models. Secrets are redacted.')
async def list_llm_models() -> str:
context = _authorized(Permission.RESOURCE_VIEW)