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
2765eea2
Commit
2765eea2
authored
Apr 20, 2026
by
Your Name
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
0.99.40
parent
bebce4e7
Changes
5
Hide whitespace changes
Inline
Side-by-side
Showing
5 changed files
with
144 additions
and
28 deletions
+144
-28
analytics.py
aisbf/analytics.py
+21
-4
cache.py
aisbf/cache.py
+62
-2
handlers.py
aisbf/handlers.py
+8
-8
base.py
aisbf/providers/base.py
+33
-10
response_cache.html
templates/dashboard/response_cache.html
+20
-4
No files found.
aisbf/analytics.py
View file @
2765eea2
...
...
@@ -366,6 +366,7 @@ class Analytics:
provider_id: Optional provider filter
from_datetime: Start datetime
to_datetime: End datetime
user_filter: Optional user ID to filter by (-1 for global only)
Returns:
Dictionary with token counts and cost estimates
...
...
@@ -380,21 +381,36 @@ class Analytics:
formatted_start
=
self
.
_format_timestamp
(
start
)
formatted_end
=
self
.
_format_timestamp
(
end
)
# Build user condition
if
user_filter
==
-
1
:
user_condition
=
" AND user_id IS NULL"
params_suffix
=
[]
elif
user_filter
is
not
None
:
user_condition
=
f
" AND user_id = {placeholder}"
params_suffix
=
[
user_filter
]
else
:
user_condition
=
""
params_suffix
=
[]
if
provider_id
:
params
=
[
provider_id
,
formatted_start
,
formatted_end
]
+
params_suffix
cursor
.
execute
(
f
'''
SELECT SUM(tokens_used) as total_tokens
FROM token_usage
WHERE provider_id = {placeholder} AND timestamp >= {placeholder} AND timestamp <= {placeholder}
'''
,
(
provider_id
,
formatted_start
,
formatted_end
))
{user_condition}
'''
,
params
)
row
=
cursor
.
fetchone
()
total_tokens
=
row
[
0
]
if
row
and
row
[
0
]
else
0
else
:
params
=
[
formatted_start
,
formatted_end
]
+
params_suffix
cursor
.
execute
(
f
'''
SELECT provider_id, SUM(tokens_used) as total_tokens
FROM token_usage
WHERE timestamp >= {placeholder} AND timestamp <= {placeholder}
{user_condition}
GROUP BY provider_id
'''
,
(
formatted_start
,
formatted_end
)
)
'''
,
params
)
provider_tokens
=
{}
total_tokens
=
0
...
...
@@ -931,6 +947,7 @@ class Analytics:
Args:
from_datetime: Optional start datetime for filtering
to_datetime: Optional end datetime for filtering
user_filter: Optional user ID to filter by (-1 for global only)
Returns:
Dictionary with cost estimates
...
...
@@ -940,10 +957,10 @@ class Analytics:
end
=
to_datetime
or
datetime
.
now
()
# Get token usage by date range
range_usage
=
self
.
get_token_usage_by_date_range
(
None
,
start
,
end
)
range_usage
=
self
.
get_token_usage_by_date_range
(
None
,
start
,
end
,
user_filter
)
# Get providers that have data
providers
=
self
.
get_all_providers_stats
(
from_datetime
,
to_datetime
)
providers
=
self
.
get_all_providers_stats
(
from_datetime
,
to_datetime
,
user_filter
)
total_cost
=
0.0
provider_costs
=
[]
...
...
aisbf/cache.py
View file @
2765eea2
...
...
@@ -1171,6 +1171,9 @@ class ResponseCache:
# Per-provider statistics
self
.
provider_stats
=
{}
# Per-user statistics
self
.
user_stats
=
{}
# Initialize backends
self
.
redis_client
=
None
...
...
@@ -1346,12 +1349,13 @@ class ResponseCache:
self
.
_memory_cache
.
pop
(
lru_key
,
None
)
self
.
_memory_timestamps
.
pop
(
lru_key
,
None
)
def
get
(
self
,
request_data
:
Dict
)
->
Optional
[
Any
]:
def
get
(
self
,
request_data
:
Dict
,
user_id
:
Optional
[
int
]
=
None
)
->
Optional
[
Any
]:
"""
Get cached response for a request.
Args:
request_data: The request data dict
user_id: Optional user ID for tracking statistics
Returns:
Cached response dict or None if not found
...
...
@@ -1369,6 +1373,7 @@ class ResponseCache:
if
data
:
self
.
stats
[
'hits'
]
+=
1
self
.
_update_provider_stats
(
provider_id
,
'hits'
)
self
.
_update_user_stats
(
user_id
,
'hits'
)
logger
.
debug
(
f
"Cache hit (Redis): {cache_key}"
)
return
self
.
_deserialize_response
(
data
)
elif
self
.
backend
==
'sqlite'
and
self
.
sqlite_backend
:
...
...
@@ -1377,6 +1382,7 @@ class ResponseCache:
if
data
:
self
.
stats
[
'hits'
]
+=
1
self
.
_update_provider_stats
(
provider_id
,
'hits'
)
self
.
_update_user_stats
(
user_id
,
'hits'
)
logger
.
debug
(
f
"Cache hit (SQLite): {cache_key}"
)
return
data
elif
self
.
backend
==
'mysql'
and
self
.
mysql_backend
:
...
...
@@ -1385,6 +1391,7 @@ class ResponseCache:
if
data
:
self
.
stats
[
'hits'
]
+=
1
self
.
_update_provider_stats
(
provider_id
,
'hits'
)
self
.
_update_user_stats
(
user_id
,
'hits'
)
logger
.
debug
(
f
"Cache hit (MySQL): {cache_key}"
)
return
data
elif
self
.
backend
==
'memory'
:
...
...
@@ -1407,11 +1414,13 @@ class ResponseCache:
self
.
stats
[
'hits'
]
+=
1
self
.
_update_provider_stats
(
provider_id
,
'hits'
)
self
.
_update_user_stats
(
user_id
,
'hits'
)
logger
.
debug
(
f
"Cache hit (Memory): {cache_key}"
)
return
self
.
_memory_cache
[
cache_key
]
self
.
stats
[
'misses'
]
+=
1
self
.
_update_provider_stats
(
provider_id
,
'misses'
)
self
.
_update_user_stats
(
user_id
,
'misses'
)
logger
.
debug
(
f
"Cache miss: {cache_key}"
)
return
None
...
...
@@ -1420,7 +1429,7 @@ class ResponseCache:
logger
.
warning
(
f
"Cache get error: {e}"
)
return
None
def
set
(
self
,
request_data
:
Dict
,
response
:
Any
,
ttl
:
Optional
[
int
]
=
None
)
->
None
:
def
set
(
self
,
request_data
:
Dict
,
response
:
Any
,
ttl
:
Optional
[
int
]
=
None
,
user_id
:
Optional
[
int
]
=
None
)
->
None
:
"""
Cache a response.
...
...
@@ -1428,6 +1437,7 @@ class ResponseCache:
request_data: The request data dict
response: The response to cache (dict or object)
ttl: TTL in seconds (uses default if None)
user_id: Optional user ID for tracking statistics
"""
if
not
self
.
enabled
:
return
...
...
@@ -1473,6 +1483,7 @@ class ResponseCache:
logger
.
debug
(
f
"Cached response (Memory): {cache_key} (TTL: {ttl_value}s)"
)
self
.
stats
[
'sets'
]
+=
1
self
.
_update_user_stats
(
user_id
,
'sets'
)
except
Exception
as
e
:
self
.
stats
[
'errors'
]
+=
1
...
...
@@ -1509,6 +1520,55 @@ class ResponseCache:
except
Exception
as
e
:
self
.
stats
[
'errors'
]
+=
1
logger
.
warning
(
f
"Cache delete error: {e}"
)
def
_update_provider_stats
(
self
,
provider_id
:
str
,
stat_type
:
str
)
->
None
:
"""Update per-provider statistics"""
if
provider_id
not
in
self
.
provider_stats
:
self
.
provider_stats
[
provider_id
]
=
{
'hits'
:
0
,
'misses'
:
0
,
'sets'
:
0
,
'deletes'
:
0
,
'errors'
:
0
}
self
.
provider_stats
[
provider_id
][
stat_type
]
+=
1
def
_update_user_stats
(
self
,
user_id
:
Optional
[
int
],
stat_type
:
str
)
->
None
:
"""Update per-user statistics"""
if
user_id
is
None
:
return
user_key
=
f
"user:{user_id}"
if
user_key
not
in
self
.
user_stats
:
self
.
user_stats
[
user_key
]
=
{
'hits'
:
0
,
'misses'
:
0
,
'sets'
:
0
,
'deletes'
:
0
,
'errors'
:
0
}
self
.
user_stats
[
user_key
][
stat_type
]
+=
1
def
get_user_stats
(
self
,
user_id
:
int
)
->
Dict
[
str
,
Any
]:
"""Get cache statistics for a specific user"""
user_key
=
f
"user:{user_id}"
if
user_key
not
in
self
.
user_stats
:
return
{
'enabled'
:
self
.
enabled
,
'hits'
:
0
,
'misses'
:
0
,
'hit_rate'
:
0.0
,
'total_requests'
:
0
,
'backend'
:
self
.
backend
}
stats
=
self
.
user_stats
[
user_key
]
.
copy
()
total
=
stats
[
'hits'
]
+
stats
[
'misses'
]
stats
[
'hit_rate'
]
=
stats
[
'hits'
]
/
total
if
total
>
0
else
0.0
stats
[
'total_requests'
]
=
total
stats
[
'enabled'
]
=
self
.
enabled
stats
[
'backend'
]
=
self
.
backend
return
stats
def
clear
(
self
)
->
None
:
"""Clear all cached responses"""
...
...
aisbf/handlers.py
View file @
2765eea2
...
...
@@ -345,7 +345,7 @@ class RequestHandler:
aisbf_config
=
self
.
config
.
get_aisbf_config
()
if
aisbf_config
and
aisbf_config
.
response_cache
and
aisbf_config
.
response_cache
.
enabled
:
response_cache
=
get_response_cache
(
aisbf_config
.
response_cache
.
model_dump
())
cached_response
=
response_cache
.
get
(
request_data
)
cached_response
=
response_cache
.
get
(
request_data
,
user_id
=
self
.
user_id
)
if
cached_response
:
logger
.
info
(
f
"Cache hit for request to provider {provider_id}"
)
return
cached_response
...
...
@@ -479,7 +479,7 @@ class RequestHandler:
aisbf_config
=
self
.
config
.
get_aisbf_config
()
if
aisbf_config
and
aisbf_config
.
response_cache
and
aisbf_config
.
response_cache
.
enabled
:
response_cache
=
get_response_cache
(
aisbf_config
.
response_cache
.
model_dump
())
response_cache
.
set
(
request_data
,
response
)
response_cache
.
set
(
request_data
,
response
,
user_id
=
self
.
user_id
)
logger
.
debug
(
f
"Cached chunked response for request to provider {provider_id}"
)
except
Exception
as
cache_error
:
logger
.
warning
(
f
"Response cache set failed for chunked request: {cache_error}"
)
...
...
@@ -521,7 +521,7 @@ class RequestHandler:
aisbf_config
=
self
.
config
.
get_aisbf_config
()
if
aisbf_config
and
aisbf_config
.
response_cache
and
aisbf_config
.
response_cache
.
enabled
:
response_cache
=
get_response_cache
(
aisbf_config
.
response_cache
.
model_dump
())
response_cache
.
set
(
request_data
,
response
)
response_cache
.
set
(
request_data
,
response
,
user_id
=
self
.
user_id
)
logger
.
debug
(
f
"Cached response for request to provider {provider_id}"
)
except
Exception
as
cache_error
:
logger
.
warning
(
f
"Response cache set failed: {cache_error}"
)
...
...
@@ -2353,7 +2353,7 @@ class RotationHandler:
aisbf_config
=
self
.
config
.
get_aisbf_config
()
if
aisbf_config
and
aisbf_config
.
response_cache
and
aisbf_config
.
response_cache
.
enabled
:
response_cache
=
get_response_cache
(
aisbf_config
.
response_cache
.
model_dump
())
cached_response
=
response_cache
.
get
(
request_data
)
cached_response
=
response_cache
.
get
(
request_data
,
user_id
=
self
.
user_id
)
if
cached_response
:
logger
.
info
(
f
"Cache hit for rotation request {rotation_id}"
)
return
cached_response
...
...
@@ -2863,7 +2863,7 @@ class RotationHandler:
aisbf_config
=
self
.
config
.
get_aisbf_config
()
if
aisbf_config
and
aisbf_config
.
response_cache
and
aisbf_config
.
response_cache
.
enabled
:
response_cache
=
get_response_cache
(
aisbf_config
.
response_cache
.
model_dump
())
response_cache
.
set
(
request_data
,
response
)
response_cache
.
set
(
request_data
,
response
,
user_id
=
self
.
user_id
)
logger
.
debug
(
f
"Cached chunked response for rotation request {rotation_id}"
)
except
Exception
as
cache_error
:
logger
.
warning
(
f
"Response cache set failed for chunked request: {cache_error}"
)
...
...
@@ -2935,7 +2935,7 @@ class RotationHandler:
aisbf_config
=
self
.
config
.
get_aisbf_config
()
if
aisbf_config
and
aisbf_config
.
response_cache
and
aisbf_config
.
response_cache
.
enabled
:
response_cache
=
get_response_cache
(
aisbf_config
.
response_cache
.
model_dump
())
response_cache
.
set
(
request_data
,
response
)
response_cache
.
set
(
request_data
,
response
,
user_id
=
self
.
user_id
)
logger
.
debug
(
f
"Cached response for rotation request {rotation_id}"
)
except
Exception
as
cache_error
:
logger
.
warning
(
f
"Response cache set failed: {cache_error}"
)
...
...
@@ -4275,7 +4275,7 @@ class AutoselectHandler:
aisbf_config
=
self
.
config
.
get_aisbf_config
()
if
aisbf_config
and
aisbf_config
.
response_cache
and
aisbf_config
.
response_cache
.
enabled
:
response_cache
=
get_response_cache
(
aisbf_config
.
response_cache
.
model_dump
())
cached_response
=
response_cache
.
get
(
request_data
)
cached_response
=
response_cache
.
get
(
request_data
,
user_id
=
self
.
user_id
)
if
cached_response
:
logger
.
info
(
f
"Cache hit for autoselect request {autoselect_id}"
)
return
cached_response
...
...
@@ -4431,7 +4431,7 @@ class AutoselectHandler:
aisbf_config
=
self
.
config
.
get_aisbf_config
()
if
aisbf_config
and
aisbf_config
.
response_cache
and
aisbf_config
.
response_cache
.
enabled
:
response_cache
=
get_response_cache
(
aisbf_config
.
response_cache
.
model_dump
())
response_cache
.
set
(
request_data
,
response
)
response_cache
.
set
(
request_data
,
response
,
user_id
=
self
.
user_id
)
logger
.
debug
(
f
"Cached response for autoselect request {autoselect_id}"
)
except
Exception
as
cache_error
:
logger
.
warning
(
f
"Response cache set failed: {cache_error}"
)
...
...
aisbf/providers/base.py
View file @
2765eea2
...
...
@@ -697,24 +697,47 @@ class AdaptiveRateLimiter:
logger
.
info
(
f
"[AdaptiveRateLimiter {self.provider_id}] Reset to initial state"
)
# Global adaptive rate limiters registry
# Global adaptive rate limiters registry - now supports user-specific limiters
# Key format: "provider_id" (global) or "user:user_id:provider_id" (user-specific)
_adaptive_rate_limiters
:
Dict
[
str
,
AdaptiveRateLimiter
]
=
{}
def
get_adaptive_rate_limiter
(
provider_id
:
str
,
config
:
Dict
=
None
)
->
AdaptiveRateLimiter
:
"""Get or create an adaptive rate limiter for a provider."""
def
get_adaptive_rate_limiter
(
provider_id
:
str
,
config
:
Dict
=
None
,
user_id
:
Optional
[
int
]
=
None
)
->
AdaptiveRateLimiter
:
"""Get or create an adaptive rate limiter for a provider
, optionally user-specific
."""
global
_adaptive_rate_limiters
if
provider_id
not
in
_adaptive_rate_limiters
:
_adaptive_rate_limiters
[
provider_id
]
=
AdaptiveRateLimiter
(
provider_id
,
config
)
# Create key based on whether user_id is provided
if
user_id
is
not
None
:
key
=
f
"user:{user_id}:{provider_id}"
else
:
key
=
provider_id
return
_adaptive_rate_limiters
[
provider_id
]
if
key
not
in
_adaptive_rate_limiters
:
_adaptive_rate_limiters
[
key
]
=
AdaptiveRateLimiter
(
key
,
config
)
return
_adaptive_rate_limiters
[
key
]
def
get_all_adaptive_rate_limiters
()
->
Dict
[
str
,
AdaptiveRateLimiter
]:
"""Get all adaptive rate limiters."""
def
get_all_adaptive_rate_limiters
(
user_id
:
Optional
[
int
]
=
None
)
->
Dict
[
str
,
AdaptiveRateLimiter
]:
"""Get all adaptive rate limiters
, optionally filtered by user
."""
global
_adaptive_rate_limiters
return
_adaptive_rate_limiters
if
user_id
is
None
:
# Return all limiters (admin view)
return
_adaptive_rate_limiters
else
:
# Return only limiters for this user + global ones
user_prefix
=
f
"user:{user_id}:"
filtered
=
{}
for
key
,
limiter
in
_adaptive_rate_limiters
.
items
():
if
key
.
startswith
(
user_prefix
)
or
not
key
.
startswith
(
"user:"
):
# Strip the user prefix for display to user
if
key
.
startswith
(
user_prefix
):
display_key
=
key
[
len
(
user_prefix
):]
else
:
display_key
=
key
filtered
[
display_key
]
=
limiter
return
filtered
class
BaseProviderHandler
:
...
...
@@ -756,7 +779,7 @@ class BaseProviderHandler:
adaptive_config
=
None
if
config
.
aisbf
and
config
.
aisbf
.
adaptive_rate_limiting
:
adaptive_config
=
config
.
aisbf
.
adaptive_rate_limiting
.
dict
()
self
.
adaptive_limiter
=
get_adaptive_rate_limiter
(
provider_id
,
adaptive_config
)
self
.
adaptive_limiter
=
get_adaptive_rate_limiter
(
provider_id
,
adaptive_config
,
user_id
)
def
parse_429_response
(
self
,
response_data
:
Union
[
Dict
,
str
],
headers
:
Dict
=
None
)
->
Optional
[
int
]:
"""
...
...
templates/dashboard/response_cache.html
View file @
2765eea2
...
...
@@ -22,7 +22,12 @@
<!-- Cache Statistics -->
<div
style=
"background: #1a1a2e; padding: 20px; border-radius: 8px; margin-bottom: 30px;"
>
<h3
style=
"margin-bottom: 20px;"
>
Cache Statistics
</h3>
{% if is_admin %}
<h3
style=
"margin-bottom: 20px;"
>
Global Cache Statistics
</h3>
{% else %}
<h3
style=
"margin-bottom: 20px;"
>
Your Personal Cache Impact
</h3>
<p
style=
"color: #a0a0a0; margin-bottom: 20px;"
>
These statistics show how the response cache has benefited your requests personally.
</p>
{% endif %}
<div
style=
"display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 20px;"
>
<div
style=
"background: #0f3460; padding: 15px; border-radius: 8px;"
>
...
...
@@ -32,26 +37,29 @@
</div>
</div>
{% if is_admin %}
<div
style=
"background: #0f3460; padding: 15px; border-radius: 8px;"
>
<div
style=
"color: #a0a0a0; font-size: 14px; margin-bottom: 5px;"
>
Backend
</div>
<div
style=
"font-size: 24px; font-weight: bold;"
>
{{ stats.backend|upper }}
</div>
</div>
{% endif %}
<div
style=
"background: #0f3460; padding: 15px; border-radius: 8px;"
>
<div
style=
"color: #a0a0a0; font-size: 14px; margin-bottom: 5px;"
>
Cache Hits
</div>
<div
style=
"color: #a0a0a0; font-size: 14px; margin-bottom: 5px;"
>
{% if is_admin %}Cache Hits{% else %}Your Cache Hits{% endif %}
</div>
<div
style=
"font-size: 24px; font-weight: bold; color: #4ade80;"
>
{{ stats.hits }}
</div>
</div>
<div
style=
"background: #0f3460; padding: 15px; border-radius: 8px;"
>
<div
style=
"color: #a0a0a0; font-size: 14px; margin-bottom: 5px;"
>
Cache Misses
</div>
<div
style=
"color: #a0a0a0; font-size: 14px; margin-bottom: 5px;"
>
{% if is_admin %}Cache Misses{% else %}Your Cache Misses{% endif %}
</div>
<div
style=
"font-size: 24px; font-weight: bold; color: #ef4444;"
>
{{ stats.misses }}
</div>
</div>
<div
style=
"background: #0f3460; padding: 15px; border-radius: 8px;"
>
<div
style=
"color: #a0a0a0; font-size: 14px; margin-bottom: 5px;"
>
Hit Rate
</div>
<div
style=
"color: #a0a0a0; font-size: 14px; margin-bottom: 5px;"
>
{% if is_admin %}Hit Rate{% else %}Your Hit Rate{% endif %}
</div>
<div
style=
"font-size: 24px; font-weight: bold; color: #60a5fa;"
>
{{ "%.1f"|format(stats.hit_rate * 100) }}%
</div>
</div>
{% if is_admin %}
<div
style=
"background: #0f3460; padding: 15px; border-radius: 8px;"
>
<div
style=
"color: #a0a0a0; font-size: 14px; margin-bottom: 5px;"
>
Cache Size
</div>
<div
style=
"font-size: 24px; font-weight: bold;"
>
{{ stats.size }}
</div>
...
...
@@ -61,9 +69,16 @@
<div
style=
"color: #a0a0a0; font-size: 14px; margin-bottom: 5px;"
>
Evictions
</div>
<div
style=
"font-size: 24px; font-weight: bold; color: #f59e0b;"
>
{{ stats.evictions }}
</div>
</div>
{% else %}
<div
style=
"background: #0f3460; padding: 15px; border-radius: 8px;"
>
<div
style=
"color: #a0a0a0; font-size: 14px; margin-bottom: 5px;"
>
Total Requests
</div>
<div
style=
"font-size: 24px; font-weight: bold; color: #a78bfa;"
>
{{ stats.total_requests }}
</div>
</div>
{% endif %}
</div>
</div>
{% if is_admin %}
<!-- Cache Actions -->
<div
style=
"background: #1a1a2e; padding: 20px; border-radius: 8px; margin-bottom: 30px;"
>
<h3
style=
"margin-bottom: 20px;"
>
Cache Actions
</h3>
...
...
@@ -78,6 +93,7 @@
</button>
</div>
</div>
{% endif %}
<!-- Error Display -->
{% if stats.error %}
...
...
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