Update to latest revision

parent 926cfaac
......@@ -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]:
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"):
raise RuntimeError(f"No active CoderAI broker session for provider '{provider_id}'")
if owner_user_id != ((snapshot.get('metadata') or {}).get('owner_user_id')):
......@@ -493,7 +499,7 @@ class CoderAIBroker:
"v": 1,
"op": op,
"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,
"payload": payload,
"reply_key": self._reply_key(request_id),
......@@ -509,7 +515,7 @@ class CoderAIBroker:
stream_queue=stream_queue,
request_snapshot={
"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,
"op": op,
},
......
......@@ -472,6 +472,26 @@ class Config:
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):
"""Ensure ~/.aisbf/ directory exists and copy default config files if needed"""
config_dir = Path.home() / '.aisbf'
......@@ -930,7 +950,7 @@ class Config:
logger = logging.getLogger(__name__)
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}")
if not aisbf_path.exists():
......
......@@ -761,7 +761,9 @@ async def dashboard_settings_save(
'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.parent.mkdir(parents=True, exist_ok=True)
with open(config_path, 'w') as f:
......
......@@ -19,11 +19,13 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
from __future__ import annotations
import asyncio
from dataclasses import dataclass
from pathlib import Path
import json
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
......@@ -593,8 +595,41 @@ def build_studio_catalog(
rotations = getattr(config, "rotations", 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 = [
*_build_provider_entries(scope, owner_id, providers),
*provider_entries,
*_build_rotation_entries(scope, owner_id, rotations),
*_build_autoselect_entries(scope, owner_id, autoselects),
]
......
......@@ -1609,8 +1609,24 @@ const BLABEL = {text:'LLM',vision:'VLM',image:'IMG',video:'VID',audio:'STT',
function renderSidebar() {
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; }
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) {
......@@ -1657,7 +1673,7 @@ function renderBindingRole(def, role) {
<div class="binding-role-state">${assignedModel ? 'Bound' : (role.optional ? 'Optional' : 'Missing')}</div>
</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>
${assignedModel ? `<button class="btn btn-ghost btn-sm binding-role-clear" onclick="clearBindingRole('${def.id}','${role.key}');return false;">Clear</button>` : ''}
</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