feat: Phase 3 model toggle — cycle chat-role slot models in UI
Replaces the role-cycle toggle with a slot model toggle in the Context & Memory panel. The active model label is shown on the button; clicking cycles through Primary → Backup 1 → Backup 2 slots configured for the Chat role. - app.js: remove activeRole()/availableRoles role-cycling; add activeChatModel()/chatModels slot cycling; update send/orchestrate payloads to send slot + chat_role:"chat"; fix updateSendBtnTitle and startRunTimer to use activeChatModel() - chat.py: add slot field to ChatRequest; pass slot= to complete(); resolve backend_label from slot config; add _chat_slot_models() helper; include chat_models in GET /backend response - HELP.md: update Model toggle description, tool count (62/16), Backends section, API chat payload example Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -47,6 +47,7 @@ class ChatRequest(BaseModel):
|
|||||||
session_id: str | None = None
|
session_id: str | None = None
|
||||||
tier: int | None = None
|
tier: int | None = None
|
||||||
model: str | None = None # legacy backend override ("claude"|"gemini"|"local")
|
model: str | None = None # legacy backend override ("claude"|"gemini"|"local")
|
||||||
|
slot: str | None = None # Phase 3: explicit slot ("primary"|"backup_1"|"backup_2")
|
||||||
chat_role: str = "chat" # active role: "chat"|"coder"|"research"|"distill" etc.
|
chat_role: str = "chat" # active role: "chat"|"coder"|"research"|"distill" etc.
|
||||||
include_long: bool = True
|
include_long: bool = True
|
||||||
include_mid: bool = True
|
include_mid: bool = True
|
||||||
@@ -109,6 +110,7 @@ async def _stream_chat(req: ChatRequest):
|
|||||||
messages=history,
|
messages=history,
|
||||||
model=req.model,
|
model=req.model,
|
||||||
role=req.chat_role,
|
role=req.chat_role,
|
||||||
|
slot=req.slot,
|
||||||
))
|
))
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -124,7 +126,11 @@ async def _stream_chat(req: ChatRequest):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
response_text, actual_backend = task.result()
|
response_text, actual_backend = task.result()
|
||||||
backend_label = _role_model_label(user, req.chat_role, actual_backend)
|
if req.slot:
|
||||||
|
slot_cfg = model_registry.get_model_for_slot(user, req.chat_role, req.slot)
|
||||||
|
backend_label = (slot_cfg or {}).get("label") or _role_model_label(user, req.chat_role, actual_backend)
|
||||||
|
else:
|
||||||
|
backend_label = _role_model_label(user, req.chat_role, actual_backend)
|
||||||
host = platform.node()
|
host = platform.node()
|
||||||
history.append({
|
history.append({
|
||||||
"role": "assistant",
|
"role": "assistant",
|
||||||
@@ -203,6 +209,25 @@ def _local_model_info(request: Request) -> dict | None:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _chat_slot_models(username: str) -> list[dict]:
|
||||||
|
"""Return [{slot, label, type}] for each configured slot in the chat role, primary first."""
|
||||||
|
registry = model_registry.get_registry(username)
|
||||||
|
role_slots = registry.get("roles", {}).get("chat", {})
|
||||||
|
result = []
|
||||||
|
for slot_key in model_registry.PRIORITY_KEYS:
|
||||||
|
model_id = role_slots.get(slot_key)
|
||||||
|
if not model_id:
|
||||||
|
continue
|
||||||
|
resolved = model_registry._resolve_model(registry, model_id)
|
||||||
|
if resolved:
|
||||||
|
result.append({
|
||||||
|
"slot": slot_key,
|
||||||
|
"label": resolved.get("label") or resolved.get("model_name") or "",
|
||||||
|
"type": resolved.get("type", ""),
|
||||||
|
})
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
def _available_roles_for_toggle(username: str) -> list[dict]:
|
def _available_roles_for_toggle(username: str) -> list[dict]:
|
||||||
"""Return roles with a primary model assigned (excluding orchestrator) for the UI toggle.
|
"""Return roles with a primary model assigned (excluding orchestrator) for the UI toggle.
|
||||||
|
|
||||||
@@ -231,6 +256,7 @@ def _available_roles_for_toggle(username: str) -> list[dict]:
|
|||||||
@router.get("/backend")
|
@router.get("/backend")
|
||||||
async def get_backend(request: Request) -> dict:
|
async def get_backend(request: Request) -> dict:
|
||||||
username = _request_user(request)
|
username = _request_user(request)
|
||||||
|
chat_models = _chat_slot_models(username) if username else []
|
||||||
available_roles = _available_roles_for_toggle(username) if username else []
|
available_roles = _available_roles_for_toggle(username) if username else []
|
||||||
p = settings.primary_backend
|
p = settings.primary_backend
|
||||||
|
|
||||||
@@ -241,7 +267,8 @@ async def get_backend(request: Request) -> dict:
|
|||||||
orch_label = orch_cfg.get("label") or orch_cfg.get("model_name") or None
|
orch_label = orch_cfg.get("label") or orch_cfg.get("model_name") or None
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"available_roles": available_roles,
|
"chat_models": chat_models, # Phase 3: [{slot, label, type}] for chat-role slots
|
||||||
|
"available_roles": available_roles, # kept for banner + backward compat
|
||||||
"orchestrator_model": orch_label,
|
"orchestrator_model": orch_label,
|
||||||
# Legacy fields kept for backward compat
|
# Legacy fields kept for backward compat
|
||||||
"primary": p,
|
"primary": p,
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
and are appended automatically by help.html when present.
|
and are appended automatically by help.html when present.
|
||||||
-->
|
-->
|
||||||
|
|
||||||
*Last updated: 2026-05-09*
|
*Last updated: 2026-05-12*
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -43,7 +43,7 @@ The **Context & Memory** panel (sliders icon with tier number) contains all conf
|
|||||||
| **Context Tier** | T1 – T4 context depth |
|
| **Context Tier** | T1 – T4 context depth |
|
||||||
| **Memory Layers** | Toggle Long / Mid / Short memory on/off |
|
| **Memory Layers** | Toggle Long / Mid / Short memory on/off |
|
||||||
| **Distill Memory** | Manually trigger Short / Mid / Long / All distillation |
|
| **Distill Memory** | Manually trigger Short / Mid / Long / All distillation |
|
||||||
| **Role** | Active LLM role — click to cycle through configured role assignments |
|
| **Model** | Active chat model — click to cycle through your configured slot models (Primary → Backup 1 → …) |
|
||||||
| **Display** | **Aa** cycles font size · **☾** toggles theme · **S/M/L** cycles input area height · **⌃↵** toggles send shortcut |
|
| **Display** | **Aa** cycles font size · **☾** toggles theme · **S/M/L** cycles input area height · **⌃↵** toggles send shortcut |
|
||||||
|
|
||||||
All settings persist in `localStorage` across page refreshes.
|
All settings persist in `localStorage` across page refreshes.
|
||||||
@@ -82,12 +82,14 @@ Orchestrated sessions persist to history exactly like regular chat.
|
|||||||
|
|
||||||
### Available Tools
|
### Available Tools
|
||||||
|
|
||||||
50 tools across 12 categories. Each tool schema is sent to the model on every orchestrated call — fewer active tools means fewer tokens per call.
|
62 tools across 16 categories. Each tool schema is sent to the model on every orchestrated call — fewer active tools means fewer tokens per call.
|
||||||
|
|
||||||
| Category | Tools |
|
| Category | Tools |
|
||||||
|---|---|
|
|---|---|
|
||||||
| **Web** | `web_search`, `http_fetch`, `web_read`, `http_post` |
|
| **Web** | `web_search`, `http_fetch`, `web_read`, `http_post` |
|
||||||
| **Files** | `file_read`, `file_list`, `file_write`, `session_read`, `session_search` |
|
| **Project Files** | `project_file_read`, `project_file_list`, `file_stat`, `file_grep`, `file_diff`, `file_syntax_check` |
|
||||||
|
| **Files** (admin) | `file_read`, `file_list`, `file_write`, `session_read`, `session_search` |
|
||||||
|
| **Git** | `git_status`, `git_log`, `git_diff` |
|
||||||
| **Shell** | `shell_exec`, `claude_allow_dir` |
|
| **Shell** | `shell_exec`, `claude_allow_dir` |
|
||||||
| **System** | `cortex_restart`, `cortex_logs`, `cortex_status`, `cortex_update` |
|
| **System** | `cortex_restart`, `cortex_logs`, `cortex_status`, `cortex_update` |
|
||||||
| **Tasks** | `task_list`, `task_create`, `task_update`, `task_complete` |
|
| **Tasks** | `task_list`, `task_create`, `task_update`, `task_complete` |
|
||||||
@@ -96,10 +98,12 @@ Orchestrated sessions persist to history exactly like regular chat.
|
|||||||
| **Scratchpad** | `scratch_read`, `scratch_write`, `scratch_append`, `scratch_clear` |
|
| **Scratchpad** | `scratch_read`, `scratch_write`, `scratch_append`, `scratch_clear` |
|
||||||
| **Notifications** | `web_push`, `email_send`, `nc_talk_send`, `nc_talk_history` |
|
| **Notifications** | `web_push`, `email_send`, `nc_talk_send`, `nc_talk_history` |
|
||||||
| **Aether Journals** | `ae_journal_list/search`, `ae_journal_entries_list`, `ae_journal_entry_read/create/update/disable/append/prepend` |
|
| **Aether Journals** | `ae_journal_list/search`, `ae_journal_entries_list`, `ae_journal_entry_read/create/update/disable/append/prepend` |
|
||||||
|
| **Aether Tasks** | `ae_task_list` |
|
||||||
| **Agent Notes** | `agent_notes_read`, `agent_notes_write`, `agent_notes_append`, `agent_notes_clear` |
|
| **Agent Notes** | `agent_notes_read`, `agent_notes_write`, `agent_notes_append`, `agent_notes_clear` |
|
||||||
| **Agents** | `spawn_agent` |
|
| **Agents** | `spawn_agent` |
|
||||||
|
| **Home Assistant** | `ha_get_state`, `ha_get_states`, `ha_call_service` |
|
||||||
|
|
||||||
File, Shell, System, Agents, and some Notification/Web tools are **admin-only** and not visible to regular users.
|
Files, Shell, System, Agents, and some Notification/Web tools are **admin-only** and not visible to regular users.
|
||||||
`http_post` requires a URL prefix allowlist in `home/{user}/http_allowlist.json`.
|
`http_post` requires a URL prefix allowlist in `home/{user}/http_allowlist.json`.
|
||||||
`nc_talk_history` requires `nc_username` and `nc_app_password` in `channels.json` under `nextcloud`.
|
`nc_talk_history` requires `nc_username` and `nc_app_password` in `channels.json` under `nextcloud`.
|
||||||
|
|
||||||
@@ -149,21 +153,14 @@ Once installed, opening Cortex from the home screen or app launcher skips the br
|
|||||||
|
|
||||||
## Backends
|
## Backends
|
||||||
|
|
||||||
Three backends are available:
|
The **Model** toggle in the Context & Memory panel cycles through the slot models configured for your Chat role (Primary → Backup 1 → Backup 2 → …). Click it to switch between models mid-session.
|
||||||
|
|
||||||
| Backend | What it is |
|
- The button label shows the active model (e.g. "GPT-4o", "Gemini 2.5 Flash")
|
||||||
|---|---|
|
- The selected slot is sent with each chat request so the correct model is used
|
||||||
| **Claude** | Anthropic Claude via the Claude CLI (OAuth — no API key needed) |
|
- If only one model is configured, the toggle does nothing
|
||||||
| **Gemini** | Google Gemini via the Gemini CLI |
|
- A system message appears in the chat when you switch models
|
||||||
| **Local** | Any OpenAI-compatible endpoint (Open WebUI, Ollama, OpenRouter, etc.) |
|
|
||||||
|
|
||||||
The **Role** toggle in the Context & Memory panel cycles through configured role assignments. Each role maps to a Primary / Backup 1 / Backup 2 model chain set in the Model Registry.
|
If the active model fails, the next configured backup slot is tried automatically.
|
||||||
|
|
||||||
- The active model label appears below the toggle button
|
|
||||||
- `auto` (default) uses the model assigned to the `chat` role in your Model Registry
|
|
||||||
- Forcing a specific backend overrides the role assignment for that session
|
|
||||||
|
|
||||||
If the active backend fails, a fallback is tried automatically. A **⚡** badge appears on the response when this happens.
|
|
||||||
|
|
||||||
Each response shows a **model tag** (bottom-right of message) with the model label and host, so you always know what responded.
|
Each response shows a **model tag** (bottom-right of message) with the model label and host, so you always know what responded.
|
||||||
|
|
||||||
@@ -447,10 +444,12 @@ Chat request body (`POST /chat`):
|
|||||||
"message": "string",
|
"message": "string",
|
||||||
"session_id": "string | null",
|
"session_id": "string | null",
|
||||||
"tier": 2,
|
"tier": 2,
|
||||||
"model": "claude | gemini | local | null",
|
"chat_role": "chat",
|
||||||
|
"slot": "primary | backup_1 | backup_2 | null",
|
||||||
"include_long": true,
|
"include_long": true,
|
||||||
"include_mid": true,
|
"include_mid": true,
|
||||||
"include_short": true
|
"include_short": true,
|
||||||
|
"off_record": false
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -313,8 +313,8 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
// ── Tools toggle ─────────────────────────────────────────────
|
// ── Tools toggle ─────────────────────────────────────────────
|
||||||
// When on: submit goes to POST /orchestrate (Gemini tool loop → active role responds).
|
// When on: submit goes to POST /orchestrate (orchestrator tool loop → active model responds).
|
||||||
// When off: submit goes to POST /chat (direct to active role, no tools).
|
// When off: submit goes to POST /chat (direct to active model, no tools).
|
||||||
let toolsEnabled = localStorage.getItem('tools-enabled') === 'true';
|
let toolsEnabled = localStorage.getItem('tools-enabled') === 'true';
|
||||||
let _runStart = 0;
|
let _runStart = 0;
|
||||||
let _runTimer = null;
|
let _runTimer = null;
|
||||||
@@ -335,9 +335,8 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
function updateSendBtnTitle() {
|
function updateSendBtnTitle() {
|
||||||
const role = activeRole();
|
const entry = activeChatModel();
|
||||||
const rmodel = role?.model_label || '(server default)';
|
const rmodel = entry?.label || '(server default)';
|
||||||
const rname = role?.label || 'Chat';
|
|
||||||
const mode = current_mode === 'otr' ? 'Off The Record'
|
const mode = current_mode === 'otr' ? 'Off The Record'
|
||||||
: current_mode === 'note' ? 'Note'
|
: current_mode === 'note' ? 'Note'
|
||||||
: 'Chat';
|
: 'Chat';
|
||||||
@@ -347,13 +346,13 @@
|
|||||||
if (useOrch) {
|
if (useOrch) {
|
||||||
const omodel = orchestratorModel || '(server default)';
|
const omodel = orchestratorModel || '(server default)';
|
||||||
lines = [
|
lines = [
|
||||||
`Role: ${rname} · ${rmodel}`,
|
`Model: ${rmodel}`,
|
||||||
`Orchestrator: ${omodel} (tool loop)`,
|
`Orchestrator: ${omodel} (tool loop)`,
|
||||||
`Mode: ${mode}`,
|
`Mode: ${mode}`,
|
||||||
];
|
];
|
||||||
} else {
|
} else {
|
||||||
lines = [
|
lines = [
|
||||||
`Role: ${rname} · ${rmodel}`,
|
`Model: ${rmodel}`,
|
||||||
`Mode: ${mode}`,
|
`Mode: ${mode}`,
|
||||||
`Engine: Direct (no tool loop)`,
|
`Engine: Direct (no tool loop)`,
|
||||||
];
|
];
|
||||||
@@ -364,14 +363,13 @@
|
|||||||
function startRunTimer() {
|
function startRunTimer() {
|
||||||
_runStart = Date.now();
|
_runStart = Date.now();
|
||||||
function tick() {
|
function tick() {
|
||||||
const secs = Math.floor((Date.now() - _runStart) / 1000);
|
const secs = Math.floor((Date.now() - _runStart) / 1000);
|
||||||
const role = activeRole();
|
const entry = activeChatModel();
|
||||||
const rname = role?.label || 'Chat';
|
|
||||||
const useOrch = toolsEnabled && current_mode !== 'note';
|
const useOrch = toolsEnabled && current_mode !== 'note';
|
||||||
const model = useOrch
|
const model = useOrch
|
||||||
? (orchestratorModel || '(server default)') + ' (tool loop)'
|
? (orchestratorModel || '(server default)') + ' (tool loop)'
|
||||||
: (role?.model_label || '(server default)');
|
: (entry?.label || '(server default)');
|
||||||
stopBtn.title = `Running: ${rname} · ${model}\nElapsed: ${secs}s — click to cancel`;
|
stopBtn.title = `Running: Chat · ${model}\nElapsed: ${secs}s — click to cancel`;
|
||||||
}
|
}
|
||||||
tick();
|
tick();
|
||||||
_runTimer = setInterval(tick, 1000);
|
_runTimer = setInterval(tick, 1000);
|
||||||
@@ -469,23 +467,24 @@
|
|||||||
document.addEventListener('click', () => personaDropEl.classList.remove('open'));
|
document.addEventListener('click', () => personaDropEl.classList.remove('open'));
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Role toggle ──────────────────────────────────────────────
|
// ── Model toggle (Phase 3) ───────────────────────────────────
|
||||||
// Cycles through roles that have a primary model assigned (excluding orchestrator).
|
// Cycles through the chat role's configured slot models (primary → backup_1 → …).
|
||||||
// Sends chat_role ("chat"|"coder"|"research"|...) in chat requests.
|
// Shows the model label on the button; sends slot + chat_role:"chat" in requests.
|
||||||
// Falls back to "chat" when no roles are configured in the registry.
|
// Falls back to "chat" / no slot when no models are configured.
|
||||||
|
|
||||||
const TYPE_CLASS = { claude_cli: '', gemini_api: 'mem-on', gemini_cli: 'mem-on', local_openai: 'local-on' };
|
const TYPE_CLASS = { claude_cli: '', gemini_api: 'mem-on', gemini_cli: 'mem-on', local_openai: 'local-on' };
|
||||||
const backendModelHint = document.getElementById('backend-model-hint');
|
const backendModelHint = document.getElementById('backend-model-hint');
|
||||||
|
|
||||||
let availableRoles = []; // [{role, label, model_label, type}] from /backend
|
let chatModels = []; // [{slot, label, type}] for chat-role slots
|
||||||
let roleIdx = 0;
|
let availableRoles = []; // [{role, label, model_label, type}] — kept for banner check
|
||||||
let orchestratorModel = null; // label of the orchestrator-role model
|
let modelIdx = 0;
|
||||||
|
let orchestratorModel = null;
|
||||||
|
|
||||||
function activeRole() {
|
function activeChatModel() {
|
||||||
return availableRoles.length > 0 ? availableRoles[roleIdx] : null;
|
return chatModels.length > 0 ? chatModels[modelIdx] : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function setRoleToggleUI(entry) {
|
function setModelToggleUI(entry) {
|
||||||
if (!entry) {
|
if (!entry) {
|
||||||
backendToggle.textContent = 'chat';
|
backendToggle.textContent = 'chat';
|
||||||
backendToggle.className = 'ctx-btn';
|
backendToggle.className = 'ctx-btn';
|
||||||
@@ -493,19 +492,16 @@
|
|||||||
backendToggle.textContent = entry.label;
|
backendToggle.textContent = entry.label;
|
||||||
backendToggle.className = 'ctx-btn ' + (TYPE_CLASS[entry.type] || '');
|
backendToggle.className = 'ctx-btn ' + (TYPE_CLASS[entry.type] || '');
|
||||||
}
|
}
|
||||||
if (backendModelHint) {
|
if (backendModelHint) backendModelHint.style.display = 'none';
|
||||||
const hint = entry?.model_label || '';
|
|
||||||
backendModelHint.textContent = hint;
|
|
||||||
backendModelHint.style.display = hint ? '' : 'none';
|
|
||||||
}
|
|
||||||
updateSendBtnTitle();
|
updateSendBtnTitle();
|
||||||
}
|
}
|
||||||
|
|
||||||
fetch('/backend').then(r => r.json()).then(d => {
|
fetch('/backend').then(r => r.json()).then(d => {
|
||||||
|
chatModels = d.chat_models || [];
|
||||||
availableRoles = d.available_roles || [];
|
availableRoles = d.available_roles || [];
|
||||||
orchestratorModel = d.orchestrator_model || null;
|
orchestratorModel = d.orchestrator_model || null;
|
||||||
roleIdx = 0;
|
modelIdx = 0;
|
||||||
setRoleToggleUI(availableRoles[0] || null);
|
setModelToggleUI(chatModels[0] || null);
|
||||||
_maybeShowNoBanner(availableRoles);
|
_maybeShowNoBanner(availableRoles);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -527,17 +523,16 @@
|
|||||||
style="background:none;border:none;color:#78350f;cursor:pointer;font-size:1rem;line-height:1;padding:0 0.2rem;"
|
style="background:none;border:none;color:#78350f;cursor:pointer;font-size:1rem;line-height:1;padding:0 0.2rem;"
|
||||||
title="Dismiss">✕</button>
|
title="Dismiss">✕</button>
|
||||||
`;
|
`;
|
||||||
// Insert at the top of #chat-col (or body if not found)
|
|
||||||
const col = document.getElementById('chat-col') || document.body.firstElementChild;
|
const col = document.getElementById('chat-col') || document.body.firstElementChild;
|
||||||
col.insertBefore(banner, col.firstChild);
|
col.insertBefore(banner, col.firstChild);
|
||||||
}
|
}
|
||||||
|
|
||||||
backendToggle.addEventListener('click', () => {
|
backendToggle.addEventListener('click', () => {
|
||||||
if (availableRoles.length <= 1) return;
|
if (chatModels.length <= 1) return;
|
||||||
roleIdx = (roleIdx + 1) % availableRoles.length;
|
modelIdx = (modelIdx + 1) % chatModels.length;
|
||||||
const entry = availableRoles[roleIdx];
|
const entry = chatModels[modelIdx];
|
||||||
setRoleToggleUI(entry);
|
setModelToggleUI(entry);
|
||||||
addMessage('system', `Role: ${entry.label} · ${entry.model_label}`);
|
addMessage('system', `Model: ${entry.label}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
// ── Sessions panel ───────────────────────────────────────────
|
// ── Sessions panel ───────────────────────────────────────────
|
||||||
@@ -1346,7 +1341,8 @@
|
|||||||
include_mid: memMid,
|
include_mid: memMid,
|
||||||
include_short: memShort,
|
include_short: memShort,
|
||||||
off_record: isOtr,
|
off_record: isOtr,
|
||||||
chat_role: activeRole()?.role || 'chat',
|
chat_role: 'chat',
|
||||||
|
slot: activeChatModel()?.slot || null,
|
||||||
user: CORTEX_USER,
|
user: CORTEX_USER,
|
||||||
persona: CORTEX_PERSONA,
|
persona: CORTEX_PERSONA,
|
||||||
};
|
};
|
||||||
@@ -1377,7 +1373,8 @@
|
|||||||
include_mid: memMid,
|
include_mid: memMid,
|
||||||
include_short: memShort,
|
include_short: memShort,
|
||||||
off_record: current_mode === 'otr',
|
off_record: current_mode === 'otr',
|
||||||
chat_role: activeRole()?.role || 'chat',
|
chat_role: 'chat',
|
||||||
|
slot: activeChatModel()?.slot || null,
|
||||||
user: CORTEX_USER,
|
user: CORTEX_USER,
|
||||||
persona: CORTEX_PERSONA,
|
persona: CORTEX_PERSONA,
|
||||||
}),
|
}),
|
||||||
|
|||||||
Reference in New Issue
Block a user