Compare commits
6 Commits
7cb21a372b
...
201f2e3df5
| Author | SHA1 | Date | |
|---|---|---|---|
| 201f2e3df5 | |||
| b017a0ec04 | |||
| 6bf9a30c33 | |||
| 8e5260561a | |||
| b4737c1ae1 | |||
| ae4e40f2d7 |
@@ -102,6 +102,7 @@ from routes.logging_config import router as logging_config_router
|
|||||||
from routes.voice import router as voice_router
|
from routes.voice import router as voice_router
|
||||||
from routes.memory import router as memory_router
|
from routes.memory import router as memory_router
|
||||||
from routes.activities import router as activities_router
|
from routes.activities import router as activities_router
|
||||||
|
from routes.models_selector import router as models_selector_router
|
||||||
|
|
||||||
app.include_router(core_router)
|
app.include_router(core_router)
|
||||||
app.include_router(mood_router)
|
app.include_router(mood_router)
|
||||||
@@ -123,6 +124,7 @@ app.include_router(logging_config_router)
|
|||||||
app.include_router(voice_router)
|
app.include_router(voice_router)
|
||||||
app.include_router(memory_router)
|
app.include_router(memory_router)
|
||||||
app.include_router(activities_router)
|
app.include_router(activities_router)
|
||||||
|
app.include_router(models_selector_router)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -112,11 +112,14 @@ class ConfigManager:
|
|||||||
|
|
||||||
# Map: config_runtime.yaml key path -> (globals attribute, converter)
|
# Map: config_runtime.yaml key path -> (globals attribute, converter)
|
||||||
_SETTINGS_MAP = {
|
_SETTINGS_MAP = {
|
||||||
"discord.language_mode": ("LANGUAGE_MODE", str),
|
"discord.language_mode": ("LANGUAGE_MODE", str),
|
||||||
"autonomous.debug_mode": ("AUTONOMOUS_DEBUG", bool),
|
"autonomous.debug_mode": ("AUTONOMOUS_DEBUG", bool),
|
||||||
"voice.debug_mode": ("VOICE_DEBUG_MODE", bool),
|
"voice.debug_mode": ("VOICE_DEBUG_MODE", bool),
|
||||||
"memory.use_cheshire_cat": ("USE_CHESHIRE_CAT", bool),
|
"memory.use_cheshire_cat": ("USE_CHESHIRE_CAT", bool),
|
||||||
"gpu.prefer_amd": ("PREFER_AMD_GPU", bool),
|
"gpu.prefer_amd": ("PREFER_AMD_GPU", bool),
|
||||||
|
"models.text": ("TEXT_MODEL", str),
|
||||||
|
"models.evil": ("EVIL_TEXT_MODEL", str),
|
||||||
|
"models.japanese": ("JAPANESE_TEXT_MODEL", str),
|
||||||
}
|
}
|
||||||
|
|
||||||
restored = []
|
restored = []
|
||||||
@@ -253,6 +256,9 @@ class ConfigManager:
|
|||||||
"voice.debug_mode": ("VOICE_DEBUG_MODE", CONFIG.voice.debug_mode),
|
"voice.debug_mode": ("VOICE_DEBUG_MODE", CONFIG.voice.debug_mode),
|
||||||
"memory.use_cheshire_cat": ("USE_CHESHIRE_CAT", CONFIG.cheshire_cat.enabled),
|
"memory.use_cheshire_cat": ("USE_CHESHIRE_CAT", CONFIG.cheshire_cat.enabled),
|
||||||
"gpu.prefer_amd": ("PREFER_AMD_GPU", CONFIG.gpu.prefer_amd),
|
"gpu.prefer_amd": ("PREFER_AMD_GPU", CONFIG.gpu.prefer_amd),
|
||||||
|
"models.text": ("TEXT_MODEL", CONFIG.models.text),
|
||||||
|
"models.evil": ("EVIL_TEXT_MODEL", CONFIG.models.evil),
|
||||||
|
"models.japanese": ("JAPANESE_TEXT_MODEL", CONFIG.models.japanese),
|
||||||
}
|
}
|
||||||
|
|
||||||
reset_items = []
|
reset_items = []
|
||||||
@@ -308,6 +314,9 @@ class ConfigManager:
|
|||||||
"bipolar_mode": getattr(g, "BIPOLAR_MODE", False),
|
"bipolar_mode": getattr(g, "BIPOLAR_MODE", False),
|
||||||
"language_mode": getattr(g, "LANGUAGE_MODE", "english"),
|
"language_mode": getattr(g, "LANGUAGE_MODE", "english"),
|
||||||
"current_gpu": self._current_gpu,
|
"current_gpu": self._current_gpu,
|
||||||
|
"text_model": getattr(g, "TEXT_MODEL", "llama3.1"),
|
||||||
|
"evil_text_model": getattr(g, "EVIL_TEXT_MODEL", "darkidol"),
|
||||||
|
"japanese_text_model": getattr(g, "JAPANESE_TEXT_MODEL", "swallow"),
|
||||||
}
|
}
|
||||||
|
|
||||||
def get_state(self, key: str, default: Any = None) -> Any:
|
def get_state(self, key: str, default: Any = None) -> Any:
|
||||||
|
|||||||
@@ -93,6 +93,9 @@ async def set_config_value(request: Request):
|
|||||||
"voice.debug_mode": ("VOICE_DEBUG_MODE", bool),
|
"voice.debug_mode": ("VOICE_DEBUG_MODE", bool),
|
||||||
"memory.use_cheshire_cat": ("USE_CHESHIRE_CAT", bool),
|
"memory.use_cheshire_cat": ("USE_CHESHIRE_CAT", bool),
|
||||||
"gpu.prefer_amd": ("PREFER_AMD_GPU", bool),
|
"gpu.prefer_amd": ("PREFER_AMD_GPU", bool),
|
||||||
|
"models.text": ("TEXT_MODEL", str),
|
||||||
|
"models.evil": ("EVIL_TEXT_MODEL", str),
|
||||||
|
"models.japanese": ("JAPANESE_TEXT_MODEL", str),
|
||||||
}
|
}
|
||||||
|
|
||||||
if key_path in _GLOBALS_SYNC:
|
if key_path in _GLOBALS_SYNC:
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ def toggle_language_mode():
|
|||||||
globals.LANGUAGE_MODE = "japanese"
|
globals.LANGUAGE_MODE = "japanese"
|
||||||
new_mode = "japanese"
|
new_mode = "japanese"
|
||||||
model_used = globals.JAPANESE_TEXT_MODEL
|
model_used = globals.JAPANESE_TEXT_MODEL
|
||||||
logger.info("Switched to Japanese mode (using Llama 3.1 Swallow)")
|
logger.info(f"Switched to Japanese mode (using {model_used})")
|
||||||
else:
|
else:
|
||||||
globals.LANGUAGE_MODE = "english"
|
globals.LANGUAGE_MODE = "english"
|
||||||
new_mode = "english"
|
new_mode = "english"
|
||||||
|
|||||||
161
bot/routes/models_selector.py
Normal file
161
bot/routes/models_selector.py
Normal file
@@ -0,0 +1,161 @@
|
|||||||
|
"""Model selection routes: query available models and set per-persona models."""
|
||||||
|
|
||||||
|
import aiohttp
|
||||||
|
import asyncio
|
||||||
|
import globals
|
||||||
|
from fastapi import APIRouter
|
||||||
|
from fastapi.responses import JSONResponse
|
||||||
|
from utils.logger import get_logger
|
||||||
|
|
||||||
|
logger = get_logger('api')
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
# Known model names from llama-swap configs (fallback if API query fails)
|
||||||
|
KNOWN_MODELS = [
|
||||||
|
"llama3.1",
|
||||||
|
"darkidol",
|
||||||
|
"swallow",
|
||||||
|
"vision",
|
||||||
|
"rocinante",
|
||||||
|
"qwen3.5",
|
||||||
|
]
|
||||||
|
|
||||||
|
# Which GPU each model is available on
|
||||||
|
MODEL_GPU_MAP = {
|
||||||
|
"llama3.1": {"nvidia", "amd"},
|
||||||
|
"darkidol": {"nvidia", "amd"},
|
||||||
|
"swallow": {"nvidia", "amd"},
|
||||||
|
"vision": {"nvidia"},
|
||||||
|
"rocinante": {"amd"},
|
||||||
|
"qwen3.5": {"amd"},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def _query_llama_swap_models(url: str, timeout: int = 10) -> list:
|
||||||
|
"""Query a llama-swap instance for its available models via /v1/models."""
|
||||||
|
try:
|
||||||
|
async with aiohttp.ClientSession() as session:
|
||||||
|
async with session.get(
|
||||||
|
f"{url}/v1/models",
|
||||||
|
timeout=aiohttp.ClientTimeout(total=timeout),
|
||||||
|
) as resp:
|
||||||
|
if resp.status == 200:
|
||||||
|
data = await resp.json()
|
||||||
|
# OpenAI-compatible format: { data: [{ id: "model_name", ... }] }
|
||||||
|
return [m["id"] for m in data.get("data", []) if "id" in m]
|
||||||
|
else:
|
||||||
|
logger.warning(f"llama-swap models query failed ({resp.status}) for {url}")
|
||||||
|
return []
|
||||||
|
except (asyncio.TimeoutError, aiohttp.ClientError) as e:
|
||||||
|
logger.warning(f"llama-swap unreachable at {url}: {e}")
|
||||||
|
return []
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Unexpected error querying {url}: {e}")
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/models/available")
|
||||||
|
async def get_available_models():
|
||||||
|
"""
|
||||||
|
Query both NVIDIA and AMD llama-swap instances for available models.
|
||||||
|
Returns model lists per GPU, their intersection, and all unique models.
|
||||||
|
Falls back to known model list if containers are unreachable.
|
||||||
|
"""
|
||||||
|
nvidia_models = await _query_llama_swap_models(globals.LLAMA_URL)
|
||||||
|
amd_models = await _query_llama_swap_models(globals.LLAMA_AMD_URL)
|
||||||
|
|
||||||
|
# If both failed, use the known model list from configs
|
||||||
|
if not nvidia_models and not amd_models:
|
||||||
|
logger.info("Both llama-swap instances unreachable, using known model list")
|
||||||
|
nvidia_set = {m for m, gpus in MODEL_GPU_MAP.items() if "nvidia" in gpus}
|
||||||
|
amd_set = {m for m, gpus in MODEL_GPU_MAP.items() if "amd" in gpus}
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"nvidia": sorted(nvidia_set),
|
||||||
|
"amd": sorted(amd_set),
|
||||||
|
"intersection": sorted(nvidia_set & amd_set),
|
||||||
|
"all": sorted(nvidia_set | amd_set),
|
||||||
|
"gpu_map": MODEL_GPU_MAP,
|
||||||
|
"source": "fallback",
|
||||||
|
}
|
||||||
|
|
||||||
|
nvidia_set = set(nvidia_models)
|
||||||
|
amd_set = set(amd_models)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"nvidia": sorted(nvidia_set),
|
||||||
|
"amd": sorted(amd_set),
|
||||||
|
"intersection": sorted(nvidia_set & amd_set),
|
||||||
|
"all": sorted(nvidia_set | amd_set),
|
||||||
|
"gpu_map": MODEL_GPU_MAP,
|
||||||
|
"source": "live",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/models/select")
|
||||||
|
async def select_model(body: dict):
|
||||||
|
"""
|
||||||
|
Set the model for a specific persona.
|
||||||
|
|
||||||
|
Body: {
|
||||||
|
"persona": "regular" | "evil" | "japanese",
|
||||||
|
"model": "model_name"
|
||||||
|
}
|
||||||
|
|
||||||
|
Persists the selection so it survives bot restarts.
|
||||||
|
"""
|
||||||
|
persona = body.get("persona", "").strip().lower()
|
||||||
|
model = body.get("model", "").strip()
|
||||||
|
|
||||||
|
valid_personas = {"regular", "evil", "japanese"}
|
||||||
|
if persona not in valid_personas:
|
||||||
|
return JSONResponse(
|
||||||
|
status_code=400,
|
||||||
|
content={"success": False, "error": f"Invalid persona '{persona}'. Must be one of: {', '.join(valid_personas)}"}
|
||||||
|
)
|
||||||
|
|
||||||
|
if not model:
|
||||||
|
return JSONResponse(
|
||||||
|
status_code=400,
|
||||||
|
content={"success": False, "error": "model is required"}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Map persona to globals attribute and config key
|
||||||
|
PERSONA_MAP = {
|
||||||
|
"regular": ("TEXT_MODEL", "models.text"),
|
||||||
|
"evil": ("EVIL_TEXT_MODEL", "models.evil"),
|
||||||
|
"japanese": ("JAPANESE_TEXT_MODEL", "models.japanese"),
|
||||||
|
}
|
||||||
|
|
||||||
|
attr_name, config_key = PERSONA_MAP[persona]
|
||||||
|
|
||||||
|
# Set the global
|
||||||
|
setattr(globals, attr_name, model)
|
||||||
|
logger.info(f"Model selection: {persona} → {model} (globals.{attr_name})")
|
||||||
|
|
||||||
|
# Persist via config manager
|
||||||
|
try:
|
||||||
|
from config_manager import config_manager
|
||||||
|
config_manager.set(config_key, model, persist=True)
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Failed to persist model selection: {e}")
|
||||||
|
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"persona": persona,
|
||||||
|
"model": model,
|
||||||
|
"message": f"{persona.capitalize()} model set to '{model}'",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/models/status")
|
||||||
|
async def get_model_status():
|
||||||
|
"""Return the current per-persona model assignments."""
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"regular": getattr(globals, "TEXT_MODEL", "llama3.1"),
|
||||||
|
"evil": getattr(globals, "EVIL_TEXT_MODEL", "darkidol"),
|
||||||
|
"japanese": getattr(globals, "JAPANESE_TEXT_MODEL", "swallow"),
|
||||||
|
}
|
||||||
@@ -663,15 +663,55 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Language Mode Status Section -->
|
<!-- Model Selection Section -->
|
||||||
<div style="margin-bottom: 1.5rem; padding: 1rem; background: #2a2a2a; border-radius: 4px;">
|
<div style="margin-bottom: 1.5rem; padding: 1rem; background: #2a2a2a; border-radius: 4px; border: 2px solid #7c4dff;">
|
||||||
<h4 style="margin-top: 0;">📊 Current Status</h4>
|
<h4 style="margin-top: 0; color: #b388ff;">🎛️ Model Selection</h4>
|
||||||
<div id="language-status-display" style="background: #1a1a1a; padding: 1rem; border-radius: 4px; font-family: monospace; font-size: 0.9rem;">
|
<p style="margin: 0.5rem 0; color: #aaa;">Choose which model each persona uses. Changes take effect immediately and persist across bot restarts.</p>
|
||||||
<p style="margin: 0.5rem 0;"><strong>Language Mode:</strong> <span id="status-language">English</span></p>
|
|
||||||
<p style="margin: 0.5rem 0;"><strong>Active Model:</strong> <span id="status-model">llama3.1</span></p>
|
<div style="margin: 1rem 0;">
|
||||||
<p style="margin: 0.5rem 0;"><strong>Available Languages:</strong> English, 日本語 (Japanese)</p>
|
<div id="model-selection-loading" style="color: #aaa;">Loading available models...</div>
|
||||||
|
|
||||||
|
<div id="model-selection-controls" style="display: none;">
|
||||||
|
<!-- Regular Miku -->
|
||||||
|
<div style="margin-bottom: 1rem; padding: 0.8rem; background: #1a1a1a; border-radius: 4px; border-left: 3px solid #69f0ae;">
|
||||||
|
<label for="model-regular" style="font-weight: bold; color: #69f0ae;">🎤 Regular Miku</label>
|
||||||
|
<div style="margin-top: 0.5rem; display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap;">
|
||||||
|
<select id="model-regular" style="flex: 1; min-width: 200px; padding: 0.5rem; background: #333; color: #fff; border: 1px solid #555; border-radius: 4px;"></select>
|
||||||
|
<span id="model-regular-badge" style="font-size: 0.75rem; padding: 0.2rem 0.5rem; border-radius: 4px;"></span>
|
||||||
|
<button onclick="selectModel('regular')" style="background: #69f0ae; color: #000; padding: 0.5rem 1rem; border: none; border-radius: 4px; cursor: pointer; font-weight: bold;">Apply</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Evil Miku -->
|
||||||
|
<div style="margin-bottom: 1rem; padding: 0.8rem; background: #1a1a1a; border-radius: 4px; border-left: 3px solid #ff5252;">
|
||||||
|
<label for="model-evil" style="font-weight: bold; color: #ff5252;">😈 Evil Miku</label>
|
||||||
|
<div style="margin-top: 0.5rem; display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap;">
|
||||||
|
<select id="model-evil" style="flex: 1; min-width: 200px; padding: 0.5rem; background: #333; color: #fff; border: 1px solid #555; border-radius: 4px;"></select>
|
||||||
|
<span id="model-evil-badge" style="font-size: 0.75rem; padding: 0.2rem 0.5rem; border-radius: 4px;"></span>
|
||||||
|
<button onclick="selectModel('evil')" style="background: #ff5252; color: #fff; padding: 0.5rem 1rem; border: none; border-radius: 4px; cursor: pointer; font-weight: bold;">Apply</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Japanese Mode -->
|
||||||
|
<div style="margin-bottom: 1rem; padding: 0.8rem; background: #1a1a1a; border-radius: 4px; border-left: 3px solid #40c4ff;">
|
||||||
|
<label for="model-japanese" style="font-weight: bold; color: #40c4ff;">🗾 Japanese Mode</label>
|
||||||
|
<div style="margin-top: 0.5rem; display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap;">
|
||||||
|
<select id="model-japanese" style="flex: 1; min-width: 200px; padding: 0.5rem; background: #333; color: #fff; border: 1px solid #555; border-radius: 4px;"></select>
|
||||||
|
<span id="model-japanese-badge" style="font-size: 0.75rem; padding: 0.2rem 0.5rem; border-radius: 4px;"></span>
|
||||||
|
<button onclick="selectModel('japanese')" style="background: #40c4ff; color: #000; padding: 0.5rem 1rem; border: none; border-radius: 4px; cursor: pointer; font-weight: bold;">Apply</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="margin-top: 0.5rem; display: flex; gap: 0.5rem; flex-wrap: wrap;">
|
||||||
|
<button onclick="loadAvailableModels()" style="background: #7c4dff; color: #fff; padding: 0.5rem 1rem; border: none; border-radius: 4px; cursor: pointer; font-weight: bold;">🔄 Refresh Models</button>
|
||||||
|
<button onclick="refreshModelStatus()" style="background: #333; color: #fff; padding: 0.5rem 1rem; border: 1px solid #555; border-radius: 4px; cursor: pointer;">📊 Refresh Status</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="model-selection-info" style="margin-top: 0.5rem; padding: 0.5rem; background: #1a1a1a; border-radius: 4px; font-size: 0.8rem; color: #888; display: none;">
|
||||||
|
<span id="model-selection-info-text"></span>
|
||||||
</div>
|
</div>
|
||||||
<button onclick="refreshLanguageStatus()" style="margin-top: 1rem;">🔄 Refresh Status</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Information Section -->
|
<!-- Information Section -->
|
||||||
@@ -1375,15 +1415,15 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/static/js/core.js?v=20260502"></script>
|
<script src="/static/js/core.js?v=20260520"></script>
|
||||||
<script src="/static/js/servers.js?v=20260502"></script>
|
<script src="/static/js/servers.js?v=20260520"></script>
|
||||||
<script src="/static/js/modes.js?v=20260502"></script>
|
<script src="/static/js/modes.js?v=20260520"></script>
|
||||||
<script src="/static/js/actions.js?v=20260502"></script>
|
<script src="/static/js/actions.js?v=20260520"></script>
|
||||||
<script src="/static/js/image-gen.js?v=20260502"></script>
|
<script src="/static/js/image-gen.js?v=20260520"></script>
|
||||||
<script src="/static/js/status.js?v=20260502"></script>
|
<script src="/static/js/status.js?v=20260520"></script>
|
||||||
<script src="/static/js/dm.js?v=20260502"></script>
|
<script src="/static/js/dm.js?v=20260520"></script>
|
||||||
<script src="/static/js/chat.js?v=20260502"></script>
|
<script src="/static/js/chat.js?v=20260520"></script>
|
||||||
<script src="/static/js/memories.js?v=20260502"></script>
|
<script src="/static/js/memories.js?v=20260520"></script>
|
||||||
<script src="/static/js/profile.js?v=20260502"></script>
|
<script src="/static/js/profile.js?v=20260520"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -168,6 +168,13 @@ function switchTab(tabId) {
|
|||||||
showTabLoading('tab6');
|
showTabLoading('tab6');
|
||||||
loadAutonomousStats().finally(() => hideTabLoading('tab6'));
|
loadAutonomousStats().finally(() => hideTabLoading('tab6'));
|
||||||
}
|
}
|
||||||
|
if (tabId === 'tab4') {
|
||||||
|
if (typeof loadAvailableModels === 'function') {
|
||||||
|
loadAvailableModels();
|
||||||
|
} else {
|
||||||
|
console.warn('loadAvailableModels not available yet (servers.js may not be loaded)');
|
||||||
|
}
|
||||||
|
}
|
||||||
if (tabId === 'tab9') {
|
if (tabId === 'tab9') {
|
||||||
console.log('🧠 Refreshing memory stats for Memories tab');
|
console.log('🧠 Refreshing memory stats for Memories tab');
|
||||||
showTabLoading('tab9');
|
showTabLoading('tab9');
|
||||||
|
|||||||
@@ -745,3 +745,213 @@ async function toggleLanguageMode() {
|
|||||||
showNotification('Failed to toggle language mode', 'error');
|
showNotification('Failed to toggle language mode', 'error');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ===== Model Selection Functions =====
|
||||||
|
|
||||||
|
let availableModelsData = null;
|
||||||
|
|
||||||
|
async function loadAvailableModels() {
|
||||||
|
console.log('📋 loadAvailableModels() called');
|
||||||
|
try {
|
||||||
|
const loadingEl = document.getElementById('model-selection-loading');
|
||||||
|
const controlsEl = document.getElementById('model-selection-controls');
|
||||||
|
const infoEl = document.getElementById('model-selection-info');
|
||||||
|
const infoTextEl = document.getElementById('model-selection-info-text');
|
||||||
|
|
||||||
|
if (loadingEl) loadingEl.style.display = 'block';
|
||||||
|
if (controlsEl) controlsEl.style.display = 'none';
|
||||||
|
if (infoEl) infoEl.style.display = 'none';
|
||||||
|
|
||||||
|
console.log('📋 Fetching /models/available...');
|
||||||
|
const result = await apiCall('/models/available');
|
||||||
|
console.log('📋 /models/available response:', result);
|
||||||
|
availableModelsData = result;
|
||||||
|
|
||||||
|
if (loadingEl) loadingEl.style.display = 'none';
|
||||||
|
if (controlsEl) controlsEl.style.display = 'block';
|
||||||
|
|
||||||
|
if (!result.success) {
|
||||||
|
showNotification('Failed to load models: ' + (result.error || 'Unknown error'), 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Populate dropdowns
|
||||||
|
const allModels = result.all || [];
|
||||||
|
const gpuMap = result.gpu_map || {};
|
||||||
|
console.log('📋 Populating dropdowns with models:', allModels);
|
||||||
|
|
||||||
|
const personas = [
|
||||||
|
{ id: 'regular', selectId: 'model-regular', badgeId: 'model-regular-badge' },
|
||||||
|
{ id: 'evil', selectId: 'model-evil', badgeId: 'model-evil-badge' },
|
||||||
|
{ id: 'japanese', selectId: 'model-japanese', badgeId: 'model-japanese-badge' },
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const p of personas) {
|
||||||
|
const select = document.getElementById(p.selectId);
|
||||||
|
if (!select) continue;
|
||||||
|
|
||||||
|
// Save current selection
|
||||||
|
const currentVal = select.value;
|
||||||
|
|
||||||
|
select.innerHTML = '';
|
||||||
|
for (const model of allModels) {
|
||||||
|
const option = document.createElement('option');
|
||||||
|
option.value = model;
|
||||||
|
option.textContent = model;
|
||||||
|
select.appendChild(option);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restore selection if still valid
|
||||||
|
if (currentVal && allModels.includes(currentVal)) {
|
||||||
|
select.value = currentVal;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update badges for current selections
|
||||||
|
updateModelBadges(allModels, gpuMap);
|
||||||
|
|
||||||
|
// Show info about source
|
||||||
|
if (infoEl && infoTextEl) {
|
||||||
|
if (result.source === 'fallback') {
|
||||||
|
infoTextEl.textContent = '⚠️ Could not reach llama-swap containers. Showing known models from config.';
|
||||||
|
infoEl.style.display = 'block';
|
||||||
|
} else {
|
||||||
|
infoEl.style.display = 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load current status to sync dropdowns
|
||||||
|
await refreshModelStatus();
|
||||||
|
|
||||||
|
console.log('Available models loaded:', result);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load available models:', error);
|
||||||
|
const loadingEl = document.getElementById('model-selection-loading');
|
||||||
|
if (loadingEl) loadingEl.textContent = '❌ Failed to load models. Click "Refresh Models" to retry.';
|
||||||
|
showNotification('Failed to load available models', 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateModelBadges(allModels, gpuMap) {
|
||||||
|
const personas = [
|
||||||
|
{ selectId: 'model-regular', badgeId: 'model-regular-badge' },
|
||||||
|
{ selectId: 'model-evil', badgeId: 'model-evil-badge' },
|
||||||
|
{ selectId: 'model-japanese', badgeId: 'model-japanese-badge' },
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const p of personas) {
|
||||||
|
const select = document.getElementById(p.selectId);
|
||||||
|
const badge = document.getElementById(p.badgeId);
|
||||||
|
if (!select || !badge) continue;
|
||||||
|
|
||||||
|
const model = select.value;
|
||||||
|
const gpus = gpuMap[model];
|
||||||
|
|
||||||
|
if (!gpus) {
|
||||||
|
badge.textContent = '';
|
||||||
|
badge.style.display = 'none';
|
||||||
|
} else if (gpus.length === 2 || (gpus.includes('nvidia') && gpus.includes('amd'))) {
|
||||||
|
badge.textContent = '✅ Both GPUs';
|
||||||
|
badge.style.background = '#1b5e20';
|
||||||
|
badge.style.color = '#a5d6a7';
|
||||||
|
badge.style.display = 'inline';
|
||||||
|
} else if (gpus.includes('nvidia')) {
|
||||||
|
badge.textContent = '⚠️ NVIDIA Only';
|
||||||
|
badge.style.background = '#e65100';
|
||||||
|
badge.style.color = '#ffcc80';
|
||||||
|
badge.style.display = 'inline';
|
||||||
|
} else if (gpus.includes('amd')) {
|
||||||
|
badge.textContent = '⚠️ AMD Only';
|
||||||
|
badge.style.background = '#e65100';
|
||||||
|
badge.style.color = '#ffcc80';
|
||||||
|
badge.style.display = 'inline';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function selectModel(persona) {
|
||||||
|
console.log(`📋 selectModel('${persona}') called`);
|
||||||
|
try {
|
||||||
|
const selectId = 'model-' + persona;
|
||||||
|
const select = document.getElementById(selectId);
|
||||||
|
if (!select) {
|
||||||
|
console.warn(`📋 select element #${selectId} not found`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const model = select.value;
|
||||||
|
if (!model) {
|
||||||
|
showNotification('Please select a model first', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`📋 Setting ${persona} model to '${model}'...`);
|
||||||
|
const result = await apiCall('/models/select', 'POST', {
|
||||||
|
persona: persona,
|
||||||
|
model: model,
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`📋 /models/select response:`, result);
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
showNotification(`${persona.charAt(0).toUpperCase() + persona.slice(1)} model set to '${model}'`, 'success');
|
||||||
|
// Update badges
|
||||||
|
if (availableModelsData) {
|
||||||
|
updateModelBadges(availableModelsData.all || [], availableModelsData.gpu_map || {});
|
||||||
|
}
|
||||||
|
// Refresh language status display (active model may have changed)
|
||||||
|
refreshLanguageStatus();
|
||||||
|
} else {
|
||||||
|
showNotification('Failed to set model: ' + (result.error || 'Unknown error'), 'error');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to select model:', error);
|
||||||
|
showNotification('Failed to set model: ' + error.message, 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function refreshModelStatus() {
|
||||||
|
console.log('📋 refreshModelStatus() called');
|
||||||
|
try {
|
||||||
|
const result = await apiCall('/models/status');
|
||||||
|
console.log('📋 /models/status response:', result);
|
||||||
|
if (!result.success) return;
|
||||||
|
|
||||||
|
// Sync dropdowns with current globals
|
||||||
|
const personas = [
|
||||||
|
{ id: 'regular', selectId: 'model-regular' },
|
||||||
|
{ id: 'evil', selectId: 'model-evil' },
|
||||||
|
{ id: 'japanese', selectId: 'model-japanese' },
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const p of personas) {
|
||||||
|
const select = document.getElementById(p.selectId);
|
||||||
|
if (!select) continue;
|
||||||
|
const currentModel = result[p.id];
|
||||||
|
// Check if this value exists in the dropdown
|
||||||
|
let found = false;
|
||||||
|
for (const option of select.options) {
|
||||||
|
if (option.value === currentModel) {
|
||||||
|
select.value = currentModel;
|
||||||
|
found = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!found && currentModel) {
|
||||||
|
// Add it if it doesn't exist (e.g., a model that wasn't in the API response)
|
||||||
|
const option = document.createElement('option');
|
||||||
|
option.value = currentModel;
|
||||||
|
option.textContent = currentModel;
|
||||||
|
select.appendChild(option);
|
||||||
|
select.value = currentModel;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update badges
|
||||||
|
if (availableModelsData) {
|
||||||
|
updateModelBadges(availableModelsData.all || [], availableModelsData.gpu_map || {});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to refresh model status:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -829,15 +829,16 @@ class CatAdapter:
|
|||||||
else:
|
else:
|
||||||
logger.debug("evil_miku_personality already active, skipping toggle")
|
logger.debug("evil_miku_personality already active, skipping toggle")
|
||||||
|
|
||||||
# Step 3: Switch LLM model to darkidol (the uncensored evil model)
|
# Step 3: Switch LLM model to the configured evil model (e.g. darkidol)
|
||||||
if not await self.set_llm_model("darkidol"):
|
evil_model = getattr(globals, "EVIL_TEXT_MODEL", "darkidol")
|
||||||
logger.error("Failed to switch Cat LLM to darkidol")
|
if not await self.set_llm_model(evil_model):
|
||||||
|
logger.error(f"Failed to switch Cat LLM to {evil_model}")
|
||||||
success = False
|
success = False
|
||||||
|
|
||||||
return success
|
return success
|
||||||
|
|
||||||
async def switch_to_normal_personality(self) -> bool:
|
async def switch_to_normal_personality(self) -> bool:
|
||||||
"""Disable evil_miku_personality, enable miku_personality, switch LLM to llama3.1.
|
"""Disable evil_miku_personality, enable miku_personality, switch LLM to the configured normal model.
|
||||||
|
|
||||||
Checks current plugin state first to avoid double-toggling.
|
Checks current plugin state first to avoid double-toggling.
|
||||||
Returns True if all operations succeed, False if any fail.
|
Returns True if all operations succeed, False if any fail.
|
||||||
@@ -865,9 +866,10 @@ class CatAdapter:
|
|||||||
else:
|
else:
|
||||||
logger.debug("miku_personality already active, skipping toggle")
|
logger.debug("miku_personality already active, skipping toggle")
|
||||||
|
|
||||||
# Step 3: Switch LLM model back to llama3.1 (normal model)
|
# Step 3: Switch LLM model to the configured normal model (e.g. llama3.1)
|
||||||
if not await self.set_llm_model("llama3.1"):
|
normal_model = getattr(globals, "TEXT_MODEL", "llama3.1")
|
||||||
logger.error("Failed to switch Cat LLM to llama3.1")
|
if not await self.set_llm_model(normal_model):
|
||||||
|
logger.error(f"Failed to switch Cat LLM to {normal_model}")
|
||||||
success = False
|
success = False
|
||||||
|
|
||||||
return success
|
return success
|
||||||
|
|||||||
Reference in New Issue
Block a user