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
1137ae24
Commit
1137ae24
authored
May 10, 2026
by
Stefy Lanza (nextime / spora )
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
refactor(studio): align catalog route behavior
parent
842f8eb4
Changes
3
Hide whitespace changes
Inline
Side-by-side
Showing
3 changed files
with
118 additions
and
35 deletions
+118
-35
providers.py
aisbf/routes/dashboard/providers.py
+2
-2
studio.py
aisbf/studio.py
+53
-31
test_dashboard_studio.py
tests/routes/test_dashboard_studio.py
+63
-2
No files found.
aisbf/routes/dashboard/providers.py
View file @
1137ae24
...
@@ -259,9 +259,9 @@ async def dashboard_studio(request: Request):
...
@@ -259,9 +259,9 @@ async def dashboard_studio(request: Request):
@
router
.
get
(
"/dashboard/studio/catalog"
)
@
router
.
get
(
"/dashboard/studio/catalog"
)
async
def
dashboard_studio_catalog
(
request
:
Request
):
async
def
dashboard_studio_catalog
(
request
:
Request
):
"""Return Studio catalog for the current dashboard principal."""
"""Return Studio catalog for the current dashboard principal."""
auth_check
=
require_
dashboard
_auth
(
request
)
auth_check
=
require_
api
_auth
(
request
)
if
auth_check
:
if
auth_check
:
return
JSONResponse
({
"entries"
:
[],
"error"
:
"unauthorized"
},
status_code
=
401
)
return
auth_check
current_user_id
=
request
.
session
.
get
(
"user_id"
)
current_user_id
=
request
.
session
.
get
(
"user_id"
)
scope
=
"admin"
if
request
.
session
.
get
(
"role"
)
==
"admin"
else
"user"
scope
=
"admin"
if
request
.
session
.
get
(
"role"
)
==
"admin"
else
"user"
...
...
aisbf/studio.py
View file @
1137ae24
...
@@ -215,6 +215,34 @@ def build_catalog_entry(
...
@@ -215,6 +215,34 @@ def build_catalog_entry(
}
}
def
_build_named_catalog_entry
(
*
,
prefix
:
str
,
scope
:
str
,
owner_id
:
Optional
[
int
],
target_id
:
str
,
label
:
str
,
description
:
Optional
[
str
],
capabilities
:
Optional
[
Iterable
[
str
]],
metadata
:
Optional
[
Dict
[
str
,
Any
]]
=
None
,
)
->
Dict
[
str
,
Any
]:
entry
=
build_catalog_entry
(
scope
=
scope
,
owner_id
=
owner_id
,
kind
=
prefix
,
source_id
=
prefix
,
target_id
=
target_id
,
label
=
label
,
description
=
description
,
capabilities
=
capabilities
,
availability_state
=
"ready"
,
availability_reason
=
None
,
metadata
=
metadata
,
)
entry
[
"id"
]
=
f
"{prefix}/{prefix}/{target_id}"
return
entry
def
_coerce_model_dict
(
model
:
Any
)
->
Dict
[
str
,
Any
]:
def
_coerce_model_dict
(
model
:
Any
)
->
Dict
[
str
,
Any
]:
if
isinstance
(
model
,
dict
):
if
isinstance
(
model
,
dict
):
return
model
return
model
...
@@ -243,10 +271,12 @@ def _provider_models_from_config(provider_config: Any) -> List[Dict[str, Any]]:
...
@@ -243,10 +271,12 @@ def _provider_models_from_config(provider_config: Any) -> List[Dict[str, Any]]:
return
[
_coerce_model_dict
(
model
)
for
model
in
(
models
or
[])]
return
[
_coerce_model_dict
(
model
)
for
model
in
(
models
or
[])]
def
_load_global_providers_from_
disk
()
->
Dict
[
str
,
Dict
[
str
,
Any
]]:
def
_load_global_providers_from_
source
()
->
Dict
[
str
,
Dict
[
str
,
Any
]]:
config_path
=
Path
.
home
()
/
".aisbf"
/
"providers.json"
config_path
=
Path
.
home
()
/
".aisbf"
/
"providers.json"
if
not
config_path
.
exists
():
if
not
config_path
.
exists
():
return
{}
config_path
=
Path
(
__file__
)
.
parent
.
parent
/
"config"
/
"providers.json"
if
not
config_path
.
exists
():
return
{}
with
open
(
config_path
)
as
handle
:
with
open
(
config_path
)
as
handle
:
payload
=
json
.
load
(
handle
)
payload
=
json
.
load
(
handle
)
...
@@ -314,23 +344,19 @@ def _build_rotation_entries(scope: str, owner_id: Optional[int], rotations: Dict
...
@@ -314,23 +344,19 @@ def _build_rotation_entries(scope: str, owner_id: Optional[int], rotations: Dict
for
rotation_id
,
rotation_config
in
rotations
.
items
():
for
rotation_id
,
rotation_config
in
rotations
.
items
():
config_data
=
rotation_config
if
isinstance
(
rotation_config
,
dict
)
else
rotation_config
.
model_dump
()
config_data
=
rotation_config
if
isinstance
(
rotation_config
,
dict
)
else
rotation_config
.
model_dump
()
entries
.
append
(
entries
.
append
(
{
_build_named_catalog_entry
(
"id"
:
f
"rotation/{rotation_id}"
,
prefix
=
"rotation"
,
"kind"
:
"rotation"
,
scope
=
scope
,
"owner_scope"
:
scope
,
owner_id
=
owner_id
,
"owner_id"
:
owner_id
,
target_id
=
rotation_id
,
"source_id"
:
rotation_id
,
label
=
config_data
.
get
(
"model_name"
)
or
rotation_id
,
"target_id"
:
rotation_id
,
description
=
config_data
.
get
(
"description"
),
"label"
:
config_data
.
get
(
"model_name"
)
or
rotation_id
,
capabilities
=
config_data
.
get
(
"capabilities"
),
"description"
:
config_data
.
get
(
"description"
),
metadata
=
{
"capabilities"
:
normalize_capabilities
(
config_data
.
get
(
"capabilities"
)),
"availability_state"
:
"ready"
,
"availability_reason"
:
None
,
"metadata"
:
{
"provider_count"
:
len
(
config_data
.
get
(
"providers"
)
or
[]),
"provider_count"
:
len
(
config_data
.
get
(
"providers"
)
or
[]),
"context_length"
:
config_data
.
get
(
"context_length"
),
"context_length"
:
config_data
.
get
(
"context_length"
),
},
},
}
)
)
)
return
entries
return
entries
...
@@ -341,24 +367,20 @@ def _build_autoselect_entries(scope: str, owner_id: Optional[int], autoselects:
...
@@ -341,24 +367,20 @@ def _build_autoselect_entries(scope: str, owner_id: Optional[int], autoselects:
config_data
=
autoselect_config
if
isinstance
(
autoselect_config
,
dict
)
else
autoselect_config
.
model_dump
()
config_data
=
autoselect_config
if
isinstance
(
autoselect_config
,
dict
)
else
autoselect_config
.
model_dump
()
available_models
=
config_data
.
get
(
"available_models"
)
or
[]
available_models
=
config_data
.
get
(
"available_models"
)
or
[]
entries
.
append
(
entries
.
append
(
{
_build_named_catalog_entry
(
"id"
:
f
"autoselect/{autoselect_id}"
,
prefix
=
"autoselect"
,
"kind"
:
"autoselect"
,
scope
=
scope
,
"owner_scope"
:
scope
,
owner_id
=
owner_id
,
"owner_id"
:
owner_id
,
target_id
=
autoselect_id
,
"source_id"
:
autoselect_id
,
label
=
config_data
.
get
(
"model_name"
)
or
autoselect_id
,
"target_id"
:
autoselect_id
,
description
=
config_data
.
get
(
"description"
),
"label"
:
config_data
.
get
(
"model_name"
)
or
autoselect_id
,
capabilities
=
config_data
.
get
(
"capabilities"
),
"description"
:
config_data
.
get
(
"description"
),
metadata
=
{
"capabilities"
:
normalize_capabilities
(
config_data
.
get
(
"capabilities"
)),
"availability_state"
:
"ready"
,
"availability_reason"
:
None
,
"metadata"
:
{
"available_model_count"
:
len
(
available_models
),
"available_model_count"
:
len
(
available_models
),
"fallback"
:
config_data
.
get
(
"fallback"
),
"fallback"
:
config_data
.
get
(
"fallback"
),
"selection_model"
:
config_data
.
get
(
"selection_model"
),
"selection_model"
:
config_data
.
get
(
"selection_model"
),
},
},
}
)
)
)
return
entries
return
entries
...
@@ -378,7 +400,7 @@ def build_studio_catalog(
...
@@ -378,7 +400,7 @@ def build_studio_catalog(
rotations
=
{
row
[
"rotation_id"
]:
row
.
get
(
"config"
,
{})
for
row
in
rotation_rows
}
rotations
=
{
row
[
"rotation_id"
]:
row
.
get
(
"config"
,
{})
for
row
in
rotation_rows
}
autoselects
=
{
row
[
"autoselect_id"
]:
row
.
get
(
"config"
,
{})
for
row
in
autoselect_rows
}
autoselects
=
{
row
[
"autoselect_id"
]:
row
.
get
(
"config"
,
{})
for
row
in
autoselect_rows
}
else
:
else
:
providers
=
getattr
(
config
,
"providers"
,
None
)
or
_load_global_providers_from_
disk
()
providers
=
getattr
(
config
,
"providers"
,
None
)
or
_load_global_providers_from_
source
()
rotations
=
getattr
(
config
,
"rotations"
,
None
)
or
{}
rotations
=
getattr
(
config
,
"rotations"
,
None
)
or
{}
autoselects
=
getattr
(
config
,
"autoselect"
,
None
)
or
{}
autoselects
=
getattr
(
config
,
"autoselect"
,
None
)
or
{}
...
...
tests/routes/test_dashboard_studio.py
View file @
1137ae24
...
@@ -107,6 +107,15 @@ def test_dashboard_studio_catalog_returns_global_resources_for_admin(monkeypatch
...
@@ -107,6 +107,15 @@ def test_dashboard_studio_catalog_returns_global_resources_for_admin(monkeypatch
}
}
def
test_dashboard_studio_catalog_uses_api_auth_json_when_logged_out
():
client
=
TestClient
(
app
)
response
=
client
.
get
(
"/dashboard/studio/catalog"
)
assert
response
.
status_code
==
401
assert
response
.
json
()
==
{
"error"
:
"Authentication required"
}
def
test_dashboard_studio_catalog_returns_user_resources_for_user
(
monkeypatch
):
def
test_dashboard_studio_catalog_returns_user_resources_for_user
(
monkeypatch
):
client
=
TestClient
(
app
)
client
=
TestClient
(
app
)
db
=
DatabaseRegistry
.
get_config_database
()
db
=
DatabaseRegistry
.
get_config_database
()
...
@@ -222,6 +231,58 @@ def test_build_studio_catalog_uses_global_config_for_admin_scope():
...
@@ -222,6 +231,58 @@ def test_build_studio_catalog_uses_global_config_for_admin_scope():
assert
provider_entry
[
"metadata"
][
"context_length"
]
==
128000
assert
provider_entry
[
"metadata"
][
"context_length"
]
==
128000
def
test_build_studio_catalog_falls_back_to_dashboard_global_provider_source
(
monkeypatch
):
monkeypatch
.
setattr
(
"aisbf.studio._load_global_providers_from_source"
,
lambda
:
{
"fallback-openai"
:
{
"type"
:
"openai"
,
"models"
:
[{
"name"
:
"gpt-4.1-mini"
,
"description"
:
"Fallback model"
,
"capabilities"
:
[
"chat"
]}],
}
},
)
catalog
=
build_studio_catalog
(
scope
=
"admin"
,
owner_id
=
None
,
config
=
None
)
assert
catalog
[
"scope"
]
==
"admin"
provider_entry
=
next
(
entry
for
entry
in
catalog
[
"entries"
]
if
entry
[
"kind"
]
==
"provider_model"
)
assert
provider_entry
[
"id"
]
==
"provider/fallback-openai/gpt-4.1-mini"
assert
provider_entry
[
"owner_scope"
]
==
"admin"
def
test_build_studio_catalog_reuses_catalog_entry_contract_for_non_provider_resources
():
class
ConfigStub
:
providers
=
{}
rotations
=
{
"team-default"
:
{
"model_name"
:
"Team default"
,
"providers"
:
[{
"provider"
:
"openai"
,
"model"
:
"gpt-4o"
}],
"capabilities"
:
[
"chat"
],
}
}
autoselect
=
{
"writer"
:
{
"model_name"
:
"Writer"
,
"description"
:
"General writing"
,
"fallback"
:
"openai/gpt-4o"
,
"selection_model"
:
"internal"
,
"available_models"
:
[{
"model_id"
:
"openai/gpt-4o"
,
"description"
:
"Primary"
}],
"capabilities"
:
[
"chat"
],
}
}
catalog
=
build_studio_catalog
(
scope
=
"admin"
,
owner_id
=
None
,
config
=
ConfigStub
())
rotation_entry
=
next
(
entry
for
entry
in
catalog
[
"entries"
]
if
entry
[
"kind"
]
==
"rotation"
)
autoselect_entry
=
next
(
entry
for
entry
in
catalog
[
"entries"
]
if
entry
[
"kind"
]
==
"autoselect"
)
assert
rotation_entry
[
"source_id"
]
==
"rotation"
assert
rotation_entry
[
"target_id"
]
==
"team-default"
assert
rotation_entry
[
"id"
]
==
"rotation/rotation/team-default"
assert
autoselect_entry
[
"source_id"
]
==
"autoselect"
assert
autoselect_entry
[
"target_id"
]
==
"writer"
assert
autoselect_entry
[
"id"
]
==
"autoselect/autoselect/writer"
def
test_build_studio_catalog_uses_user_owned_resources_for_user_scope
():
def
test_build_studio_catalog_uses_user_owned_resources_for_user_scope
():
class
DbStub
:
class
DbStub
:
def
get_user_providers
(
self
,
user_id
):
def
get_user_providers
(
self
,
user_id
):
...
@@ -261,8 +322,8 @@ def test_build_studio_catalog_uses_user_owned_resources_for_user_scope():
...
@@ -261,8 +322,8 @@ def test_build_studio_catalog_uses_user_owned_resources_for_user_scope():
assert
all
(
entry
[
"owner_scope"
]
==
"user"
for
entry
in
catalog
[
"entries"
])
assert
all
(
entry
[
"owner_scope"
]
==
"user"
for
entry
in
catalog
[
"entries"
])
assert
{
entry
[
"id"
]
for
entry
in
catalog
[
"entries"
]}
==
{
assert
{
entry
[
"id"
]
for
entry
in
catalog
[
"entries"
]}
==
{
"provider/local-openai/gpt-4o-mini"
,
"provider/local-openai/gpt-4o-mini"
,
"rotation/my-rotation"
,
"rotation/
rotation/
my-rotation"
,
"autoselect/my-autoselect"
,
"autoselect/
autoselect/
my-autoselect"
,
}
}
...
...
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