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
e1f4e104
Commit
e1f4e104
authored
May 10, 2026
by
Stefy Lanza (nextime / spora )
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
feat(studio): persist inferred model capabilities
parent
f00d8e85
Changes
5
Hide whitespace changes
Inline
Side-by-side
Showing
5 changed files
with
190 additions
and
1 deletion
+190
-1
config.py
aisbf/config.py
+5
-0
providers.py
aisbf/routes/dashboard/providers.py
+18
-1
studio.py
aisbf/studio.py
+21
-0
test_dashboard_studio.py
tests/routes/test_dashboard_studio.py
+103
-0
test_studio.py
tests/test_studio.py
+43
-0
No files found.
aisbf/config.py
View file @
e1f4e104
...
...
@@ -48,6 +48,11 @@ class ProviderModelConfig(BaseModel):
privacy
:
bool
=
False
# Model can handle privacy-sensitive content
# Response caching control
enable_response_cache
:
Optional
[
bool
]
=
None
# Enable/disable response caching for this model (None = use provider default)
# Studio capability metadata
studio_capabilities
:
Optional
[
List
[
str
]]
=
None
studio_capability_source
:
Optional
[
str
]
=
None
studio_capability_unknown
:
Optional
[
bool
]
=
None
studio_capability_notes
:
Optional
[
List
[
str
]]
=
None
class
CondensationConfig
(
BaseModel
):
...
...
aisbf/routes/dashboard/providers.py
View file @
e1f4e104
...
...
@@ -7,7 +7,7 @@ from datetime import datetime, timedelta
from
aisbf.database
import
DatabaseRegistry
from
aisbf.database
import
_hash_password
as
_db_hash_password
from
aisbf
import
__version__
from
aisbf.studio
import
build_studio_catalog
from
aisbf.studio
import
build_studio_catalog
,
stamp_inferred_capabilities
from
aisbf.app.templates
import
url_for
,
get_base_url
from
aisbf.app.startup
import
_reload_global_config
,
_apply_condense_defaults_provider
,
_apply_condense_defaults_rotation
,
_providers_json_path
,
_rotations_json_path
,
_autoselect_json_path
,
_claude_cli_mode
from
aisbf.app.middleware
import
_is_local_client
...
...
@@ -22,6 +22,19 @@ _server_config = None
logger
=
logging
.
getLogger
(
__name__
)
def
_stamp_provider_models
(
provider_config
:
dict
)
->
dict
:
stamped
=
dict
(
provider_config
)
provider_type
=
stamped
.
get
(
"type"
,
"openai"
)
models
=
stamped
.
get
(
"models"
)
if
isinstance
(
models
,
list
):
stamped
[
"models"
]
=
[
stamp_inferred_capabilities
(
model
,
provider_type
)
if
isinstance
(
model
,
dict
)
else
model
for
model
in
models
]
return
stamped
def
init
(
config
,
templates
,
server_config
=
None
):
global
_config
,
_templates
,
_server_config
_config
=
config
...
...
@@ -398,6 +411,8 @@ async def _auto_detect_provider_models(provider_key: str, provider: dict) -> lis
'max_request_tokens'
:
int
(
context_size
)
if
context_size
else
100000
,
'context_size'
:
int
(
context_size
)
if
context_size
else
100000
})
detected_models
=
[
stamp_inferred_capabilities
(
model
,
provider_type
)
for
model
in
detected_models
]
logger
.
info
(
f
"Auto-detected {len(detected_models)} models for provider '{provider_key}' from {models_url}"
)
return
detected_models
...
...
@@ -429,6 +444,7 @@ async def dashboard_providers_save(request: Request, config: str = Form(...)):
if
'condense_method'
in
model
and
model
.
get
(
'condense_method'
):
if
'condense_context'
not
in
model
or
model
.
get
(
'condense_context'
)
is
None
:
model
[
'condense_context'
]
=
80
providers_data
[
provider_key
]
=
_stamp_provider_models
(
provider
)
if
is_config_admin
:
# Config admin: save to JSON files
...
...
@@ -1452,6 +1468,7 @@ async def api_provider_save(request: Request):
return
JSONResponse
({
"success"
:
False
,
"error"
:
"provider_id required"
},
status_code
=
400
)
_apply_condense_defaults_provider
(
provider_config
)
provider_config
=
_stamp_provider_models
(
provider_config
)
if
is_config_admin
:
config_path
=
_providers_json_path
()
...
...
aisbf/studio.py
View file @
e1f4e104
...
...
@@ -93,6 +93,27 @@ def normalize_capabilities(values: Optional[Iterable[str]]) -> List[str]:
return
_dedupe
(
normalized
)
def
stamp_inferred_capabilities
(
model
:
Dict
[
str
,
Any
],
provider_type
:
str
)
->
Dict
[
str
,
Any
]:
stamped
=
dict
(
model
)
capability_result
=
infer_model_capabilities
(
model_name
=
stamped
.
get
(
"name"
)
or
stamped
.
get
(
"id"
)
or
""
,
provider_type
=
provider_type
,
explicit_capabilities
=
stamped
.
get
(
"capabilities"
)
or
stamped
.
get
(
"studio_capabilities"
),
architecture
=
stamped
.
get
(
"architecture"
),
provider_metadata
=
stamped
,
)
stamped
[
"capabilities"
]
=
capability_result
.
capabilities
stamped
[
"studio_capabilities"
]
=
capability_result
.
capabilities
stamped
[
"studio_capability_source"
]
=
capability_result
.
source
stamped
[
"studio_capability_unknown"
]
=
capability_result
.
unknown
if
capability_result
.
notes
:
stamped
[
"studio_capability_notes"
]
=
capability_result
.
notes
elif
"studio_capability_notes"
in
stamped
:
stamped
.
pop
(
"studio_capability_notes"
,
None
)
return
stamped
def
infer_model_capabilities
(
model_name
:
str
,
provider_type
:
str
,
...
...
tests/routes/test_dashboard_studio.py
View file @
e1f4e104
...
...
@@ -327,6 +327,109 @@ def test_build_studio_catalog_uses_user_owned_resources_for_user_scope():
}
def
test_api_provider_save_persists_inferred_studio_metadata_for_manual_models
(
monkeypatch
):
client
=
TestClient
(
app
)
_login_as_admin
(
client
)
saved_payload
=
{}
monkeypatch
.
setattr
(
dashboard_providers
,
"_providers_json_path"
,
lambda
:
Path
(
"/tmp/providers-source.json"
))
monkeypatch
.
setattr
(
dashboard_providers
,
"_reload_global_config"
,
lambda
:
None
)
monkeypatch
.
setattr
(
dashboard_providers
,
"open"
,
lambda
*
args
,
**
kwargs
:
DummyOpen
(
saved_payload
),
raising
=
False
)
response
=
client
.
post
(
"/dashboard/api/provider"
,
json
=
{
"provider_id"
:
"openai"
,
"config"
:
{
"type"
:
"openai"
,
"models"
:
[
{
"name"
:
"whisper-large-v3"
,
"architecture"
:
{
"input_modalities"
:
[
"audio"
],
"output_modalities"
:
[
"text"
]},
}
],
},
},
)
assert
response
.
status_code
==
200
saved
=
json
.
loads
(
saved_payload
[
"writes"
][
-
1
])
model
=
saved
[
"providers"
][
"openai"
][
"models"
][
0
]
assert
model
[
"studio_capabilities"
]
==
[
"audio_input"
,
"transcription"
]
assert
model
[
"studio_capability_source"
]
in
{
"provider_metadata"
,
"heuristic"
}
assert
model
[
"studio_capability_unknown"
]
is
False
def
test_dashboard_providers_save_persists_inferred_studio_metadata_for_bulk_save
(
monkeypatch
):
client
=
TestClient
(
app
)
_login_as_admin
(
client
)
saved_payload
=
{}
monkeypatch
.
setattr
(
dashboard_providers
,
"_reload_global_config"
,
lambda
:
None
)
monkeypatch
.
setattr
(
dashboard_providers
,
"open"
,
lambda
*
args
,
**
kwargs
:
DummyOpen
(
saved_payload
),
raising
=
False
)
response
=
client
.
post
(
"/dashboard/providers"
,
data
=
{
"config"
:
json
.
dumps
(
{
"openai"
:
{
"type"
:
"openai"
,
"models"
:
[
{
"name"
:
"whisper-large-v3"
,
"architecture"
:
{
"input_modalities"
:
[
"audio"
],
"output_modalities"
:
[
"text"
]},
}
],
}
}
)
},
)
assert
response
.
status_code
==
200
saved
=
json
.
loads
(
saved_payload
[
"writes"
][
-
1
])
model
=
saved
[
"providers"
][
"openai"
][
"models"
][
0
]
assert
model
[
"studio_capabilities"
]
==
[
"audio_input"
,
"transcription"
]
assert
model
[
"studio_capability_source"
]
in
{
"provider_metadata"
,
"heuristic"
}
assert
model
[
"studio_capability_unknown"
]
is
False
class
DummyOpen
:
def
__init__
(
self
,
sink
):
self
.
sink
=
sink
self
.
buffer
=
""
self
.
sink
.
setdefault
(
"writes"
,
[])
def
__call__
(
self
,
path
,
mode
=
"r"
,
*
args
,
**
kwargs
):
self
.
path
=
str
(
path
)
self
.
mode
=
mode
self
.
buffer
=
""
return
self
def
__enter__
(
self
):
if
"w"
in
getattr
(
self
,
"mode"
,
""
):
self
.
sink
.
setdefault
(
"writes"
,
[])
.
append
(
""
)
return
self
def
__exit__
(
self
,
exc_type
,
exc
,
tb
):
return
False
def
read
(
self
):
if
any
(
token
in
getattr
(
self
,
"path"
,
""
)
for
token
in
[
"providers-source"
,
".aisbf/providers.json"
,
"routes/dashboard/config/providers.json"
]):
return
json
.
dumps
({
"providers"
:
{}})
writes
=
self
.
sink
.
get
(
"writes"
)
or
[]
return
writes
[
-
1
]
if
writes
else
json
.
dumps
({
"providers"
:
{}})
def
write
(
self
,
data
):
self
.
buffer
+=
data
self
.
sink
.
setdefault
(
"writes"
,
[
""
])
if
not
self
.
sink
[
"writes"
]:
self
.
sink
[
"writes"
]
.
append
(
""
)
self
.
sink
[
"writes"
][
-
1
]
=
self
.
buffer
def
_find_session_secret
()
->
str
:
for
middleware
in
app
.
user_middleware
:
kwargs
=
getattr
(
middleware
,
"kwargs"
,
{})
...
...
tests/test_studio.py
View file @
e1f4e104
...
...
@@ -5,6 +5,7 @@ from aisbf.studio import (
build_catalog_entry
,
infer_model_capabilities
,
merge_capabilities
,
stamp_inferred_capabilities
,
)
...
...
@@ -97,3 +98,45 @@ def test_infer_model_capabilities_does_not_fallback_to_chat_for_media_models(mod
)
assert
"chat"
not
in
result
.
capabilities
def
test_stamp_inferred_capabilities_persists_inferred_metadata_for_model_dict
():
stamped
=
stamp_inferred_capabilities
(
{
"name"
:
"whisper-large-v3"
,
"architecture"
:
{
"input_modalities"
:
[
"audio"
],
"output_modalities"
:
[
"text"
]},
},
provider_type
=
"openai"
,
)
assert
stamped
[
"studio_capabilities"
]
==
[
"audio_input"
,
"transcription"
]
assert
stamped
[
"studio_capability_source"
]
in
{
"provider_metadata"
,
"heuristic"
}
assert
stamped
[
"studio_capability_unknown"
]
is
False
assert
stamped
[
"capabilities"
]
==
[
"audio_input"
,
"transcription"
]
def
test_build_catalog_entry_uses_persisted_studio_capability_metadata_when_available
():
entry
=
build_catalog_entry
(
scope
=
"admin"
,
owner_id
=
None
,
kind
=
"provider_model"
,
source_id
=
"openai"
,
target_id
=
"whisper-large-v3"
,
label
=
"Whisper Large V3"
,
description
=
None
,
capabilities
=
[
"audio_input"
,
"transcription"
],
availability_state
=
"ready"
,
availability_reason
=
None
,
metadata
=
{
"provider_type"
:
"openai"
,
"capability_source"
:
"heuristic"
,
"capability_notes"
:
[],
"studio_capabilities"
:
[
"audio_input"
,
"transcription"
],
"studio_capability_source"
:
"heuristic"
,
"studio_capability_unknown"
:
False
,
},
)
assert
entry
[
"metadata"
][
"studio_capabilities"
]
==
[
"audio_input"
,
"transcription"
]
assert
entry
[
"metadata"
][
"studio_capability_source"
]
==
"heuristic"
assert
entry
[
"metadata"
][
"studio_capability_unknown"
]
is
False
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