Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Contribute to GitLab
Sign in
Toggle navigation
A
aisbf
Project
Project
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
nexlab
aisbf
Commits
ce6a2513
Commit
ce6a2513
authored
May 14, 2026
by
Stefy Lanza (nextime / spora )
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Update to latest revision
parent
926cfaac
Changes
8
Expand all
Show whitespace changes
Inline
Side-by-side
Showing
8 changed files
with
341 additions
and
7 deletions
+341
-7
coderai_broker.py
aisbf/coderai_broker.py
+8
-2
config.py
aisbf/config.py
+21
-1
settings.py
aisbf/routes/dashboard/settings.py
+4
-2
studio.py
aisbf/studio.py
+36
-1
aisbf-oauth2-extension.zip
static/aisbf-oauth2-extension.zip
+0
-0
studio.js
static/dashboard/studio.js
+17
-1
test_dashboard_settings_feature_controls.py
tests/routes/test_dashboard_settings_feature_controls.py
+212
-0
test_studio_sidebar_and_coderai_catalog.py
tests/test_studio_sidebar_and_coderai_catalog.py
+43
-0
No files found.
aisbf/coderai_broker.py
View file @
ce6a2513
...
@@ -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
,
},
},
...
...
aisbf/config.py
View file @
ce6a2513
...
@@ -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
():
...
...
aisbf/routes/dashboard/settings.py
View file @
ce6a2513
...
@@ -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
:
...
...
aisbf/studio.py
View file @
ce6a2513
...
@@ -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
),
]
]
...
...
static/aisbf-oauth2-extension.zip
View file @
ce6a2513
No preview for this file type
static/dashboard/studio.js
View file @
ce6a2513
...
@@ -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>`
;
...
...
tests/routes/test_dashboard_settings_feature_controls.py
View file @
ce6a2513
This diff is collapsed.
Click to expand it.
tests/test_studio_sidebar_and_coderai_catalog.py
0 → 100644
View file @
ce6a2513
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
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment