Update to latest revision

parent 926cfaac
...@@ -480,6 +480,12 @@ class CoderAIBroker: ...@@ -480,6 +480,12 @@ class CoderAIBroker:
async def send_request(self, provider_id: str, op: str, payload: Dict[str, Any], timeout: float = 300.0, client_id: Optional[str] = None, owner_user_id: Optional[int] = None, extra: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: async def send_request(self, provider_id: str, op: str, payload: Dict[str, Any], timeout: float = 300.0, client_id: Optional[str] = None, owner_user_id: Optional[int] = None, extra: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
snapshot = await self.get_session_snapshot(provider_id, client_id) snapshot = await self.get_session_snapshot(provider_id, client_id)
if (not snapshot or not snapshot.get("connected")) and client_id:
fallback_snapshot = await self.get_session_snapshot(client_id, client_id)
fallback_metadata = (fallback_snapshot or {}).get("metadata") or {}
if fallback_snapshot and fallback_snapshot.get("connected") and fallback_metadata.get("owner_user_id") == owner_user_id:
snapshot = fallback_snapshot
provider_id = snapshot.get("provider_id") or provider_id
if not snapshot or not snapshot.get("connected"): if not snapshot or not snapshot.get("connected"):
raise RuntimeError(f"No active CoderAI broker session for provider '{provider_id}'") raise RuntimeError(f"No active CoderAI broker session for provider '{provider_id}'")
if owner_user_id != ((snapshot.get('metadata') or {}).get('owner_user_id')): if owner_user_id != ((snapshot.get('metadata') or {}).get('owner_user_id')):
...@@ -493,7 +499,7 @@ class CoderAIBroker: ...@@ -493,7 +499,7 @@ class CoderAIBroker:
"v": 1, "v": 1,
"op": op, "op": op,
"request_id": request_id, "request_id": request_id,
"provider_id": provider_id, "provider_id": snapshot.get("provider_id") or provider_id,
"client_id": snapshot.get("client_id") or client_id, "client_id": snapshot.get("client_id") or client_id,
"payload": payload, "payload": payload,
"reply_key": self._reply_key(request_id), "reply_key": self._reply_key(request_id),
...@@ -509,7 +515,7 @@ class CoderAIBroker: ...@@ -509,7 +515,7 @@ class CoderAIBroker:
stream_queue=stream_queue, stream_queue=stream_queue,
request_snapshot={ request_snapshot={
"session_id": snapshot.get("session_id"), "session_id": snapshot.get("session_id"),
"provider_id": provider_id, "provider_id": snapshot.get("provider_id") or provider_id,
"client_id": snapshot.get("client_id") or client_id, "client_id": snapshot.get("client_id") or client_id,
"op": op, "op": op,
}, },
......
...@@ -472,6 +472,26 @@ class Config: ...@@ -472,6 +472,26 @@ class Config:
raise FileNotFoundError("Could not find configuration files") raise FileNotFoundError("Could not find configuration files")
def _get_aisbf_config_path(self) -> Path:
"""Resolve the active aisbf.json path consistently across startup and reloads."""
candidates = []
if self._custom_config_dir and self._custom_config_dir.exists():
candidates.append(self._custom_config_dir / 'aisbf.json')
candidates += [
Path.home() / '.aisbf' / 'aisbf.json',
Path.home() / '.local' / 'share' / 'aisbf' / 'aisbf.json',
Path('/usr/local/share/aisbf/aisbf.json'),
Path('/usr/share/aisbf/aisbf.json'),
Path(__file__).parent.parent / 'config' / 'aisbf.json',
]
for candidate in candidates:
if candidate.exists():
return candidate
return candidates[-1]
def _ensure_config_directory(self): def _ensure_config_directory(self):
"""Ensure ~/.aisbf/ directory exists and copy default config files if needed""" """Ensure ~/.aisbf/ directory exists and copy default config files if needed"""
config_dir = Path.home() / '.aisbf' config_dir = Path.home() / '.aisbf'
...@@ -930,7 +950,7 @@ class Config: ...@@ -930,7 +950,7 @@ class Config:
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
logger.info(f"=== Config._load_aisbf_config START ===") logger.info(f"=== Config._load_aisbf_config START ===")
aisbf_path = Path.home() / '.aisbf' / 'aisbf.json' aisbf_path = self._get_aisbf_config_path()
logger.info(f"Looking for AISBF config in: {aisbf_path}") logger.info(f"Looking for AISBF config in: {aisbf_path}")
if not aisbf_path.exists(): if not aisbf_path.exists():
......
...@@ -761,7 +761,9 @@ async def dashboard_settings_save( ...@@ -761,7 +761,9 @@ async def dashboard_settings_save(
'requests_per_hour': max(0, client_rl_general_rph) 'requests_per_hour': max(0, client_rl_general_rph)
} }
# Save config # Save config back to the same resolved path we loaded from
config_path = get_aisbf_config_path()
if not config_path.exists():
config_path = Path.home() / '.aisbf' / 'aisbf.json' config_path = Path.home() / '.aisbf' / 'aisbf.json'
config_path.parent.mkdir(parents=True, exist_ok=True) config_path.parent.mkdir(parents=True, exist_ok=True)
with open(config_path, 'w') as f: with open(config_path, 'w') as f:
......
...@@ -19,11 +19,13 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. ...@@ -19,11 +19,13 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
from __future__ import annotations from __future__ import annotations
import asyncio
from dataclasses import dataclass from dataclasses import dataclass
from pathlib import Path from pathlib import Path
import json import json
from typing import Any, Dict, Iterable, List, Optional from typing import Any, Dict, Iterable, List, Optional
from aisbf.app.model_cache import get_provider_models
from aisbf.studio_adapters import effective_studio_adapter, infer_studio_adapter_profile from aisbf.studio_adapters import effective_studio_adapter, infer_studio_adapter_profile
...@@ -593,8 +595,41 @@ def build_studio_catalog( ...@@ -593,8 +595,41 @@ def build_studio_catalog(
rotations = getattr(config, "rotations", None) or {} rotations = getattr(config, "rotations", None) or {}
autoselects = getattr(config, "autoselect", None) or {} autoselects = getattr(config, "autoselect", None) or {}
provider_entries = _build_provider_entries(scope, owner_id, providers)
missing_provider_ids = {
provider_id
for provider_id, provider_config in (providers or {}).items()
if not _provider_models_from_config(provider_config)
}
if missing_provider_ids and config is not None:
for provider_id in missing_provider_ids:
provider_config = providers.get(provider_id)
if provider_config is None:
continue
try:
live_models = asyncio.run(get_provider_models(provider_id, provider_config, config, user_id=owner_id if scope == "user" else None))
except RuntimeError:
live_models = []
except Exception:
live_models = []
if not live_models:
continue
live_model_names = {
(model.get("name") or model.get("model_name") or model.get("id") or "").split("/", 1)[-1]
for model in live_models if isinstance(model, dict)
}
provider_entries = [
entry for entry in provider_entries
if not (entry.get("kind") == "provider_model" and entry.get("source_id") == provider_id and entry.get("target_id") in live_model_names)
]
hydrated_provider = provider_config if isinstance(provider_config, dict) else provider_config.model_dump()
hydrated_provider = dict(hydrated_provider)
hydrated_provider["models"] = live_models
provider_entries.extend(_build_provider_entries(scope, owner_id, {provider_id: hydrated_provider}))
entries = [ entries = [
*_build_provider_entries(scope, owner_id, providers), *provider_entries,
*_build_rotation_entries(scope, owner_id, rotations), *_build_rotation_entries(scope, owner_id, rotations),
*_build_autoselect_entries(scope, owner_id, autoselects), *_build_autoselect_entries(scope, owner_id, autoselects),
] ]
......
...@@ -1609,8 +1609,24 @@ const BLABEL = {text:'LLM',vision:'VLM',image:'IMG',video:'VID',audio:'STT', ...@@ -1609,8 +1609,24 @@ const BLABEL = {text:'LLM',vision:'VLM',image:'IMG',video:'VID',audio:'STT',
function renderSidebar() { function renderSidebar() {
const el = $('model-list'); const el = $('model-list');
const activeEl = document.activeElement;
const activeIsBindingSearch = activeEl && activeEl.classList && activeEl.classList.contains('binding-role-search');
const activeValue = activeIsBindingSearch ? activeEl.value : '';
const activeSelectionStart = activeIsBindingSearch && typeof activeEl.selectionStart === 'number' ? activeEl.selectionStart : null;
const activeSelectionEnd = activeIsBindingSearch && typeof activeEl.selectionEnd === 'number' ? activeEl.selectionEnd : null;
const restoreKey = activeIsBindingSearch ? activeEl.getAttribute('data-search-key') : null;
if (!functionBindingDefs.length) { el.innerHTML='<div class="muted small" style="padding:.5rem .6rem">No Studio bindings</div>'; return; } if (!functionBindingDefs.length) { el.innerHTML='<div class="muted small" style="padding:.5rem .6rem">No Studio bindings</div>'; return; }
el.innerHTML = `<div class="binding-list">${functionBindingDefs.map(renderBindingCard).join('')}</div>`; el.innerHTML = `<div class="binding-list">${functionBindingDefs.map(renderBindingCard).join('')}</div>`;
if (restoreKey) {
const nextEl = el.querySelector(`.binding-role-search[data-search-key="${CSS.escape(restoreKey)}"]`);
if (nextEl) {
nextEl.focus();
if (nextEl.value !== activeValue) nextEl.value = activeValue;
if (activeSelectionStart !== null && activeSelectionEnd !== null && typeof nextEl.setSelectionRange === 'function') {
nextEl.setSelectionRange(activeSelectionStart, activeSelectionEnd);
}
}
}
} }
function renderBindingCard(def) { function renderBindingCard(def) {
...@@ -1657,7 +1673,7 @@ function renderBindingRole(def, role) { ...@@ -1657,7 +1673,7 @@ function renderBindingRole(def, role) {
<div class="binding-role-state">${assignedModel ? 'Bound' : (role.optional ? 'Optional' : 'Missing')}</div> <div class="binding-role-state">${assignedModel ? 'Bound' : (role.optional ? 'Optional' : 'Missing')}</div>
</div> </div>
<div class="binding-role-meta">${currentMeta}</div> <div class="binding-role-meta">${currentMeta}</div>
<input class="fi binding-role-search" type="search" value="${escapeHtml(query)}" placeholder="Search provider/model, rotation, autoselect" oninput="updateBindingSearch('${def.id}','${role.key}', this.value)"> <input class="fi binding-role-search" type="search" data-search-key="${escapeHtml(searchKey)}" value="${escapeHtml(query)}" placeholder="Search provider/model, rotation, autoselect" oninput="updateBindingSearch('${def.id}','${role.key}', this.value)">
<div class="binding-role-results">${results}</div> <div class="binding-role-results">${results}</div>
${assignedModel ? `<button class="btn btn-ghost btn-sm binding-role-clear" onclick="clearBindingRole('${def.id}','${role.key}');return false;">Clear</button>` : ''} ${assignedModel ? `<button class="btn btn-ghost btn-sm binding-role-clear" onclick="clearBindingRole('${def.id}','${role.key}');return false;">Clear</button>` : ''}
</div>`; </div>`;
......
import json
from aisbf.coderai_broker import CoderAIBroker
def test_coderai_broker_send_request_falls_back_to_client_id_named_provider_snapshot(tmp_path):
broker = CoderAIBroker()
broker._state_path = tmp_path / "coderai_broker_sessions.json"
session_key = broker._session_meta_key("zeiss-nvidia", "zeiss-nvidia")
broker._cache.broker_set(
session_key,
{
"session_id": "sess-1",
"provider_id": "actual-coderai-provider",
"client_id": "zeiss-nvidia",
"closed": False,
"metadata": {"owner_user_id": None},
},
ttl=120,
)
import asyncio
async def _run():
task = asyncio.create_task(
broker.send_request("zeiss-nvidia", "models.list", {}, client_id="zeiss-nvidia", owner_user_id=None, timeout=0.01)
)
await asyncio.sleep(0)
pending = next(iter(broker._pending.values()))
assert pending.request_snapshot["provider_id"] == "actual-coderai-provider"
task.cancel()
try:
await task
except BaseException:
pass
asyncio.run(_run())
def test_studio_sidebar_search_input_declares_stable_search_key():
studio_js = open("/working/aisbf/static/dashboard/studio.js", "r", encoding="utf-8").read()
assert "data-search-key" in studio_js
assert "setSelectionRange" in studio_js
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