feat: integrate studio bindings and executable pipelines

parent 7b4a5b2d
......@@ -149,6 +149,28 @@ Shared Studio integration helpers:
- Normalizes dashboard-visible provider, rotation, and autoselect resources into a unified Studio catalog
- Infers and merges Studio capability metadata for autodetected and manually configured models
- Supports admin/global and user-owned dashboard catalog scopes
- Carries effective Studio adapter metadata for provider models so Studio workflow proxying can choose provider-aware payload shaping
### aisbf/studio_adapters.py
Studio adapter inference helpers:
- Defines manual/automatic Studio adapter choices for provider models
- Defines provider-specific Studio adapter profile choices layered above adapter family inference
- Infers the best adapter from provider type and model capabilities when no manual override is set
- Infers the best adapter profile from the configured provider identity, endpoint, and model metadata when no manual override is set
- Supplies the effective adapter used by Studio workflow payload normalization
- Contains provider-family-aware Studio payload transformation helpers used by the generic proxy path before requests are forwarded upstream
- Contains explicit profile-specific request shaping for OpenAI-style, OpenRouter-style, Gemini-style, Anthropic-style, and Ollama-style Studio media workflows
- Includes provider-id/endpoint specific profiles such as Kilo/OpenRouter-style, GitHub Models-style, and Azure OpenAI-style request shaping
### aisbf/studio_services.py
Studio persistence and runtime helpers for dashboard Studio:
- Stores characters, environments, voices, archive items, and custom pipelines per Studio scope
- Uses file-backed storage for the config admin scope under `~/.aisbf/studio/`
- Uses database-backed storage for user-owned Studio assets and pipelines
- Stores admin custom pipelines in `~/.aisbf/pipelines.json` as a plain JSON array
- Stores persistent Studio functionality-to-model bindings in `~/.aisbf/studio_bindings.json` for config admin scope and in the user prompt override key `studio_function_bindings` for user scope
- Loads Studio chat system prompt from `STUDIO_SYSTEM.md`, using file-backed admin defaults and database-backed user overrides
- Normalizes Studio profile payloads and pipeline definitions for both admin and user scopes
### main.py
FastAPI application:
......@@ -959,6 +981,17 @@ This AI.PROMPT file is automatically updated when significant changes are made t
- Provider model save and autodetect flows now persist normalized Studio capability metadata for manual and inferred models
- Studio catalog entries can expose partial aggregate capabilities so incomplete metadata stays visible instead of disappearing
**2026-05-11 - Dashboard Studio Runtime and Persistence Expansion**
- Replaced the partial dashboard Studio shell with the original full Studio UI adapted to dashboard templates, CSS, and proxy-aware routing
- Added scope-aware Studio API bases so the config admin uses `/api/v1` while user-scoped sessions use `/api/u/{username}`
- Restricted global Studio scope to the config admin defined by `aisbf.json`; database-backed admins remain user-scoped
- Added `aisbf/studio_services.py` for Studio asset and pipeline persistence across admin/file and user/database scopes
- Added `studio_assets` and `studio_pipelines` database tables for user-owned Studio storage, created through normal startup migrations
- Kept global config admin characters, environments, voices, archive items, and pipelines file-backed under `~/.aisbf/studio/`
- Saved admin custom pipelines in `~/.aisbf/pipelines.json` and user custom pipelines in the database
- Added `/api/v1/...` and `/api/u/{username}/...` Studio aliases for profile, archive, pipeline, and proxied advanced media routes
- Dashboard Studio advanced media tools must proxy to provider-facing `v1/...` endpoints; no local ffmpeg/OpenCV/InsightFace/Real-ESRGAN processing is assumed
**2026-03-22 - Configuration Refactoring**
- Centralized API key storage in providers.json
- API keys are now stored only in provider definitions, not in rotation/autoselect configs
......
......@@ -5,3 +5,4 @@
- [ ] Add support for mempalace
- [ ] Add support for caveman mode
- [ ] Integrate github larsderidder context-lens project
- [ ] Integrate https://github.com/PromptSail/prompt_sail
This diff is collapsed.
......@@ -12,6 +12,7 @@ from aisbf.app.startup import (_reload_global_config, _apply_condense_defaults_p
_apply_condense_defaults_rotation, _providers_json_path, _rotations_json_path,
_autoselect_json_path, get_aisbf_config_path)
from aisbf.routes.auth import require_dashboard_auth, require_api_auth, require_api_admin, require_admin
from aisbf.studio_services import studio_service
import httpx
router = APIRouter()
......@@ -20,6 +21,121 @@ _templates = None
logger = logging.getLogger(__name__)
@router.get("/admin/api/cached-models")
async def admin_cached_models(request: Request):
auth_check = require_dashboard_auth(request)
if auth_check:
return auth_check
return JSONResponse(studio_service.get_cached_models())
@router.get("/admin/api/tokens")
async def admin_tokens(request: Request):
auth_check = require_dashboard_auth(request)
if auth_check:
return auth_check
user_id = request.session.get('user_id')
if not user_id:
return JSONResponse([])
db = DatabaseRegistry.get_config_database()
return JSONResponse(db.get_user_api_tokens(user_id))
@router.get("/admin/api/characters")
async def admin_characters(request: Request):
auth_check = require_dashboard_auth(request)
if auth_check:
return auth_check
return JSONResponse(studio_service.list_characters("admin", None))
@router.get("/admin/api/characters/{name}")
async def admin_character_detail(request: Request, name: str):
auth_check = require_dashboard_auth(request)
if auth_check:
return auth_check
item = studio_service.get_character("admin", None, name)
if not item:
raise HTTPException(status_code=404, detail="Character not found")
return JSONResponse(item)
@router.delete("/admin/api/characters/{name}")
async def admin_character_delete(request: Request, name: str):
auth_check = require_dashboard_auth(request)
if auth_check:
return auth_check
studio_service.delete_character("admin", None, name)
return JSONResponse({"success": True})
@router.get("/admin/api/characters/{name}/thumbnail")
async def admin_character_thumbnail(request: Request, name: str):
auth_check = require_dashboard_auth(request)
if auth_check:
return auth_check
payload = studio_service.get_character_thumbnail_bytes("admin", None, name)
if not payload:
raise HTTPException(status_code=404, detail="Thumbnail not found")
return Response(content=payload, media_type="image/png")
@router.get("/admin/api/environments")
async def admin_environments(request: Request):
auth_check = require_dashboard_auth(request)
if auth_check:
return auth_check
return JSONResponse(studio_service.list_environments("admin", None))
@router.get("/admin/api/environments/{name}")
async def admin_environment_detail(request: Request, name: str):
auth_check = require_dashboard_auth(request)
if auth_check:
return auth_check
item = studio_service.get_environment("admin", None, name)
if not item:
raise HTTPException(status_code=404, detail="Environment not found")
return JSONResponse(item)
@router.delete("/admin/api/environments/{name}")
async def admin_environment_delete(request: Request, name: str):
auth_check = require_dashboard_auth(request)
if auth_check:
return auth_check
studio_service.delete_environment("admin", None, name)
return JSONResponse({"success": True})
@router.get("/admin/api/environments/{name}/thumbnail")
async def admin_environment_thumbnail(request: Request, name: str):
auth_check = require_dashboard_auth(request)
if auth_check:
return auth_check
payload = studio_service.get_environment_thumbnail_bytes("admin", None, name)
if not payload:
raise HTTPException(status_code=404, detail="Thumbnail not found")
return Response(content=payload, media_type="image/png")
@router.get("/admin/api/voices")
async def admin_voices(request: Request):
auth_check = require_dashboard_auth(request)
if auth_check:
return auth_check
return JSONResponse(studio_service.list_voices("admin", None))
@router.delete("/admin/api/voices/{name}")
async def admin_voice_delete(request: Request, name: str):
auth_check = require_dashboard_auth(request)
if auth_check:
return auth_check
studio_service.delete_voice("admin", None, name)
return JSONResponse({"success": True})
def init(config, templates):
global _config, _templates
_config = config
......
......@@ -150,6 +150,7 @@ async def dashboard_prompts(request: Request):
{'key': 'condensation_conversational', 'name': 'Condensation - Conversational', 'filename': 'condensation_conversational.md'},
{'key': 'condensation_semantic', 'name': 'Condensation - Semantic', 'filename': 'condensation_semantic.md'},
{'key': 'autoselect', 'name': 'Autoselect - Model Selection', 'filename': 'autoselect.md'},
{'key': 'studio_system', 'name': 'Studio - System Prompt', 'filename': 'STUDIO_SYSTEM.md'},
]
prompts_data = []
......@@ -239,6 +240,7 @@ async def dashboard_prompts_save(request: Request, prompt_key: str = Form(...),
'condensation_conversational': 'condensation_conversational.md',
'condensation_semantic': 'condensation_semantic.md',
'autoselect': 'autoselect.md',
'studio_system': 'STUDIO_SYSTEM.md',
}
if prompt_key not in prompt_map:
......
This diff is collapsed.
......@@ -24,6 +24,8 @@ from pathlib import Path
import json
from typing import Any, Dict, Iterable, List, Optional
from aisbf.studio_adapters import effective_studio_adapter, infer_studio_adapter_profile
STUDIO_CAPABILITY_MAP = {
"t2t": "chat",
......@@ -63,6 +65,41 @@ STUDIO_CAPABILITY_MAP = {
"animation": "animation",
}
STUDIO_CAPABILITY_CHOICES = [
"chat",
"vision",
"image_generation",
"image_edit",
"video_generation",
"video_understanding",
"audio_input",
"transcription",
"speech_generation",
"audio_generation",
"audio_to_audio",
"embeddings",
"tool_use",
"reasoning",
"code_generation",
"code_completion",
"translation",
"summarization",
"classification",
"sentiment_analysis",
"ner",
"question_answering",
"search",
"moderation",
"fine_tuning",
"multimodal",
"ocr",
"image_captioning",
"object_detection",
"segmentation",
"3d_generation",
"animation",
]
DEFAULT_CHAT_PROVIDER_TYPES = {"openai", "anthropic", "google", "kilo", "claude", "qwen", "codex"}
NON_CHAT_MEDIA_TOKENS = {
"dall-e",
......@@ -112,6 +149,10 @@ def normalize_capabilities(values: Optional[Iterable[str]]) -> List[str]:
return _dedupe(normalized)
def serialize_studio_capability_choices() -> List[str]:
return list(STUDIO_CAPABILITY_CHOICES)
def stamp_inferred_capabilities(model: Dict[str, Any], provider_type: str) -> Dict[str, Any]:
stamped = dict(model)
capability_result = infer_model_capabilities(
......@@ -154,6 +195,7 @@ def infer_model_capabilities(
output_modalities = architecture.get("output_modalities") or []
if not capabilities:
metadata_text = json.dumps(provider_metadata, sort_keys=True).lower() if provider_metadata else ""
if not any(token in name for token in ["embedding", "embed", "whisper", "tts", *NON_CHAT_MEDIA_TOKENS]):
capabilities.append("chat")
if any(token in name for token in ["vision", "gpt-4-turbo", "gpt-4o", "claude-3", "gemini-1.5", "gemini-2.0", "gemini-pro-vision", "llava", "blip"]):
......@@ -172,6 +214,8 @@ def infer_model_capabilities(
capabilities.append("speech_generation")
if any(token in name for token in ["musicgen", "audiogen", "riffusion", "a2a"]):
capabilities.append("audio_generation")
if any(token in name for token in ["voice", "audio-to-audio", "voice conversion", "rvc", "a2a"]):
capabilities.append("audio_to_audio")
if any(token in name for token in ["embedding", "embed", "ada-002", "bge", "e5", "instructor"]):
capabilities.append("embeddings")
if any(token in name for token in ["gpt-4", "gpt-3.5-turbo", "claude-3", "gemini", "function", "tool"]):
......@@ -180,6 +224,36 @@ def infer_model_capabilities(
capabilities.extend(["code_generation", "code_completion"])
if any(token in name for token in ["reasoning", "cot", "o1", "o3"]):
capabilities.append("reasoning")
if any(token in name for token in ["ocr"]):
capabilities.extend(["ocr", "image_captioning"])
if any(token in name for token in ["detect", "detection", "yolo"]):
capabilities.append("object_detection")
if any(token in name for token in ["segment", "segmentation", "sam"]):
capabilities.append("segmentation")
if any(token in name for token in ["3d", "mesh", "gaussian splat", "nerf"]):
capabilities.append("3d_generation")
if any(token in name for token in ["animate", "animation"]):
capabilities.append("animation")
if metadata_text:
if 'image' in metadata_text and 'input_modalit' in metadata_text:
capabilities.append("vision")
if 'audio' in metadata_text and 'input_modalit' in metadata_text:
capabilities.append("audio_input")
if 'transcrib' in metadata_text or 'speech_to_text' in metadata_text:
capabilities.extend(["audio_input", "transcription"])
if 'text_to_speech' in metadata_text or 'speech_generation' in metadata_text:
capabilities.append("speech_generation")
if 'audio_generation' in metadata_text or 'text-to-audio' in metadata_text:
capabilities.append("audio_generation")
if 'audio_to_audio' in metadata_text or 'voice conversion' in metadata_text:
capabilities.append("audio_to_audio")
if 'embedding' in metadata_text:
capabilities.append("embeddings")
if 'tool' in metadata_text or 'function_call' in metadata_text:
capabilities.append("tool_use")
if 'moderat' in metadata_text:
capabilities.append("moderation")
if "image" in input_modalities:
capabilities.append("vision")
......@@ -397,6 +471,7 @@ def _build_provider_entries(scope: str, owner_id: Optional[int], providers: Dict
)
metadata = {
"provider_type": provider_type,
"provider_endpoint": model.get("endpoint") or provider_config.get("endpoint") if isinstance(provider_config, dict) else getattr(provider_config, "endpoint", None),
}
if model.get("context_length") is not None:
metadata["context_length"] = model.get("context_length")
......@@ -414,6 +489,12 @@ def _build_provider_entries(scope: str, owner_id: Optional[int], providers: Dict
metadata["capability_source"] = capability_result.source
if capability_result.notes:
metadata["capability_notes"] = capability_result.notes
metadata["studio_adapter"] = effective_studio_adapter(provider_type, model)
metadata["studio_adapter_profile"] = infer_studio_adapter_profile(provider_id, provider_type, {**model, **metadata})
if model.get("studio_adapter_override") is not None:
metadata["studio_adapter_override"] = model.get("studio_adapter_override")
if model.get("studio_adapter_profile_override") is not None:
metadata["studio_adapter_profile_override"] = model.get("studio_adapter_profile_override")
entries.append(
build_catalog_entry(
......
This diff is collapsed.
This diff is collapsed.
You are AiSBF, a general assistant for the AISBF Studio interface.
You help users brainstorm, plan, explain, and execute creative or technical work across chat, image, audio, video, profiles, and pipelines.
Guidelines:
- Be clear, practical, and concise.
- Prefer actionable answers over abstract theory.
- When the user is working inside Studio, adapt suggestions to the currently selected model and available Studio tools.
- If a requested action depends on unsupported or unavailable capabilities, say so plainly and suggest the closest supported workflow.
- Preserve user intent, constraints, and style preferences across the conversation.
- For creative tasks, be collaborative and generative without becoming verbose.
- For technical tasks, be precise and structured.
When responding:
- Answer directly.
- Ask only the minimum necessary clarification if the request is too ambiguous to complete well.
- If the user wants content to reuse in another Studio panel, format it so it can be copied easily.
......@@ -163,6 +163,8 @@ setup(
('share/aisbf/aisbf', [
'aisbf/__init__.py',
'aisbf/studio.py',
'aisbf/studio_adapters.py',
'aisbf/studio_services.py',
'aisbf/config.py',
'aisbf/models.py',
'aisbf/handlers.py',
......
This diff is collapsed.
This diff is collapsed.
......@@ -831,9 +831,9 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
<div class="nav">
<a href="{{ url_for(request, '/dashboard') }}" {% if request.path == '/dashboard' %}class="active"{% endif %} data-i18n="nav.overview">Overview</a>
<a href="{{ url_for(request, '/dashboard/providers') }}" {% if '/providers' in request.path %}class="active"{% endif %} data-i18n="nav.providers">Providers</a>
<a href="{{ url_for(request, '/dashboard/studio') }}" {% if '/studio' in request.path %}class="active"{% endif %} data-i18n="nav.studio">Studio</a>
<a href="{{ url_for(request, '/dashboard/rotations') }}" {% if '/rotations' in request.path %}class="active"{% endif %} data-i18n="nav.rotations">Rotations</a>
<a href="{{ url_for(request, '/dashboard/autoselect') }}" {% if '/autoselect' in request.path %}class="active"{% endif %} data-i18n="nav.autoselect">Autoselect</a>
<a href="{{ url_for(request, '/dashboard/studio') }}" {% if '/studio' in request.path %}class="active"{% endif %} data-i18n="nav.studio">Studio</a>
<a href="{{ url_for(request, '/dashboard/prompts') }}" {% if '/prompts' in request.path %}class="active"{% endif %} data-i18n="nav.prompts">Prompts</a>
<a href="{{ url_for(request, '/dashboard/analytics') }}" {% if '/analytics' in request.path %}class="active"{% endif %} data-i18n="nav.analytics">Analytics</a>
{% if request.session.user_id %}
......
......@@ -350,6 +350,14 @@ function renderAutoselectDetails(autoselectKey) {
const container = document.getElementById(`autoselect-details-${autoselectKey}`);
const autoselect = autoselectConfig[autoselectKey];
const safeAKey = autoselectKey.replace(/\\/g, '\\\\').replace(/'/g, "\\'");
const inheritedCaps = Array.isArray(autoselect.capabilities) ? autoselect.capabilities : [];
const partialCaps = Array.isArray(autoselect.partial_capabilities) ? autoselect.partial_capabilities : [];
const inheritedCapsHtml = inheritedCaps.length
? inheritedCaps.map(cap => `<span style="display:inline-block;margin:2px 6px 0 0;padding:3px 8px;border-radius:999px;background:var(--bg-accent);font-size:12px;">${escHtmlAttr(cap)}</span>`).join('')
: '<span style="color: var(--color-muted);">No capability is currently shared by every available model in this autoselection.</span>';
const partialCapsHtml = partialCaps.length
? `<div style="margin-top:8px;font-size:12px;color:var(--color-muted);">Partial across only some models: ${partialCaps.map(cap => escHtmlAttr(cap)).join(', ')}</div>`
: '';
// Default to "internal" if selection_model is not set
const selectionValue = autoselect.selection_model || 'internal';
......@@ -388,6 +396,9 @@ function renderAutoselectDetails(autoselectKey) {
<div class="form-group">
<label>${window.i18n.t('autoselect.capabilities')}</label>
<input type="text" value="${autoselect.capabilities ? autoselect.capabilities.join(', ') : ''}" onchange="updateAutoselectCapabilities('${autoselectKey}', this.value)" placeholder="${window.i18n.t('autoselect.capabilities_placeholder')}">
<small style="color: var(--color-muted); display:block; margin-top:5px;">This field is auto-inherited from the capabilities shared by all available models when you save.</small>
<div style="margin-top:8px;">${inheritedCapsHtml}</div>
${partialCapsHtml}
</div>
<div class="form-group">
......
......@@ -180,6 +180,9 @@ const BASE_PATH = {{ (request.scope.get('root_path', '') or '') | tojson }};
// Marker used by the AISBF Chrome Extension to auto-detect this page and configure itself.
window.AISBF_PROVIDERS_PAGE = { serverUrl: window.location.origin + BASE_PATH };
let providersData = {{ providers_json | replace("</script>", "<\\/script>") | safe }};
const STUDIO_CAPABILITY_CHOICES = {{ studio_capability_choices_json | safe }};
const STUDIO_ADAPTER_CHOICES = {{ studio_adapter_choices_json | safe }};
const STUDIO_ADAPTER_PROFILE_CHOICES = {{ studio_adapter_profile_choices_json | safe }};
let expandedProviders = new Set();
let currentProviderPage = 0;
const PROVIDERS_PAGE_SIZE = 10;
......@@ -1359,6 +1362,22 @@ function renderModels(providerKey) {
container.innerHTML = '';
provider.models.forEach((model, index) => {
const selectedStudioCaps = Array.isArray(model.studio_capabilities) ? model.studio_capabilities : [];
const inferredCaps = Array.isArray(model.studio_capabilities) ? model.studio_capabilities : [];
const studioCapabilityOptions = STUDIO_CAPABILITY_CHOICES.map(cap => {
const selected = selectedStudioCaps.includes(cap) ? 'selected' : '';
return `<option value="${escHtmlAttr(cap)}" ${selected}>${escHtmlAttr(cap)}</option>`;
}).join('');
const capabilitySource = model.studio_capability_source || 'heuristic';
const selectedAdapter = model.studio_adapter_override || 'auto';
const effectiveAdapter = model.studio_adapter || 'auto';
const adapterOptions = STUDIO_ADAPTER_CHOICES.map(adapter => `<option value="${escHtmlAttr(adapter)}" ${selectedAdapter === adapter ? 'selected' : ''}>${escHtmlAttr(adapter)}</option>`).join('');
const selectedAdapterProfile = model.studio_adapter_profile_override || 'auto';
const effectiveAdapterProfile = model.studio_adapter_profile || 'auto';
const adapterProfileOptions = STUDIO_ADAPTER_PROFILE_CHOICES.map(profile => `<option value="${escHtmlAttr(profile)}" ${selectedAdapterProfile === profile ? 'selected' : ''}>${escHtmlAttr(profile)}</option>`).join('');
const capabilityNotes = Array.isArray(model.studio_capability_notes) && model.studio_capability_notes.length
? `<div style="margin-top:6px;color:var(--color-muted);font-size:12px;">${model.studio_capability_notes.map(note => escHtmlAttr(note)).join(' | ')}</div>`
: '';
const modelDiv = document.createElement('div');
modelDiv.style.cssText = 'border: 1px solid var(--color-border); padding: 15px; margin-bottom: 10px; border-radius: 3px; background: var(--bg-page);';
......@@ -1426,6 +1445,35 @@ function renderModels(providerKey) {
Privacy
</label>
</div>
<div class="form-group">
<label>Studio capabilities</label>
<select multiple size="8" onchange="updateModelStudioCapabilities('${providerKey}', ${index}, this)" style="width:100%;padding:8px;border:1px solid var(--bg-accent);border-radius:3px;background:var(--bg-page);color:var(--color-text);font-size:14px;">
${studioCapabilityOptions}
</select>
<small style="color: var(--color-muted); display:block; margin-top:5px;">Used by Studio to decide which model can power chat, vision, image, audio, video, embeddings, and advanced tools.</small>
<div style="margin-top:6px;font-size:12px;color:var(--color-muted);">Source: ${escHtmlAttr(capabilitySource)}${model.studio_capability_unknown ? ' · low confidence' : ''}</div>
<div style="margin-top:6px;font-size:12px;color:var(--color-text);">Current: ${inferredCaps.length ? inferredCaps.map(cap => `<span style=&quot;display:inline-block;margin:2px 4px 0 0;padding:2px 6px;border-radius:999px;background:var(--bg-accent);&quot;>${escHtmlAttr(cap)}</span>`).join('') : '<span style="color:var(--color-muted);">none</span>'}</div>
${capabilityNotes}
</div>
<div class="form-group">
<label>Studio adapter override</label>
<select onchange="updateModelStudioAdapter('${providerKey}', ${index}, this.value)">
${adapterOptions}
</select>
<small style="color: var(--color-muted); display:block; margin-top:5px;">Optional manual override for how AISBF adapts Studio workflow payloads for this model. Leave on auto to infer the best adapter from provider type and capabilities.</small>
<div style="margin-top:6px;font-size:12px;color:var(--color-muted);">Effective adapter: ${escHtmlAttr(effectiveAdapter)}</div>
</div>
<div class="form-group">
<label>Studio adapter profile override</label>
<select onchange="updateModelStudioAdapterProfile('${providerKey}', ${index}, this.value)">
${adapterProfileOptions}
</select>
<small style="color: var(--color-muted); display:block; margin-top:5px;">Optional provider-specific profile layered above adapter family inference. Use this when a specific configured provider behaves differently from the default family mapping.</small>
<div style="margin-top:6px;font-size:12px;color:var(--color-muted);">Effective profile: ${escHtmlAttr(effectiveAdapterProfile)}</div>
</div>
`;
container.appendChild(modelDiv);
......@@ -2356,6 +2404,23 @@ function updateModel(providerKey, index, field, value) {
providersData[providerKey].models[index][field] = value;
}
function updateModelStudioCapabilities(providerKey, index, selectEl) {
const values = Array.from(selectEl.selectedOptions).map(option => option.value).filter(Boolean);
providersData[providerKey].models[index].studio_capabilities = values;
providersData[providerKey].models[index].studio_capability_source = values.length ? 'manual' : 'heuristic';
providersData[providerKey].models[index].studio_capability_unknown = values.length === 0;
}
function updateModelStudioAdapter(providerKey, index, value) {
providersData[providerKey].models[index].studio_adapter_override = value === 'auto' ? null : value;
providersData[providerKey].models[index].studio_adapter = value || 'auto';
}
function updateModelStudioAdapterProfile(providerKey, index, value) {
providersData[providerKey].models[index].studio_adapter_profile_override = value === 'auto' ? null : value;
providersData[providerKey].models[index].studio_adapter_profile = value || 'auto';
}
function updateModelCondenseMethod(providerKey, index, value) {
const trimmed = value.trim();
if (!trimmed) {
......
......@@ -348,6 +348,14 @@ function renderRotationDetails(rotationKey) {
const container = document.getElementById(`rotation-details-${rotationKey}`);
const rotation = rotationsConfig.rotations[rotationKey];
const safeRKey = rotationKey.replace(/\\/g, '\\\\').replace(/'/g, "\\'");
const inheritedCaps = Array.isArray(rotation.capabilities) ? rotation.capabilities : [];
const partialCaps = Array.isArray(rotation.partial_capabilities) ? rotation.partial_capabilities : [];
const inheritedCapsHtml = inheritedCaps.length
? inheritedCaps.map(cap => `<span style="display:inline-block;margin:2px 6px 0 0;padding:3px 8px;border-radius:999px;background:var(--bg-accent);font-size:12px;">${escHtmlAttr(cap)}</span>`).join('')
: '<span style="color: var(--color-muted);">No capability is currently shared by every model in this rotation.</span>';
const partialCapsHtml = partialCaps.length
? `<div style="margin-top:8px;font-size:12px;color:var(--color-muted);">Partial across only some models: ${partialCaps.map(cap => escHtmlAttr(cap)).join(', ')}</div>`
: '';
container.innerHTML = `
<div class="form-group">
......@@ -365,6 +373,9 @@ function renderRotationDetails(rotationKey) {
<div class="form-group">
<label>${window.i18n.t('rotations.capabilities')}</label>
<input type="text" value="${rotation.capabilities ? rotation.capabilities.join(', ') : ''}" onchange="updateRotationCapabilities('${rotationKey}', this.value)" placeholder="${window.i18n.t('rotations.capabilities_placeholder')}">
<small style="color: var(--color-muted); display:block; margin-top:5px;">This field is auto-inherited from the capabilities shared by all models in the rotation when you save.</small>
<div style="margin-top:8px;">${inheritedCapsHtml}</div>
${partialCapsHtml}
</div>
<div class="form-group">
......
This diff is collapsed.
......@@ -358,6 +358,14 @@ function renderAutoselectDetails(autoselectKey) {
const container = document.getElementById(`autoselect-details-${autoselectKey}`);
const autoselect = autoselectConfig[autoselectKey];
const safeAKey = autoselectKey.replace(/\\/g, '\\\\').replace(/'/g, "\\'");
const inheritedCaps = Array.isArray(autoselect.capabilities) ? autoselect.capabilities : [];
const partialCaps = Array.isArray(autoselect.partial_capabilities) ? autoselect.partial_capabilities : [];
const inheritedCapsHtml = inheritedCaps.length
? inheritedCaps.map(cap => `<span style="display:inline-block;margin:2px 6px 0 0;padding:3px 8px;border-radius:999px;background:var(--bg-accent);font-size:12px;">${escHtmlAttr(cap)}</span>`).join('')
: '<span style="color: var(--color-muted);">No capability is currently shared by every available model in this autoselection.</span>';
const partialCapsHtml = partialCaps.length
? `<div style="margin-top:8px;font-size:12px;color:var(--color-muted);">Partial across only some models: ${partialCaps.map(cap => escHtmlAttr(cap)).join(', ')}</div>`
: '';
// Default to "internal" if selection_model is not set
const selectionValue = autoselect.selection_model || 'internal';
......@@ -396,6 +404,9 @@ function renderAutoselectDetails(autoselectKey) {
<div class="form-group">
<label>${window.i18n.t('autoselect.capabilities')}</label>
<input type="text" value="${escHtml(autoselect.capabilities ? autoselect.capabilities.join(', ') : '')}" onchange="updateAutoselectCapabilities('${escHtml(autoselectKey)}', this.value)" placeholder="${window.i18n.t('autoselect.capabilities_placeholder')}">
<small style="color: var(--color-muted); display:block; margin-top:5px;">This field is auto-inherited from the capabilities shared by all available models when you save.</small>
<div style="margin-top:8px;">${inheritedCapsHtml}</div>
${partialCapsHtml}
</div>
<div class="form-group">
......
......@@ -277,6 +277,9 @@ async function apiCall(method, url, body) {
}
let providersData = {};
const STUDIO_CAPABILITY_CHOICES = {{ studio_capability_choices_json | safe }};
const STUDIO_ADAPTER_CHOICES = {{ studio_adapter_choices_json | safe }};
const STUDIO_ADAPTER_PROFILE_CHOICES = {{ studio_adapter_profile_choices_json | safe }};
let expandedProviders = new Set();
let currentProviderPage = 0;
const PROVIDERS_PAGE_SIZE = 10;
......@@ -1367,6 +1370,22 @@ function renderModels(providerKey) {
container.innerHTML = '';
provider.models.forEach((model, index) => {
const selectedStudioCaps = Array.isArray(model.studio_capabilities) ? model.studio_capabilities : [];
const inferredCaps = Array.isArray(model.studio_capabilities) ? model.studio_capabilities : [];
const studioCapabilityOptions = STUDIO_CAPABILITY_CHOICES.map(cap => {
const selected = selectedStudioCaps.includes(cap) ? 'selected' : '';
return `<option value="${escHtmlAttr(cap)}" ${selected}>${escHtmlAttr(cap)}</option>`;
}).join('');
const capabilitySource = model.studio_capability_source || 'heuristic';
const selectedAdapter = model.studio_adapter_override || 'auto';
const effectiveAdapter = model.studio_adapter || 'auto';
const adapterOptions = STUDIO_ADAPTER_CHOICES.map(adapter => `<option value="${escHtmlAttr(adapter)}" ${selectedAdapter === adapter ? 'selected' : ''}>${escHtmlAttr(adapter)}</option>`).join('');
const selectedAdapterProfile = model.studio_adapter_profile_override || 'auto';
const effectiveAdapterProfile = model.studio_adapter_profile || 'auto';
const adapterProfileOptions = STUDIO_ADAPTER_PROFILE_CHOICES.map(profile => `<option value="${escHtmlAttr(profile)}" ${selectedAdapterProfile === profile ? 'selected' : ''}>${escHtmlAttr(profile)}</option>`).join('');
const capabilityNotes = Array.isArray(model.studio_capability_notes) && model.studio_capability_notes.length
? `<div style="margin-top:6px;color:var(--color-muted);font-size:12px;">${model.studio_capability_notes.map(note => escHtmlAttr(note)).join(' | ')}</div>`
: '';
const modelDiv = document.createElement('div');
modelDiv.style.cssText = 'border: 1px solid var(--color-border); padding: 15px; margin-bottom: 10px; border-radius: 3px; background: var(--bg-page);';
......@@ -1445,6 +1464,35 @@ function renderModels(providerKey) {
Overrides both provider-level cache setting and global cache setting.
</small>
</div>
<div class="form-group">
<label>Studio capabilities</label>
<select multiple size="8" onchange="updateModelStudioCapabilities('${providerKey}', ${index}, this)" style="width:100%;padding:8px;border:1px solid var(--bg-accent);border-radius:3px;background:var(--bg-page);color:var(--color-text);font-size:14px;">
${studioCapabilityOptions}
</select>
<small style="color: var(--color-muted); display:block; margin-top:5px;">Used by Studio to decide which model can power chat, vision, image, audio, video, embeddings, and advanced tools.</small>
<div style="margin-top:6px;font-size:12px;color:var(--color-muted);">Source: ${escHtmlAttr(capabilitySource)}${model.studio_capability_unknown ? ' · low confidence' : ''}</div>
<div style="margin-top:6px;font-size:12px;color:var(--color-text);">Current: ${inferredCaps.length ? inferredCaps.map(cap => `<span style=&quot;display:inline-block;margin:2px 4px 0 0;padding:2px 6px;border-radius:999px;background:var(--bg-accent);&quot;>${escHtmlAttr(cap)}</span>`).join('') : '<span style="color:var(--color-muted);">none</span>'}</div>
${capabilityNotes}
</div>
<div class="form-group">
<label>Studio adapter override</label>
<select onchange="updateModelStudioAdapter('${providerKey}', ${index}, this.value)">
${adapterOptions}
</select>
<small style="color: var(--color-muted); display:block; margin-top:5px;">Optional manual override for how AISBF adapts Studio workflow payloads for this model. Leave on auto to infer the best adapter from provider type and capabilities.</small>
<div style="margin-top:6px;font-size:12px;color:var(--color-muted);">Effective adapter: ${escHtmlAttr(effectiveAdapter)}</div>
</div>
<div class="form-group">
<label>Studio adapter profile override</label>
<select onchange="updateModelStudioAdapterProfile('${providerKey}', ${index}, this.value)">
${adapterProfileOptions}
</select>
<small style="color: var(--color-muted); display:block; margin-top:5px;">Optional provider-specific profile layered above adapter family inference. Use this when a specific configured provider behaves differently from the default family mapping.</small>
<div style="margin-top:6px;font-size:12px;color:var(--color-muted);">Effective profile: ${escHtmlAttr(effectiveAdapterProfile)}</div>
</div>
`;
container.appendChild(modelDiv);
......@@ -2365,6 +2413,23 @@ function updateModel(providerKey, index, field, value) {
providersData[providerKey].models[index][field] = value;
}
function updateModelStudioCapabilities(providerKey, index, selectEl) {
const values = Array.from(selectEl.selectedOptions).map(option => option.value).filter(Boolean);
providersData[providerKey].models[index].studio_capabilities = values;
providersData[providerKey].models[index].studio_capability_source = values.length ? 'manual' : 'heuristic';
providersData[providerKey].models[index].studio_capability_unknown = values.length === 0;
}
function updateModelStudioAdapter(providerKey, index, value) {
providersData[providerKey].models[index].studio_adapter_override = value === 'auto' ? null : value;
providersData[providerKey].models[index].studio_adapter = value || 'auto';
}
function updateModelStudioAdapterProfile(providerKey, index, value) {
providersData[providerKey].models[index].studio_adapter_profile_override = value === 'auto' ? null : value;
providersData[providerKey].models[index].studio_adapter_profile = value || 'auto';
}
function updateModelCondenseMethod(providerKey, index, value) {
const trimmed = value.trim();
if (!trimmed) {
......
......@@ -338,6 +338,14 @@ function renderRotationDetails(rotationKey) {
const container = document.getElementById(`rotation-details-${rotationKey}`);
const rotation = rotationsConfig.rotations[rotationKey];
const safeRKey = rotationKey.replace(/\\/g, '\\\\').replace(/'/g, "\\'");
const inheritedCaps = Array.isArray(rotation.capabilities) ? rotation.capabilities : [];
const partialCaps = Array.isArray(rotation.partial_capabilities) ? rotation.partial_capabilities : [];
const inheritedCapsHtml = inheritedCaps.length
? inheritedCaps.map(cap => `<span style="display:inline-block;margin:2px 6px 0 0;padding:3px 8px;border-radius:999px;background:var(--bg-accent);font-size:12px;">${escHtmlAttr(cap)}</span>`).join('')
: '<span style="color: var(--color-muted);">No capability is currently shared by every model in this rotation.</span>';
const partialCapsHtml = partialCaps.length
? `<div style="margin-top:8px;font-size:12px;color:var(--color-muted);">Partial across only some models: ${partialCaps.map(cap => escHtmlAttr(cap)).join(', ')}</div>`
: '';
container.innerHTML = `
<div class="form-group">
......@@ -359,6 +367,9 @@ function renderRotationDetails(rotationKey) {
<div class="form-group">
<label>${window.i18n.t('rotations.capabilities')}</label>
<input type="text" value="${rotation.capabilities ? rotation.capabilities.join(', ') : ''}" onchange="updateRotationCapabilities('${rotationKey}', this.value)" placeholder="${window.i18n.t('rotations.capabilities_placeholder')}">
<small style="color: var(--color-muted); display:block; margin-top:5px;">This field is auto-inherited from the capabilities shared by all models in the rotation when you save.</small>
<div style="margin-top:8px;">${inheritedCapsHtml}</div>
${partialCapsHtml}
</div>
<div class="form-group">
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment