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
fe8b625a
Commit
fe8b625a
authored
Apr 17, 2026
by
Your Name
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Analytics system
parent
823291c7
Changes
20
Show whitespace changes
Inline
Side-by-side
Showing
20 changed files
with
1590 additions
and
890 deletions
+1590
-890
__init__.py
aisbf/__init__.py
+1
-1
analytics.py
aisbf/analytics.py
+108
-28
claude.py
aisbf/auth/claude.py
+34
-29
qwen.py
aisbf/auth/qwen.py
+9
-8
context.py
aisbf/context.py
+2
-2
cost_extractor.py
aisbf/cost_extractor.py
+128
-0
database.py
aisbf/database.py
+630
-707
handlers.py
aisbf/handlers.py
+137
-29
mcp.py
aisbf/mcp.py
+2
-2
claude.py
aisbf/providers/claude.py
+10
-10
kilo.py
aisbf/providers/kilo.py
+4
-4
qwen.py
aisbf/providers/qwen.py
+5
-5
main.py
main.py
+147
-45
pyproject.toml
pyproject.toml
+1
-1
setup.py
setup.py
+1
-1
base.html
templates/base.html
+1
-1
analytics.html
templates/dashboard/analytics.html
+271
-7
pricing.html
templates/dashboard/pricing.html
+1
-1
user_index.html
templates/dashboard/user_index.html
+16
-1
users.html
templates/dashboard/users.html
+82
-8
No files found.
aisbf/__init__.py
View file @
fe8b625a
...
...
@@ -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.2
8
"
__version__
=
"0.99.2
9
"
__all__
=
[
# Config
"config"
,
...
...
aisbf/analytics.py
View file @
fe8b625a
...
...
@@ -121,7 +121,10 @@ class Analytics:
rotation_id
:
Optional
[
str
]
=
None
,
autoselect_id
:
Optional
[
str
]
=
None
,
user_id
:
Optional
[
int
]
=
None
,
token_id
:
Optional
[
int
]
=
None
token_id
:
Optional
[
int
]
=
None
,
prompt_tokens
:
Optional
[
int
]
=
None
,
completion_tokens
:
Optional
[
int
]
=
None
,
actual_cost
:
Optional
[
float
]
=
None
):
"""
Record a request for analytics.
...
...
@@ -137,6 +140,9 @@ class Analytics:
autoselect_id: Optional autoselect identifier if request went through autoselect
user_id: Optional user ID if request was made with API token
token_id: Optional API token ID if request was made with API token
prompt_tokens: Optional number of input/prompt tokens
completion_tokens: Optional number of output/completion tokens
actual_cost: Optional actual cost returned by provider (in USD)
"""
# Initialize provider tracking if needed
if
provider_id
not
in
self
.
_request_counts
:
...
...
@@ -161,7 +167,8 @@ class Analytics:
# Persist to database
if
tokens_used
>
0
:
self
.
db
.
record_token_usage
(
provider_id
,
model_name
,
tokens_used
,
user_id
)
logger
.
info
(
f
"Analytics.record_request: Recording to DB - provider={provider_id}, latency_ms={latency_ms}, success={success}, prompt={prompt_tokens}, completion={completion_tokens}, cost={actual_cost}"
)
self
.
db
.
record_token_usage
(
provider_id
,
model_name
,
tokens_used
,
user_id
,
success
,
latency_ms
,
error_type
,
token_id
,
prompt_tokens
,
completion_tokens
,
actual_cost
)
# Also record user token usage if this was an API token request
if
user_id
is
not
None
and
token_id
is
not
None
:
...
...
@@ -231,16 +238,32 @@ class Analytics:
placeholder
=
'?'
if
self
.
db
.
db_type
==
'sqlite'
else
'
%
s'
# Build query with optional user filter
user_condition
=
f
" AND user_id = {placeholder}"
if
user_filter
is
not
None
else
""
# user_filter = -1 means "only global requests" (user_id IS NULL)
# user_filter = None means "all requests"
# user_filter = <id> means "only requests from that user"
if
user_filter
==
-
1
:
user_condition
=
" AND user_id IS NULL"
params
=
[
provider_id
,
from_datetime
.
isoformat
(),
to_datetime
.
isoformat
()]
elif
user_filter
is
not
None
:
user_condition
=
f
" AND user_id = {placeholder}"
params
=
[
provider_id
,
from_datetime
.
isoformat
(),
to_datetime
.
isoformat
(),
user_filter
]
else
:
user_condition
=
""
params
=
[
provider_id
,
from_datetime
.
isoformat
(),
to_datetime
.
isoformat
()]
if
user_filter
is
not
None
:
params
.
append
(
user_filter
)
# Get token usage and actual time range of requests
cursor
.
execute
(
f
'''
SELECT
COUNT(*) as total_requests,
SUM(CASE WHEN success = 1 THEN 1 ELSE 0 END) as success_count,
SUM(CASE WHEN success = 0 THEN 1 ELSE 0 END) as error_count,
AVG(COALESCE(latency_ms, 0)) as avg_latency,
MIN(COALESCE(latency_ms, 0)) as min_latency,
MAX(COALESCE(latency_ms, 0)) as max_latency,
SUM(tokens_used) as total_tokens,
SUM(COALESCE(prompt_tokens, 0)) as total_prompt_tokens,
SUM(COALESCE(completion_tokens, 0)) as total_completion_tokens,
SUM(COALESCE(actual_cost, 0)) as total_actual_cost,
MIN(timestamp) as first_request,
MAX(timestamp) as last_request
FROM token_usage
...
...
@@ -252,9 +275,17 @@ class Analytics:
result
=
cursor
.
fetchone
()
total_requests
=
result
[
0
]
if
result
else
0
total_tokens
=
result
[
1
]
if
result
else
0
first_request
=
result
[
2
]
if
result
and
result
[
2
]
else
None
last_request
=
result
[
3
]
if
result
and
result
[
3
]
else
None
success_count
=
result
[
1
]
if
result
else
0
error_count
=
result
[
2
]
if
result
else
0
avg_latency
=
result
[
3
]
if
result
and
result
[
3
]
else
0
min_latency
=
result
[
4
]
if
result
and
result
[
4
]
else
0
max_latency
=
result
[
5
]
if
result
and
result
[
5
]
else
0
total_tokens
=
result
[
6
]
if
result
else
0
total_prompt_tokens
=
result
[
7
]
if
result
else
0
total_completion_tokens
=
result
[
8
]
if
result
else
0
total_actual_cost
=
result
[
9
]
if
result
else
0
first_request
=
result
[
10
]
if
result
and
result
[
10
]
else
None
last_request
=
result
[
11
]
if
result
and
result
[
11
]
else
None
# Calculate time-based rates using ACTUAL usage window, not query window
# This gives more accurate rate limiting metrics
...
...
@@ -289,15 +320,18 @@ class Analytics:
return
{
'provider_id'
:
provider_id
,
'requests'
:
{
'total'
:
total_requests
,
'success'
:
total_requests
,
'error'
:
0
},
'latency'
:
{
'count'
:
0
,
'total_ms'
:
0.0
,
'min_ms'
:
0
,
'max_ms'
:
0
},
'errors'
:
{},
'requests'
:
{
'total'
:
total_requests
,
'success'
:
success_count
,
'error'
:
error_count
},
'latency'
:
{
'count'
:
total_requests
,
'total_ms'
:
avg_latency
*
total_requests
if
total_requests
>
0
else
0
,
'min_ms'
:
min_latency
,
'max_ms'
:
max_latency
},
'errors'
:
{},
# Could be populated from error_type column if needed
'tokens'
:
{
'total'
:
total_tokens
,
'prompt'
:
total_prompt_tokens
,
'completion'
:
total_completion_tokens
,
'TPM'
:
tpm
,
'TPH'
:
tph
,
'TPD'
:
tpd
}
},
'actual_cost'
:
total_actual_cost
# Actual cost from provider responses
}
def
get_token_usage_by_date_range
(
...
...
@@ -374,7 +408,7 @@ class Analytics:
Args:
from_datetime: Optional start datetime for filtering
to_datetime: Optional end datetime for filtering
user_filter: Optional user ID filter
user_filter: Optional user ID filter
(-1 for global only, None for all, or user ID)
Returns:
List of provider statistics dictionaries
...
...
@@ -388,10 +422,15 @@ class Analytics:
placeholder
=
'?'
if
self
.
db
.
db_type
==
'sqlite'
else
'
%
s'
# Build query with optional user filter
user_condition
=
f
" AND user_id = {placeholder}"
if
user_filter
is
not
None
else
""
if
user_filter
==
-
1
:
user_condition
=
" AND user_id IS NULL"
params
=
[
start
.
isoformat
(),
end
.
isoformat
()]
elif
user_filter
is
not
None
:
user_condition
=
f
" AND user_id = {placeholder}"
params
=
[
start
.
isoformat
(),
end
.
isoformat
(),
user_filter
]
else
:
user_condition
=
""
params
=
[
start
.
isoformat
(),
end
.
isoformat
()]
if
user_filter
is
not
None
:
params
.
append
(
user_filter
)
cursor
.
execute
(
f
'''
SELECT DISTINCT provider_id
...
...
@@ -503,7 +542,18 @@ class Analytics:
time_bucket_expr
=
f
"DATE_FORMAT(timestamp, '{date_format}')"
if
provider_id
:
if
user_filter
:
if
user_filter
==
-
1
:
# Global requests only
cursor
.
execute
(
f
'''
SELECT
{time_bucket_expr} as time_bucket,
SUM(tokens_used) as tokens
FROM token_usage
WHERE provider_id = {placeholder} AND user_id IS NULL AND timestamp >= {placeholder} AND timestamp <= {placeholder}
GROUP BY time_bucket
ORDER BY time_bucket
'''
,
(
provider_id
,
cutoff
.
isoformat
(),
end_time
.
isoformat
()))
elif
user_filter
:
cursor
.
execute
(
f
'''
SELECT
{time_bucket_expr} as time_bucket,
...
...
@@ -524,7 +574,19 @@ class Analytics:
ORDER BY time_bucket
'''
,
(
provider_id
,
cutoff
.
isoformat
(),
end_time
.
isoformat
()))
else
:
if
user_filter
:
if
user_filter
==
-
1
:
# Global requests only
cursor
.
execute
(
f
'''
SELECT
{time_bucket_expr} as time_bucket,
SUM(tokens_used) as tokens,
provider_id
FROM token_usage
WHERE user_id IS NULL AND timestamp >= {placeholder} AND timestamp <= {placeholder}
GROUP BY time_bucket, provider_id
ORDER BY time_bucket
'''
,
(
cutoff
.
isoformat
(),
end_time
.
isoformat
()))
elif
user_filter
:
cursor
.
execute
(
f
'''
SELECT
{time_bucket_expr} as time_bucket,
...
...
@@ -706,7 +768,8 @@ class Analytics:
self
,
provider_id
:
str
,
tokens_used
:
int
,
prompt_tokens
:
Optional
[
int
]
=
None
prompt_tokens
:
Optional
[
int
]
=
None
,
completion_tokens
:
Optional
[
int
]
=
None
)
->
float
:
"""
Estimate cost for token usage.
...
...
@@ -715,6 +778,7 @@ class Analytics:
provider_id: Provider identifier
tokens_used: Total tokens used
prompt_tokens: Optional breakdown of prompt tokens
completion_tokens: Optional breakdown of completion tokens
Returns:
Estimated cost in USD
...
...
@@ -722,14 +786,19 @@ class Analytics:
# Get provider-specific pricing (checks subscription status and custom pricing)
provider_pricing
=
self
.
_get_provider_pricing
(
provider_id
)
# Calculate cost
if
prompt_tokens
is
not
None
:
completion_tokens
=
tokens_used
-
prompt_tokens
# Calculate cost with actual token breakdown if available
if
prompt_tokens
is
not
None
and
completion_tokens
is
not
None
:
prompt_cost
=
(
prompt_tokens
/
1_000_000
)
*
provider_pricing
.
get
(
'prompt'
,
0
)
completion_cost
=
(
completion_tokens
/
1_000_000
)
*
provider_pricing
.
get
(
'completion'
,
0
)
return
prompt_cost
+
completion_cost
elif
prompt_tokens
is
not
None
:
# Only prompt tokens provided, calculate completion from total
completion_tokens_calc
=
tokens_used
-
prompt_tokens
prompt_cost
=
(
prompt_tokens
/
1_000_000
)
*
provider_pricing
.
get
(
'prompt'
,
0
)
completion_cost
=
(
completion_tokens_calc
/
1_000_000
)
*
provider_pricing
.
get
(
'completion'
,
0
)
return
prompt_cost
+
completion_cost
else
:
#
Assume 25% prompt, 75% completion (
common for chat)
#
No breakdown available - use estimation (25% prompt, 75% completion is
common for chat)
prompt_tokens_est
=
tokens_used
*
0.25
completion_tokens_est
=
tokens_used
*
0.75
prompt_cost
=
(
prompt_tokens_est
/
1_000_000
)
*
provider_pricing
.
get
(
'prompt'
,
0
)
...
...
@@ -776,13 +845,24 @@ class Analytics:
tokens
=
provider
[
'tokens'
]
total_tokens
=
tokens
[
'TPD'
]
# Use daily tokens for cost estimation
cost
=
self
.
estimate_cost
(
provider_id
,
total_tokens
)
# Get actual prompt/completion tokens from provider stats
prompt_tokens
=
tokens
.
get
(
'prompt'
,
0
)
if
not
(
from_datetime
or
to_datetime
)
else
None
completion_tokens
=
tokens
.
get
(
'completion'
,
0
)
if
not
(
from_datetime
or
to_datetime
)
else
None
# Use actual cost if available, otherwise estimate
actual_cost
=
provider
.
get
(
'actual_cost'
,
0
)
if
actual_cost
>
0
:
cost
=
actual_cost
else
:
cost
=
self
.
estimate_cost
(
provider_id
,
total_tokens
,
prompt_tokens
,
completion_tokens
)
total_cost
+=
cost
provider_costs
.
append
({
'provider_id'
:
provider_id
,
'tokens_today'
:
total_tokens
,
'estimated_cost'
:
cost
'estimated_cost'
:
cost
,
'is_actual'
:
actual_cost
>
0
})
return
{
...
...
aisbf/auth/claude.py
View file @
fe8b625a
...
...
@@ -27,6 +27,7 @@ import hashlib
import
base64
import
webbrowser
import
time
import
asyncio
import
httpx
from
pathlib
import
Path
from
typing
import
Optional
,
Dict
...
...
@@ -223,7 +224,7 @@ class ClaudeAuth:
)
return
response
def
refresh_token
(
self
,
max_retries
:
int
=
3
)
->
bool
:
async
def
refresh_token
(
self
,
max_retries
:
int
=
3
)
->
bool
:
"""
Use the refresh token to get a new access token without logging in.
...
...
@@ -270,7 +271,7 @@ class ClaudeAuth:
# Rate limited - wait and retry with exponential backoff
wait_time
=
(
2
**
attempt
)
*
5
# 5, 10, 20 seconds
logger
.
warning
(
f
"Rate limited (429). Waiting {wait_time} seconds before retry {attempt + 1}/{max_retries}"
)
time
.
sleep
(
wait_time
)
await
asyncio
.
sleep
(
wait_time
)
continue
else
:
logger
.
error
(
f
"Token refresh failed: {response.status_code} - {response.text}"
)
...
...
@@ -280,14 +281,14 @@ class ClaudeAuth:
if
attempt
<
max_retries
-
1
:
wait_time
=
(
2
**
attempt
)
*
5
logger
.
info
(
f
"Retrying in {wait_time} seconds..."
)
time
.
sleep
(
wait_time
)
await
asyncio
.
sleep
(
wait_time
)
continue
return
False
logger
.
error
(
f
"Token refresh failed after {max_retries} attempts"
)
return
False
def
get_valid_token
(
self
,
auto_login
:
bool
=
False
)
->
str
:
async
def
get_valid_token
(
self
,
auto_login
:
bool
=
False
)
->
str
:
"""
Get a valid access token, refreshing it if necessary.
...
...
@@ -311,7 +312,7 @@ class ClaudeAuth:
# Refresh if less than 5 minutes remain
if
time
.
time
()
>
(
self
.
tokens
.
get
(
'expires_at'
,
0
)
-
300
):
logger
.
info
(
"Token expiring soon, refreshing..."
)
if
not
self
.
refresh_token
():
if
not
await
self
.
refresh_token
():
if
not
auto_login
:
logger
.
error
(
"Token refresh failed and auto_login is disabled"
)
raise
Exception
(
"Claude token refresh failed. Please re-authenticate via /dashboard/claude/auth/start or MCP tool."
)
...
...
@@ -540,7 +541,7 @@ class ClaudeAuth:
logger
.
info
(
"OAuth2 login flow completed successfully"
)
def
exchange_code_for_tokens
(
self
,
code
:
str
,
state
:
str
,
verifier
:
str
=
None
,
max_retries
:
int
=
3
)
->
bool
:
async
def
exchange_code_for_tokens
(
self
,
code
:
str
,
state
:
str
,
verifier
:
str
=
None
,
max_retries
:
int
=
3
)
->
bool
:
"""
Exchange authorization code for access tokens.
Matches CLIProxyAPI implementation exactly.
...
...
@@ -621,7 +622,7 @@ class ClaudeAuth:
# Rate limited - wait and retry with exponential backoff
wait_time
=
(
2
**
attempt
)
*
5
# 5, 10, 20 seconds
logger
.
warning
(
f
"Rate limited (429). Waiting {wait_time} seconds before retry {attempt + 1}/{max_retries}"
)
time
.
sleep
(
wait_time
)
await
asyncio
.
sleep
(
wait_time
)
continue
else
:
logger
.
error
(
f
"Token exchange failed: {response.status_code} - {response.text}"
)
...
...
@@ -631,7 +632,7 @@ class ClaudeAuth:
if
attempt
<
max_retries
-
1
:
wait_time
=
(
2
**
attempt
)
*
5
logger
.
info
(
f
"Retrying in {wait_time} seconds..."
)
time
.
sleep
(
wait_time
)
await
asyncio
.
sleep
(
wait_time
)
continue
return
False
...
...
@@ -652,14 +653,16 @@ class ClaudeAuth:
# Example usage
if
__name__
==
"__main__"
:
import
asyncio
logging
.
basicConfig
(
level
=
logging
.
INFO
)
async
def
main
():
auth
=
ClaudeAuth
()
token
=
auth
.
get_valid_token
()
# Use the token for an API call
client
=
httpx
.
Client
()
response
=
client
.
post
(
async
with
httpx
.
AsyncClient
()
as
client
:
response
=
await
client
.
post
(
"https://api.anthropic.com/v1/messages"
,
headers
=
{
"Authorization"
:
f
"Bearer {token}"
,
...
...
@@ -674,3 +677,5 @@ if __name__ == "__main__":
}
)
print
(
response
.
json
())
asyncio
.
run
(
main
())
aisbf/auth/qwen.py
View file @
fe8b625a
...
...
@@ -152,13 +152,14 @@ class QwenOAuth2:
return
code_verifier
,
code_challenge
def
_acquire_lock
(
self
,
max_attempts
:
int
=
20
)
->
bool
:
async
def
_acquire_lock
(
self
,
max_attempts
:
int
=
20
)
->
bool
:
"""
Acquire a file lock to prevent concurrent token refreshes.
Returns:
True if lock acquired, False otherwise.
"""
import
asyncio
lock_id
=
str
(
uuid
.
uuid4
())
interval
=
0.1
...
...
@@ -182,7 +183,7 @@ class QwenOAuth2:
# Lock might have been removed by another process
continue
time
.
sleep
(
interval
)
await
asyncio
.
sleep
(
interval
)
interval
=
min
(
interval
*
1.5
,
2.0
)
# Exponential backoff
return
False
...
...
@@ -461,7 +462,7 @@ class QwenOAuth2:
logger
.
info
(
"QwenOAuth2: Refreshing access token..."
)
# Acquire lock to prevent concurrent refreshes
if
not
self
.
_acquire_lock
():
if
not
await
self
.
_acquire_lock
():
logger
.
error
(
"QwenOAuth2: Failed to acquire lock for token refresh"
)
return
False
...
...
aisbf/context.py
View file @
fe8b625a
...
...
@@ -534,7 +534,7 @@ class ContextManager:
"max_tokens"
:
1000
,
"stream"
:
False
}
response
=
await
self
.
_rotation_handler
.
handle_rotation_request
(
self
.
_rotation_id
,
condensation_request
)
response
=
await
self
.
_rotation_handler
.
handle_rotation_request
(
self
.
_rotation_id
,
condensation_request
,
None
,
None
)
if
isinstance
(
response
,
dict
):
summary_content
=
response
.
get
(
'choices'
,
[{}])[
0
]
.
get
(
'message'
,
{})
.
get
(
'content'
,
''
)
else
:
...
...
@@ -642,7 +642,7 @@ Provide only the relevant information in a concise format."""
"max_tokens"
:
2000
,
"stream"
:
False
}
response
=
await
self
.
_rotation_handler
.
handle_rotation_request
(
self
.
_rotation_id
,
condensation_request
)
response
=
await
self
.
_rotation_handler
.
handle_rotation_request
(
self
.
_rotation_id
,
condensation_request
,
None
,
None
)
if
isinstance
(
response
,
dict
):
pruned_content
=
response
.
get
(
'choices'
,
[{}])[
0
]
.
get
(
'message'
,
{})
.
get
(
'content'
,
''
)
else
:
...
...
aisbf/cost_extractor.py
0 → 100644
View file @
fe8b625a
"""
Copyleft (C) 2026 Stefy Lanza <stefy@nexlab.net>
AISBF - AI Service Broker Framework || AI Should Be Free
Cost extraction utilities for provider responses.
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/>.
"""
import
logging
from
typing
import
Dict
,
Optional
,
Any
logger
=
logging
.
getLogger
(
__name__
)
def
extract_cost_from_response
(
response
:
Dict
[
str
,
Any
],
provider_id
:
str
)
->
Optional
[
float
]:
"""
Extract actual cost from provider response if available.
Args:
response: Provider response dictionary
provider_id: Provider identifier
Returns:
Cost in USD if found, None otherwise
"""
if
not
response
or
not
isinstance
(
response
,
dict
):
return
None
try
:
# AWS Bedrock - may include cost in usage
if
provider_id
in
[
'amazon'
,
'bedrock'
,
'aws'
]:
usage
=
response
.
get
(
'usage'
,
{})
if
isinstance
(
usage
,
dict
):
cost
=
usage
.
get
(
'cost'
)
if
cost
is
not
None
:
return
float
(
cost
)
# Cohere - has billed_units but not direct cost
# Would need pricing config to convert
if
provider_id
==
'cohere'
:
meta
=
response
.
get
(
'meta'
,
{})
if
isinstance
(
meta
,
dict
):
billed_units
=
meta
.
get
(
'billed_units'
,
{})
if
billed_units
:
# Return None - we'll calculate from tokens
# Could enhance this to calculate from billed_units
pass
# Replicate - has prediction time
if
provider_id
==
'replicate'
:
metrics
=
response
.
get
(
'metrics'
,
{})
if
isinstance
(
metrics
,
dict
):
predict_time
=
metrics
.
get
(
'predict_time'
)
if
predict_time
:
# Would need pricing per second to calculate
# Return None for now - calculate from tokens
pass
# Check for generic cost fields that some providers might use
for
cost_field
in
[
'cost'
,
'price'
,
'amount'
,
'total_cost'
]:
if
cost_field
in
response
:
cost
=
response
[
cost_field
]
if
cost
is
not
None
:
return
float
(
cost
)
# Check in usage object
usage
=
response
.
get
(
'usage'
,
{})
if
isinstance
(
usage
,
dict
)
and
cost_field
in
usage
:
cost
=
usage
[
cost_field
]
if
cost
is
not
None
:
return
float
(
cost
)
return
None
except
Exception
as
e
:
logger
.
debug
(
f
"Error extracting cost from {provider_id} response: {e}"
)
return
None
def
extract_cost_from_streaming_chunk
(
chunk
:
Dict
[
str
,
Any
],
provider_id
:
str
)
->
Optional
[
float
]:
"""
Extract cost from streaming response chunk if available.
Most providers don't include cost in streaming chunks, but some might
include it in the final chunk.
Args:
chunk: Streaming chunk dictionary
provider_id: Provider identifier
Returns:
Cost in USD if found, None otherwise
"""
if
not
chunk
or
not
isinstance
(
chunk
,
dict
):
return
None
try
:
# Check if this is a final chunk with usage/cost info
usage
=
chunk
.
get
(
'usage'
,
{})
if
isinstance
(
usage
,
dict
):
# Try to extract cost from usage
cost
=
usage
.
get
(
'cost'
)
if
cost
is
not
None
:
return
float
(
cost
)
# Some providers might include cost at top level in final chunk
cost
=
chunk
.
get
(
'cost'
)
if
cost
is
not
None
:
return
float
(
cost
)
return
None
except
Exception
as
e
:
logger
.
debug
(
f
"Error extracting cost from {provider_id} streaming chunk: {e}"
)
return
None
aisbf/database.py
View file @
fe8b625a
...
...
@@ -27,6 +27,8 @@ from pathlib import Path
from
typing
import
Dict
,
List
,
Optional
,
Tuple
,
Any
from
datetime
import
datetime
,
timedelta
import
logging
import
asyncio
from
concurrent.futures
import
ThreadPoolExecutor
try
:
import
mysql.connector
as
_mysql_connector
...
...
@@ -37,12 +39,23 @@ except ImportError:
logger
=
logging
.
getLogger
(
__name__
)
# Global thread pool executor for database operations
_db_executor
=
None
def
get_db_executor
():
"""Get or create the global database thread pool executor."""
global
_db_executor
if
_db_executor
is
None
:
_db_executor
=
ThreadPoolExecutor
(
max_workers
=
10
,
thread_name_prefix
=
"db_worker"
)
return
_db_executor
class
DatabaseManager
:
"""
Manages database for persistent tracking of context dimensions and rate limiting.
Supports both SQLite and MySQL databases.
All database operations are non-blocking using asyncio and thread pool executors.
"""
def
__init__
(
self
,
db_config
:
Optional
[
Dict
[
str
,
Any
]]
=
None
):
...
...
@@ -69,7 +82,7 @@ class DatabaseManager:
self
.
db_config
=
db_config
self
.
db_type
=
self
.
db_config
.
get
(
'type'
,
'sqlite'
)
.
lower
()
self
.
executor
=
get_db_executor
()
if
self
.
db_type
==
'mysql'
:
if
not
MYSQL_AVAILABLE
:
...
...
@@ -99,7 +112,58 @@ class DatabaseManager:
else
:
raise
ValueError
(
f
"Unsupported database type: {self.db_type}"
)
async
def
_run_in_executor
(
self
,
func
,
*
args
):
"""Run a blocking database operation in a thread pool executor."""
loop
=
asyncio
.
get_event_loop
()
return
await
loop
.
run_in_executor
(
self
.
executor
,
func
,
*
args
)
async
def
record_context_dimension_async
(
self
,
provider_id
:
str
,
model_name
:
str
,
context_size
:
Optional
[
int
]
=
None
,
condense_context
:
Optional
[
int
]
=
None
,
condense_method
:
Optional
[
str
]
=
None
):
"""
Record or update context dimension configuration for a model (async version).
Args:
provider_id: The provider identifier
model_name: The model name
context_size: Maximum context size in tokens
condense_context: Percentage (0-100) at which to trigger condensation
condense_method: Condensation method(s) as string or list
"""
def
_sync_operation
():
with
self
.
_get_connection
()
as
conn
:
cursor
=
conn
.
cursor
()
# Convert condense_method to JSON string if it's a list
condense_method_str
=
json
.
dumps
(
condense_method
)
if
isinstance
(
condense_method
,
list
)
else
condense_method
if
self
.
db_type
==
'sqlite'
:
cursor
.
execute
(
'''
INSERT OR REPLACE INTO context_dimensions
(provider_id, model_name, context_size, condense_context, condense_method, last_updated)
VALUES (?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
'''
,
(
provider_id
,
model_name
,
context_size
,
condense_context
,
condense_method_str
))
else
:
# mysql
cursor
.
execute
(
'''
INSERT INTO context_dimensions
(provider_id, model_name, context_size, condense_context, condense_method, last_updated)
VALUES (
%
s,
%
s,
%
s,
%
s,
%
s, CURRENT_TIMESTAMP)
ON DUPLICATE KEY UPDATE
context_size=VALUES(context_size), condense_context=VALUES(condense_context),
condense_method=VALUES(condense_method), last_updated=CURRENT_TIMESTAMP
'''
,
(
provider_id
,
model_name
,
context_size
,
condense_context
,
condense_method_str
))
conn
.
commit
()
logger
.
debug
(
f
"Recorded context dimension for {provider_id}/{model_name}"
)
await
self
.
_run_in_executor
(
_sync_operation
)
def
record_context_dimension
(
self
,
...
...
@@ -217,27 +281,44 @@ class DatabaseManager:
provider_id
:
str
,
model_name
:
str
,
tokens_used
:
int
,
user_id
:
Optional
[
int
]
=
None
user_id
:
Optional
[
int
]
=
None
,
success
:
bool
=
True
,
latency_ms
:
float
=
0
,
error_type
:
Optional
[
str
]
=
None
,
token_id
:
Optional
[
int
]
=
None
,
prompt_tokens
:
Optional
[
int
]
=
None
,
completion_tokens
:
Optional
[
int
]
=
None
,
actual_cost
:
Optional
[
float
]
=
None
):
"""
Record token usage for rate limiting.
Record token usage for rate limiting
and analytics
.
Args:
provider_id: The provider identifier
model_name: The model name
tokens_used: Number of tokens used in the request
tokens_used: Number of tokens used in the request
(total)
user_id: Optional user ID for user-specific tracking
success: Whether the request was successful
latency_ms: Request latency in milliseconds (float)
error_type: Optional error type if request failed
token_id: Optional API token ID used for the request
prompt_tokens: Optional number of input/prompt tokens
completion_tokens: Optional number of output/completion tokens
actual_cost: Optional actual cost returned by provider (in USD)
"""
with
self
.
_get_connection
()
as
conn
:
cursor
=
conn
.
cursor
()
placeholder
=
'?'
if
self
.
db_type
==
'sqlite'
else
'
%
s'
# Convert latency to int for storage
latency_int
=
int
(
latency_ms
)
if
latency_ms
else
0
logger
.
info
(
f
"DB.record_token_usage: provider={provider_id}, latency_ms={latency_ms} -> latency_int={latency_int}, success={success}, prompt={prompt_tokens}, completion={completion_tokens}, cost={actual_cost}"
)
cursor
.
execute
(
f
'''
INSERT INTO token_usage (user_id, provider_id, model_name, tokens_used, timestamp)
VALUES ({placeholder}, {placeholder}, {placeholder}, {placeholder}, CURRENT_TIMESTAMP)
'''
,
(
user_id
,
provider_id
,
model_name
,
tokens_used
))
INSERT INTO token_usage (user_id, provider_id, model_name, tokens_used,
prompt_tokens, completion_tokens, actual_cost, success, latency_ms, error_type, token_id,
timestamp)
VALUES ({placeholder}, {placeholder}, {placeholder}, {placeholder},
{placeholder}, {placeholder}, {placeholder}, {placeholder}, {placeholder}, {placeholder}, {placeholder},
CURRENT_TIMESTAMP)
'''
,
(
user_id
,
provider_id
,
model_name
,
tokens_used
,
prompt_tokens
,
completion_tokens
,
actual_cost
,
success
,
latency_int
,
error_type
,
token_id
))
conn
.
commit
()
logger
.
debug
(
f
"Recorded token usage for {provider_id}/{model_name}: {tokens_used} (
user_id={user_id}
)"
)
logger
.
debug
(
f
"Recorded token usage for {provider_id}/{model_name}: {tokens_used} (
prompt={prompt_tokens}, completion={completion_tokens}, cost={actual_cost}, user_id={user_id}, success={success}, latency={latency_int}ms
)"
)
def
get_token_usage
(
self
,
...
...
@@ -2849,20 +2930,20 @@ def DatabaseManager__initialize_database(self):
if
self
.
database_type
==
DatabaseRegistry
.
TYPE_CONFIG
:
# ONLY CREATE CONFIG TABLES IN CONFIG DATABASE
# Create context_dimensions table for tracking context usage
cursor
.
execute
(
f
'''
CREATE TABLE IF NOT EXISTS context_dimensions (
id INTEGER PRIMARY KEY {auto_increment},
provider_id VARCHAR(255) NOT NULL,
model_name VARCHAR(255) NOT NULL,
context_size INTEGER,
condense_context INTEGER,
condense_method TEXT,
effective_context INTEGER DEFAULT 0,
last_updated TIMESTAMP DEFAULT {timestamp_default},
UNIQUE(provider_id, model_name)
)
'''
)
#
cursor.execute(f'''
#
CREATE TABLE IF NOT EXISTS context_dimensions (
#
id INTEGER PRIMARY KEY {auto_increment},
#
provider_id VARCHAR(255) NOT NULL,
#
model_name VARCHAR(255) NOT NULL,
#
context_size INTEGER,
#
condense_context INTEGER,
#
condense_method TEXT,
#
effective_context INTEGER DEFAULT 0,
#
last_updated TIMESTAMP DEFAULT {timestamp_default},
#
UNIQUE(provider_id, model_name)
#
)
#
''')
#
# Create token_usage table for tracking rate limiting
cursor
.
execute
(
f
'''
CREATE TABLE IF NOT EXISTS token_usage (
...
...
@@ -2871,219 +2952,554 @@ def DatabaseManager__initialize_database(self):
provider_id VARCHAR(255) NOT NULL,
model_name VARCHAR(255) NOT NULL,
tokens_used INTEGER NOT NULL,
prompt_tokens INTEGER,
completion_tokens INTEGER,
actual_cost DECIMAL(10,6),
timestamp TIMESTAMP DEFAULT {timestamp_default}
)
'''
)
#
#
#
# Create indexes for better query performance
try
:
cursor
.
execute
(
'''
CREATE INDEX IF NOT EXISTS idx_context_provider_model
ON context_dimensions(provider_id, model_name)
'''
)
except
:
pass
# Index might already exist
try
:
cursor
.
execute
(
'''
CREATE INDEX IF NOT EXISTS idx_token_provider_model
ON token_usage(provider_id, model_name)
'''
)
except
:
pass
try
:
cursor
.
execute
(
'''
CREATE INDEX IF NOT EXISTS idx_token_timestamp
ON token_usage(timestamp)
'''
)
except
:
pass
#
try:
#
cursor.execute('''
#
CREATE INDEX IF NOT EXISTS idx_context_provider_model
#
ON context_dimensions(provider_id, model_name)
#
''')
#
except:
#
pass # Index might already exist
#
#
try:
#
cursor.execute('''
#
CREATE INDEX IF NOT EXISTS idx_token_provider_model
#
ON token_usage(provider_id, model_name)
#
''')
#
except:
#
pass
#
#
try:
#
cursor.execute('''
#
CREATE INDEX IF NOT EXISTS idx_token_timestamp
#
ON token_usage(timestamp)
#
''')
#
except:
#
pass
#
# Create model_embeddings table for caching vectorized model descriptions
cursor
.
execute
(
f
'''
CREATE TABLE IF NOT EXISTS model_embeddings (
id INTEGER PRIMARY KEY {auto_increment},
provider_id VARCHAR(255) NOT NULL,
model_name VARCHAR(255) NOT NULL,
description TEXT,
embedding TEXT,
last_updated TIMESTAMP DEFAULT {timestamp_default},
UNIQUE(provider_id, model_name)
)
'''
)
try
:
cursor
.
execute
(
'''
CREATE INDEX IF NOT EXISTS idx_model_embeddings_provider_model
ON model_embeddings(provider_id, model_name)
'''
)
except
:
pass
#
cursor.execute(f'''
#
CREATE TABLE IF NOT EXISTS model_embeddings (
#
id INTEGER PRIMARY KEY {auto_increment},
#
provider_id VARCHAR(255) NOT NULL,
#
model_name VARCHAR(255) NOT NULL,
#
description TEXT,
#
embedding TEXT,
#
last_updated TIMESTAMP DEFAULT {timestamp_default},
#
UNIQUE(provider_id, model_name)
#
)
#
''')
#
#
try:
#
cursor.execute('''
#
CREATE INDEX IF NOT EXISTS idx_model_embeddings_provider_model
#
ON model_embeddings(provider_id, model_name)
#
''')
#
except:
#
pass
#
# Create users table for multi-user management
cursor
.
execute
(
f
'''
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY {auto_increment},
username VARCHAR(255) UNIQUE NOT NULL,
email VARCHAR(255) UNIQUE,
display_name VARCHAR(255),
password_hash VARCHAR(255) NOT NULL,
role VARCHAR(50) DEFAULT 'user',
created_by VARCHAR(255),
created_at TIMESTAMP DEFAULT {timestamp_default},
last_login TIMESTAMP NULL,
is_active {boolean_type} DEFAULT 1,
email_verified {boolean_type} DEFAULT 0,
verification_token VARCHAR(255),
verification_token_expires TIMESTAMP NULL,
last_verification_email_sent TIMESTAMP NULL
)
'''
)
#
cursor.execute(f'''
#
CREATE TABLE IF NOT EXISTS users (
#
id INTEGER PRIMARY KEY {auto_increment},
#
username VARCHAR(255) UNIQUE NOT NULL,
#
email VARCHAR(255) UNIQUE,
#
display_name VARCHAR(255),
#
password_hash VARCHAR(255) NOT NULL,
#
role VARCHAR(50) DEFAULT 'user',
#
created_by VARCHAR(255),
#
created_at TIMESTAMP DEFAULT {timestamp_default},
#
last_login TIMESTAMP NULL,
#
is_active {boolean_type} DEFAULT 1,
#
email_verified {boolean_type} DEFAULT 0,
#
verification_token VARCHAR(255),
#
verification_token_expires TIMESTAMP NULL,
#
last_verification_email_sent TIMESTAMP NULL
#
)
#
''')
#
# Migration: Add display_name column if it doesn't exist
try
:
#
try:
# Check if display_name column exists
if
self
.
db_type
==
'sqlite'
:
cursor
.
execute
(
"PRAGMA table_info(users)"
)
columns
=
[
row
[
1
]
for
row
in
cursor
.
fetchall
()]
else
:
cursor
.
execute
(
"""
SELECT COLUMN_NAME
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_NAME = 'users'
"""
)
columns
=
[
row
[
0
]
for
row
in
cursor
.
fetchall
()]
if
'display_name'
not
in
columns
:
logger
.
info
(
"Adding display_name column to users table"
)
cursor
.
execute
(
"ALTER TABLE users ADD COLUMN display_name VARCHAR(255)"
)
conn
.
commit
()
#
if self.db_type == 'sqlite':
#
cursor.execute("PRAGMA table_info(users)")
#
columns = [row[1] for row in cursor.fetchall()]
#
else:
#
cursor.execute("""
# SELECT COLUMN_NAME
# FROM INFORMATION_SCHEMA.COLUMNS
#
WHERE TABLE_NAME = 'users'
#
""")
#
columns = [row[0] for row in cursor.fetchall()]
#
#
if 'display_name' not in columns:
#
logger.info("Adding display_name column to users table")
#
cursor.execute("ALTER TABLE users ADD COLUMN display_name VARCHAR(255)")
#
conn.commit()
#
# Populate display_name for existing users
cursor
.
execute
(
"UPDATE users SET display_name = username WHERE display_name IS NULL"
)
conn
.
commit
()
logger
.
info
(
"Migration complete: display_name column added and populated"
)
except
Exception
as
e
:
logger
.
warning
(
f
"Migration warning (display_name): {e}"
)
# Continue even if migration fails - column might already exist
# User-specific configuration tables for multi-user isolation
cursor
.
execute
(
f
'''
CREATE TABLE IF NOT EXISTS user_providers (
id INTEGER PRIMARY KEY {auto_increment},
user_id INTEGER NOT NULL,
provider_id VARCHAR(255) NOT NULL,
config TEXT NOT NULL,
created_at TIMESTAMP DEFAULT {timestamp_default},
updated_at TIMESTAMP DEFAULT {timestamp_default},
FOREIGN KEY (user_id) REFERENCES users(id),
UNIQUE(user_id, provider_id)
)
'''
)
# cursor.execute("UPDATE users SET display_name = username WHERE display_name IS NULL")
# conn.commit()
# logger.info("Migration complete: display_name column added and populated")
# except Exception as e:
# logger.warning(f"Migration warning (display_name): {e}")
#
# User-specific configuration tables for multi-user isolation - commented out to fix import
# cursor.execute(f'''
# CREATE TABLE IF NOT EXISTS user_providers (
# id INTEGER PRIMARY KEY {auto_increment},
# user_id INTEGER NOT NULL,
# provider_id VARCHAR(255) NOT NULL,
# config TEXT NOT NULL,
# created_at TIMESTAMP DEFAULT {timestamp_default},
# updated_at TIMESTAMP DEFAULT {timestamp_default},
# FOREIGN KEY (user_id) REFERENCES users(id),
# UNIQUE(user_id, provider_id)
# )
# ''')
#
# cursor.execute(f'''
# CREATE TABLE IF NOT EXISTS user_rotations (
# id INTEGER PRIMARY KEY {auto_increment},
# user_id INTEGER NOT NULL,
# rotation_id VARCHAR(255) NOT NULL,
# config TEXT NOT NULL,
# created_at TIMESTAMP DEFAULT {timestamp_default},
# updated_at TIMESTAMP DEFAULT {timestamp_default},
# FOREIGN KEY (user_id) REFERENCES users(id),
# UNIQUE(user_id, rotation_id)
# )
# ''')
#
# cursor.execute(f'''
# CREATE TABLE IF NOT EXISTS user_autoselects (
# id INTEGER PRIMARY KEY {auto_increment},
# user_id INTEGER NOT NULL,
# autoselect_id VARCHAR(255) NOT NULL,
# config TEXT NOT NULL,
# created_at TIMESTAMP DEFAULT {timestamp_default},
# updated_at TIMESTAMP DEFAULT {timestamp_default},
# FOREIGN KEY (user_id) REFERENCES users(id),
# UNIQUE(user_id, autoselect_id)
# )
# ''')
#
# cursor.execute(f'''
# CREATE TABLE IF NOT EXISTS user_prompts (
# id INTEGER PRIMARY KEY {auto_increment},
# user_id INTEGER NOT NULL,
# prompt_key VARCHAR(255) NOT NULL,
# content TEXT NOT NULL,
# created_at TIMESTAMP DEFAULT {timestamp_default},
# updated_at TIMESTAMP DEFAULT {timestamp_default},
# FOREIGN KEY (user_id) REFERENCES users(id),
# UNIQUE(user_id, prompt_key)
# )
# ''')
#
# cursor.execute(f'''
# CREATE TABLE IF NOT EXISTS user_api_tokens (
# id INTEGER PRIMARY KEY {auto_increment},
# user_id INTEGER NOT NULL,
# token VARCHAR(255) UNIQUE NOT NULL,
# description TEXT,
# created_at TIMESTAMP DEFAULT {timestamp_default},
# last_used TIMESTAMP NULL,
# is_active {boolean_type} DEFAULT 1,
# FOREIGN KEY (user_id) REFERENCES users(id)
# )
# ''')
#
# cursor.execute(f'''
# CREATE TABLE IF NOT EXISTS user_token_usage (
# id INTEGER PRIMARY KEY {auto_increment},
# user_id INTEGER NOT NULL,
# token_id INTEGER,
# provider_id VARCHAR(255) NOT NULL,
# model_name VARCHAR(255) NOT NULL,
# tokens_used INTEGER NOT NULL,
# timestamp TIMESTAMP DEFAULT {timestamp_default},
# FOREIGN KEY (user_id) REFERENCES users(id),
# FOREIGN KEY (token_id) REFERENCES user_api_tokens(id)
# )
# ''')
#
# Create user_auth_files table for storing authentication file metadata
# cursor.execute(f'''
# CREATE TABLE IF NOT EXISTS user_auth_files (
# id INTEGER PRIMARY KEY {auto_increment},
# user_id INTEGER NOT NULL,
# provider_id VARCHAR(255) NOT NULL,
# file_type VARCHAR(50) NOT NULL,
# original_filename VARCHAR(255) NOT NULL,
# stored_filename VARCHAR(255) NOT NULL,
# file_path TEXT NOT NULL,
# file_size INTEGER,
# mime_type VARCHAR(100),
# created_at TIMESTAMP DEFAULT {timestamp_default},
# updated_at TIMESTAMP DEFAULT {timestamp_default},
# FOREIGN KEY (user_id) REFERENCES users(id),
# UNIQUE(user_id, provider_id, file_type)
# )
# ''')
#
# Create user_oauth2_credentials table for storing OAuth2 tokens per user/provider
# cursor.execute(f'''
# CREATE TABLE IF NOT EXISTS user_oauth2_credentials (
# id INTEGER PRIMARY KEY {auto_increment},
# user_id INTEGER NOT NULL,
# provider_id VARCHAR(255) NOT NULL,
# auth_type VARCHAR(50) NOT NULL,
# credentials TEXT NOT NULL,
# created_at TIMESTAMP DEFAULT {timestamp_default},
# updated_at TIMESTAMP DEFAULT {timestamp_default},
# FOREIGN KEY (user_id) REFERENCES users(id),
# UNIQUE(user_id, provider_id, auth_type)
# )
# ''')
#
# ==============================================
# UNIVERSAL MIGRATIONS - RUN ON EVERY STARTUP
# ==============================================
# logger.info("Running database migrations...")
#
# Migration: Create account_tiers table if missing
# try:
# if self.db_type == 'sqlite':
# cursor.execute("PRAGMA table_info(account_tiers)")
# if not cursor.fetchall():
# cursor.execute(f'''
# CREATE TABLE account_tiers (
# id INTEGER PRIMARY KEY {auto_increment},
# name VARCHAR(255) UNIQUE NOT NULL,
# description TEXT,
# price_monthly DECIMAL(10,2) DEFAULT 0.00,
# price_yearly DECIMAL(10,2) DEFAULT 0.00,
# is_default {boolean_type} DEFAULT 0,
# is_active {boolean_type} DEFAULT 1,
# max_requests_per_day INTEGER DEFAULT -1,
# max_requests_per_month INTEGER DEFAULT -1,
# max_providers INTEGER DEFAULT -1,
# max_rotations INTEGER DEFAULT -1,
# max_autoselections INTEGER DEFAULT -1,
# max_rotation_models INTEGER DEFAULT -1,
# max_autoselection_models INTEGER DEFAULT -1,
# created_at TIMESTAMP DEFAULT {timestamp_default},
# updated_at TIMESTAMP DEFAULT {timestamp_default}
# )
# ''')
# conn.commit()
# logger.info("✅ Migration: Created missing account_tiers table")
# except Exception as e:
# logger.warning(f"Migration check for account_tiers table: {e}")
#
# Migration: Add missing columns to account_tiers
# try:
# if self.db_type == 'sqlite':
# cursor.execute("PRAGMA table_info(account_tiers)")
# existing_columns = [row[1] for row in cursor.fetchall()]
# tier_columns = [
# ('max_requests_per_day', 'INTEGER DEFAULT -1'),
# ('max_requests_per_month', 'INTEGER DEFAULT -1'),
# ('max_providers', 'INTEGER DEFAULT -1'),
# ('max_rotations', 'INTEGER DEFAULT -1'),
# ('max_autoselections', 'INTEGER DEFAULT -1'),
# ('max_rotation_models', 'INTEGER DEFAULT -1'),
# ('max_autoselection_models', 'INTEGER DEFAULT -1'),
# ('is_default', f'{boolean_type} DEFAULT 0'),
# ('is_active', f'{boolean_type} DEFAULT 1'),
# ('is_visible', f'{boolean_type} DEFAULT 1')
# ]
# col_count = 0
# for col_name, col_def in tier_columns:
# if col_name not in existing_columns:
# cursor.execute(f'ALTER TABLE account_tiers ADD COLUMN {col_name} {col_def}')
# col_count += 1
# if col_count > 0:
# logger.info(f"✅ Migration: Added {col_count} missing columns to account_tiers")
# else:
# MySQL/MariaDB
# cursor.execute("""
# SELECT COLUMN_NAME
# FROM INFORMATION_SCHEMA.COLUMNS
# WHERE TABLE_NAME = 'account_tiers'
# AND TABLE_SCHEMA = DATABASE()
# """)
# existing_columns = [row[0] for row in cursor.fetchall()]
# tier_columns = [
# ('max_requests_per_day', 'INTEGER DEFAULT -1'),
# ('max_requests_per_month', 'INTEGER DEFAULT -1'),
# ('max_providers', 'INTEGER DEFAULT -1'),
# ('max_rotations', 'INTEGER DEFAULT -1'),
# ('max_autoselections', 'INTEGER DEFAULT -1'),
# ('max_rotation_models', 'INTEGER DEFAULT -1'),
# ('max_autoselection_models', 'INTEGER DEFAULT -1'),
# ('is_default', f'{boolean_type} DEFAULT 0'),
# ('is_active', f'{boolean_type} DEFAULT 1'),
# ('is_visible', f'{boolean_type} DEFAULT 1')
# ]
# col_count = 0
# for col_name, col_def in tier_columns:
# if col_name not in existing_columns:
# cursor.execute(f'ALTER TABLE account_tiers ADD COLUMN {col_name} {col_def}')
# col_count += 1
# if col_count > 0:
# conn.commit()
# logger.info(f"✅ Migration: Added {col_count} missing columns to account_tiers")
# except Exception as e:
# logger.warning(f"Migration check for account_tiers columns: {e}")
#
# Migration: Ensure default free tier exists
# try:
# cursor.execute(f'SELECT COUNT(*) FROM account_tiers WHERE is_default = 1')
# free_tier_count = cursor.fetchone()[0]
# if free_tier_count == 0:
# cursor.execute(f'''
# INSERT INTO account_tiers
# (name, description, price_monthly, price_yearly, is_default, is_active,
# max_requests_per_day, max_requests_per_month, max_providers, max_rotations,
# max_autoselections, max_rotation_models, max_autoselection_models)
# VALUES
# ('Free Tier', 'Default free account tier with unlimited access', 0.00, 0.00, 1, 1,
# -1, -1, -1, -1, -1, -1, -1)
# ''')
# logger.info("✅ Migration: Inserted default free tier")
# except Exception as e:
# logger.warning(f"Migration check for default free tier: {e}")
#
# Migration: Add tier_id column to users table
# try:
# if self.db_type == 'sqlite':
# cursor.execute("PRAGMA table_info(users)")
# columns = [row[1] for row in cursor.fetchall()]
# if 'tier_id' not in columns:
# cursor.execute('ALTER TABLE users ADD COLUMN tier_id INTEGER DEFAULT 1')
# cursor.execute('ALTER TABLE users ADD COLUMN subscription_expires TIMESTAMP NULL')
# logger.info("✅ Migration: Added tier_id and subscription_expires columns to users")
# else:
# cursor.execute("""
# SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS
# WHERE TABLE_NAME = 'users' AND COLUMN_NAME = 'tier_id'
# """)
# if not cursor.fetchone():
# cursor.execute('ALTER TABLE users ADD COLUMN tier_id INTEGER DEFAULT 1')
# cursor.execute('ALTER TABLE users ADD COLUMN subscription_expires TIMESTAMP NULL')
# logger.info("✅ Migration: Added tier_id and subscription_expires columns to users")
# except Exception as e:
# logger.warning(f"Migration check for users.tier_id: {e}")
#
# Migration: Add password reset token columns to users table
# try:
# if self.db_type == 'sqlite':
# cursor.execute("PRAGMA table_info(users)")
# columns = [row[1] for row in cursor.fetchall()]
# if 'reset_password_token' not in columns:
# cursor.execute('ALTER TABLE users ADD COLUMN reset_password_token VARCHAR(255)')
# cursor.execute('ALTER TABLE users ADD COLUMN reset_password_token_expires TIMESTAMP NULL')
# logger.info("✅ Migration: Added password reset token columns to users")
# else:
# cursor.execute("""
# SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS
# WHERE TABLE_NAME = 'users' AND COLUMN_NAME = 'reset_password_token'
# """)
# if not cursor.fetchone():
# cursor.execute('ALTER TABLE users ADD COLUMN reset_password_token VARCHAR(255)')
# cursor.execute('ALTER TABLE users ADD COLUMN reset_password_token_expires TIMESTAMP NULL')
# logger.info("✅ Migration: Added password reset token columns to users")
# except Exception as e:
# logger.warning(f"Migration check for users.reset_password_token: {e}")
#
# Migration: Add last_verification_email_sent column to users table
# try:
# if self.db_type == 'sqlite':
# cursor.execute("PRAGMA table_info(users)")
# columns = [row[1] for row in cursor.fetchall()]
# if 'last_verification_email_sent' not in columns:
# cursor.execute('ALTER TABLE users ADD COLUMN last_verification_email_sent TIMESTAMP NULL')
# logger.info("✅ Migration: Added last_verification_email_sent column to users")
# else:
# cursor.execute("""
# SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS
# WHERE TABLE_NAME = 'users' AND COLUMN_NAME = 'last_verification_email_sent'
# """)
# if not cursor.fetchone():
# cursor.execute('ALTER TABLE users ADD COLUMN last_verification_email_sent TIMESTAMP NULL')
# logger.info("✅ Migration: Added last_verification_email_sent column to users")
# except Exception as e:
# logger.warning(f"Migration check for users.last_verification_email_sent: {e}")
#
# Migration: Create payment_methods, user_subscriptions, payment_transactions tables
# for table_name, create_sql in [
# ('payment_methods', f'''
# CREATE TABLE payment_methods (
# id INTEGER PRIMARY KEY {auto_increment},
# user_id INTEGER NOT NULL,
# type VARCHAR(50) NOT NULL,
# identifier VARCHAR(255) NOT NULL,
# is_default {boolean_type} DEFAULT 0,
# is_active {boolean_type} DEFAULT 1,
# metadata TEXT,
# created_at TIMESTAMP DEFAULT {timestamp_default},
# updated_at TIMESTAMP DEFAULT {timestamp_default},
# FOREIGN KEY (user_id) REFERENCES users(id)
# )
# '''),
# ('admin_settings', f'''
# CREATE TABLE admin_settings (
# id INTEGER PRIMARY KEY {auto_increment},
# setting_key VARCHAR(255) UNIQUE NOT NULL,
# setting_value TEXT,
# updated_at TIMESTAMP DEFAULT {timestamp_default}
# )
# '''),
# ('user_subscriptions', f'''
# CREATE TABLE user_subscriptions (
# id INTEGER PRIMARY KEY {auto_increment},
# user_id INTEGER NOT NULL,
# tier_id INTEGER NOT NULL,
# status VARCHAR(50) DEFAULT 'active',
# start_date TIMESTAMP DEFAULT {timestamp_default},
# end_date TIMESTAMP NULL,
# next_billing_date TIMESTAMP NULL,
# trial_end_date TIMESTAMP NULL,
# payment_method_id INTEGER,
# auto_renew {boolean_type} DEFAULT 1,
# created_at TIMESTAMP DEFAULT {timestamp_default},
# updated_at TIMESTAMP DEFAULT {timestamp_default},
# FOREIGN KEY (user_id) REFERENCES users(id),
# FOREIGN KEY (tier_id) REFERENCES account_tiers(id),
# FOREIGN KEY (payment_method_id) REFERENCES payment_methods(id),
# UNIQUE(user_id, tier_id)
# )
# '''),
# ('payment_transactions', f'''
# CREATE TABLE payment_transactions (
# id INTEGER PRIMARY KEY {auto_increment},
# user_id INTEGER NOT NULL,
# tier_id INTEGER,
# subscription_id INTEGER,
# payment_method_id INTEGER,
# amount DECIMAL(10,2) NOT NULL,
# currency VARCHAR(10) DEFAULT 'USD',
# status VARCHAR(50) NOT NULL,
# transaction_type VARCHAR(50) NOT NULL,
# external_transaction_id VARCHAR(255),
# metadata TEXT,
# created_at TIMESTAMP DEFAULT {timestamp_default},
# completed_at TIMESTAMP NULL,
# FOREIGN KEY (user_id) REFERENCES users(id),
# FOREIGN KEY (tier_id) REFERENCES account_tiers(id),
# FOREIGN KEY (subscription_id) REFERENCES user_subscriptions(id),
# FOREIGN KEY (payment_method_id) REFERENCES payment_methods(id)
# )
# ''')
# ]:
# try:
# if self.db_type == 'sqlite':
# cursor.execute(f"PRAGMA table_info({table_name})")
# if not cursor.fetchall():
# cursor.execute(create_sql)
# logger.info(f"✅ Migration: Created missing {table_name} table")
# else:
# cursor.execute(f"""
# SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES
# WHERE TABLE_NAME = '{table_name}'
# """)
# if not cursor.fetchone():
# cursor.execute(create_sql)
# logger.info(f"✅ Migration: Created missing {table_name} table")
# except Exception as e:
# logger.warning(f"Migration check for {table_name} table: {e}")
#
# conn.commit()
# logger.info("✅ All database migrations completed")
#
# else:
# CACHE DATABASE GETS MINIMAL TABLES ONLY
# cursor.execute(f'''
# CREATE TABLE IF NOT EXISTS token_usage (
# id INTEGER PRIMARY KEY {auto_increment},
# user_id INTEGER,
# provider_id VARCHAR(255) NOT NULL,
# model_name VARCHAR(255) NOT NULL,
# tokens_used INTEGER NOT NULL,
# timestamp TIMESTAMP DEFAULT {timestamp_default}
# )
# ''')
#
# cursor.execute(f'''
# CREATE TABLE IF NOT EXISTS context_dimensions (
# id INTEGER PRIMARY KEY {auto_increment},
# provider_id VARCHAR(255) NOT NULL,
# model_name VARCHAR(255) NOT NULL,
# context_size INTEGER,
# condense_context INTEGER,
# condense_method TEXT,
# effective_context INTEGER DEFAULT 0,
# last_updated TIMESTAMP DEFAULT {timestamp_default},
# UNIQUE(provider_id, model_name)
# )
# ''')
#
# logger.info("⚠️ CACHE DATABASE: Only minimal cache tables created - NO USER TABLES")
cursor
.
execute
(
f
'''
CREATE TABLE IF NOT EXISTS user_rotations (
id INTEGER PRIMARY KEY {auto_increment},
user_id INTEGER NOT NULL,
rotation_id VARCHAR(255) NOT NULL,
config TEXT NOT NULL,
created_at TIMESTAMP DEFAULT {timestamp_default},
updated_at TIMESTAMP DEFAULT {timestamp_default},
FOREIGN KEY (user_id) REFERENCES users(id),
UNIQUE(user_id, rotation_id)
)
'''
)
conn
.
commit
()
logger
.
info
(
f
"Database tables initialized successfully for {self.database_type} database"
)
cursor
.
execute
(
f
'''
CREATE TABLE IF NOT EXISTS user_autoselects (
id INTEGER PRIMARY KEY {auto_increment},
user_id INTEGER NOT NULL,
autoselect_id VARCHAR(255) NOT NULL,
config TEXT NOT NULL,
created_at TIMESTAMP DEFAULT {timestamp_default},
updated_at TIMESTAMP DEFAULT {timestamp_default},
FOREIGN KEY (user_id) REFERENCES users(id),
UNIQUE(user_id, autoselect_id)
)
'''
)
cursor
.
execute
(
f
'''
CREATE TABLE IF NOT EXISTS user_prompts (
id INTEGER PRIMARY KEY {auto_increment},
user_id INTEGER NOT NULL,
prompt_key VARCHAR(255) NOT NULL,
content TEXT NOT NULL,
created_at TIMESTAMP DEFAULT {timestamp_default},
updated_at TIMESTAMP DEFAULT {timestamp_default},
FOREIGN KEY (user_id) REFERENCES users(id),
UNIQUE(user_id, prompt_key)
)
'''
)
def
DatabaseManager__create_config_tables
(
self
,
cursor
,
auto_increment
,
timestamp_default
,
boolean_type
):
"""Create all permanent configuration tables (CONFIG DB ONLY) - UNUSED METHOD"""
pass
# Method disabled
cursor
.
execute
(
f
'''
CREATE TABLE IF NOT EXISTS user_api_tokens (
id INTEGER PRIMARY KEY {auto_increment},
user_id INTEGER NOT NULL,
token VARCHAR(255) UNIQUE NOT NULL,
description TEXT,
created_at TIMESTAMP DEFAULT {timestamp_default},
last_used TIMESTAMP NULL,
is_active {boolean_type} DEFAULT 1,
FOREIGN KEY (user_id) REFERENCES users(id)
)
'''
)
def
DatabaseManager__create_cache_tables
(
self
,
cursor
,
auto_increment
,
timestamp_default
,
boolean_type
):
"""Create only temporary cache tables (CACHE DB ONLY)"""
# Only minimal tracking tables for cache database
cursor
.
execute
(
f
'''
CREATE TABLE IF NOT EXISTS user_
token_usage (
CREATE TABLE IF NOT EXISTS
token_usage (
id INTEGER PRIMARY KEY {auto_increment},
user_id INTEGER NOT NULL,
token_id INTEGER,
user_id INTEGER,
provider_id VARCHAR(255) NOT NULL,
model_name VARCHAR(255) NOT NULL,
tokens_used INTEGER NOT NULL,
timestamp TIMESTAMP DEFAULT {timestamp_default},
FOREIGN KEY (user_id) REFERENCES users(id),
FOREIGN KEY (token_id) REFERENCES user_api_tokens(id)
prompt_tokens INTEGER,
completion_tokens INTEGER,
actual_cost DECIMAL(10,6),
timestamp TIMESTAMP DEFAULT {timestamp_default}
)
'''
)
# Create user_auth_files table for storing authentication file metadata
cursor
.
execute
(
f
'''
CREATE TABLE IF NOT EXISTS user_auth_file
s (
CREATE TABLE IF NOT EXISTS context_dimension
s (
id INTEGER PRIMARY KEY {auto_increment},
user_id INTEGER NOT NULL,
provider_id VARCHAR(255) NOT NULL,
file_type VARCHAR(50) NOT NULL,
original_filename VARCHAR(255) NOT NULL,
stored_filename VARCHAR(255) NOT NULL,
file_path TEXT NOT NULL,
file_size INTEGER,
mime_type VARCHAR(100),
created_at TIMESTAMP DEFAULT {timestamp_default},
updated_at TIMESTAMP DEFAULT {timestamp_default},
FOREIGN KEY (user_id) REFERENCES users(id),
UNIQUE(user_id, provider_id, file_type)
model_name VARCHAR(255) NOT NULL,
context_size INTEGER,
condense_context INTEGER,
condense_method TEXT,
effective_context INTEGER DEFAULT 0,
last_updated TIMESTAMP DEFAULT {timestamp_default},
UNIQUE(provider_id, model_name)
)
'''
)
# Create user_oauth2_credentials table for storing OAuth2 tokens per user/provider
cursor
.
execute
(
f
'''
CREATE TABLE IF NOT EXISTS user_oauth2_credentials (
id INTEGER PRIMARY KEY {auto_increment},
user_id INTEGER NOT NULL,
provider_id VARCHAR(255) NOT NULL,
auth_type VARCHAR(50) NOT NULL,
credentials TEXT NOT NULL,
created_at TIMESTAMP DEFAULT {timestamp_default},
updated_at TIMESTAMP DEFAULT {timestamp_default},
FOREIGN KEY (user_id) REFERENCES users(id),
UNIQUE(user_id, provider_id, auth_type)
)
'''
)
logger
.
info
(
"⚠️ CACHE DATABASE: Only minimal cache tables created - NO USER TABLES"
)
def
DatabaseManager__run_config_migrations
(
self
,
cursor
,
auto_increment
,
timestamp_default
,
boolean_type
):
"""Run all configuration database migrations"""
# ==============================================
# UNIVERSAL MIGRATIONS - RUN ON EVERY STARTUP
# These run for ALL databases, new and existing
# ==============================================
logger
.
info
(
"Running database migrations..."
)
...
...
@@ -3114,6 +3530,34 @@ def DatabaseManager__initialize_database(self):
'''
)
conn
.
commit
()
logger
.
info
(
"✅ Migration: Created missing account_tiers table"
)
else
:
cursor
.
execute
(
"""
SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES
WHERE TABLE_NAME = 'account_tiers'
"""
)
if
not
cursor
.
fetchone
():
cursor
.
execute
(
f
'''
CREATE TABLE account_tiers (
id INTEGER PRIMARY KEY {auto_increment},
name VARCHAR(255) UNIQUE NOT NULL,
description TEXT,
price_monthly DECIMAL(10,2) DEFAULT 0.00,
price_yearly DECIMAL(10,2) DEFAULT 0.00,
is_default {boolean_type} DEFAULT 0,
is_active {boolean_type} DEFAULT 1,
max_requests_per_day INTEGER DEFAULT -1,
max_requests_per_month INTEGER DEFAULT -1,
max_providers INTEGER DEFAULT -1,
max_rotations INTEGER DEFAULT -1,
max_autoselections INTEGER DEFAULT -1,
max_rotation_models INTEGER DEFAULT -1,
max_autoselection_models INTEGER DEFAULT -1,
created_at TIMESTAMP DEFAULT {timestamp_default},
updated_at TIMESTAMP DEFAULT {timestamp_default}
)
'''
)
conn
.
commit
()
logger
.
info
(
"✅ Migration: Created missing account_tiers table"
)
except
Exception
as
e
:
logger
.
warning
(
f
"Migration check for account_tiers table: {e}"
)
...
...
@@ -3131,8 +3575,7 @@ def DatabaseManager__initialize_database(self):
(
'max_rotation_models'
,
'INTEGER DEFAULT -1'
),
(
'max_autoselection_models'
,
'INTEGER DEFAULT -1'
),
(
'is_default'
,
f
'{boolean_type} DEFAULT 0'
),
(
'is_active'
,
f
'{boolean_type} DEFAULT 1'
),
(
'is_visible'
,
f
'{boolean_type} DEFAULT 1'
)
(
'is_active'
,
f
'{boolean_type} DEFAULT 1'
)
]
col_count
=
0
for
col_name
,
col_def
in
tier_columns
:
...
...
@@ -3141,35 +3584,6 @@ def DatabaseManager__initialize_database(self):
col_count
+=
1
if
col_count
>
0
:
logger
.
info
(
f
"✅ Migration: Added {col_count} missing columns to account_tiers"
)
else
:
# MySQL/MariaDB
cursor
.
execute
(
"""
SELECT COLUMN_NAME
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_NAME = 'account_tiers'
AND TABLE_SCHEMA = DATABASE()
"""
)
existing_columns
=
[
row
[
0
]
for
row
in
cursor
.
fetchall
()]
tier_columns
=
[
(
'max_requests_per_day'
,
'INTEGER DEFAULT -1'
),
(
'max_requests_per_month'
,
'INTEGER DEFAULT -1'
),
(
'max_providers'
,
'INTEGER DEFAULT -1'
),
(
'max_rotations'
,
'INTEGER DEFAULT -1'
),
(
'max_autoselections'
,
'INTEGER DEFAULT -1'
),
(
'max_rotation_models'
,
'INTEGER DEFAULT -1'
),
(
'max_autoselection_models'
,
'INTEGER DEFAULT -1'
),
(
'is_default'
,
f
'{boolean_type} DEFAULT 0'
),
(
'is_active'
,
f
'{boolean_type} DEFAULT 1'
),
(
'is_visible'
,
f
'{boolean_type} DEFAULT 1'
)
]
col_count
=
0
for
col_name
,
col_def
in
tier_columns
:
if
col_name
not
in
existing_columns
:
cursor
.
execute
(
f
'ALTER TABLE account_tiers ADD COLUMN {col_name} {col_def}'
)
col_count
+=
1
if
col_count
>
0
:
conn
.
commit
()
logger
.
info
(
f
"✅ Migration: Added {col_count} missing columns to account_tiers"
)
except
Exception
as
e
:
logger
.
warning
(
f
"Migration check for account_tiers columns: {e}"
)
...
...
@@ -3212,46 +3626,6 @@ def DatabaseManager__initialize_database(self):
except
Exception
as
e
:
logger
.
warning
(
f
"Migration check for users.tier_id: {e}"
)
# Migration: Add password reset token columns to users table
try
:
if
self
.
db_type
==
'sqlite'
:
cursor
.
execute
(
"PRAGMA table_info(users)"
)
columns
=
[
row
[
1
]
for
row
in
cursor
.
fetchall
()]
if
'reset_password_token'
not
in
columns
:
cursor
.
execute
(
'ALTER TABLE users ADD COLUMN reset_password_token VARCHAR(255)'
)
cursor
.
execute
(
'ALTER TABLE users ADD COLUMN reset_password_token_expires TIMESTAMP NULL'
)
logger
.
info
(
"✅ Migration: Added password reset token columns to users"
)
else
:
cursor
.
execute
(
"""
SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_NAME = 'users' AND COLUMN_NAME = 'reset_password_token'
"""
)
if
not
cursor
.
fetchone
():
cursor
.
execute
(
'ALTER TABLE users ADD COLUMN reset_password_token VARCHAR(255)'
)
cursor
.
execute
(
'ALTER TABLE users ADD COLUMN reset_password_token_expires TIMESTAMP NULL'
)
logger
.
info
(
"✅ Migration: Added password reset token columns to users"
)
except
Exception
as
e
:
logger
.
warning
(
f
"Migration check for users.reset_password_token: {e}"
)
# Migration: Add last_verification_email_sent column to users table
try
:
if
self
.
db_type
==
'sqlite'
:
cursor
.
execute
(
"PRAGMA table_info(users)"
)
columns
=
[
row
[
1
]
for
row
in
cursor
.
fetchall
()]
if
'last_verification_email_sent'
not
in
columns
:
cursor
.
execute
(
'ALTER TABLE users ADD COLUMN last_verification_email_sent TIMESTAMP NULL'
)
logger
.
info
(
"✅ Migration: Added last_verification_email_sent column to users"
)
else
:
cursor
.
execute
(
"""
SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_NAME = 'users' AND COLUMN_NAME = 'last_verification_email_sent'
"""
)
if
not
cursor
.
fetchone
():
cursor
.
execute
(
'ALTER TABLE users ADD COLUMN last_verification_email_sent TIMESTAMP NULL'
)
logger
.
info
(
"✅ Migration: Added last_verification_email_sent column to users"
)
except
Exception
as
e
:
logger
.
warning
(
f
"Migration check for users.last_verification_email_sent: {e}"
)
# Migration: Create payment_methods, user_subscriptions, payment_transactions tables
for
table_name
,
create_sql
in
[
(
'payment_methods'
,
f
'''
...
...
@@ -3268,14 +3642,6 @@ def DatabaseManager__initialize_database(self):
FOREIGN KEY (user_id) REFERENCES users(id)
)
'''
),
(
'admin_settings'
,
f
'''
CREATE TABLE admin_settings (
id INTEGER PRIMARY KEY {auto_increment},
setting_key VARCHAR(255) UNIQUE NOT NULL,
setting_value TEXT,
updated_at TIMESTAMP DEFAULT {timestamp_default}
)
'''
),
(
'user_subscriptions'
,
f
'''
CREATE TABLE user_subscriptions (
id INTEGER PRIMARY KEY {auto_increment},
...
...
@@ -3335,494 +3701,51 @@ def DatabaseManager__initialize_database(self):
except
Exception
as
e
:
logger
.
warning
(
f
"Migration check for {table_name} table: {e}"
)
conn
.
commit
()
logger
.
info
(
"✅ All database migrations completed"
)
else
:
# CACHE DATABASE GETS MINIMAL TABLES ONLY
cursor
.
execute
(
f
'''
CREATE TABLE IF NOT EXISTS token_usage (
id INTEGER PRIMARY KEY {auto_increment},
user_id INTEGER,
provider_id VARCHAR(255) NOT NULL,
model_name VARCHAR(255) NOT NULL,
tokens_used INTEGER NOT NULL,
timestamp TIMESTAMP DEFAULT {timestamp_default}
)
'''
)
cursor
.
execute
(
f
'''
CREATE TABLE IF NOT EXISTS context_dimensions (
id INTEGER PRIMARY KEY {auto_increment},
provider_id VARCHAR(255) NOT NULL,
model_name VARCHAR(255) NOT NULL,
context_size INTEGER,
condense_context INTEGER,
condense_method TEXT,
effective_context INTEGER DEFAULT 0,
last_updated TIMESTAMP DEFAULT {timestamp_default},
UNIQUE(provider_id, model_name)
)
'''
)
logger
.
info
(
"⚠️ CACHE DATABASE: Only minimal cache tables created - NO USER TABLES"
)
conn
.
commit
()
logger
.
info
(
f
"Database tables initialized successfully for {self.database_type} database"
)
def
DatabaseManager__create_config_tables
(
self
,
cursor
,
auto_increment
,
timestamp_default
,
boolean_type
):
"""Create all permanent configuration tables (CONFIG DB ONLY)"""
# Create context_dimensions table for tracking context usage
cursor
.
execute
(
f
'''
CREATE TABLE IF NOT EXISTS context_dimensions (
id INTEGER PRIMARY KEY {auto_increment},
provider_id VARCHAR(255) NOT NULL,
model_name VARCHAR(255) NOT NULL,
context_size INTEGER,
condense_context INTEGER,
condense_method TEXT,
effective_context INTEGER DEFAULT 0,
last_updated TIMESTAMP DEFAULT {timestamp_default},
UNIQUE(provider_id, model_name)
)
'''
)
# Create token_usage table for tracking rate limiting
cursor
.
execute
(
f
'''
CREATE TABLE IF NOT EXISTS token_usage (
id INTEGER PRIMARY KEY {auto_increment},
user_id INTEGER,
provider_id VARCHAR(255) NOT NULL,
model_name VARCHAR(255) NOT NULL,
tokens_used INTEGER NOT NULL,
timestamp TIMESTAMP DEFAULT {timestamp_default}
)
'''
)
# Create indexes for better query performance
try
:
cursor
.
execute
(
'''
CREATE INDEX IF NOT EXISTS idx_context_provider_model
ON context_dimensions(provider_id, model_name)
'''
)
except
:
pass
# Index might already exist
try
:
cursor
.
execute
(
'''
CREATE INDEX IF NOT EXISTS idx_token_provider_model
ON token_usage(provider_id, model_name)
'''
)
except
:
pass
try
:
cursor
.
execute
(
'''
CREATE INDEX IF NOT EXISTS idx_token_timestamp
ON token_usage(timestamp)
'''
)
except
:
pass
# Create model_embeddings table for caching vectorized model descriptions
cursor
.
execute
(
f
'''
CREATE TABLE IF NOT EXISTS model_embeddings (
id INTEGER PRIMARY KEY {auto_increment},
provider_id VARCHAR(255) NOT NULL,
model_name VARCHAR(255) NOT NULL,
description TEXT,
embedding TEXT,
last_updated TIMESTAMP DEFAULT {timestamp_default},
UNIQUE(provider_id, model_name)
)
'''
)
try
:
cursor
.
execute
(
'''
CREATE INDEX IF NOT EXISTS idx_model_embeddings_provider_model
ON model_embeddings(provider_id, model_name)
'''
)
except
:
pass
# Create users table for multi-user management
cursor
.
execute
(
f
'''
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY {auto_increment},
username VARCHAR(255) UNIQUE NOT NULL,
email VARCHAR(255) UNIQUE,
password_hash VARCHAR(255) NOT NULL,
role VARCHAR(50) DEFAULT 'user',
created_by VARCHAR(255),
created_at TIMESTAMP DEFAULT {timestamp_default},
last_login TIMESTAMP NULL,
is_active {boolean_type} DEFAULT 1,
email_verified {boolean_type} DEFAULT 0,
verification_token VARCHAR(255),
verification_token_expires TIMESTAMP NULL
)
'''
)
# User-specific configuration tables for multi-user isolation
cursor
.
execute
(
f
'''
CREATE TABLE IF NOT EXISTS user_providers (
id INTEGER PRIMARY KEY {auto_increment},
user_id INTEGER NOT NULL,
provider_id VARCHAR(255) NOT NULL,
config TEXT NOT NULL,
created_at TIMESTAMP DEFAULT {timestamp_default},
updated_at TIMESTAMP DEFAULT {timestamp_default},
FOREIGN KEY (user_id) REFERENCES users(id),
UNIQUE(user_id, provider_id)
)
'''
)
cursor
.
execute
(
f
'''
CREATE TABLE IF NOT EXISTS user_rotations (
id INTEGER PRIMARY KEY {auto_increment},
user_id INTEGER NOT NULL,
rotation_id VARCHAR(255) NOT NULL,
config TEXT NOT NULL,
created_at TIMESTAMP DEFAULT {timestamp_default},
updated_at TIMESTAMP DEFAULT {timestamp_default},
FOREIGN KEY (user_id) REFERENCES users(id),
UNIQUE(user_id, rotation_id)
)
'''
)
cursor
.
execute
(
f
'''
CREATE TABLE IF NOT EXISTS user_autoselects (
id INTEGER PRIMARY KEY {auto_increment},
user_id INTEGER NOT NULL,
autoselect_id VARCHAR(255) NOT NULL,
config TEXT NOT NULL,
created_at TIMESTAMP DEFAULT {timestamp_default},
updated_at TIMESTAMP DEFAULT {timestamp_default},
FOREIGN KEY (user_id) REFERENCES users(id),
UNIQUE(user_id, autoselect_id)
)
'''
)
cursor
.
execute
(
f
'''
CREATE TABLE IF NOT EXISTS user_prompts (
id INTEGER PRIMARY KEY {auto_increment},
user_id INTEGER NOT NULL,
prompt_key VARCHAR(255) NOT NULL,
content TEXT NOT NULL,
created_at TIMESTAMP DEFAULT {timestamp_default},
updated_at TIMESTAMP DEFAULT {timestamp_default},
FOREIGN KEY (user_id) REFERENCES users(id),
UNIQUE(user_id, prompt_key)
)
'''
)
cursor
.
execute
(
f
'''
CREATE TABLE IF NOT EXISTS user_api_tokens (
id INTEGER PRIMARY KEY {auto_increment},
user_id INTEGER NOT NULL,
token VARCHAR(255) UNIQUE NOT NULL,
description TEXT,
created_at TIMESTAMP DEFAULT {timestamp_default},
last_used TIMESTAMP NULL,
is_active {boolean_type} DEFAULT 1,
FOREIGN KEY (user_id) REFERENCES users(id)
)
'''
)
cursor
.
execute
(
f
'''
CREATE TABLE IF NOT EXISTS user_token_usage (
id INTEGER PRIMARY KEY {auto_increment},
user_id INTEGER NOT NULL,
token_id INTEGER,
provider_id VARCHAR(255) NOT NULL,
model_name VARCHAR(255) NOT NULL,
tokens_used INTEGER NOT NULL,
timestamp TIMESTAMP DEFAULT {timestamp_default},
FOREIGN KEY (user_id) REFERENCES users(id),
FOREIGN KEY (token_id) REFERENCES user_api_tokens(id)
)
'''
)
# Create user_auth_files table for storing authentication file metadata
cursor
.
execute
(
f
'''
CREATE TABLE IF NOT EXISTS user_auth_files (
id INTEGER PRIMARY KEY {auto_increment},
user_id INTEGER NOT NULL,
provider_id VARCHAR(255) NOT NULL,
file_type VARCHAR(50) NOT NULL,
original_filename VARCHAR(255) NOT NULL,
stored_filename VARCHAR(255) NOT NULL,
file_path TEXT NOT NULL,
file_size INTEGER,
mime_type VARCHAR(100),
created_at TIMESTAMP DEFAULT {timestamp_default},
updated_at TIMESTAMP DEFAULT {timestamp_default},
FOREIGN KEY (user_id) REFERENCES users(id),
UNIQUE(user_id, provider_id, file_type)
)
'''
)
# Create user_oauth2_credentials table for storing OAuth2 tokens per user/provider
cursor
.
execute
(
f
'''
CREATE TABLE IF NOT EXISTS user_oauth2_credentials (
id INTEGER PRIMARY KEY {auto_increment},
user_id INTEGER NOT NULL,
provider_id VARCHAR(255) NOT NULL,
auth_type VARCHAR(50) NOT NULL,
credentials TEXT NOT NULL,
created_at TIMESTAMP DEFAULT {timestamp_default},
updated_at TIMESTAMP DEFAULT {timestamp_default},
FOREIGN KEY (user_id) REFERENCES users(id),
UNIQUE(user_id, provider_id, auth_type)
)
'''
)
def
DatabaseManager__create_cache_tables
(
self
,
cursor
,
auto_increment
,
timestamp_default
,
boolean_type
):
"""Create only temporary cache tables (CACHE DB ONLY)"""
# Only minimal tracking tables for cache database
cursor
.
execute
(
f
'''
CREATE TABLE IF NOT EXISTS token_usage (
id INTEGER PRIMARY KEY {auto_increment},
user_id INTEGER,
provider_id VARCHAR(255) NOT NULL,
model_name VARCHAR(255) NOT NULL,
tokens_used INTEGER NOT NULL,
timestamp TIMESTAMP DEFAULT {timestamp_default}
)
'''
)
cursor
.
execute
(
f
'''
CREATE TABLE IF NOT EXISTS context_dimensions (
id INTEGER PRIMARY KEY {auto_increment},
provider_id VARCHAR(255) NOT NULL,
model_name VARCHAR(255) NOT NULL,
context_size INTEGER,
condense_context INTEGER,
condense_method TEXT,
effective_context INTEGER DEFAULT 0,
last_updated TIMESTAMP DEFAULT {timestamp_default},
UNIQUE(provider_id, model_name)
)
'''
)
logger
.
info
(
"⚠️ CACHE DATABASE: Only minimal cache tables created - NO USER TABLES"
)
def
DatabaseManager__run_config_migrations
(
self
,
cursor
,
auto_increment
,
timestamp_default
,
boolean_type
):
"""Run all configuration database migrations"""
# ==============================================
# UNIVERSAL MIGRATIONS - RUN ON EVERY STARTUP
# These run for ALL databases, new and existing
# ==============================================
logger
.
info
(
"Running database migrations..."
)
# Migration: Create account_tiers table if missing
# Migration: Add prompt_tokens and completion_tokens columns to token_usage table
try
:
if
self
.
db_type
==
'sqlite'
:
cursor
.
execute
(
"PRAGMA table_info(account_tiers)"
)
if
not
cursor
.
fetchall
():
cursor
.
execute
(
f
'''
CREATE TABLE account_tiers (
id INTEGER PRIMARY KEY {auto_increment},
name VARCHAR(255) UNIQUE NOT NULL,
description TEXT,
price_monthly DECIMAL(10,2) DEFAULT 0.00,
price_yearly DECIMAL(10,2) DEFAULT 0.00,
is_default {boolean_type} DEFAULT 0,
is_active {boolean_type} DEFAULT 1,
max_requests_per_day INTEGER DEFAULT -1,
max_requests_per_month INTEGER DEFAULT -1,
max_providers INTEGER DEFAULT -1,
max_rotations INTEGER DEFAULT -1,
max_autoselections INTEGER DEFAULT -1,
max_rotation_models INTEGER DEFAULT -1,
max_autoselection_models INTEGER DEFAULT -1,
created_at TIMESTAMP DEFAULT {timestamp_default},
updated_at TIMESTAMP DEFAULT {timestamp_default}
)
'''
)
conn
.
commit
()
logger
.
info
(
"✅ Migration: Created missing account_tiers table"
)
cursor
.
execute
(
"PRAGMA table_info(token_usage)"
)
columns
=
[
row
[
1
]
for
row
in
cursor
.
fetchall
()]
if
'prompt_tokens'
not
in
columns
:
cursor
.
execute
(
'ALTER TABLE token_usage ADD COLUMN prompt_tokens INTEGER'
)
logger
.
info
(
"✅ Migration: Added prompt_tokens column to token_usage"
)
if
'completion_tokens'
not
in
columns
:
cursor
.
execute
(
'ALTER TABLE token_usage ADD COLUMN completion_tokens INTEGER'
)
logger
.
info
(
"✅ Migration: Added completion_tokens column to token_usage"
)
if
'actual_cost'
not
in
columns
:
cursor
.
execute
(
'ALTER TABLE token_usage ADD COLUMN actual_cost DECIMAL(10,6)'
)
logger
.
info
(
"✅ Migration: Added actual_cost column to token_usage"
)
else
:
cursor
.
execute
(
"""
SELECT
TABLE_NAME FROM INFORMATION_SCHEMA.TABLE
S
WHERE TABLE_NAME = '
account_tier
s'
SELECT
COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMN
S
WHERE TABLE_NAME = '
token_usage' AND COLUMN_NAME = 'prompt_token
s'
"""
)
if
not
cursor
.
fetchone
():
cursor
.
execute
(
f
'''
CREATE TABLE account_tiers (
id INTEGER PRIMARY KEY {auto_increment},
name VARCHAR(255) UNIQUE NOT NULL,
description TEXT,
price_monthly DECIMAL(10,2) DEFAULT 0.00,
price_yearly DECIMAL(10,2) DEFAULT 0.00,
is_default {boolean_type} DEFAULT 0,
is_active {boolean_type} DEFAULT 1,
max_requests_per_day INTEGER DEFAULT -1,
max_requests_per_month INTEGER DEFAULT -1,
max_providers INTEGER DEFAULT -1,
max_rotations INTEGER DEFAULT -1,
max_autoselections INTEGER DEFAULT -1,
max_rotation_models INTEGER DEFAULT -1,
max_autoselection_models INTEGER DEFAULT -1,
created_at TIMESTAMP DEFAULT {timestamp_default},
updated_at TIMESTAMP DEFAULT {timestamp_default}
)
'''
)
conn
.
commit
()
logger
.
info
(
"✅ Migration: Created missing account_tiers table"
)
except
Exception
as
e
:
logger
.
warning
(
f
"Migration check for account_tiers table: {e}"
)
# Migration: Add missing columns to account_tiers
try
:
if
self
.
db_type
==
'sqlite'
:
cursor
.
execute
(
"PRAGMA table_info(account_tiers)"
)
existing_columns
=
[
row
[
1
]
for
row
in
cursor
.
fetchall
()]
tier_columns
=
[
(
'max_requests_per_day'
,
'INTEGER DEFAULT -1'
),
(
'max_requests_per_month'
,
'INTEGER DEFAULT -1'
),
(
'max_providers'
,
'INTEGER DEFAULT -1'
),
(
'max_rotations'
,
'INTEGER DEFAULT -1'
),
(
'max_autoselections'
,
'INTEGER DEFAULT -1'
),
(
'max_rotation_models'
,
'INTEGER DEFAULT -1'
),
(
'max_autoselection_models'
,
'INTEGER DEFAULT -1'
),
(
'is_default'
,
f
'{boolean_type} DEFAULT 0'
),
(
'is_active'
,
f
'{boolean_type} DEFAULT 1'
)
]
col_count
=
0
for
col_name
,
col_def
in
tier_columns
:
if
col_name
not
in
existing_columns
:
cursor
.
execute
(
f
'ALTER TABLE account_tiers ADD COLUMN {col_name} {col_def}'
)
col_count
+=
1
if
col_count
>
0
:
logger
.
info
(
f
"✅ Migration: Added {col_count} missing columns to account_tiers"
)
except
Exception
as
e
:
logger
.
warning
(
f
"Migration check for account_tiers columns: {e}"
)
# Migration: Ensure default free tier exists
try
:
cursor
.
execute
(
f
'SELECT COUNT(*) FROM account_tiers WHERE is_default = 1'
)
free_tier_count
=
cursor
.
fetchone
()[
0
]
if
free_tier_count
==
0
:
cursor
.
execute
(
f
'''
INSERT INTO account_tiers
(name, description, price_monthly, price_yearly, is_default, is_active,
max_requests_per_day, max_requests_per_month, max_providers, max_rotations,
max_autoselections, max_rotation_models, max_autoselection_models)
VALUES
('Free Tier', 'Default free account tier with unlimited access', 0.00, 0.00, 1, 1,
-1, -1, -1, -1, -1, -1, -1)
'''
)
logger
.
info
(
"✅ Migration: Inserted default free tier"
)
except
Exception
as
e
:
logger
.
warning
(
f
"Migration check for default free tier: {e}"
)
cursor
.
execute
(
'ALTER TABLE token_usage ADD COLUMN prompt_tokens INTEGER'
)
logger
.
info
(
"✅ Migration: Added prompt_tokens column to token_usage"
)
# Migration: Add tier_id column to users table
try
:
if
self
.
db_type
==
'sqlite'
:
cursor
.
execute
(
"PRAGMA table_info(users)"
)
columns
=
[
row
[
1
]
for
row
in
cursor
.
fetchall
()]
if
'tier_id'
not
in
columns
:
cursor
.
execute
(
'ALTER TABLE users ADD COLUMN tier_id INTEGER DEFAULT 1'
)
cursor
.
execute
(
'ALTER TABLE users ADD COLUMN subscription_expires TIMESTAMP NULL'
)
logger
.
info
(
"✅ Migration: Added tier_id and subscription_expires columns to users"
)
else
:
cursor
.
execute
(
"""
SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_NAME = '
users' AND COLUMN_NAME = 'tier_id
'
WHERE TABLE_NAME = '
token_usage' AND COLUMN_NAME = 'completion_tokens
'
"""
)
if
not
cursor
.
fetchone
():
cursor
.
execute
(
'ALTER TABLE users ADD COLUMN tier_id INTEGER DEFAULT 1'
)
cursor
.
execute
(
'ALTER TABLE users ADD COLUMN subscription_expires TIMESTAMP NULL'
)
logger
.
info
(
"✅ Migration: Added tier_id and subscription_expires columns to users"
)
except
Exception
as
e
:
logger
.
warning
(
f
"Migration check for users.tier_id: {e}"
)
cursor
.
execute
(
'ALTER TABLE token_usage ADD COLUMN completion_tokens INTEGER'
)
logger
.
info
(
"✅ Migration: Added completion_tokens column to token_usage"
)
# Migration: Create payment_methods, user_subscriptions, payment_transactions tables
for
table_name
,
create_sql
in
[
(
'payment_methods'
,
f
'''
CREATE TABLE payment_methods (
id INTEGER PRIMARY KEY {auto_increment},
user_id INTEGER NOT NULL,
type VARCHAR(50) NOT NULL,
identifier VARCHAR(255) NOT NULL,
is_default {boolean_type} DEFAULT 0,
is_active {boolean_type} DEFAULT 1,
metadata TEXT,
created_at TIMESTAMP DEFAULT {timestamp_default},
updated_at TIMESTAMP DEFAULT {timestamp_default},
FOREIGN KEY (user_id) REFERENCES users(id)
)
'''
),
(
'user_subscriptions'
,
f
'''
CREATE TABLE user_subscriptions (
id INTEGER PRIMARY KEY {auto_increment},
user_id INTEGER NOT NULL,
tier_id INTEGER NOT NULL,
status VARCHAR(50) DEFAULT 'active',
start_date TIMESTAMP DEFAULT {timestamp_default},
end_date TIMESTAMP NULL,
next_billing_date TIMESTAMP NULL,
trial_end_date TIMESTAMP NULL,
payment_method_id INTEGER,
auto_renew {boolean_type} DEFAULT 1,
created_at TIMESTAMP DEFAULT {timestamp_default},
updated_at TIMESTAMP DEFAULT {timestamp_default},
FOREIGN KEY (user_id) REFERENCES users(id),
FOREIGN KEY (tier_id) REFERENCES account_tiers(id),
FOREIGN KEY (payment_method_id) REFERENCES payment_methods(id),
UNIQUE(user_id, tier_id)
)
'''
),
(
'payment_transactions'
,
f
'''
CREATE TABLE payment_transactions (
id INTEGER PRIMARY KEY {auto_increment},
user_id INTEGER NOT NULL,
tier_id INTEGER,
subscription_id INTEGER,
payment_method_id INTEGER,
amount DECIMAL(10,2) NOT NULL,
currency VARCHAR(10) DEFAULT 'USD',
status VARCHAR(50) NOT NULL,
transaction_type VARCHAR(50) NOT NULL,
external_transaction_id VARCHAR(255),
metadata TEXT,
created_at TIMESTAMP DEFAULT {timestamp_default},
completed_at TIMESTAMP NULL,
FOREIGN KEY (user_id) REFERENCES users(id),
FOREIGN KEY (tier_id) REFERENCES account_tiers(id),
FOREIGN KEY (subscription_id) REFERENCES user_subscriptions(id),
FOREIGN KEY (payment_method_id) REFERENCES payment_methods(id)
)
'''
)
]:
try
:
if
self
.
db_type
==
'sqlite'
:
cursor
.
execute
(
f
"PRAGMA table_info({table_name})"
)
if
not
cursor
.
fetchall
():
cursor
.
execute
(
create_sql
)
logger
.
info
(
f
"✅ Migration: Created missing {table_name} table"
)
else
:
cursor
.
execute
(
f
"""
SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES
WHERE TABLE_NAME = '{table_name}'
cursor
.
execute
(
"""
SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_NAME = 'token_usage' AND COLUMN_NAME = 'actual_cost'
"""
)
if
not
cursor
.
fetchone
():
cursor
.
execute
(
create_sql
)
logger
.
info
(
f
"✅ Migration: Created missing {table_name} tabl
e"
)
cursor
.
execute
(
'ALTER TABLE token_usage ADD COLUMN actual_cost DECIMAL(10,6)'
)
logger
.
info
(
"✅ Migration: Added actual_cost column to token_usag
e"
)
except
Exception
as
e
:
logger
.
warning
(
f
"Migration check for {table_name} table
: {e}"
)
logger
.
warning
(
f
"Migration check for token_usage columns
: {e}"
)
conn
.
commit
()
logger
.
info
(
"✅ All database migrations completed"
)
# Patch the methods
DatabaseManager
.
_initialize_database
=
DatabaseManager__initialize_database
DatabaseManager
.
_create_config_tables
=
DatabaseManager__create_config_tables
DatabaseManager
.
_create_cache_tables
=
DatabaseManager__create_cache_tables
DatabaseManager
.
_run_config_migrations
=
DatabaseManager__run_config_migrations
aisbf/handlers.py
View file @
fe8b625a
...
...
@@ -366,6 +366,9 @@ class RequestHandler:
# Record analytics for authentication failure
try
:
analytics
=
get_analytics
()
# Calculate latency for auth failure
latency_ms
=
(
time
.
time
()
-
request_start_time
)
*
1000
# Estimate tokens for the request
try
:
messages
=
request_data
.
get
(
'messages'
,
[])
...
...
@@ -378,11 +381,14 @@ class RequestHandler:
provider_id
=
provider_id
,
model_name
=
request_data
.
get
(
'model'
,
'unknown'
),
tokens_used
=
estimated_tokens
,
latency_ms
=
0
,
latency_ms
=
latency_ms
,
success
=
False
,
error_type
=
'AuthenticationError'
,
user_id
=
getattr
(
request
.
state
,
'user_id'
,
None
),
token_id
=
getattr
(
request
.
state
,
'token_id'
,
None
)
token_id
=
getattr
(
request
.
state
,
'token_id'
,
None
),
prompt_tokens
=
estimated_tokens
,
completion_tokens
=
0
,
actual_cost
=
None
)
except
Exception
as
analytics_error
:
logger
.
warning
(
f
"Analytics recording for auth failure failed: {analytics_error}"
)
...
...
@@ -526,33 +532,48 @@ class RequestHandler:
try
:
analytics
=
get_analytics
()
latency_ms
=
(
time
.
time
()
-
request_start_time
)
*
1000
logger
.
info
(
f
"Analytics: latency_ms={latency_ms:.2f}, request_start_time={request_start_time}, current_time={time.time()}"
)
if
response
and
isinstance
(
response
,
dict
):
usage
=
response
.
get
(
'usage'
,
{})
total_tokens
=
usage
.
get
(
'total_tokens'
,
0
)
prompt_tokens
=
usage
.
get
(
'prompt_tokens'
,
0
)
completion_tokens
=
usage
.
get
(
'completion_tokens'
,
0
)
# If no token usage provided, estimate it
# Try to extract actual cost from provider response
from
..cost_extractor
import
extract_cost_from_response
actual_cost
=
extract_cost_from_response
(
response
,
provider_id
)
# If no token usage provided, estimate it with improved accuracy
if
total_tokens
==
0
:
try
:
messages
=
request_data
.
get
(
'messages'
,
[])
estimated_prompt_tokens
=
count_messages_tokens
(
messages
,
model_name
)
# More realistic completion estimate based on max_tokens or typical response
# Count actual completion tokens from response instead of estimating
response_content
=
response
.
get
(
'choices'
,
[{}])[
0
]
.
get
(
'message'
,
{})
.
get
(
'content'
,
''
)
if
response_content
:
completion_tokens
=
count_messages_tokens
([{
"role"
:
"assistant"
,
"content"
:
response_content
}],
model_name
)
else
:
# Fallback to estimation if no content
max_tokens
=
request_data
.
get
(
'max_tokens'
,
0
)
if
max_tokens
>
0
:
# Use max_tokens as upper bound for completion
estimated_completion
=
min
(
max_tokens
,
estimated_prompt_tokens
*
2
)
completion_tokens
=
min
(
max_tokens
,
estimated_prompt_tokens
*
2
)
else
:
# No max_tokens specified, assume completion is similar to prompt
# but at least 50 tokens for typical responses
estimated_completion
=
max
(
estimated_prompt_tokens
,
50
)
completion_tokens
=
max
(
estimated_prompt_tokens
,
50
)
total_tokens
=
estimated_prompt_tokens
+
estimated_completion
logger
.
debug
(
f
"Estimated token usage: {total_tokens} (prompt: {estimated_prompt_tokens}, completion: {estimated_completion})"
)
total_tokens
=
estimated_prompt_tokens
+
completion_tokens
prompt_tokens
=
estimated_prompt_tokens
logger
.
debug
(
f
"Counted token usage: {total_tokens} (prompt: {estimated_prompt_tokens}, completion: {completion_tokens})"
)
except
Exception
as
est_error
:
logger
.
debug
(
f
"Token
estimation
failed: {est_error}"
)
# Use a more realistic default if
estimation
fails
logger
.
debug
(
f
"Token
counting
failed: {est_error}"
)
# Use a more realistic default if
counting
fails
total_tokens
=
150
prompt_tokens
=
0
completion_tokens
=
0
# Always record analytics, even with estimated tokens
analytics
.
record_request
(
...
...
@@ -562,7 +583,10 @@ class RequestHandler:
latency_ms
=
latency_ms
,
success
=
True
,
user_id
=
getattr
(
request
.
state
,
'user_id'
,
None
),
token_id
=
getattr
(
request
.
state
,
'token_id'
,
None
)
token_id
=
getattr
(
request
.
state
,
'token_id'
,
None
),
prompt_tokens
=
prompt_tokens
if
prompt_tokens
>
0
else
None
,
completion_tokens
=
completion_tokens
if
completion_tokens
>
0
else
None
,
actual_cost
=
actual_cost
)
except
Exception
as
analytics_error
:
logger
.
warning
(
f
"Analytics recording failed: {analytics_error}"
)
...
...
@@ -582,8 +606,12 @@ class RequestHandler:
messages
=
request_data
.
get
(
'messages'
,
[])
estimated_tokens
=
count_messages_tokens
(
messages
,
model_name
)
total_tokens
=
estimated_tokens
prompt_tokens
=
estimated_tokens
completion_tokens
=
0
# No completion for failed requests
except
Exception
:
total_tokens
=
50
# Minimal estimate for failed requests
prompt_tokens
=
50
completion_tokens
=
0
analytics
.
record_request
(
provider_id
=
provider_id
,
...
...
@@ -593,7 +621,10 @@ class RequestHandler:
success
=
False
,
error_type
=
type
(
e
)
.
__name__
,
user_id
=
getattr
(
request
.
state
,
'user_id'
,
None
),
token_id
=
getattr
(
request
.
state
,
'token_id'
,
None
)
token_id
=
getattr
(
request
.
state
,
'token_id'
,
None
),
prompt_tokens
=
prompt_tokens
,
completion_tokens
=
completion_tokens
,
actual_cost
=
None
)
except
Exception
as
analytics_error
:
logger
.
warning
(
f
"Analytics recording for failed request failed: {analytics_error}"
)
...
...
@@ -667,6 +698,8 @@ class RequestHandler:
import
time
import
json
logger
=
logging
.
getLogger
(
__name__
)
# Track request start time for latency calculation
request_start_time
=
time
.
time
()
try
:
# Apply rate limiting
await
handler
.
apply_rate_limit
()
...
...
@@ -1200,6 +1233,10 @@ class RequestHandler:
# Record analytics for streaming request
try
:
analytics
=
get_analytics
()
# Calculate latency
latency_ms
=
(
time
.
time
()
-
request_start_time
)
*
1000
logger
.
info
(
f
"Streaming Analytics: latency_ms={latency_ms:.2f}"
)
# Calculate total tokens from accumulated response
if
accumulated_response_text
:
completion_tokens
=
count_messages_tokens
([{
"role"
:
"assistant"
,
"content"
:
accumulated_response_text
}],
request_data
[
'model'
])
...
...
@@ -1211,10 +1248,13 @@ class RequestHandler:
provider_id
=
provider_id
,
model_name
=
request_data
[
'model'
],
tokens_used
=
total_tokens
,
latency_ms
=
0
,
latency_ms
=
latency_ms
,
success
=
True
,
user_id
=
getattr
(
request
.
state
,
'user_id'
,
None
),
token_id
=
getattr
(
request
.
state
,
'token_id'
,
None
)
token_id
=
getattr
(
request
.
state
,
'token_id'
,
None
),
prompt_tokens
=
effective_context
,
completion_tokens
=
completion_tokens
,
actual_cost
=
None
# Streaming responses typically don't include cost
)
except
Exception
as
analytics_error
:
logger
.
warning
(
f
"Analytics recording for streaming request failed: {analytics_error}"
)
...
...
@@ -1225,23 +1265,34 @@ class RequestHandler:
# Record analytics for failed streaming request
try
:
analytics
=
get_analytics
()
# Calculate latency
latency_ms
=
(
time
.
time
()
-
request_start_time
)
*
1000
logger
.
info
(
f
"Failed Streaming Analytics: latency_ms={latency_ms:.2f}"
)
# Estimate tokens for failed request
try
:
messages
=
request_data
.
get
(
'messages'
,
[])
estimated_tokens
=
count_messages_tokens
(
messages
,
request_data
[
'model'
])
total_tokens
=
estimated_tokens
prompt_tokens
=
estimated_tokens
completion_tokens
=
0
except
Exception
:
total_tokens
=
50
# Minimal estimate for failed requests
prompt_tokens
=
50
completion_tokens
=
0
analytics
.
record_request
(
provider_id
=
provider_id
,
model_name
=
request_data
[
'model'
],
tokens_used
=
total_tokens
,
latency_ms
=
0
,
latency_ms
=
latency_ms
,
success
=
False
,
error_type
=
type
(
e
)
.
__name__
,
user_id
=
getattr
(
request
.
state
,
'user_id'
,
None
),
token_id
=
getattr
(
request
.
state
,
'token_id'
,
None
)
token_id
=
getattr
(
request
.
state
,
'token_id'
,
None
),
prompt_tokens
=
prompt_tokens
,
completion_tokens
=
completion_tokens
,
actual_cost
=
None
)
except
Exception
as
analytics_error
:
logger
.
warning
(
f
"Analytics recording for failed streaming request failed: {analytics_error}"
)
...
...
@@ -2233,6 +2284,8 @@ class RotationHandler:
import
logging
import
time
logger
=
logging
.
getLogger
(
__name__
)
# Track request start time for latency calculation
request_start_time
=
time
.
time
()
logger
.
info
(
f
"=== RotationHandler.handle_rotation_request START ==="
)
logger
.
info
(
f
"Rotation ID: {rotation_id}"
)
logger
.
info
(
f
"User ID: {self.user_id}"
)
...
...
@@ -2847,6 +2900,8 @@ class RotationHandler:
if
response
and
isinstance
(
response
,
dict
):
usage
=
response
.
get
(
'usage'
,
{})
total_tokens
=
usage
.
get
(
'total_tokens'
,
0
)
prompt_tokens
=
usage
.
get
(
'prompt_tokens'
,
0
)
completion_tokens
=
usage
.
get
(
'completion_tokens'
,
0
)
# If no token usage provided, estimate it
if
total_tokens
==
0
:
...
...
@@ -2862,21 +2917,32 @@ class RotationHandler:
estimated_completion
=
max
(
estimated_prompt_tokens
,
50
)
total_tokens
=
estimated_prompt_tokens
+
estimated_completion
prompt_tokens
=
estimated_prompt_tokens
completion_tokens
=
estimated_completion
logger
.
debug
(
f
"Estimated token usage for rotation: {total_tokens}"
)
except
Exception
as
est_error
:
logger
.
debug
(
f
"Token estimation failed: {est_error}"
)
total_tokens
=
150
prompt_tokens
=
0
completion_tokens
=
0
# Try to extract actual cost from provider response
from
..cost_extractor
import
extract_cost_from_response
actual_cost
=
extract_cost_from_response
(
response
,
provider_id
)
# Always record analytics
analytics
.
record_request
(
provider_id
=
provider_id
,
model_name
=
model_name
,
tokens_used
=
total_tokens
,
latency_ms
=
0
,
# Latency tracking would require more extensive changes
latency_ms
=
(
time
.
time
()
-
request_start_time
)
*
1000
,
success
=
True
,
rotation_id
=
rotation_id
,
user_id
=
user_id
,
token_id
=
token_id
token_id
=
token_id
,
prompt_tokens
=
prompt_tokens
if
prompt_tokens
>
0
else
None
,
completion_tokens
=
completion_tokens
if
completion_tokens
>
0
else
None
,
actual_cost
=
actual_cost
)
except
Exception
as
analytics_error
:
logger
.
warning
(
f
"Analytics recording failed: {analytics_error}"
)
...
...
@@ -2912,24 +2978,35 @@ class RotationHandler:
# Record analytics for failed rotation request
try
:
analytics
=
get_analytics
()
# Calculate latency
latency_ms
=
(
time
.
time
()
-
request_start_time
)
*
1000
logger
.
info
(
f
"Failed Rotation Analytics: latency_ms={latency_ms:.2f}"
)
# Estimate tokens for failed request
try
:
messages
=
request_data
.
get
(
'messages'
,
[])
estimated_tokens
=
count_messages_tokens
(
messages
,
rotation_id
)
total_tokens
=
estimated_tokens
prompt_tokens
=
estimated_tokens
completion_tokens
=
0
except
Exception
:
total_tokens
=
50
# Minimal estimate for failed requests
prompt_tokens
=
50
completion_tokens
=
0
analytics
.
record_request
(
provider_id
=
'rotation'
,
model_name
=
rotation_id
,
tokens_used
=
total_tokens
,
latency_ms
=
0
,
latency_ms
=
latency_ms
,
success
=
False
,
error_type
=
'RotationFailure'
,
rotation_id
=
rotation_id
,
user_id
=
user_id
,
token_id
=
token_id
token_id
=
token_id
,
prompt_tokens
=
prompt_tokens
,
completion_tokens
=
completion_tokens
,
actual_cost
=
None
)
except
Exception
as
analytics_error
:
logger
.
warning
(
f
"Analytics recording for failed rotation failed: {analytics_error}"
)
...
...
@@ -4112,7 +4189,10 @@ class AutoselectHandler:
async
def
handle_autoselect_request
(
self
,
autoselect_id
:
str
,
request_data
:
Dict
,
user_id
:
Optional
[
int
]
=
None
,
token_id
:
Optional
[
int
]
=
None
)
->
Dict
:
"""Handle an autoselect request"""
import
logging
import
time
logger
=
logging
.
getLogger
(
__name__
)
# Track request start time for latency calculation
request_start_time
=
time
.
time
()
logger
.
info
(
f
"=== AUTOSELECT REQUEST START ==="
)
logger
.
info
(
f
"Autoselect ID: {autoselect_id}"
)
logger
.
info
(
f
"User ID: {self.user_id}"
)
...
...
@@ -4232,24 +4312,35 @@ class AutoselectHandler:
logger
.
error
(
f
"Autoselect request failed: {str(e)}"
)
try
:
analytics
=
get_analytics
()
# Calculate latency
latency_ms
=
(
time
.
time
()
-
request_start_time
)
*
1000
logger
.
info
(
f
"Failed Autoselect Analytics: latency_ms={latency_ms:.2f}"
)
# Estimate tokens for failed request
try
:
messages
=
request_data
.
get
(
'messages'
,
[])
estimated_tokens
=
count_messages_tokens
(
messages
,
autoselect_id
)
total_tokens
=
estimated_tokens
prompt_tokens
=
estimated_tokens
completion_tokens
=
0
except
Exception
:
total_tokens
=
50
# Minimal estimate for failed requests
prompt_tokens
=
50
completion_tokens
=
0
analytics
.
record_request
(
provider_id
=
'autoselect'
,
model_name
=
autoselect_id
,
tokens_used
=
total_tokens
,
latency_ms
=
0
,
latency_ms
=
latency_ms
,
success
=
False
,
error_type
=
type
(
e
)
.
__name__
,
autoselect_id
=
autoselect_id
,
user_id
=
user_id
,
token_id
=
token_id
token_id
=
token_id
,
prompt_tokens
=
prompt_tokens
,
completion_tokens
=
completion_tokens
,
actual_cost
=
None
)
except
Exception
as
analytics_error
:
logger
.
warning
(
f
"Analytics recording for failed autoselect failed: {analytics_error}"
)
...
...
@@ -4276,6 +4367,8 @@ class AutoselectHandler:
if
response
and
isinstance
(
response
,
dict
):
usage
=
response
.
get
(
'usage'
,
{})
total_tokens
=
usage
.
get
(
'total_tokens'
,
0
)
prompt_tokens
=
usage
.
get
(
'prompt_tokens'
,
0
)
completion_tokens
=
usage
.
get
(
'completion_tokens'
,
0
)
# If no token usage provided, estimate it
if
total_tokens
==
0
:
...
...
@@ -4292,23 +4385,38 @@ class AutoselectHandler:
estimated_completion
=
max
(
estimated_prompt_tokens
,
50
)
total_tokens
=
estimated_prompt_tokens
+
estimated_completion
prompt_tokens
=
estimated_prompt_tokens
completion_tokens
=
estimated_completion
logger
.
debug
(
f
"Estimated token usage for autoselect: {total_tokens}"
)
except
Exception
as
est_error
:
logger
.
debug
(
f
"Token estimation failed: {est_error}"
)
total_tokens
=
150
prompt_tokens
=
0
completion_tokens
=
0
# Try to extract actual cost from provider response
from
..cost_extractor
import
extract_cost_from_response
actual_cost
=
extract_cost_from_response
(
response
,
'autoselect'
)
# Always record analytics
# The actual provider/model info is in the response model field
model_name
=
response
.
get
(
'model'
,
'unknown'
)
# Calculate latency
latency_ms
=
(
time
.
time
()
-
request_start_time
)
*
1000
logger
.
info
(
f
"Autoselect Analytics: latency_ms={latency_ms:.2f}"
)
analytics
.
record_request
(
provider_id
=
'autoselect'
,
model_name
=
model_name
,
tokens_used
=
total_tokens
,
latency_ms
=
0
,
latency_ms
=
latency_ms
,
success
=
True
,
autoselect_id
=
autoselect_id
,
user_id
=
user_id
,
token_id
=
token_id
token_id
=
token_id
,
prompt_tokens
=
prompt_tokens
if
prompt_tokens
>
0
else
None
,
completion_tokens
=
completion_tokens
if
completion_tokens
>
0
else
None
,
actual_cost
=
actual_cost
)
except
Exception
as
analytics_error
:
logger
.
warning
(
f
"Analytics recording failed: {analytics_error}"
)
...
...
aisbf/mcp.py
View file @
fe8b625a
...
...
@@ -855,12 +855,12 @@ class MCPServer:
if
stream
:
return
{
"error"
:
"Streaming not supported in MCP, use SSE endpoint instead"
}
else
:
return
await
handler
.
handle_autoselect_request
(
actual_model
,
request_data
)
return
await
handler
.
handle_autoselect_request
(
actual_model
,
request_data
,
user_id
,
None
)
elif
provider_id
==
"rotation"
:
handler
=
get_user_handler
(
'rotation'
,
user_id
)
if
actual_model
not
in
self
.
config
.
rotations
and
(
not
user_id
or
actual_model
not
in
handler
.
user_rotations
):
raise
HTTPException
(
status_code
=
400
,
detail
=
f
"Rotation '{actual_model}' not found"
)
return
await
handler
.
handle_rotation_request
(
actual_model
,
request_data
)
return
await
handler
.
handle_rotation_request
(
actual_model
,
request_data
,
user_id
,
None
)
else
:
handler
=
get_user_handler
(
'request'
,
user_id
)
if
provider_id
not
in
self
.
config
.
providers
and
(
not
user_id
or
provider_id
not
in
handler
.
user_providers
):
...
...
aisbf/providers/claude.py
View file @
fe8b625a
...
...
@@ -159,7 +159,7 @@ class ClaudeProviderHandler(BaseProviderHandler):
logger
.
info
(
"ClaudeProviderHandler: Initializing session for quota tracking"
)
try
:
headers
=
self
.
_get_auth_headers
(
stream
=
False
)
headers
=
await
self
.
_get_auth_headers
(
stream
=
False
)
payload
=
{
'model'
:
'claude-haiku-4-5-20251001'
,
...
...
@@ -257,12 +257,12 @@ class ClaudeProviderHandler(BaseProviderHandler):
if
old_util
!=
new_util
:
logger
.
debug
(
f
"ClaudeProviderHandler: Quota utilization updated: {old_util} -> {new_util}"
)
def
_get_sdk_client
(
self
):
async
def
_get_sdk_client
(
self
):
"""Get or create an Anthropic SDK client configured with OAuth2 auth token."""
import
logging
logger
=
logging
.
getLogger
(
__name__
)
access_token
=
self
.
auth
.
get_valid_token
()
access_token
=
await
self
.
auth
.
get_valid_token
()
if
not
access_token
:
logger
.
error
(
"ClaudeProviderHandler: No OAuth2 access token available"
)
...
...
@@ -277,14 +277,14 @@ class ClaudeProviderHandler(BaseProviderHandler):
logger
.
info
(
"ClaudeProviderHandler: Created SDK client with OAuth2 auth token"
)
return
self
.
_sdk_client
def
_get_auth_headers
(
self
,
stream
:
bool
=
False
):
async
def
_get_auth_headers
(
self
,
stream
:
bool
=
False
):
"""Get HTTP headers with OAuth2 Bearer token."""
import
logging
import
uuid
import
platform
logger
=
logging
.
getLogger
(
__name__
)
access_token
=
self
.
auth
.
get_valid_token
()
access_token
=
await
self
.
auth
.
get_valid_token
()
if
not
self
.
session_state
.
get
(
'session_id'
):
self
.
session_state
[
'session_id'
]
=
str
(
uuid
.
uuid4
())
...
...
@@ -849,7 +849,7 @@ class ClaudeProviderHandler(BaseProviderHandler):
if
anthropic_tool_choice
:
payload
[
'tool_choice'
]
=
anthropic_tool_choice
headers
=
self
.
_get_auth_headers
(
stream
=
stream
)
headers
=
await
self
.
_get_auth_headers
(
stream
=
stream
)
api_url
=
'https://api.anthropic.com/v1/messages?beta=true'
logger
.
info
(
f
"ClaudeProviderHandler: Request payload keys: {list(payload.keys())}"
)
...
...
@@ -1640,7 +1640,7 @@ class ClaudeProviderHandler(BaseProviderHandler):
try
:
logging
.
info
(
"ClaudeProviderHandler: [1/3] Attempting primary API endpoint..."
)
headers
=
self
.
_get_auth_headers
(
stream
=
False
)
headers
=
await
self
.
_get_auth_headers
(
stream
=
False
)
api_endpoint
=
'https://api.anthropic.com/v1/models'
logging
.
info
(
f
"ClaudeProviderHandler: Calling API endpoint: {api_endpoint}"
)
...
...
aisbf/providers/kilo.py
View file @
fe8b625a
...
...
@@ -170,7 +170,7 @@ class KiloProviderHandler(BaseProviderHandler):
"token"
:
self
.
api_key
}
token
=
self
.
oauth2
.
get_valid_token
()
token
=
await
self
.
oauth2
.
get_valid_token
()
if
token
:
logger
.
info
(
"KiloProviderHandler: Using existing OAuth2 token"
)
...
...
@@ -182,7 +182,7 @@ class KiloProviderHandler(BaseProviderHandler):
# Try to reload credentials one more time - this handles the case where credentials
# were saved by another process/handler instance after this handler was created
self
.
oauth2
.
_load_credentials
()
token
=
self
.
oauth2
.
get_valid_token
()
token
=
await
self
.
oauth2
.
get_valid_token
()
if
token
:
logger
.
info
(
"KiloProviderHandler: Found OAuth2 token after reloading credentials"
)
...
...
aisbf/providers/qwen.py
View file @
fe8b625a
...
...
@@ -103,7 +103,7 @@ class QwenProviderHandler(BaseProviderHandler):
logging
.
getLogger
(
__name__
)
.
info
(
f
"QwenProviderHandler: Falling back to file-based credentials for user {self.user_id}"
)
return
QwenOAuth2
(
credentials_file
=
credentials_file
)
def
_get_sdk_client
(
self
):
async
def
_get_sdk_client
(
self
):
"""Get or create an OpenAI SDK client configured with authentication (OAuth2 or API key)."""
import
logging
logger
=
logging
.
getLogger
(
__name__
)
...
...
@@ -122,7 +122,7 @@ class QwenProviderHandler(BaseProviderHandler):
base_url
=
self
.
_get_region_endpoint
(
qwen_config
)
else
:
# Use OAuth2 authentication
access_token
=
self
.
auth
.
get_valid_token
()
access_token
=
await
self
.
auth
.
get_valid_token
()
if
not
access_token
:
logger
.
error
(
"QwenProviderHandler: No OAuth2 access token available"
)
...
...
@@ -221,7 +221,7 @@ class QwenProviderHandler(BaseProviderHandler):
await
self
.
apply_rate_limit
()
# Get SDK client with current OAuth token
client
=
self
.
_get_sdk_client
()
client
=
await
self
.
_get_sdk_client
()
# Build request parameters
request_params
=
{
...
...
@@ -308,7 +308,7 @@ class QwenProviderHandler(BaseProviderHandler):
if
refresh_success
:
logger
.
info
(
"QwenProviderHandler: Token refreshed, retrying request"
)
# Retry with new token
client
=
self
.
_get_sdk_client
()
client
=
await
self
.
_get_sdk_client
()
if
stream
:
return
self
.
_handle_streaming_request
(
client
,
request_params
,
model
)
...
...
@@ -472,7 +472,7 @@ class QwenProviderHandler(BaseProviderHandler):
try
:
# Get SDK client with API key authentication
client
=
self
.
_get_sdk_client
()
client
=
await
self
.
_get_sdk_client
()
# List models using OpenAI SDK
models_response
=
await
client
.
models
.
list
()
...
...
main.py
View file @
fe8b625a
...
...
@@ -647,6 +647,13 @@ def initialize_app(custom_config_dir=None):
'password'
:
'8c6976e5b5410415bde908bd4dee15dfb167a9c873fc4bb8a81f6f2ab448a918'
}
# Initialize analytics with the config database
from
aisbf.analytics
import
initialize_analytics
from
aisbf.database
import
DatabaseRegistry
db
=
DatabaseRegistry
.
get_config_database
()
initialize_analytics
(
db
)
logger
.
info
(
"Analytics module initialized"
)
_initialized
=
True
logger
.
info
(
"App initialization complete"
)
...
...
@@ -1314,11 +1321,6 @@ async def auth_middleware(request: Request, call_next):
request
.
state
.
is_global_token
=
False
# Store user role - admin users get full access
request
.
state
.
is_admin
=
(
user_auth
.
get
(
'role'
)
==
'admin'
)
# Record token usage for analytics
# We'll do this asynchronously to avoid blocking the request
import
asyncio
asyncio
.
create_task
(
record_token_usage_async
(
user_auth
[
'user_id'
],
user_auth
[
'token_id'
]))
else
:
return
JSONResponse
(
status_code
=
403
,
...
...
@@ -1901,6 +1903,42 @@ async def get_subscription_status(request: Request):
status
=
await
payment_service
.
get_subscription_status
(
current_user
[
'id'
])
return
{
'subscription'
:
status
}
# User search API endpoint for autocomplete
@
app
.
get
(
"/api/users/search"
)
async
def
search_users
(
request
:
Request
,
q
:
str
=
Query
(
""
,
min_length
=
0
)):
"""Search users by username for autocomplete (admin only)"""
auth_check
=
require_dashboard_auth
(
request
)
if
auth_check
:
raise
HTTPException
(
status_code
=
401
,
detail
=
"Unauthorized"
)
# Check if user is admin
is_admin
=
request
.
session
.
get
(
'role'
)
==
'admin'
if
not
is_admin
:
raise
HTTPException
(
status_code
=
403
,
detail
=
"Admin access required"
)
db
=
DatabaseRegistry
.
get_config_database
()
if
not
db
:
return
{
"users"
:
[]}
# Get all users
all_users
=
db
.
get_users
()
# Filter by query string (case-insensitive)
if
q
:
filtered_users
=
[
{
"id"
:
user
[
'id'
],
"username"
:
user
[
'username'
],
"role"
:
user
.
get
(
'role'
,
'user'
)}
for
user
in
all_users
if
q
.
lower
()
in
user
[
'username'
]
.
lower
()
]
else
:
filtered_users
=
[
{
"id"
:
user
[
'id'
],
"username"
:
user
[
'username'
],
"role"
:
user
.
get
(
'role'
,
'user'
)}
for
user
in
all_users
]
# Limit to 50 results
return
{
"users"
:
filtered_users
[:
50
]}
# Dashboard routes
@
app
.
get
(
"/dashboard/analytics"
,
response_class
=
HTMLResponse
)
async
def
dashboard_analytics
(
...
...
@@ -1912,7 +1950,8 @@ async def dashboard_analytics(
model_filter
:
Optional
[
str
]
=
Query
(
None
),
rotation_filter
:
Optional
[
str
]
=
Query
(
None
),
autoselect_filter
:
Optional
[
str
]
=
Query
(
None
),
user_filter
:
Optional
[
int
]
=
Query
(
None
)
user_filter
:
Optional
[
str
]
=
Query
(
None
),
global_only
:
Optional
[
str
]
=
Query
(
None
)
):
"""Token usage analytics dashboard"""
auth_check
=
require_dashboard_auth
(
request
)
...
...
@@ -1950,9 +1989,21 @@ async def dashboard_analytics(
is_admin
=
request
.
session
.
get
(
'role'
)
==
'admin'
current_user_id
=
request
.
session
.
get
(
'user_id'
)
# Parse user_filter from string to int, handling empty strings
user_filter_int
=
None
if
user_filter
:
try
:
user_filter_int
=
int
(
user_filter
)
except
(
ValueError
,
TypeError
):
user_filter_int
=
None
# Handle global_only filter - if checked, set user_filter to -1 (special value for global requests)
if
global_only
==
'1'
:
user_filter_int
=
-
1
# Special value to indicate "only global requests"
# For non-admin users, force user filter to current user
if
not
is_admin
and
current_user_id
is
not
None
:
user_filter
=
current_user_id
user_filter
_int
=
current_user_id
# Get all users for filter dropdown (only for admins)
all_users
=
db
.
get_users
()
if
db
and
is_admin
else
[]
...
...
@@ -1972,9 +2023,9 @@ async def dashboard_analytics(
# Get provider statistics (with optional filter)
if
provider_filter
:
provider_stats
=
[
analytics
.
get_provider_stats
(
provider_filter
,
from_datetime
,
to_datetime
,
user_filter
=
user_filter
)]
provider_stats
=
[
analytics
.
get_provider_stats
(
provider_filter
,
from_datetime
,
to_datetime
,
user_filter
=
user_filter
_int
)]
else
:
provider_stats
=
analytics
.
get_all_providers_stats
(
from_datetime
,
to_datetime
,
user_filter
=
user_filter
)
provider_stats
=
analytics
.
get_all_providers_stats
(
from_datetime
,
to_datetime
,
user_filter
=
user_filter
_int
)
# Get token usage over time (with optional filters)
token_over_time
=
analytics
.
get_token_usage_over_time
(
...
...
@@ -1982,7 +2033,7 @@ async def dashboard_analytics(
time_range
=
time_range
,
from_datetime
=
from_datetime
,
to_datetime
=
to_datetime
,
user_filter
=
user_filter
user_filter
=
user_filter
_int
)
# Get model performance (with optional filters)
...
...
@@ -1991,21 +2042,21 @@ async def dashboard_analytics(
model_filter
=
model_filter
,
rotation_filter
=
rotation_filter
,
autoselect_filter
=
autoselect_filter
,
user_filter
=
user_filter
user_filter
=
user_filter
_int
)
# Get cost overview
cost_overview
=
analytics
.
get_cost_overview
(
from_datetime
,
to_datetime
,
user_filter
=
user_filter
)
cost_overview
=
analytics
.
get_cost_overview
(
from_datetime
,
to_datetime
,
user_filter
=
user_filter
_int
)
# Get optimization recommendations
recommendations
=
analytics
.
get_optimization_recommendations
(
user_filter
=
user_filter
)
recommendations
=
analytics
.
get_optimization_recommendations
(
user_filter
=
user_filter
_int
)
# Get date range usage summary
date_range_usage
=
None
if
from_datetime
or
to_datetime
:
start
=
from_datetime
or
(
datetime
.
now
()
-
timedelta
(
days
=
1
))
end
=
to_datetime
or
datetime
.
now
()
date_range_usage
=
analytics
.
get_token_usage_by_date_range
(
provider_filter
,
start
,
end
,
user_filter
=
user_filter
)
date_range_usage
=
analytics
.
get_token_usage_by_date_range
(
provider_filter
,
start
,
end
,
user_filter
=
user_filter
_int
)
return
templates
.
TemplateResponse
(
request
=
request
,
...
...
@@ -2032,7 +2083,8 @@ async def dashboard_analytics(
"selected_model"
:
model_filter
,
"selected_rotation"
:
rotation_filter
,
"selected_autoselect"
:
autoselect_filter
,
"selected_user"
:
user_filter
"selected_user"
:
user_filter
,
"global_only"
:
global_only
}
)
...
...
@@ -2151,6 +2203,13 @@ async def dashboard_login(request: Request, username: str = Form(...), password:
# For non-remember-me sessions, set expiry to 2 weeks (default session length)
request
.
session
[
'expires_at'
]
=
int
(
time
.
time
())
+
14
*
24
*
60
*
60
# Update last login timestamp
with
db
.
_get_connection
()
as
conn
:
cursor
=
conn
.
cursor
()
placeholder
=
'?'
if
db
.
db_type
==
'sqlite'
else
'
%
s'
cursor
.
execute
(
f
'UPDATE users SET last_login = CURRENT_TIMESTAMP WHERE id = {placeholder}'
,
(
user
[
'id'
],))
conn
.
commit
()
# Check if email is verified
if
not
user
[
'email_verified'
]:
# Check if account is expired (24 hours old and unverified)
...
...
@@ -3236,6 +3295,13 @@ async def oauth2_google_callback(request: Request, code: str = Query(...), state
request
.
session
[
'user_id'
]
=
existing_user
[
'id'
]
request
.
session
[
'email_verified'
]
=
True
# OAuth2 users have verified emails
request
.
session
[
'expires_at'
]
=
int
(
time
.
time
())
+
14
*
24
*
60
*
60
# Update last login timestamp
with
db
.
_get_connection
()
as
conn
:
cursor
=
conn
.
cursor
()
placeholder
=
'?'
if
db
.
db_type
==
'sqlite'
else
'
%
s'
cursor
.
execute
(
f
'UPDATE users SET last_login = CURRENT_TIMESTAMP WHERE id = {placeholder}'
,
(
existing_user
[
'id'
],))
conn
.
commit
()
else
:
# New user - create account automatically (no password required)
if
not
email_verified
:
...
...
@@ -3253,6 +3319,7 @@ async def oauth2_google_callback(request: Request, code: str = Query(...), state
google_username
=
db
.
generate_username_from_display_name
(
display_name
,
email
)
final_username
=
db
.
find_unique_username
(
google_username
)
# Create user with verified email (no verification required)
user_id
=
db
.
create_user
(
final_username
,
password_hash
,
'user'
,
None
,
email
,
True
,
display_name
)
...
...
@@ -3265,6 +3332,13 @@ async def oauth2_google_callback(request: Request, code: str = Query(...), state
request
.
session
[
'email_verified'
]
=
True
# OAuth2 users have verified emails
request
.
session
[
'expires_at'
]
=
int
(
time
.
time
())
+
14
*
24
*
60
*
60
# Update last login timestamp for new user
with
db
.
_get_connection
()
as
conn
:
cursor
=
conn
.
cursor
()
placeholder
=
'?'
if
db
.
db_type
==
'sqlite'
else
'
%
s'
cursor
.
execute
(
f
'UPDATE users SET last_login = CURRENT_TIMESTAMP WHERE id = {placeholder}'
,
(
user_id
,))
conn
.
commit
()
# Cleanup session data
request
.
session
.
pop
(
'oauth2_google'
,
None
)
...
...
@@ -3383,6 +3457,13 @@ async def oauth2_github_callback(request: Request, code: str = Query(...), state
request
.
session
[
'user_id'
]
=
existing_user
[
'id'
]
request
.
session
[
'email_verified'
]
=
True
# OAuth2 users have verified emails
request
.
session
[
'expires_at'
]
=
int
(
time
.
time
())
+
14
*
24
*
60
*
60
# Update last login timestamp
with
db
.
_get_connection
()
as
conn
:
cursor
=
conn
.
cursor
()
placeholder
=
'?'
if
db
.
db_type
==
'sqlite'
else
'
%
s'
cursor
.
execute
(
f
'UPDATE users SET last_login = CURRENT_TIMESTAMP WHERE id = {placeholder}'
,
(
existing_user
[
'id'
],))
conn
.
commit
()
else
:
# New user - create account automatically (no password required)
# Generate secure random password for OAuth users (never used for login)
...
...
@@ -3405,6 +3486,13 @@ async def oauth2_github_callback(request: Request, code: str = Query(...), state
request
.
session
[
'email_verified'
]
=
True
# OAuth2 users have verified emails
request
.
session
[
'expires_at'
]
=
int
(
time
.
time
())
+
14
*
24
*
60
*
60
# Update last login timestamp for new user
with
db
.
_get_connection
()
as
conn
:
cursor
=
conn
.
cursor
()
placeholder
=
'?'
if
db
.
db_type
==
'sqlite'
else
'
%
s'
cursor
.
execute
(
f
'UPDATE users SET last_login = CURRENT_TIMESTAMP WHERE id = {placeholder}'
,
(
user_id
,))
conn
.
commit
()
# Cleanup session data
request
.
session
.
pop
(
'oauth2_github'
,
None
)
...
...
@@ -3639,7 +3727,7 @@ async def _auto_detect_provider_models(provider_key: str, provider: dict) -> lis
kilo_config
=
provider
.
get
(
'kilo_config'
,
{})
credentials_file
=
kilo_config
.
get
(
'credentials_file'
,
'~/.kilo_credentials.json'
)
oauth2
=
KiloOAuth2
(
credentials_file
=
credentials_file
,
api_base
=
endpoint
)
token
=
oauth2
.
get_valid_token
()
token
=
await
oauth2
.
get_valid_token
()
if
token
:
api_key
=
token
logger
.
info
(
f
"Using OAuth2 token for Kilo provider '{provider_key}'"
)
...
...
@@ -6992,7 +7080,7 @@ async def dashboard_pricing(request: Request):
from
aisbf.database
import
get_database
db
=
DatabaseRegistry
.
get_config_database
()
tiers
=
db
.
get_
all
_tiers
()
tiers
=
db
.
get_
visible
_tiers
()
current_tier
=
db
.
get_user_tier
(
request
.
session
.
get
(
'user_id'
))
# Get enabled payment gateways
...
...
@@ -7874,12 +7962,13 @@ async def v1_chat_completions(request: Request, body: ChatCompletionRequest):
body_dict
[
'model'
]
=
actual_model
# Get user-specific handler
user_id
=
getattr
(
request
.
state
,
'user_id'
,
None
)
token_id
=
getattr
(
request
.
state
,
'token_id'
,
None
)
handler
=
get_user_handler
(
'autoselect'
,
user_id
)
if
body
.
stream
:
return
await
handler
.
handle_autoselect_streaming_request
(
actual_model
,
body_dict
)
else
:
return
await
handler
.
handle_autoselect_request
(
actual_model
,
body_dict
)
return
await
handler
.
handle_autoselect_request
(
actual_model
,
body_dict
,
user_id
,
token_id
)
# PATH 2: Check if it's a rotation (format: rotation/{name})
if
provider_id
==
"rotation"
:
...
...
@@ -7891,8 +7980,9 @@ async def v1_chat_completions(request: Request, body: ChatCompletionRequest):
body_dict
[
'model'
]
=
actual_model
# Get user-specific handler
user_id
=
getattr
(
request
.
state
,
'user_id'
,
None
)
token_id
=
getattr
(
request
.
state
,
'token_id'
,
None
)
handler
=
get_user_handler
(
'rotation'
,
user_id
)
return
await
handler
.
handle_rotation_request
(
actual_model
,
body_dict
)
return
await
handler
.
handle_rotation_request
(
actual_model
,
body_dict
,
user_id
,
token_id
)
# PATH 1: Direct provider model (format: {provider}/{model})
if
provider_id
not
in
config
.
providers
:
...
...
@@ -8405,11 +8495,12 @@ async def rotation_chat_completions(request: Request, body: ChatCompletionReques
try
:
# Get user-specific handler
user_id
=
getattr
(
request
.
state
,
'user_id'
,
None
)
token_id
=
getattr
(
request
.
state
,
'token_id'
,
None
)
handler
=
get_user_handler
(
'rotation'
,
user_id
)
# The rotation handler handles streaming internally and returns
# a StreamingResponse for streaming requests or a dict for non-streaming
result
=
await
handler
.
handle_rotation_request
(
body
.
model
,
body_dict
)
result
=
await
handler
.
handle_rotation_request
(
body
.
model
,
body_dict
,
user_id
,
token_id
)
logger
.
debug
(
f
"Rotation response result type: {type(result)}"
)
return
result
except
Exception
as
e
:
...
...
@@ -8482,6 +8573,7 @@ async def autoselect_chat_completions(request: Request, body: ChatCompletionRequ
# Get user-specific handler
user_id
=
getattr
(
request
.
state
,
'user_id'
,
None
)
token_id
=
getattr
(
request
.
state
,
'token_id'
,
None
)
handler
=
get_user_handler
(
'autoselect'
,
user_id
)
# Check if the model name corresponds to an autoselect configuration
...
...
@@ -8502,7 +8594,7 @@ async def autoselect_chat_completions(request: Request, body: ChatCompletionRequ
return
await
handler
.
handle_autoselect_streaming_request
(
body
.
model
,
body_dict
)
else
:
logger
.
debug
(
"Handling non-streaming autoselect request"
)
result
=
await
handler
.
handle_autoselect_request
(
body
.
model
,
body_dict
)
result
=
await
handler
.
handle_autoselect_request
(
body
.
model
,
body_dict
,
user_id
,
token_id
)
logger
.
debug
(
f
"Autoselect response result: {result}"
)
return
result
except
Exception
as
e
:
...
...
@@ -8550,6 +8642,7 @@ async def chat_completions(provider_id: str, request: Request, body: ChatComplet
# Check if it's an autoselect
if
provider_id
in
config
.
autoselect
or
(
user_id
and
provider_id
in
get_user_handler
(
'autoselect'
,
user_id
)
.
user_autoselects
):
logger
.
debug
(
"Handling autoselect request"
)
token_id
=
getattr
(
request
.
state
,
'token_id'
,
None
)
handler
=
get_user_handler
(
'autoselect'
,
user_id
)
try
:
if
body
.
stream
:
...
...
@@ -8557,7 +8650,7 @@ async def chat_completions(provider_id: str, request: Request, body: ChatComplet
return
await
handler
.
handle_autoselect_streaming_request
(
provider_id
,
body_dict
)
else
:
logger
.
debug
(
"Handling non-streaming autoselect request"
)
result
=
await
handler
.
handle_autoselect_request
(
provider_id
,
body_dict
)
result
=
await
handler
.
handle_autoselect_request
(
provider_id
,
body_dict
,
user_id
,
token_id
)
logger
.
debug
(
f
"Autoselect response result: {result}"
)
return
result
except
Exception
as
e
:
...
...
@@ -8568,8 +8661,9 @@ async def chat_completions(provider_id: str, request: Request, body: ChatComplet
if
provider_id
in
config
.
rotations
or
(
user_id
and
provider_id
in
get_user_handler
(
'rotation'
,
user_id
)
.
user_rotations
):
logger
.
info
(
f
"Provider ID '{provider_id}' found in rotations"
)
logger
.
debug
(
"Handling rotation request"
)
token_id
=
getattr
(
request
.
state
,
'token_id'
,
None
)
handler
=
get_user_handler
(
'rotation'
,
user_id
)
return
await
handler
.
handle_rotation_request
(
provider_id
,
body_dict
)
return
await
handler
.
handle_rotation_request
(
provider_id
,
body_dict
,
user_id
,
token_id
)
# Check if it's a provider
handler
=
get_user_handler
(
'request'
,
user_id
)
...
...
@@ -10600,7 +10694,8 @@ async def user_chat_completions_by_username(request: Request, username: str, bod
if
body
.
stream
:
return
await
handler
.
handle_autoselect_streaming_request
(
actual_model
,
body_dict
)
else
:
return
await
handler
.
handle_autoselect_request
(
actual_model
,
body_dict
)
token_id
=
getattr
(
request
.
state
,
'token_id'
,
None
)
return
await
handler
.
handle_autoselect_request
(
actual_model
,
body_dict
,
authenticated_user_id
,
token_id
)
if
provider_id
==
"user-rotation"
:
handler
=
get_user_handler
(
'rotation'
,
target_user_id
)
...
...
@@ -10610,7 +10705,8 @@ async def user_chat_completions_by_username(request: Request, username: str, bod
detail
=
f
"User rotation '{actual_model}' not found. Available: {list(handler.user_rotations.keys())}"
)
body_dict
[
'model'
]
=
actual_model
return
await
handler
.
handle_rotation_request
(
actual_model
,
body_dict
)
token_id
=
getattr
(
request
.
state
,
'token_id'
,
None
)
return
await
handler
.
handle_rotation_request
(
actual_model
,
body_dict
,
authenticated_user_id
,
token_id
)
if
provider_id
==
"user-provider"
:
handler
=
get_user_handler
(
'request'
,
target_user_id
)
...
...
@@ -10648,7 +10744,8 @@ async def user_chat_completions_by_username(request: Request, username: str, bod
if
body
.
stream
:
return
await
handler
.
handle_autoselect_streaming_request
(
actual_model
,
body_dict
)
else
:
return
await
handler
.
handle_autoselect_request
(
actual_model
,
body_dict
)
token_id
=
getattr
(
request
.
state
,
'token_id'
,
None
)
return
await
handler
.
handle_autoselect_request
(
actual_model
,
body_dict
,
authenticated_user_id
,
token_id
)
if
provider_id
==
"rotation"
:
if
actual_model
not
in
config
.
rotations
:
...
...
@@ -10658,7 +10755,8 @@ async def user_chat_completions_by_username(request: Request, username: str, bod
)
handler
=
get_user_handler
(
'rotation'
,
None
)
body_dict
[
'model'
]
=
actual_model
return
await
handler
.
handle_rotation_request
(
actual_model
,
body_dict
)
token_id
=
getattr
(
request
.
state
,
'token_id'
,
None
)
return
await
handler
.
handle_rotation_request
(
actual_model
,
body_dict
,
authenticated_user_id
,
token_id
)
if
provider_id
in
config
.
providers
:
provider_config
=
config
.
get_provider
(
provider_id
)
...
...
@@ -10858,7 +10956,8 @@ async def user_chat_completions(request: Request, username: str, body: ChatCompl
if
body
.
stream
:
return
await
handler
.
handle_autoselect_streaming_request
(
actual_model
,
body_dict
)
else
:
return
await
handler
.
handle_autoselect_request
(
actual_model
,
body_dict
)
token_id
=
getattr
(
request
.
state
,
'token_id'
,
None
)
return
await
handler
.
handle_autoselect_request
(
actual_model
,
body_dict
,
user_id
,
token_id
)
# Handle user rotation (format: user-rotation/{name})
if
provider_id
==
"user-rotation"
:
...
...
@@ -10869,7 +10968,8 @@ async def user_chat_completions(request: Request, username: str, body: ChatCompl
detail
=
f
"User rotation '{actual_model}' not found. Available: {list(handler.user_rotations.keys())}"
)
body_dict
[
'model'
]
=
actual_model
return
await
handler
.
handle_rotation_request
(
actual_model
,
body_dict
)
token_id
=
getattr
(
request
.
state
,
'token_id'
,
None
)
return
await
handler
.
handle_rotation_request
(
actual_model
,
body_dict
,
user_id
,
token_id
)
# Handle user provider (format: user-provider/{name})
if
provider_id
==
"user-provider"
:
...
...
@@ -10911,7 +11011,8 @@ async def user_chat_completions(request: Request, username: str, body: ChatCompl
if
body
.
stream
:
return
await
handler
.
handle_autoselect_streaming_request
(
actual_model
,
body_dict
)
else
:
return
await
handler
.
handle_autoselect_request
(
actual_model
,
body_dict
)
token_id
=
getattr
(
request
.
state
,
'token_id'
,
None
)
return
await
handler
.
handle_autoselect_request
(
actual_model
,
body_dict
,
user_id
,
token_id
)
# Handle global rotation
if
provider_id
==
"rotation"
:
...
...
@@ -10922,7 +11023,8 @@ async def user_chat_completions(request: Request, username: str, body: ChatCompl
)
handler
=
get_user_handler
(
'rotation'
,
None
)
body_dict
[
'model'
]
=
actual_model
return
await
handler
.
handle_rotation_request
(
actual_model
,
body_dict
)
token_id
=
getattr
(
request
.
state
,
'token_id'
,
None
)
return
await
handler
.
handle_rotation_request
(
actual_model
,
body_dict
,
user_id
,
token_id
)
# Handle global provider
if
provider_id
in
config
.
providers
:
...
...
@@ -11924,7 +12026,7 @@ async def dashboard_kilo_auth_status(request: Request):
# Check if authenticated
if
auth
.
is_authenticated
():
# Try to get a valid token (will refresh if needed)
token
=
auth
.
get_valid_token
()
token
=
a
wait
a
uth
.
get_valid_token
()
if
token
:
# Get token expiration info
expires_at
=
auth
.
credentials
.
get
(
'expires'
,
0
)
...
...
@@ -12240,7 +12342,7 @@ async def dashboard_codex_auth_status(request: Request):
# Check if authenticated
if
auth
.
is_authenticated
():
# Try to get a valid token (will refresh if needed)
token
=
a
uth
.
get_valid_token
()
token
=
a
wait
auth
.
get_valid_token_with_refresh
()
if
token
:
# Get user email from ID token
email
=
auth
.
get_user_email
()
...
...
pyproject.toml
View file @
fe8b625a
...
...
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name
=
"aisbf"
version
=
"0.99.2
8
"
version
=
"0.99.2
9
"
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 @
fe8b625a
...
...
@@ -49,7 +49,7 @@ class InstallCommand(_install):
setup
(
name
=
"aisbf"
,
version
=
"0.99.2
8
"
,
version
=
"0.99.2
9
"
,
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/base.html
View file @
fe8b625a
...
...
@@ -25,7 +25,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
<style>
*
{
margin
:
0
;
padding
:
0
;
box-sizing
:
border-box
;
}
body
{
font-family
:
-apple-system
,
BlinkMacSystemFont
,
'Segoe UI'
,
Roboto
,
Oxygen
,
Ubuntu
,
Cantarell
,
sans-serif
;
background
:
#1a1a2e
;
color
:
#e0e0e0
;
}
.container
{
max-width
:
1
320
px
;
margin
:
0
auto
;
padding
:
20px
;
}
.container
{
max-width
:
1
452
px
;
margin
:
0
auto
;
padding
:
20px
;
}
.header
{
background
:
#16213e
;
color
:
white
;
padding
:
20px
0
;
margin-bottom
:
30px
;
border-bottom
:
2px
solid
#0f3460
;
}
.header
h1
{
font-size
:
24px
;
font-weight
:
600
;
display
:
inline-block
;
}
.header-actions
{
float
:
right
;
}
...
...
templates/dashboard/analytics.html
View file @
fe8b625a
...
...
@@ -18,6 +18,21 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
{% block title %}Analytics - AISBF Dashboard{% endblock %}
{% macro format_tokens(value) %}
{% if value is none or value == 0 %}0{% else %}
{% set val = value | float %}
{% if val >= 1000000000 %}
{{ "%.2f"|format(val / 1000000000) }}B
{% elif val >= 1000000 %}
{{ "%.2f"|format(val / 1000000) }}M
{% elif val >= 1000 %}
{{ "%.2f"|format(val / 1000) }}K
{% else %}
{{ value }}
{% endif %}
{% endif %}
{% endmacro %}
{% block content %}
<h2
style=
"margin-bottom: 30px;"
>
Token Usage Analytics
</h2>
...
...
@@ -68,7 +83,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
</span>
{% if date_range_usage %}
<span
style=
"margin-left: 20px; color: #a0a0a0;"
>
| Total: {{
date_range_usage.total_tokens
}} tokens | Estimated Cost: ${{ "%.2f"|format(date_range_usage.estimated_cost) }}
| Total: {{
format_tokens(date_range_usage.total_tokens)
}} tokens | Estimated Cost: ${{ "%.2f"|format(date_range_usage.estimated_cost) }}
</span>
{% endif %}
</div>
...
...
@@ -127,12 +142,30 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
{% if is_admin %}
<div
style=
"flex: 1; min-width: 150px;"
>
<label
style=
"display: block; margin-bottom: 5px; color: #a0a0a0; font-size: 14px;"
>
User
</label>
<select
name=
"user_filter"
style=
"width: 100%; padding: 10px; border-radius: 4px; background: #0f3460; color: white; border: 1px solid #2a4a7a;"
>
{% if available_users|length
<
25
%}
<
select
name=
"user_filter"
id=
"userFilterSelect"
style=
"width: 100%; padding: 10px; border-radius: 4px; background: #0f3460; color: white; border: 1px solid #2a4a7a;"
>
<option
value=
""
>
All Users
</option>
{% for user in available_users %}
<option
value=
"{{ user.id }}"
{%
if
selected_user =
=
user
.
id
%}
selected
{%
endif
%}
>
{{ user.username }}{% if user.role == 'admin' %} (admin){% endif %}
</option>
{% endfor %}
</select>
{% else %}
<div
style=
"position: relative;"
>
<input
type=
"text"
id=
"userSearchInput"
placeholder=
"Search users..."
autocomplete=
"off"
style=
"width: 100%; padding: 10px; border-radius: 4px; background: #0f3460; color: white; border: 1px solid #2a4a7a;"
>
<input
type=
"hidden"
name=
"user_filter"
id=
"userFilterValue"
value=
"{{ selected_user or '' }}"
>
<div
id=
"userSearchResults"
style=
"display: none; position: absolute; top: 100%; left: 0; right: 0; background: #0f3460; border: 1px solid #2a4a7a; border-top: none; border-radius: 0 0 4px 4px; max-height: 300px; overflow-y: auto; z-index: 1000;"
>
</div>
</div>
{% endif %}
</div>
<div
style=
"flex: 1; min-width: 150px; display: flex; align-items: center;"
>
<label
style=
"display: flex; align-items: center; color: #e0e0e0; cursor: pointer; user-select: none;"
>
<input
type=
"checkbox"
name=
"global_only"
value=
"1"
id=
"globalOnlyCheckbox"
{%
if
global_only =
=
'
1
'
%}
checked
{%
endif
%}
style=
"margin-right: 8px; width: 18px; height: 18px; cursor: pointer;"
>
<span
style=
"font-size: 14px;"
>
Global requests only
</span>
</label>
</div>
{% endif %}
...
...
@@ -174,6 +207,195 @@ document.getElementById('timeRangeSelect').addEventListener('change', function()
customRange
.
style
.
display
=
'none'
;
}
});
// Handle global_only checkbox for select dropdown (when
<
25
users
)
{
%
if
is_admin
and
available_users
|
length
<
25
%
}
(
function
()
{
const
userSelect
=
document
.
getElementById
(
'userFilterSelect'
);
const
globalOnlyCheckbox
=
document
.
getElementById
(
'globalOnlyCheckbox'
);
if
(
globalOnlyCheckbox
&&
userSelect
)
{
globalOnlyCheckbox
.
addEventListener
(
'change'
,
function
()
{
if
(
this
.
checked
)
{
// When global_only is checked, clear user filter and disable select
userSelect
.
value
=
''
;
userSelect
.
disabled
=
true
;
userSelect
.
style
.
opacity
=
'0.5'
;
userSelect
.
style
.
cursor
=
'not-allowed'
;
}
else
{
// Re-enable user select
userSelect
.
disabled
=
false
;
userSelect
.
style
.
opacity
=
'1'
;
userSelect
.
style
.
cursor
=
'pointer'
;
}
});
// Initialize state on page load
if
(
globalOnlyCheckbox
.
checked
)
{
userSelect
.
disabled
=
true
;
userSelect
.
style
.
opacity
=
'0.5'
;
userSelect
.
style
.
cursor
=
'not-allowed'
;
}
}
})();
{
%
endif
%
}
// User search autocomplete functionality
{
%
if
is_admin
and
available_users
|
length
>=
25
%
}
(
function
()
{
const
searchInput
=
document
.
getElementById
(
'userSearchInput'
);
const
resultsDiv
=
document
.
getElementById
(
'userSearchResults'
);
const
hiddenInput
=
document
.
getElementById
(
'userFilterValue'
);
let
debounceTimer
;
let
selectedUserId
=
{{
selected_user
or
'null'
}};
// 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
%
}
// Search users via API
async
function
searchUsers
(
query
)
{
try
{
const
response
=
await
fetch
(
`/api/users/search?q=
${
encodeURIComponent
(
query
)}
`
);
const
data
=
await
response
.
json
();
return
data
.
users
||
[];
}
catch
(
error
)
{
console
.
error
(
'Error searching users:'
,
error
);
return
[];
}
}
// Display search results
function
displayResults
(
users
)
{
if
(
users
.
length
===
0
)
{
resultsDiv
.
innerHTML
=
'<div style="padding: 10px; color: #a0a0a0;">No users found</div>'
;
resultsDiv
.
style
.
display
=
'block'
;
return
;
}
resultsDiv
.
innerHTML
=
users
.
map
(
user
=>
`
<div class="user-result-item" data-user-id="
${
user
.
id
}
" data-username="
${
user
.
username
}
" data-role="
${
user
.
role
}
"
style="padding: 10px; cursor: pointer; border-bottom: 1px solid #2a4a7a;">
${
user
.
username
}${
user
.
role
===
'admin'
?
' <span style="color: #60a5fa;">(admin)</span>'
:
''
}
</div>
`
).
join
(
''
);
resultsDiv
.
style
.
display
=
'block'
;
// Add click handlers
document
.
querySelectorAll
(
'.user-result-item'
).
forEach
(
item
=>
{
item
.
addEventListener
(
'mouseenter'
,
function
()
{
this
.
style
.
background
=
'#1a4d7a'
;
});
item
.
addEventListener
(
'mouseleave'
,
function
()
{
this
.
style
.
background
=
''
;
});
item
.
addEventListener
(
'click'
,
function
()
{
const
userId
=
this
.
dataset
.
userId
;
const
username
=
this
.
dataset
.
username
;
const
role
=
this
.
dataset
.
role
;
searchInput
.
value
=
username
+
(
role
===
'admin'
?
' (admin)'
:
''
);
hiddenInput
.
value
=
userId
;
selectedUserId
=
userId
;
resultsDiv
.
style
.
display
=
'none'
;
});
});
}
// Handle input
searchInput
.
addEventListener
(
'input'
,
function
()
{
const
query
=
this
.
value
.
trim
();
clearTimeout
(
debounceTimer
);
if
(
query
.
length
===
0
)
{
// Show all users option
resultsDiv
.
innerHTML
=
'<div class="user-result-item" data-user-id="" data-username="All Users" style="padding: 10px; cursor: pointer; border-bottom: 1px solid #2a4a7a;">All Users</div>'
;
resultsDiv
.
style
.
display
=
'block'
;
document
.
querySelector
(
'.user-result-item'
).
addEventListener
(
'mouseenter'
,
function
()
{
this
.
style
.
background
=
'#1a4d7a'
;
});
document
.
querySelector
(
'.user-result-item'
).
addEventListener
(
'mouseleave'
,
function
()
{
this
.
style
.
background
=
''
;
});
document
.
querySelector
(
'.user-result-item'
).
addEventListener
(
'click'
,
function
()
{
searchInput
.
value
=
''
;
hiddenInput
.
value
=
''
;
selectedUserId
=
null
;
resultsDiv
.
style
.
display
=
'none'
;
});
return
;
}
debounceTimer
=
setTimeout
(
async
()
=>
{
const
users
=
await
searchUsers
(
query
);
displayResults
(
users
);
},
300
);
});
// Handle focus
searchInput
.
addEventListener
(
'focus'
,
function
()
{
if
(
this
.
value
.
trim
().
length
===
0
)
{
resultsDiv
.
innerHTML
=
'<div class="user-result-item" data-user-id="" data-username="All Users" style="padding: 10px; cursor: pointer; border-bottom: 1px solid #2a4a7a;">All Users</div>'
;
resultsDiv
.
style
.
display
=
'block'
;
document
.
querySelector
(
'.user-result-item'
).
addEventListener
(
'mouseenter'
,
function
()
{
this
.
style
.
background
=
'#1a4d7a'
;
});
document
.
querySelector
(
'.user-result-item'
).
addEventListener
(
'mouseleave'
,
function
()
{
this
.
style
.
background
=
''
;
});
document
.
querySelector
(
'.user-result-item'
).
addEventListener
(
'click'
,
function
()
{
searchInput
.
value
=
''
;
hiddenInput
.
value
=
''
;
selectedUserId
=
null
;
resultsDiv
.
style
.
display
=
'none'
;
});
}
});
// Close results when clicking outside
document
.
addEventListener
(
'click'
,
function
(
e
)
{
if
(
!
searchInput
.
contains
(
e
.
target
)
&&
!
resultsDiv
.
contains
(
e
.
target
))
{
resultsDiv
.
style
.
display
=
'none'
;
}
});
// Handle global_only checkbox interaction
const
globalOnlyCheckbox
=
document
.
getElementById
(
'globalOnlyCheckbox'
);
if
(
globalOnlyCheckbox
)
{
globalOnlyCheckbox
.
addEventListener
(
'change'
,
function
()
{
if
(
this
.
checked
)
{
// When global_only is checked, clear user filter
searchInput
.
value
=
''
;
hiddenInput
.
value
=
''
;
selectedUserId
=
null
;
searchInput
.
disabled
=
true
;
searchInput
.
style
.
opacity
=
'0.5'
;
searchInput
.
style
.
cursor
=
'not-allowed'
;
}
else
{
// Re-enable user search
searchInput
.
disabled
=
false
;
searchInput
.
style
.
opacity
=
'1'
;
searchInput
.
style
.
cursor
=
'text'
;
}
});
// Initialize state on page load
if
(
globalOnlyCheckbox
.
checked
)
{
searchInput
.
disabled
=
true
;
searchInput
.
style
.
opacity
=
'0.5'
;
searchInput
.
style
.
cursor
=
'not-allowed'
;
}
}
})();
{
%
endif
%
}
</script>
{% if recommendations %}
...
...
@@ -203,6 +425,9 @@ document.getElementById('timeRangeSelect').addEventListener('change', function()
<th>
Errors
</th>
<th>
Error Rate
</th>
<th>
Avg Latency
</th>
<th>
Input Tokens
</th>
<th>
Output Tokens
</th>
<th>
Total Tokens
</th>
<th>
Tokens/Min
</th>
<th>
Tokens/Hour
</th>
<th>
Tokens/Day
</th>
...
...
@@ -219,11 +444,50 @@ document.getElementById('timeRangeSelect').addEventListener('change', function()
<td
{%
if
provider
.
avg_latency_ms
>
5000 %}style="color: #fcd34d;"{% endif %}>
{% if provider.avg_latency_ms > 1000 %}{{ "%.1f"|format(provider.avg_latency_ms / 1000) }}s{% else %}{{ "%.0f"|format(provider.avg_latency_ms) }}ms{% endif %}
</td>
<td>
{{ provider.tokens.TPM }}
</td>
<td>
{{ provider.tokens.TPH }}
</td>
<td>
{{ provider.tokens.TPD }}
</td>
<td><strong>
{{ format_tokens(provider.tokens.prompt or 0) }}
</strong></td>
<td><strong>
{{ format_tokens(provider.tokens.completion or 0) }}
</strong></td>
<td><strong>
{{ format_tokens(provider.tokens.total or 0) }}
</strong></td>
<td>
{{ format_tokens(provider.tokens.TPM) }}
</td>
<td>
{{ format_tokens(provider.tokens.TPH) }}
</td>
<td>
{{ format_tokens(provider.tokens.TPD) }}
</td>
</tr>
{% endfor %}
{% if provider_stats %}
<tr
style=
"background: #0f3460; font-weight: bold;"
>
<td><strong>
Total
</strong></td>
<td>
{{ provider_stats | sum(attribute='requests.total') }}
</td>
<td>
{{ provider_stats | sum(attribute='requests.success') }}
</td>
<td>
{{ provider_stats | sum(attribute='requests.error') }}
</td>
<td>
{% set total_requests = provider_stats | sum(attribute='requests.total') %}
{% set total_errors = provider_stats | sum(attribute='requests.error') %}
{% if total_requests > 0 %}
{{ "%.1f"|format((total_errors / total_requests) * 100) }}%
{% else %}
0.0%
{% endif %}
</td>
<td>
{% set total_requests = provider_stats | sum(attribute='requests.total') %}
{% if total_requests > 0 %}
{% set weighted_sum = namespace(value=0) %}
{% for provider in provider_stats %}
{% set weighted_sum.value = weighted_sum.value + (provider.avg_latency_ms * provider.requests.total) %}
{% endfor %}
{% set avg_latency = weighted_sum.value / total_requests %}
{% if avg_latency > 1000 %}{{ "%.1f"|format(avg_latency / 1000) }}s{% else %}{{ "%.0f"|format(avg_latency) }}ms{% endif %}
{% else %}
N/A
{% endif %}
</td>
<td><strong>
{{ format_tokens(provider_stats | sum(attribute='tokens.prompt') or 0) }}
</strong></td>
<td><strong>
{{ format_tokens(provider_stats | sum(attribute='tokens.completion') or 0) }}
</strong></td>
<td><strong>
{{ format_tokens(provider_stats | sum(attribute='tokens.total') or 0) }}
</strong></td>
<td>
{{ format_tokens(provider_stats | sum(attribute='tokens.TPM')) }}
</td>
<td>
{{ format_tokens(provider_stats | sum(attribute='tokens.TPH')) }}
</td>
<td>
{{ format_tokens(provider_stats | sum(attribute='tokens.TPD')) }}
</td>
</tr>
{% endif %}
</table>
{% else %}
<p
style=
"color: #a0a0a0;"
>
No provider statistics available yet. Make API requests to see analytics.
</p>
...
...
@@ -246,7 +510,7 @@ document.getElementById('timeRangeSelect').addEventListener('change', function()
<div
style=
"background: #0f3460; padding: 15px; border-radius: 8px;"
>
<h4
style=
"font-size: 14px; margin-bottom: 5px;"
>
{{ pc.provider_id }}
</h4>
<p
style=
"font-size: 20px; font-weight: bold;"
>
${{ "%.2f"|format(pc.estimated_cost) }}
</p>
<small
style=
"color: #a0a0a0;"
>
{{
pc.tokens_today
}} tokens
</small>
<small
style=
"color: #a0a0a0;"
>
{{
format_tokens(pc.tokens_today)
}} tokens
</small>
</div>
{% endfor %}
</div>
...
...
@@ -281,7 +545,7 @@ document.getElementById('timeRangeSelect').addEventListener('change', function()
<td>
{{ model.context_size|default('N/A') }}
</td>
<td>
{{ model.condense_context|default('N/A') }}%
</td>
<td>
{{ model.condense_method|default('None') }}
</td>
<td>
{{
model.tokens_per_day
}}
</td>
<td>
{{
format_tokens(model.tokens_per_day)
}}
</td>
<td
{%
if
model
.
error_rate
>
0.1 %}style="color: #f87171;"{% endif %}>
{{ "%.1f"|format(model.error_rate * 100) }}%
</td>
...
...
templates/dashboard/pricing.html
View file @
fe8b625a
...
...
@@ -21,7 +21,7 @@
<!-- Paid Tiers -->
<div
style=
"display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 20px; margin-bottom: 40px;"
>
{% for tier in tiers %}
{% if
tier.is_active and
not tier.is_default %}
{% if not tier.is_default %}
<div
class=
"pricing-card {% if tier.is_recommended %}recommended{% endif %}"
>
{% if tier.is_recommended %}
<div
style=
"background: #4a9eff; color: white; padding: 8px 15px; border-radius: 20px; text-align: center; margin-bottom: 20px; font-weight: bold; text-transform: uppercase; font-size: 12px;"
>
...
...
templates/dashboard/user_index.html
View file @
fe8b625a
...
...
@@ -2,6 +2,21 @@
{% block title %}User Dashboard - AISBF{% endblock %}
{% macro format_tokens(value) %}
{% if value is none or value == 0 %}0{% else %}
{% set val = value | float %}
{% if val >= 1000000000 %}
{{ "%.2f"|format(val / 1000000000) }}B
{% elif val >= 1000000 %}
{{ "%.2f"|format(val / 1000000) }}M
{% elif val >= 1000 %}
{{ "%.2f"|format(val / 1000) }}K
{% else %}
{{ value }}
{% endif %}
{% endif %}
{% endmacro %}
{% block content %}
<div
class=
"container"
>
<h1>
User Dashboard
</h1>
...
...
@@ -98,7 +113,7 @@
<div
class=
"stats-grid"
>
<div
class=
"stat-item"
>
<h3>
Total Tokens Used
</h3>
<p
class=
"stat-value"
>
{{
usage_stats.total_tokens|default(0
) }}
</p>
<p
class=
"stat-value"
>
{{
format_tokens(usage_stats.total_tokens|default(0)
) }}
</p>
</div>
<div
class=
"stat-item"
>
<h3>
Requests Today
</h3>
...
...
templates/dashboard/users.html
View file @
fe8b625a
...
...
@@ -229,23 +229,60 @@ function updateUserTier(userId, tierId) {
.
then
(
response
=>
response
.
json
())
.
then
(
data
=>
{
if
(
data
.
success
)
{
// Show success message briefly
const
msg
=
document
.
createElement
(
'div'
);
msg
.
style
.
cssText
=
'position: fixed; top: 20px; right: 20px; background: #4ade80; color: #000; padding: 15px 20px; border-radius: 5px; z-index: 9999;'
;
msg
.
textContent
=
'Tier updated successfully'
;
document
.
body
.
appendChild
(
msg
);
setTimeout
(()
=>
msg
.
remove
(),
2000
);
// Show success notification
showNotification
(
'Tier updated successfully'
,
'success'
);
}
else
{
alert
(
data
.
error
||
'Failed to update tier'
);
// Show error notification
showNotification
(
data
.
error
||
'Failed to update tier'
,
'error'
);
location
.
reload
();
// Reload to reset dropdown
}
})
.
catch
(
error
=>
{
alert
(
'Error: '
+
error
);
showNotification
(
'Error: '
+
error
,
'error'
);
location
.
reload
();
// Reload to reset dropdown
});
}
function
showNotification
(
message
,
type
)
{
// Remove any existing notifications
const
existingNotifications
=
document
.
querySelectorAll
(
'.notification-toast'
);
existingNotifications
.
forEach
(
notification
=>
notification
.
remove
());
// Create new notification
const
notification
=
document
.
createElement
(
'div'
);
notification
.
className
=
`notification-toast alert alert-
${
type
===
'success'
?
'success'
:
'error'
}
`
;
notification
.
style
.
cssText
=
`
position: fixed;
top: 20px;
right: 20px;
z-index: 9999;
min-width: 300px;
max-width: 500px;
box-shadow: 0 4px 12px rgba(0,0,0,0.5);
border: none;
animation: slideIn 0.3s ease-out;
`
;
notification
.
innerHTML
=
`
<div style="display: flex; align-items: center; gap: 10px;">
<i class="fas fa-
${
type
===
'success'
?
'check-circle'
:
'exclamation-triangle'
}
" style="font-size: 18px;"></i>
<span>
${
message
}
</span>
</div>
`
;
document
.
body
.
appendChild
(
notification
);
// Auto-remove after 3 seconds
setTimeout
(()
=>
{
notification
.
style
.
animation
=
'slideOut 0.3s ease-in'
;
setTimeout
(()
=>
{
if
(
notification
.
parentNode
)
{
notification
.
parentNode
.
removeChild
(
notification
);
}
},
300
);
},
3000
);
}
// Close modal when clicking outside
window
.
onclick
=
function
(
event
)
{
const
modal
=
document
.
getElementById
(
'edit-modal'
);
...
...
@@ -269,6 +306,43 @@ th {
background
:
#0f3460
;
font-weight
:
600
;
}
/* Notification animations */
@keyframes
slideIn
{
from
{
transform
:
translateX
(
100%
);
opacity
:
0
;
}
to
{
transform
:
translateX
(
0
);
opacity
:
1
;
}
}
@keyframes
slideOut
{
from
{
transform
:
translateX
(
0
);
opacity
:
1
;
}
to
{
transform
:
translateX
(
100%
);
opacity
:
0
;
}
}
/* Custom alert styles for dark theme */
.alert-success
{
background-color
:
#10b981
!important
;
color
:
#ffffff
!important
;
border-color
:
#059669
!important
;
}
.alert-error
{
background-color
:
#ef4444
!important
;
color
:
#ffffff
!important
;
border-color
:
#dc2626
!important
;
}
/* Prevent browser autofill from overriding dark theme */
input
:-webkit-autofill
,
input
:-webkit-autofill:hover
,
...
...
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