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
006441ea
Commit
006441ea
authored
May 12, 2026
by
Stefy Lanza (nextime / spora )
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Fix market
parent
a8b5285c
Changes
11
Show whitespace changes
Inline
Side-by-side
Showing
11 changed files
with
295 additions
and
75 deletions
+295
-75
middleware.py
aisbf/app/middleware.py
+19
-1
database.py
aisbf/database.py
+71
-0
providers.py
aisbf/routes/dashboard/providers.py
+91
-49
studio.css
static/dashboard/studio.css
+13
-0
studio.js
static/dashboard/studio.js
+13
-0
admin_market.html
templates/dashboard/admin_market.html
+3
-1
providers.html
templates/dashboard/providers.html
+7
-6
studio.html
templates/dashboard/studio.html
+3
-2
user_providers.html
templates/dashboard/user_providers.html
+5
-5
test_market_reference_imports.py
tests/routes/test_market_reference_imports.py
+18
-7
test_geolocation.py
tests/test_geolocation.py
+52
-4
No files found.
aisbf/app/middleware.py
View file @
006441ea
...
...
@@ -5,6 +5,7 @@ import time
import
logging
import
threading
import
hmac
as
_hmac
import
ipaddress
from
typing
import
Optional
from
fastapi
import
Request
from
fastapi.responses
import
JSONResponse
,
RedirectResponse
...
...
@@ -83,6 +84,23 @@ def _is_local_client(request: Request) -> bool:
return
ip
in
_LOCAL_IPS
if
ip
else
False
def
_is_private_or_local_ip
(
ip
:
Optional
[
str
])
->
bool
:
if
not
ip
:
return
False
if
ip
in
_LOCAL_IPS
:
return
True
try
:
addr
=
ipaddress
.
ip_address
(
ip
)
except
ValueError
:
return
False
return
any
((
addr
.
is_private
,
addr
.
is_loopback
,
addr
.
is_link_local
,
addr
.
is_reserved
,
))
class
GenocidalBlockingMiddleware
(
BaseHTTPMiddleware
):
"""Block Israeli IPs/domains."""
...
...
@@ -105,7 +123,7 @@ class GenocidalBlockingMiddleware(BaseHTTPMiddleware):
return
True
from
aisbf
import
geolocation
client_ip
=
_get_client_ip
(
request
)
if
client_ip
:
if
client_ip
and
not
_is_private_or_local_ip
(
client_ip
)
:
country
=
await
geolocation
.
get_ip_country
(
client_ip
)
if
country
==
'IL'
:
return
True
...
...
aisbf/database.py
View file @
006441ea
...
...
@@ -3534,6 +3534,77 @@ class DatabaseManager:
cursor
.
execute
(
query
)
return
[
self
.
_load_market_listing_row
(
row
)
for
row
in
cursor
.
fetchall
()]
def
list_market_listings_paginated
(
self
,
page
:
int
=
1
,
limit
:
int
=
25
,
search
:
Optional
[
str
]
=
None
,
source_type
:
Optional
[
str
]
=
None
,
active_filter
:
Optional
[
str
]
=
None
,
online_filter
:
Optional
[
str
]
=
None
,
owner_username
:
Optional
[
str
]
=
None
,
)
->
Dict
[
str
,
Any
]:
page
=
max
(
int
(
page
or
1
),
1
)
limit
=
max
(
1
,
min
(
int
(
limit
or
25
),
100
))
offset
=
(
page
-
1
)
*
limit
where_clauses
=
[]
params
:
List
[
Any
]
=
[]
placeholder
=
self
.
placeholder
if
search
:
like
=
f
"
%
{search.strip()}
%
"
where_clauses
.
append
(
f
"(title LIKE {placeholder} OR description LIKE {placeholder} OR source_id LIKE {placeholder} OR provider_id LIKE {placeholder} OR model_id LIKE {placeholder} OR owner_username LIKE {placeholder})"
)
params
.
extend
([
like
,
like
,
like
,
like
,
like
,
like
])
if
source_type
:
where_clauses
.
append
(
f
"source_type = {placeholder}"
)
params
.
append
(
source_type
)
if
owner_username
:
where_clauses
.
append
(
f
"owner_username = {placeholder}"
)
params
.
append
(
owner_username
)
if
active_filter
==
'active'
:
where_clauses
.
append
(
"is_active = 1"
)
elif
active_filter
==
'inactive'
:
where_clauses
.
append
(
"is_active = 0"
)
if
online_filter
==
'online'
:
where_clauses
.
append
(
"provider_id IS NOT NULL"
)
elif
online_filter
==
'offline'
:
where_clauses
.
append
(
"provider_id IS NULL"
)
where_sql
=
f
" WHERE {' AND '.join(where_clauses)}"
if
where_clauses
else
""
with
self
.
_get_connection
()
as
conn
:
cursor
=
conn
.
cursor
()
cursor
.
execute
(
f
"SELECT COUNT(*) FROM market_listings{where_sql}"
,
tuple
(
params
))
total_row
=
cursor
.
fetchone
()
total
=
int
(
total_row
[
0
]
if
total_row
else
0
)
query
=
f
'''
SELECT id, owner_user_id, owner_username, source_scope, source_type, source_id, listing_key,
title, description, provider_id, model_id, endpoint, currency_code,
price_per_million_tokens, metadata, config_snapshot, price_per_1000_requests,
provider_price_per_million_tokens, provider_price_per_1000_requests, is_active,
created_at, updated_at
FROM market_listings
{where_sql}
ORDER BY created_at DESC
LIMIT {placeholder} OFFSET {placeholder}
'''
cursor
.
execute
(
query
,
tuple
(
params
+
[
limit
,
offset
]))
items
=
[
self
.
_load_market_listing_row
(
row
)
for
row
in
cursor
.
fetchall
()]
return
{
'items'
:
items
,
'total'
:
total
,
'page'
:
page
,
'limit'
:
limit
,
}
def
get_market_listing
(
self
,
listing_id
:
int
)
->
Optional
[
Dict
[
str
,
Any
]]:
with
self
.
_get_connection
()
as
conn
:
cursor
=
conn
.
cursor
()
...
...
aisbf/routes/dashboard/providers.py
View file @
006441ea
...
...
@@ -153,6 +153,19 @@ def _stamp_provider_models(provider_config: dict) -> dict:
return
stamped
def
_json_parse_bootstrap
(
payload
)
->
str
:
raw_json
=
json
.
dumps
(
payload
)
escaped_json
=
(
raw_json
.
replace
(
"<"
,
"
\\
u003c"
)
.
replace
(
">"
,
"
\\
u003e"
)
.
replace
(
"&"
,
"
\\
u0026"
)
)
quoted
=
escaped_json
.
replace
(
"
\\
"
,
"
\\\\
"
)
.
replace
(
'"'
,
'
\\
"'
)
quoted
=
quoted
.
replace
(
"
\\\\
u003c"
,
"
\\
u003c"
)
.
replace
(
"
\\\\
u003e"
,
"
\\
u003e"
)
.
replace
(
"
\\\\
u0026"
,
"
\\
u0026"
)
return
f
'"{quoted}"'
def
_ensure_coderai_token
(
provider_config
:
dict
)
->
dict
:
stamped
=
dict
(
provider_config
or
{})
if
stamped
.
get
(
'type'
)
!=
'coderai'
:
...
...
@@ -249,18 +262,22 @@ def _augment_provider_broker_status(provider_id: str, provider_config: dict, bro
return
stamped
status
=
broker_status_map
.
get
(
provider_id
)
coderai_config
=
dict
(
stamped
.
get
(
'coderai_config'
)
or
{})
status_metadata
=
dict
((
status
or
{})
.
get
(
'metadata'
)
or
{})
status_performance
=
dict
((
status
or
{})
.
get
(
'performance'
)
or
{})
coderai_config
[
'broker_session'
]
=
{
'connected'
:
bool
(
status
),
'client_id'
:
status
.
get
(
'client_id'
)
if
status
else
None
,
'session_id'
:
status
.
get
(
'session_id'
)
if
status
else
None
,
'connected_at'
:
status
.
get
(
'connected_at'
)
if
status
else
None
,
'last_seen'
:
status
.
get
(
'last_seen'
)
if
status
else
None
,
'owner_user_id'
:
((
status
or
{})
.
get
(
'metadata'
)
or
{})
.
get
(
'owner_user_id'
),
'transport'
:
((
status
or
{})
.
get
(
'metadata'
)
or
{})
.
get
(
'transport'
),
'endpoint'
:
((
status
or
{})
.
get
(
'metadata'
)
or
{})
.
get
(
'endpoint'
),
'studio_endpoints'
:
((
status
or
{})
.
get
(
'metadata'
)
or
{})
.
get
(
'studio_endpoints'
)
or
[],
'owner_user_id'
:
status_metadata
.
get
(
'owner_user_id'
),
'transport'
:
status_metadata
.
get
(
'transport'
),
'endpoint'
:
status_metadata
.
get
(
'endpoint'
),
'studio_endpoints'
:
status_metadata
.
get
(
'studio_endpoints'
)
or
[],
'capabilities'
:
(
status
or
{})
.
get
(
'capabilities'
)
or
{},
'connection_state'
:
((
status
or
{})
.
get
(
'metadata'
)
or
{})
.
get
(
'connection_state'
)
or
(
'connected'
if
status
else
'disconnected'
),
'connection_state'
:
status_metadata
.
get
(
'connection_state'
)
or
(
'connected'
if
status
else
'disconnected'
),
'metadata'
:
status_metadata
,
'performance'
:
status_performance
,
}
stamped
[
'coderai_config'
]
=
coderai_config
return
stamped
...
...
@@ -272,6 +289,14 @@ def init(config, templates, server_config=None):
_server_config
=
server_config
def
_get_templates
():
global
_templates
if
_templates
is
None
:
from
main
import
templates
as
main_templates
_templates
=
main_templates
return
_templates
def
get_user_auth_files_dir
(
user_id
)
->
Path
:
auth_files_dir
=
Path
.
home
()
/
'.aisbf'
/
'user_auth_files'
/
str
(
user_id
)
auth_files_dir
.
mkdir
(
parents
=
True
,
exist_ok
=
True
)
...
...
@@ -333,7 +358,7 @@ async def dashboard_index(request: Request):
# Admin dashboard
db
=
DatabaseRegistry
.
get_config_database
()
users_count
=
len
(
db
.
get_users
())
return
_
templates
.
TemplateResponse
(
return
_
get_templates
()
.
TemplateResponse
(
request
=
request
,
name
=
"dashboard/index.html"
,
context
=
{
...
...
@@ -403,7 +428,7 @@ async def dashboard_index(request: Request):
elif
all_tiers
:
upgrade_tiers
=
[
t
for
t
in
all_tiers
if
not
t
.
get
(
'is_default'
)]
return
_
templates
.
TemplateResponse
(
return
_
get_templates
()
.
TemplateResponse
(
request
=
request
,
name
=
"dashboard/user_index.html"
,
context
=
{
...
...
@@ -438,7 +463,17 @@ async def dashboard_providers(request: Request):
broker_status_map
=
await
_load_coderai_broker_status_map
()
if
is_config_admin
:
# Config admin: load from JSON files
# Config admin: prefer live in-memory config when available
live_config
=
_config
if
live_config
is
None
:
from
aisbf.config
import
config
as
global_config
live_config
=
global_config
if
live_config
and
getattr
(
live_config
,
'providers'
,
None
):
providers_data
=
{
provider_id
:
(
provider
.
model_dump
()
if
hasattr
(
provider
,
'model_dump'
)
else
dict
(
provider
))
for
provider_id
,
provider
in
live_config
.
providers
.
items
()
}
else
:
config_path
=
Path
.
home
()
/
'.aisbf'
/
'providers.json'
if
not
config_path
.
exists
():
config_path
=
Path
(
__file__
)
.
parent
/
'config'
/
'providers.json'
...
...
@@ -503,10 +538,10 @@ async def dashboard_providers(request: Request):
"request"
:
request
,
"session"
:
request
.
session
,
"__version__"
:
__version__
,
"providers_
json"
:
json
.
dumps
(
providers_data
)
,
"studio_capability_choices
_json"
:
json
.
dumps
(
serialize_studio_capability_choices
()
),
"studio_adapter_choices
_json"
:
json
.
dumps
(
serialize_studio_adapter_choices
()
),
"studio_adapter_profile_choices
_json"
:
json
.
dumps
(
serialize_studio_adapter_profile_choices
()
),
"providers_
data"
:
providers_data
,
"studio_capability_choices
"
:
serialize_studio_capability_choices
(
),
"studio_adapter_choices
"
:
serialize_studio_adapter_choices
(
),
"studio_adapter_profile_choices
"
:
serialize_studio_adapter_profile_choices
(
),
"claude_cli_mode"
:
_claude_cli_mode
,
"is_local_client"
:
_is_local_client
(
request
),
"success"
:
"Configuration saved successfully!"
if
success
else
None
...
...
@@ -521,10 +556,11 @@ async def dashboard_providers(request: Request):
"request"
:
request
,
"session"
:
request
.
session
,
"__version__"
:
__version__
,
"user_providers_json"
:
json
.
dumps
(
providers_data
),
"studio_capability_choices_json"
:
json
.
dumps
(
serialize_studio_capability_choices
()),
"studio_adapter_choices_json"
:
json
.
dumps
(
serialize_studio_adapter_choices
()),
"studio_adapter_profile_choices_json"
:
json
.
dumps
(
serialize_studio_adapter_profile_choices
()),
"user_providers_data"
:
providers_data
,
"user_providers_bootstrap_json"
:
_json_parse_bootstrap
(
providers_data
),
"studio_capability_choices"
:
serialize_studio_capability_choices
(),
"studio_adapter_choices"
:
serialize_studio_adapter_choices
(),
"studio_adapter_profile_choices"
:
serialize_studio_adapter_profile_choices
(),
"user_id"
:
current_user_id
,
"claude_cli_mode"
:
_claude_cli_mode
,
"is_local_client"
:
_is_local_client
(
request
),
...
...
@@ -551,7 +587,7 @@ async def dashboard_studio(request: Request):
db
=
db
,
)
return
_
templates
.
TemplateResponse
(
return
_
get_templates
()
.
TemplateResponse
(
request
=
request
,
name
=
"dashboard/studio.html"
,
context
=
{
...
...
@@ -814,17 +850,19 @@ async def dashboard_providers_save(request: Request, config: str = Form(...)):
if
is_config_admin
:
success_msg
=
"Configuration saved successfully!"
return
_templates
.
TemplateResponse
(
if
_templates
is
None
:
return
JSONResponse
({
"success"
:
True
,
"message"
:
success_msg
,
"providers_data"
:
providers_data
})
return
_get_templates
()
.
TemplateResponse
(
request
=
request
,
name
=
"dashboard/providers.html"
,
context
=
{
"request"
:
request
,
"session"
:
request
.
session
,
"__version__"
:
__version__
,
"providers_
json"
:
json
.
dumps
(
providers_data
)
,
"studio_capability_choices
_json"
:
json
.
dumps
(
serialize_studio_capability_choices
()
),
"studio_adapter_choices
_json"
:
json
.
dumps
(
serialize_studio_adapter_choices
()
),
"studio_adapter_profile_choices
_json"
:
json
.
dumps
(
serialize_studio_adapter_profile_choices
()
),
"providers_
data"
:
providers_data
,
"studio_capability_choices
"
:
serialize_studio_capability_choices
(
),
"studio_adapter_choices
"
:
serialize_studio_adapter_choices
(
),
"studio_adapter_profile_choices
"
:
serialize_studio_adapter_profile_choices
(
),
"claude_cli_mode"
:
_claude_cli_mode
,
"is_local_client"
:
_is_local_client
(
request
),
"success"
:
success_msg
...
...
@@ -832,18 +870,21 @@ async def dashboard_providers_save(request: Request, config: str = Form(...)):
)
else
:
success_msg
=
"Configuration saved successfully!"
if
_templates
is
None
:
return
JSONResponse
({
"success"
:
True
,
"message"
:
success_msg
,
"providers_data"
:
providers_data
})
return
_
templates
.
TemplateResponse
(
return
_
get_templates
()
.
TemplateResponse
(
request
=
request
,
name
=
"dashboard/user_providers.html"
,
context
=
{
"request"
:
request
,
"session"
:
request
.
session
,
"__version__"
:
__version__
,
"user_providers_json"
:
json
.
dumps
(
providers_data
),
"studio_capability_choices_json"
:
json
.
dumps
(
serialize_studio_capability_choices
()),
"studio_adapter_choices_json"
:
json
.
dumps
(
serialize_studio_adapter_choices
()),
"studio_adapter_profile_choices_json"
:
json
.
dumps
(
serialize_studio_adapter_profile_choices
()),
"user_providers_data"
:
providers_data
,
"user_providers_bootstrap_json"
:
_json_parse_bootstrap
(
providers_data
),
"studio_capability_choices"
:
serialize_studio_capability_choices
(),
"studio_adapter_choices"
:
serialize_studio_adapter_choices
(),
"studio_adapter_profile_choices"
:
serialize_studio_adapter_profile_choices
(),
"user_id"
:
current_user_id
,
"claude_cli_mode"
:
_claude_cli_mode
,
"is_local_client"
:
_is_local_client
(
request
),
...
...
@@ -868,17 +909,17 @@ async def dashboard_providers_save(request: Request, config: str = Form(...)):
else
:
providers_data
=
{
k
:
v
for
k
,
v
in
full_config
.
items
()
if
k
!=
'condensation'
}
return
_
templates
.
TemplateResponse
(
return
_
get_templates
()
.
TemplateResponse
(
request
=
request
,
name
=
"dashboard/providers.html"
,
context
=
{
"request"
:
request
,
"session"
:
request
.
session
,
"__version__"
:
__version__
,
"providers_
json"
:
json
.
dumps
(
providers_data
)
,
"studio_capability_choices
_json"
:
json
.
dumps
(
serialize_studio_capability_choices
()
),
"studio_adapter_choices
_json"
:
json
.
dumps
(
serialize_studio_adapter_choices
()
),
"studio_adapter_profile_choices
_json"
:
json
.
dumps
(
serialize_studio_adapter_profile_choices
()
),
"providers_
data"
:
providers_data
,
"studio_capability_choices
"
:
serialize_studio_capability_choices
(
),
"studio_adapter_choices
"
:
serialize_studio_adapter_choices
(
),
"studio_adapter_profile_choices
"
:
serialize_studio_adapter_profile_choices
(
),
"claude_cli_mode"
:
_claude_cli_mode
,
"is_local_client"
:
_is_local_client
(
request
),
"error"
:
f
"Invalid JSON: {str(e)}"
...
...
@@ -888,17 +929,18 @@ async def dashboard_providers_save(request: Request, config: str = Form(...)):
db
=
DatabaseRegistry
.
get_config_database
()
user_providers
=
db
.
get_user_providers
(
current_user_id
)
return
_
templates
.
TemplateResponse
(
return
_
get_templates
()
.
TemplateResponse
(
request
=
request
,
name
=
"dashboard/user_providers.html"
,
context
=
{
"request"
:
request
,
"session"
:
request
.
session
,
"__version__"
:
__version__
,
"user_providers_json"
:
json
.
dumps
(
user_providers
),
"studio_capability_choices_json"
:
json
.
dumps
(
serialize_studio_capability_choices
()),
"studio_adapter_choices_json"
:
json
.
dumps
(
serialize_studio_adapter_choices
()),
"studio_adapter_profile_choices_json"
:
json
.
dumps
(
serialize_studio_adapter_profile_choices
()),
"user_providers_data"
:
user_providers
,
"user_providers_bootstrap_json"
:
_json_parse_bootstrap
(
user_providers
),
"studio_capability_choices"
:
serialize_studio_capability_choices
(),
"studio_adapter_choices"
:
serialize_studio_adapter_choices
(),
"studio_adapter_profile_choices"
:
serialize_studio_adapter_profile_choices
(),
"user_id"
:
current_user_id
,
"claude_cli_mode"
:
_claude_cli_mode
,
"is_local_client"
:
_is_local_client
(
request
),
...
...
static/dashboard/studio.css
View file @
006441ea
...
...
@@ -2,6 +2,19 @@
* Copyright (C) 2026 Stefy Lanza <stefy@nexlab.net>
*
* AISBF - AI Service Broker Framework || AI Should Be Free
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
.studio
{
...
...
static/dashboard/studio.js
View file @
006441ea
...
...
@@ -2,6 +2,19 @@
* Copyright (C) 2026 Stefy Lanza <stefy@nexlab.net>
*
* AISBF - AI Service Broker Framework || AI Should Be Free
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
// ─────────────────────────────────────────────────────────────────
...
...
templates/dashboard/admin_market.html
View file @
006441ea
...
...
@@ -98,7 +98,9 @@
{% if pagination.has_prev %}
<a
class=
"btn"
href=
"{{ url_for(request, '/dashboard/admin/market') }}?q={{ filters.q|urlencode }}&source_type={{ filters.source_type|urlencode }}&active_filter={{ filters.active_filter|urlencode }}&online_filter={{ filters.online_filter|urlencode }}&owner_username={{ filters.owner_username|urlencode }}&limit={{ filters.limit }}&page={{ pagination.prev_page }}"
style=
"background: var(--bg-accent); color: var(--color-text);"
>
Previous
</a>
{% endif %}
<a
class=
"btn"
href=
"{{ url_for(request, '/dashboard/admin/market') }}?q={{ filters.q|urlencode }}&source_type={{ filters.source_type|urlencode }}&active_filter={{ filters.active_filter|urlencode }}&online_filter={{ filters.online_filter|urlencode }}&owner_username={{ filters.owner_username|urlencode }}&limit={{ filters.limit }}&page={{ pagination.page }}"
style=
"background: var(--color-link); color: white;"
>
Current Page
</a>
{% if pagination.total_pages > 1 %}
<a
class=
"btn"
href=
"{{ url_for(request, '/dashboard/admin/market') }}?q={{ filters.q|urlencode }}&source_type={{ filters.source_type|urlencode }}&active_filter={{ filters.active_filter|urlencode }}&online_filter={{ filters.online_filter|urlencode }}&owner_username={{ filters.owner_username|urlencode }}&limit={{ filters.limit }}&page={{ pagination.page }}"
style=
"background: var(--color-link); color: white;"
>
{{ pagination.page }}
</a>
{% endif %}
{% if pagination.has_next %}
<a
class=
"btn"
href=
"{{ url_for(request, '/dashboard/admin/market') }}?q={{ filters.q|urlencode }}&source_type={{ filters.source_type|urlencode }}&active_filter={{ filters.active_filter|urlencode }}&online_filter={{ filters.online_filter|urlencode }}&owner_username={{ filters.owner_username|urlencode }}&limit={{ filters.limit }}&page={{ pagination.next_page }}"
style=
"background: var(--bg-accent); color: var(--color-text);"
>
Next
</a>
{% endif %}
...
...
templates/dashboard/providers.html
View file @
006441ea
...
...
@@ -19,6 +19,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
{% block title %}Providers - AISBF Dashboard{% endblock %}
{% block content %}
<script
id=
"providers-bootstrap"
type=
"application/json"
>
{{
providers_data
|
tojson
}}
</script>
<h2
style=
"margin-bottom: 30px;"
>
Providers Configuration
</h2>
{% if success %}
...
...
@@ -306,10 +307,10 @@ const IS_LOCAL_CLIENT = {{ 'true' if is_local_client else 'false' }};
const BASE_PATH = {{ (request.scope.get('root_path', '') or '') | tojson }};
// Marker used by the AISBF Chrome Extension to auto-detect this page and configure itself.
window.AISBF_PROVIDERS_PAGE = { serverUrl: window.location.origin + BASE_PATH };
let providersData =
JSON.parse({{ providers_json | tojson }})
;
const STUDIO_CAPABILITY_CHOICES =
JSON.parse({{ studio_capability_choices_json | tojson }})
;
const STUDIO_ADAPTER_CHOICES =
JSON.parse({{ studio_adapter_choices_json | tojson }})
;
const STUDIO_ADAPTER_PROFILE_CHOICES =
JSON.parse({{ studio_adapter_profile_choices_json | tojson }})
;
let providersData =
{{ providers_data | tojson }}
;
const STUDIO_CAPABILITY_CHOICES =
{{ studio_capability_choices | tojson }}
;
const STUDIO_ADAPTER_CHOICES =
{{ studio_adapter_choices | tojson }}
;
const STUDIO_ADAPTER_PROFILE_CHOICES =
{{ studio_adapter_profile_choices | tojson }}
;
let expandedProviders = new Set();
let currentProviderPage = 0;
const PROVIDERS_PAGE_SIZE = 10;
...
...
@@ -1208,11 +1209,11 @@ function renderProviderDetails(key) {
<div><strong>Connected At:</strong>
${
escHtmlAttr
(
formatBrokerTimestamp
(
brokerSession
.
connected_at
))}
</div>
<div><strong>Remote Endpoint:</strong>
${
escHtmlAttr
(
brokerSession
.
endpoint
||
'Unknown'
)}
</div>
<div><strong>Transport:</strong>
${
escHtmlAttr
(
brokerSession
.
transport
||
'broker'
)}
</div>
<div><strong>GPU Count:</strong>
${
escHtmlAttr
(
String
(
brokerMetadata
.
gpu_count
??
(
Array
.
isArray
(
brokerMetadata
.
gpus
)
?
brokerMetadata
.
gpus
.
length
:
0
)
||
'0'
))}
<
/div
>
<div><strong>GPU Count:</strong>
${
escHtmlAttr
(
String
(
(
brokerMetadata
.
gpu_count
??
(
Array
.
isArray
(
brokerMetadata
.
gpus
)
?
brokerMetadata
.
gpus
.
length
:
0
)
)
||
'0'
))}
<
/div
>
<
div
><
strong
>
VRAM
:
<
/strong> ${escHtmlAttr
(
formatVramMb
(
brokerMetadata.available_vram_mb
))
} free /
$
{
escHtmlAttr
(
formatVramMb
(
brokerMetadata
.
total_vram_mb
))}
total
<
/div
>
<
div
><
strong
>
GPUs
:
<
/strong> ${escHtmlAttr
(
renderGpuSummary
(
brokerMetadata
))
}</
div
>
<
div
><
strong
>
Avg
Latency
:
<
/strong> ${escHtmlAttr
(
formatPerfNumber
(
performance.avg_latency_ms, 1, ' ms'
))
}</
div
>
<
div
><
strong
>
Avg
Throughput
:
<
/strong> ${escHtmlAttr
(
formatPerfNumber
(
performance.avg_tokens_per_second,
2
, ' tok/
s
'))}</div>
<
div
><
strong
>
Avg
Throughput
:
<
/strong> ${escHtmlAttr
(
formatPerfNumber
(
performance.avg_tokens_per_second,
1
, ' tok/
s
'))}</div>
<div><strong>Avg Tokens:</strong> ${escHtmlAttr(formatPerfNumber(performance.avg_total_tokens, 1))}</div>
<div><strong>Success Rate:</strong> ${escHtmlAttr(formatPerfNumber((performance.success_rate ?? 0) * 100, 1, '
%
'))}</div>
<div><strong>Samples:</strong> ${escHtmlAttr(String(performance.sample_count ?? 0))} / ${escHtmlAttr(String(performance.window_size ?? 100))}</div>
...
...
templates/dashboard/studio.html
View file @
006441ea
...
...
@@ -27,7 +27,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
{% block content %}
<script
id=
"studio-bootstrap"
type=
"application/json"
>
{{
studio_bootstrap_json
|
safe
}}
</script>
<div
class=
"studio"
>
<div
class=
"studio"
data-studio-shell=
"dashboard"
>
<!-- Sidebar -->
<aside
class=
"sidebar"
>
...
...
@@ -1039,9 +1039,10 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
<!-- ═══════════════ PIPELINES ═══════════════ -->
<div
class=
"panel"
id=
"panel-pipe"
>
<div
class=
"pipe-panel"
>
<div
class=
"diag-card"
id=
"studio-diagnostics"
>
<div
class=
"diag-card"
id=
"studio-diagnostics"
data-empty-message=
"No diagnostics yet."
>
<div
class=
"diag-title"
>
Diagnostics
</div>
<div
class=
"diag-sub"
>
Evaluated from the current model type, declared capabilities, and existing Studio fallback rules.
</div>
<span
data-i18n=
"studio.diagnostics_empty"
>
No diagnostics yet.
</span>
<div
class=
"diag-groups"
id=
"diag-groups"
></div>
</div>
<div
class=
"hist-card"
id=
"studio-history"
>
...
...
templates/dashboard/user_providers.html
View file @
006441ea
...
...
@@ -311,14 +311,14 @@ async function apiCall(method, url, body) {
}
let
providersData
=
{};
const
STUDIO_CAPABILITY_CHOICES
=
JSON
.
parse
({{
studio_capability_choices_json
|
tojson
}})
;
const
STUDIO_ADAPTER_CHOICES
=
JSON
.
parse
({{
studio_adapter_choices_json
|
tojson
}})
;
const
STUDIO_ADAPTER_PROFILE_CHOICES
=
JSON
.
parse
({{
studio_adapter_profile_choices_json
|
tojson
}})
;
const
STUDIO_CAPABILITY_CHOICES
=
{{
studio_capability_choices
|
tojson
}}
;
const
STUDIO_ADAPTER_CHOICES
=
{{
studio_adapter_choices
|
tojson
}}
;
const
STUDIO_ADAPTER_PROFILE_CHOICES
=
{{
studio_adapter_profile_choices
|
tojson
}}
;
let
expandedProviders
=
new
Set
();
let
currentProviderPage
=
0
;
const
PROVIDERS_PAGE_SIZE
=
10
;
let
providerSearchFilter
=
''
;
let
rawProviders
=
JSON
.
parse
({{
user_providers_
json
|
tojson
}});
let
rawProviders
=
JSON
.
parse
({{
user_providers_
bootstrap_json
|
safe
}});
let
providerMasterOrder
=
[];
let
_providerDS
=
null
;
...
...
@@ -1125,7 +1125,7 @@ function renderProviderDetails(key) {
<div><strong>Connected At:</strong>
${
escHtmlAttr
(
formatBrokerTimestamp
(
brokerSession
.
connected_at
))}
</div>
<div><strong>Remote Endpoint:</strong>
${
escHtmlAttr
(
brokerSession
.
endpoint
||
'Unknown'
)}
</div>
<div><strong>Transport:</strong>
${
escHtmlAttr
(
brokerSession
.
transport
||
'broker'
)}
</div>
<div><strong>GPU Count:</strong>
${
escHtmlAttr
(
String
(
brokerMetadata
.
gpu_count
??
(
Array
.
isArray
(
brokerMetadata
.
gpus
)
?
brokerMetadata
.
gpus
.
length
:
0
)
||
'0'
))}
<
/div
>
<div><strong>GPU Count:</strong>
${
escHtmlAttr
(
String
(
(
brokerMetadata
.
gpu_count
??
(
Array
.
isArray
(
brokerMetadata
.
gpus
)
?
brokerMetadata
.
gpus
.
length
:
0
)
)
||
'0'
))}
<
/div
>
<
div
><
strong
>
VRAM
:
<
/strong> ${escHtmlAttr
(
formatVramMb
(
brokerMetadata.available_vram_mb
))
} free /
$
{
escHtmlAttr
(
formatVramMb
(
brokerMetadata
.
total_vram_mb
))}
total
<
/div
>
<
div
><
strong
>
GPUs
:
<
/strong> ${escHtmlAttr
(
renderGpuSummary
(
brokerMetadata
))
}</
div
>
<
div
><
strong
>
Avg
Latency
:
<
/strong> ${escHtmlAttr
(
formatPerfNumber
(
performance.avg_latency_ms, 1, ' ms'
))
}</
div
>
...
...
tests/routes/test_market_reference_imports.py
View file @
006441ea
...
...
@@ -337,6 +337,17 @@ def _load_json_parse_bootstrap(response_text: str, marker: str):
return
json
.
loads
(
json
.
loads
(
js_string_literal
))
def
test_json_parse_bootstrap_escapes_inline_script_sequences
():
from
aisbf.routes.dashboard.providers
import
_json_parse_bootstrap
payload
=
[{
"config"
:
{
"name"
:
'Alice
\'
s "Provider" </script><script>alert(1)</script>'
}}]
bootstrap
=
_json_parse_bootstrap
(
payload
)
assert
'</script><script>alert(1)</script>'
not
in
bootstrap
assert
'
\\
u003c/script
\\
u003e
\\
u003cscript
\\
u003ealert(1)
\\
u003c/script
\\
u003e'
in
bootstrap
assert
json
.
loads
(
json
.
loads
(
bootstrap
))[
0
][
"config"
][
"name"
]
==
'Alice
\'
s "Provider" </script><script>alert(1)</script>'
@
pytest
.
fixture
def
runtime_fixture
(
monkeypatch
):
db
=
MarketReferenceRuntimeDbStub
()
...
...
@@ -624,10 +635,10 @@ def test_dashboard_admin_providers_bootstrap_uses_json_parse(monkeypatch):
"request"
:
request
,
"session"
:
{},
"__version__"
:
"test"
,
"providers_
json"
:
json
.
dumps
(
providers_payload
)
,
"studio_capability_choices
_json"
:
"[]"
,
"studio_adapter_choices
_json"
:
"[]"
,
"studio_adapter_profile_choices
_json"
:
"[]"
,
"providers_
data"
:
providers_payload
,
"studio_capability_choices
"
:
[]
,
"studio_adapter_choices
"
:
[]
,
"studio_adapter_profile_choices
"
:
[]
,
"claude_cli_mode"
:
False
,
"is_local_client"
:
True
,
"success"
:
None
,
...
...
@@ -637,9 +648,9 @@ def test_dashboard_admin_providers_bootstrap_uses_json_parse(monkeypatch):
response_text
=
response
.
body
.
decode
()
assert
response
.
status_code
==
200
assert
"let providersData =
JSON.parse(
"
in
response_text
providers_bootstrap
=
_load_json_parse_bootstrap
(
response_text
,
"let providersData"
)
bootstrap_fragment
=
response_text
.
split
(
"let providersData = JSON.parse("
,
1
)[
1
]
.
split
(
"
\n
"
,
1
)[
0
]
assert
"let providersData =
{
"
in
response_text
bootstrap_fragment
=
response_text
.
split
(
"let providersData = "
,
1
)[
1
]
.
split
(
";
\n
"
,
1
)[
0
]
providers_bootstrap
=
json
.
loads
(
bootstrap_fragment
)
assert
'</script><script>alert(2)</script>'
not
in
bootstrap_fragment
assert
'
\\
u003c/script
\\
u003e
\\
u003cscript
\\
u003ealert(2)
\\
u003c/script
\\
u003e'
in
bootstrap_fragment
assert
providers_bootstrap
[
"danger-provider"
][
"name"
]
==
'Admin "Provider" </script><script>alert(2)</script>'
...
...
tests/test_geolocation.py
View file @
006441ea
...
...
@@ -2,14 +2,20 @@ import pytest
import
time
import
ipaddress
from
unittest.mock
import
patch
,
AsyncMock
,
Mock
from
aisbf.geolocation
import
get_ip_country
,
_subnet_cache
,
_find_in_cache
,
_fallback_prefix
from
fastapi
import
FastAPI
from
fastapi.testclient
import
TestClient
from
aisbf.geolocation
import
get_ip_country
,
_subnet_cache
,
_failure_cache
,
_find_in_cache
,
_fallback_prefix
from
aisbf.app.middleware
import
GenocidalBlockingMiddleware
@
pytest
.
fixture
(
autouse
=
True
)
def
clear_cache
():
_subnet_cache
.
clear
()
_failure_cache
.
clear
()
yield
_subnet_cache
.
clear
()
_failure_cache
.
clear
()
def
_mock_json_response
(
country
:
str
,
network
:
str
,
status
:
int
=
200
):
...
...
@@ -47,6 +53,47 @@ def test_find_in_cache_expired_entry_removed():
assert
"10.0.0.0/8"
not
in
_subnet_cache
def
_build_geo_test_client
():
app
=
FastAPI
()
@
app
.
get
(
"/"
)
async
def
root
():
return
{
"ok"
:
True
}
app
.
add_middleware
(
GenocidalBlockingMiddleware
,
server_ip_blocked_ref
=
lambda
:
False
)
return
TestClient
(
app
)
def
test_localhost_ip_skips_geolocation_lookup
():
client
=
_build_geo_test_client
()
with
patch
(
"aisbf.geolocation.get_ip_country"
,
new_callable
=
AsyncMock
)
as
mock_geo
:
response
=
client
.
get
(
"/"
,
headers
=
{
"X-Forwarded-For"
:
"127.0.0.1"
})
assert
response
.
status_code
==
200
mock_geo
.
assert_not_called
()
def
test_private_rfc1918_ip_skips_geolocation_lookup
():
client
=
_build_geo_test_client
()
with
patch
(
"aisbf.geolocation.get_ip_country"
,
new_callable
=
AsyncMock
)
as
mock_geo
:
response
=
client
.
get
(
"/"
,
headers
=
{
"X-Forwarded-For"
:
"192.168.1.25"
})
assert
response
.
status_code
==
200
mock_geo
.
assert_not_called
()
def
test_public_ip_still_checks_geolocation
():
client
=
_build_geo_test_client
()
with
patch
(
"aisbf.geolocation.get_ip_country"
,
new_callable
=
AsyncMock
,
return_value
=
None
)
as
mock_geo
:
response
=
client
.
get
(
"/"
,
headers
=
{
"X-Forwarded-For"
:
"8.8.8.8"
})
assert
response
.
status_code
==
200
mock_geo
.
assert_awaited_once_with
(
"8.8.8.8"
)
# --- invalid IP ---
@
pytest
.
mark
.
asyncio
...
...
@@ -136,7 +183,7 @@ async def test_non_200_not_cached():
@
pytest
.
mark
.
asyncio
async
def
test_failure_then_success_
retries
():
async
def
test_failure_then_success_
uses_failure_backoff
():
fail
=
Mock
(
status_code
=
500
)
ok
=
_mock_json_response
(
"IT"
,
"1.2.0.0/16"
)
with
patch
(
'httpx.AsyncClient.get'
,
new_callable
=
AsyncMock
)
as
mock_get
:
...
...
@@ -146,8 +193,9 @@ async def test_failure_then_success_retries():
mock_get
.
return_value
=
ok
result
=
await
get_ip_country
(
"1.2.3.4"
)
assert
result
==
"IT"
assert
"1.2.0.0/16"
in
_subnet_cache
assert
result
is
None
mock_get
.
assert_called_once
()
assert
"1.2.0.0/16"
not
in
_subnet_cache
# --- TTL expiry ---
...
...
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