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
e7663cb5
Commit
e7663cb5
authored
Apr 17, 2026
by
Your Name
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Update version 0.99.31
parent
0a61873a
Changes
8
Hide whitespace changes
Inline
Side-by-side
Showing
8 changed files
with
132 additions
and
29 deletions
+132
-29
__init__.py
aisbf/__init__.py
+1
-1
pyproject.toml
pyproject.toml
+1
-1
setup.py
setup.py
+1
-1
analytics.html
templates/dashboard/analytics.html
+15
-9
autoselect.html
templates/dashboard/autoselect.html
+12
-5
rate_limits.html
templates/dashboard/rate_limits.html
+7
-3
rotations.html
templates/dashboard/rotations.html
+10
-4
users.html
templates/dashboard/users.html
+85
-5
No files found.
aisbf/__init__.py
View file @
e7663cb5
...
...
@@ -54,7 +54,7 @@ from .auth.qwen import QwenOAuth2
from
.handlers
import
RequestHandler
,
RotationHandler
,
AutoselectHandler
from
.utils
import
count_messages_tokens
,
split_messages_into_chunks
,
get_max_request_tokens_for_model
__version__
=
"0.99.
29
"
__version__
=
"0.99.
31
"
__all__
=
[
# Config
"config"
,
...
...
pyproject.toml
View file @
e7663cb5
...
...
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name
=
"aisbf"
version
=
"0.99.
29
"
version
=
"0.99.
31
"
description
=
"AISBF - AI Service Broker Framework || AI Should Be Free - A modular proxy server for managing multiple AI provider integrations"
readme
=
"README.md"
license
=
"GPL-3.0-or-later"
...
...
setup.py
View file @
e7663cb5
...
...
@@ -49,7 +49,7 @@ class InstallCommand(_install):
setup
(
name
=
"aisbf"
,
version
=
"0.99.
29
"
,
version
=
"0.99.
31
"
,
author
=
"AISBF Contributors"
,
author_email
=
"stefy@nexlab.net"
,
description
=
"AISBF - AI Service Broker Framework || AI Should Be Free - A modular proxy server for managing multiple AI provider integrations"
,
...
...
templates/dashboard/analytics.html
View file @
e7663cb5
...
...
@@ -213,6 +213,13 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
{% endif %}
</div>
<script>
const
analyticsData
=
{
selectedUser
:
{{
selected_user
or
'null'
}},
availableUsers
:
{{
available_users
|
tojson
}}
};
</script>
<script>
document
.
getElementById
(
'timeRangeSelect'
).
addEventListener
(
'change'
,
function
()
{
var
customRange
=
document
.
getElementById
(
'customDateRange'
);
...
...
@@ -262,16 +269,15 @@ document.getElementById('timeRangeSelect').addEventListener('change', function()
const
resultsDiv
=
document
.
getElementById
(
'userSearchResults'
);
const
hiddenInput
=
document
.
getElementById
(
'userFilterValue'
);
let
debounceTimer
;
let
selectedUserId
=
{{
selected_user
or
'null'
}}
;
let
selectedUserId
=
analyticsData
.
selectedUser
;
// Set initial display value if a user is selected
{
%
if
selected_user
%
}
{
%
for
user
in
available_users
%
}
{
%
if
user
.
id
==
selected_user
%
}
searchInput
.
value
=
'{{ user.username }}{% if user.role == "admin" %} (admin){% endif %}'
;
{
%
endif
%
}
{
%
endfor
%
}
{
%
endif
%
}
if
(
analyticsData
.
selectedUser
)
{
const
user
=
analyticsData
.
availableUsers
.
find
(
u
=>
u
.
id
==
analyticsData
.
selectedUser
);
if
(
user
)
{
searchInput
.
value
=
user
.
username
+
(
user
.
role
===
'admin'
?
' (admin)'
:
''
);
}
}
// Search users via API
async
function
searchUsers
(
query
)
{
...
...
templates/dashboard/autoselect.html
View file @
e7663cb5
...
...
@@ -40,9 +40,16 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
</div>
<script>
let
autoselectConfig
=
{{
autoselect_json
|
safe
}};
let
availableRotations
=
{{
available_rotations
|
safe
}};
let
availableModels
=
{{
available_models
|
safe
}};
const
autoselectData
=
{
config
:
{{
autoselect_json
|
safe
}},
rotations
:
{{
available_rotations
|
safe
}},
models
:
{{
available_models
|
safe
}},
saveUrl
:
"{{ url_for(request, '/dashboard/autoselect') }}"
,
successUrl
:
"{{ url_for(request, '/dashboard/autoselect?success=1') }}"
};
let
autoselectConfig
=
autoselectData
.
config
;
let
availableRotations
=
autoselectData
.
rotations
;
let
availableModels
=
autoselectData
.
models
;
let
expandedAutoselects
=
new
Set
();
function
renderAutoselectList
()
{
...
...
@@ -381,7 +388,7 @@ function updateAutoselectModel(autoselectKey, index, field, value) {
async
function
saveAutoselect
()
{
try
{
const
response
=
await
fetch
(
'{{ url_for(request, "/dashboard/autoselect") }}'
,
{
const
response
=
await
fetch
(
autoselectData
.
saveUrl
,
{
method
:
'POST'
,
headers
:
{
'Content-Type'
:
'application/x-www-form-urlencoded'
,
...
...
@@ -390,7 +397,7 @@ async function saveAutoselect() {
});
if
(
response
.
ok
)
{
window
.
location
.
href
=
'{{ url_for(request, "/dashboard/autoselect?success=1") }}'
;
window
.
location
.
href
=
autoselectData
.
successUrl
;
}
else
{
alert
(
'Error saving configuration'
);
}
...
...
templates/dashboard/rate_limits.html
View file @
e7663cb5
...
...
@@ -21,6 +21,8 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
{% block content %}
<h2
style=
"margin-bottom: 30px;"
>
Adaptive Rate Limits
</h2>
<div
id=
"rate-limits-data"
data-data-url=
"{{ url_for(request, '/dashboard/rate-limits/data') }}"
style=
"display:none;"
></div>
<!-- Quick Links Navigation -->
<div
style=
"background: #1a1a2e; padding: 15px; border-radius: 8px; margin-bottom: 30px;"
>
<div
style=
"display: flex; gap: 15px; flex-wrap: wrap;"
>
...
...
@@ -93,12 +95,14 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
</style>
<script>
const
dataUrl
=
document
.
getElementById
(
'rate-limits-data'
).
dataset
.
dataUrl
;
async
function
loadRateLimits
()
{
const
content
=
document
.
getElementById
(
'rate-limits-content'
);
content
.
innerHTML
=
'<p>Loading rate limit data...</p>'
;
try
{
const
response
=
await
fetch
(
'{{ url_for(request, "/dashboard/rate-limits/data") }}'
);
const
response
=
await
fetch
(
dataUrl
);
const
data
=
await
response
.
json
();
if
(
Object
.
keys
(
data
).
length
===
0
)
{
...
...
@@ -196,7 +200,7 @@ async function clearAllRateLimiters() {
// First get the list of providers
try
{
const
response
=
await
fetch
(
'{{ url_for(request, "/dashboard/rate-limits/data") }}'
);
const
response
=
await
fetch
(
dataUrl
);
const
data
=
await
response
.
json
();
for
(
const
providerId
of
Object
.
keys
(
data
))
{
...
...
templates/dashboard/rotations.html
View file @
e7663cb5
...
...
@@ -47,8 +47,14 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
</div>
<script>
let
rotationsConfig
=
{{
rotations_json
|
safe
}};
let
availableProviders
=
{{
available_providers
|
safe
}};
const
rotationsData
=
{
config
:
{{
rotations_json
|
safe
}},
providers
:
{{
available_providers
|
safe
}},
saveUrl
:
"{{ url_for(request, '/dashboard/rotations') }}"
,
successUrl
:
"{{ url_for(request, '/dashboard/rotations?success=1') }}"
};
let
rotationsConfig
=
rotationsData
.
config
;
let
availableProviders
=
rotationsData
.
providers
;
let
expandedRotations
=
new
Set
();
document
.
getElementById
(
'global-notify'
).
checked
=
rotationsConfig
.
notifyerrors
||
false
;
...
...
@@ -376,7 +382,7 @@ function updateRotationModelCondenseMethod(rotationKey, providerIndex, modelInde
async
function
saveRotations
()
{
try
{
const
response
=
await
fetch
(
'{{ url_for(request, "/dashboard/rotations") }}'
,
{
const
response
=
await
fetch
(
rotationsData
.
saveUrl
,
{
method
:
'POST'
,
headers
:
{
'Content-Type'
:
'application/x-www-form-urlencoded'
,
...
...
@@ -385,7 +391,7 @@ async function saveRotations() {
});
if
(
response
.
ok
)
{
window
.
location
.
href
=
'{{ url_for(request, "/dashboard/rotations?success=1") }}'
;
window
.
location
.
href
=
rotationsData
.
successUrl
;
}
else
{
alert
(
'Error saving configuration'
);
}
...
...
templates/dashboard/users.html
View file @
e7663cb5
...
...
@@ -21,6 +21,8 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
{% block content %}
<h2
style=
"margin-bottom: 30px;"
>
User Management
</h2>
<div
id=
"url-data"
data-base-url=
"{{ url_for(request, '/dashboard/users/') }}"
style=
"display:none;"
></div>
{% if success %}
<div
class=
"alert alert-success"
>
{{ success }}
</div>
{% endif %}
...
...
@@ -280,6 +282,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
<script>
// Utility functions
const
baseUrl
=
document
.
getElementById
(
'url-data'
).
dataset
.
baseUrl
;
function
getUrlParams
()
{
const
params
=
new
URLSearchParams
(
window
.
location
.
search
);
return
{
...
...
@@ -335,6 +338,14 @@ function updateUsers(params) {
tableContainer
.
innerHTML
=
originalContent
;
document
.
querySelector
(
'table'
).
replaceWith
(
newTable
);
// Reset select-all state
const
selectAll
=
document
.
getElementById
(
'select-all'
);
if
(
selectAll
)
{
selectAll
.
checked
=
false
;
selectAll
.
indeterminate
=
false
;
}
updateBulkActionsVisibility
();
const
existingPagination
=
document
.
querySelector
(
'[style*="margin-top: 20px"]'
);
if
(
existingPagination
)
{
existingPagination
.
replaceWith
(
newPagination
);
...
...
@@ -414,9 +425,34 @@ document.addEventListener('DOMContentLoaded', function() {
document
.
querySelector
(
'table'
).
after
(
newPagination
);
}
// Reset select-all state
const
selectAll
=
document
.
getElementById
(
'select-all'
);
if
(
selectAll
)
{
selectAll
.
checked
=
false
;
selectAll
.
indeterminate
=
false
;
}
updateBulkActionsVisibility
();
// Re-attach event listeners to new elements
attachPaginationListeners
();
attachSortingListeners
();
// Reattach select-all listener
if
(
selectAll
)
{
selectAll
.
addEventListener
(
'change'
,
function
()
{
const
checkboxes
=
document
.
querySelectorAll
(
'.user-checkbox'
);
checkboxes
.
forEach
(
cb
=>
cb
.
checked
=
this
.
checked
);
updateBulkActionsVisibility
();
});
}
// Reattach individual checkbox listeners
document
.
querySelectorAll
(
'.user-checkbox'
).
forEach
(
checkbox
=>
{
checkbox
.
addEventListener
(
'change'
,
function
()
{
updateBulkActionsVisibility
();
updateSelectAllState
();
});
});
})
.
catch
(
error
=>
{
console
.
error
(
'Error updating users:'
,
error
);
...
...
@@ -529,7 +565,33 @@ document.addEventListener('DOMContentLoaded', function() {
document
.
getElementById
(
'select-all'
).
addEventListener
(
'change'
,
function
()
{
const
checkboxes
=
document
.
querySelectorAll
(
'.user-checkbox'
);
checkboxes
.
forEach
(
cb
=>
cb
.
checked
=
this
.
checked
);
updateBulkActionsVisibility
();
updateBulkActionsVisibility
();
});
// Individual checkbox change handlers
document
.
querySelectorAll
(
'.user-checkbox'
).
forEach
(
checkbox
=>
{
checkbox
.
addEventListener
(
'change'
,
function
()
{
updateBulkActionsVisibility
();
updateSelectAllState
();
});
});
// Bulk action button handlers
document
.
getElementById
(
'bulk-enable'
)?.
addEventListener
(
'click'
,
function
()
{
performBulkAction
(
'enable'
,
'enable these users'
);
});
document
.
getElementById
(
'bulk-disable'
)?.
addEventListener
(
'click'
,
function
()
{
performBulkAction
(
'disable'
,
'disable these users'
);
});
document
.
getElementById
(
'bulk-delete'
)?.
addEventListener
(
'click'
,
function
()
{
performBulkAction
(
'delete'
,
'delete these users'
,
true
);
});
document
.
getElementById
(
'bulk-clear'
)?.
addEventListener
(
'click'
,
function
()
{
clearSelection
();
});
});
// Function to attach event listeners to dynamic elements
...
...
@@ -576,6 +638,24 @@ function attachEventListeners() {
updateUsers
({
limit
:
this
.
value
,
page
:
1
});
});
}
// Reattach select-all listener
const
selectAll
=
document
.
getElementById
(
'select-all'
);
if
(
selectAll
)
{
selectAll
.
addEventListener
(
'change'
,
function
()
{
const
checkboxes
=
document
.
querySelectorAll
(
'.user-checkbox'
);
checkboxes
.
forEach
(
cb
=>
cb
.
checked
=
this
.
checked
);
updateBulkActionsVisibility
();
});
}
// Reattach individual checkbox listeners
document
.
querySelectorAll
(
'.user-checkbox'
).
forEach
(
checkbox
=>
{
checkbox
.
addEventListener
(
'change'
,
function
()
{
updateBulkActionsVisibility
();
updateSelectAllState
();
});
});
}
function
updateBulkActionsVisibility
()
{
...
...
@@ -666,7 +746,7 @@ function editUser(userId, username, email, role, isActive) {
document
.
getElementById
(
'edit-email'
).
value
=
email
;
document
.
getElementById
(
'edit-role'
).
value
=
role
;
document
.
getElementById
(
'edit-is-active'
).
checked
=
isActive
;
document
.
getElementById
(
'edit-form'
).
action
=
'{{ url_for(request, "/dashboard/users/") }}'
+
userId
+
'/edit'
;
document
.
getElementById
(
'edit-form'
).
action
=
baseUrl
+
userId
+
'/edit'
;
document
.
getElementById
(
'edit-modal'
).
style
.
display
=
'block'
;
}
...
...
@@ -677,7 +757,7 @@ function closeEditModal() {
function
toggleUserStatus
(
userId
,
currentStatus
)
{
const
action
=
currentStatus
?
'disable'
:
'enable'
;
if
(
confirm
(
'Are you sure you want to '
+
action
+
' this user?'
))
{
fetch
(
'{{ url_for(request, "/dashboard/users/") }}'
+
userId
+
'/toggle'
,
{
fetch
(
baseUrl
+
userId
+
'/toggle'
,
{
method
:
'POST'
,
headers
:
{
'Content-Type'
:
'application/json'
}
})
...
...
@@ -697,7 +777,7 @@ function toggleUserStatus(userId, currentStatus) {
function
deleteUser
(
userId
,
username
)
{
if
(
confirm
(
'Are you sure you want to delete user "'
+
username
+
'"? This action cannot be undone.'
))
{
fetch
(
'{{ url_for(request, "/dashboard/users/") }}'
+
userId
+
'/delete'
,
{
fetch
(
baseUrl
+
userId
+
'/delete'
,
{
method
:
'POST'
,
headers
:
{
'Content-Type'
:
'application/json'
}
})
...
...
@@ -716,7 +796,7 @@ function deleteUser(userId, username) {
}
function
updateUserTier
(
userId
,
tierId
)
{
fetch
(
'{{ url_for(request, "/dashboard/users/") }}'
+
userId
+
'/tier'
,
{
fetch
(
baseUrl
+
userId
+
'/tier'
,
{
method
:
'POST'
,
headers
:
{
'Content-Type'
:
'application/json'
},
body
:
JSON
.
stringify
({
tier_id
:
parseInt
(
tierId
)
})
...
...
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