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
Hide 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
...
@@ -54,7 +54,7 @@ from .auth.qwen import QwenOAuth2
from
.handlers
import
RequestHandler
,
RotationHandler
,
AutoselectHandler
from
.handlers
import
RequestHandler
,
RotationHandler
,
AutoselectHandler
from
.utils
import
count_messages_tokens
,
split_messages_into_chunks
,
get_max_request_tokens_for_model
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__
=
[
__all__
=
[
# Config
# Config
"config"
,
"config"
,
...
...
aisbf/analytics.py
View file @
fe8b625a
...
@@ -121,7 +121,10 @@ class Analytics:
...
@@ -121,7 +121,10 @@ class Analytics:
rotation_id
:
Optional
[
str
]
=
None
,
rotation_id
:
Optional
[
str
]
=
None
,
autoselect_id
:
Optional
[
str
]
=
None
,
autoselect_id
:
Optional
[
str
]
=
None
,
user_id
:
Optional
[
int
]
=
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.
Record a request for analytics.
...
@@ -137,6 +140,9 @@ class Analytics:
...
@@ -137,6 +140,9 @@ class Analytics:
autoselect_id: Optional autoselect identifier if request went through autoselect
autoselect_id: Optional autoselect identifier if request went through autoselect
user_id: Optional user ID if request was made with API token
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
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
# Initialize provider tracking if needed
if
provider_id
not
in
self
.
_request_counts
:
if
provider_id
not
in
self
.
_request_counts
:
...
@@ -161,7 +167,8 @@ class Analytics:
...
@@ -161,7 +167,8 @@ class Analytics:
# Persist to database
# Persist to database
if
tokens_used
>
0
:
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
# Also record user token usage if this was an API token request
if
user_id
is
not
None
and
token_id
is
not
None
:
if
user_id
is
not
None
and
token_id
is
not
None
:
...
@@ -231,16 +238,32 @@ class Analytics:
...
@@ -231,16 +238,32 @@ class Analytics:
placeholder
=
'?'
if
self
.
db
.
db_type
==
'sqlite'
else
'
%
s'
placeholder
=
'?'
if
self
.
db
.
db_type
==
'sqlite'
else
'
%
s'
# Build query with optional user filter
# 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)
params
=
[
provider_id
,
from_datetime
.
isoformat
(),
to_datetime
.
isoformat
()]
# user_filter = None means "all requests"
if
user_filter
is
not
None
:
# user_filter = <id> means "only requests from that user"
params
.
append
(
user_filter
)
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
()]
# Get token usage and actual time range of requests
# Get token usage and actual time range of requests
cursor
.
execute
(
f
'''
cursor
.
execute
(
f
'''
SELECT
SELECT
COUNT(*) as total_requests,
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(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,
MIN(timestamp) as first_request,
MAX(timestamp) as last_request
MAX(timestamp) as last_request
FROM token_usage
FROM token_usage
...
@@ -252,9 +275,17 @@ class Analytics:
...
@@ -252,9 +275,17 @@ class Analytics:
result
=
cursor
.
fetchone
()
result
=
cursor
.
fetchone
()
total_requests
=
result
[
0
]
if
result
else
0
total_requests
=
result
[
0
]
if
result
else
0
total_tokens
=
result
[
1
]
if
result
else
0
success_count
=
result
[
1
]
if
result
else
0
first_request
=
result
[
2
]
if
result
and
result
[
2
]
else
None
error_count
=
result
[
2
]
if
result
else
0
last_request
=
result
[
3
]
if
result
and
result
[
3
]
else
None
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
# Calculate time-based rates using ACTUAL usage window, not query window
# This gives more accurate rate limiting metrics
# This gives more accurate rate limiting metrics
...
@@ -289,15 +320,18 @@ class Analytics:
...
@@ -289,15 +320,18 @@ class Analytics:
return
{
return
{
'provider_id'
:
provider_id
,
'provider_id'
:
provider_id
,
'requests'
:
{
'total'
:
total_requests
,
'success'
:
total_requests
,
'error'
:
0
},
'requests'
:
{
'total'
:
total_requests
,
'success'
:
success_count
,
'error'
:
error_count
},
'latency'
:
{
'count'
:
0
,
'total_ms'
:
0.0
,
'min_ms'
:
0
,
'max_ms'
:
0
},
'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'
:
{},
'errors'
:
{},
# Could be populated from error_type column if needed
'tokens'
:
{
'tokens'
:
{
'total'
:
total_tokens
,
'total'
:
total_tokens
,
'prompt'
:
total_prompt_tokens
,
'completion'
:
total_completion_tokens
,
'TPM'
:
tpm
,
'TPM'
:
tpm
,
'TPH'
:
tph
,
'TPH'
:
tph
,
'TPD'
:
tpd
'TPD'
:
tpd
}
},
'actual_cost'
:
total_actual_cost
# Actual cost from provider responses
}
}
def
get_token_usage_by_date_range
(
def
get_token_usage_by_date_range
(
...
@@ -374,7 +408,7 @@ class Analytics:
...
@@ -374,7 +408,7 @@ class Analytics:
Args:
Args:
from_datetime: Optional start datetime for filtering
from_datetime: Optional start datetime for filtering
to_datetime: Optional end 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:
Returns:
List of provider statistics dictionaries
List of provider statistics dictionaries
...
@@ -388,10 +422,15 @@ class Analytics:
...
@@ -388,10 +422,15 @@ class Analytics:
placeholder
=
'?'
if
self
.
db
.
db_type
==
'sqlite'
else
'
%
s'
placeholder
=
'?'
if
self
.
db
.
db_type
==
'sqlite'
else
'
%
s'
# Build query with optional user filter
# Build query with optional user filter
user_condition
=
f
" AND user_id = {placeholder}"
if
user_filter
is
not
None
else
""
if
user_filter
==
-
1
:
params
=
[
start
.
isoformat
(),
end
.
isoformat
()]
user_condition
=
" AND user_id IS NULL"
if
user_filter
is
not
None
:
params
=
[
start
.
isoformat
(),
end
.
isoformat
()]
params
.
append
(
user_filter
)
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
()]
cursor
.
execute
(
f
'''
cursor
.
execute
(
f
'''
SELECT DISTINCT provider_id
SELECT DISTINCT provider_id
...
@@ -503,7 +542,18 @@ class Analytics:
...
@@ -503,7 +542,18 @@ class Analytics:
time_bucket_expr
=
f
"DATE_FORMAT(timestamp, '{date_format}')"
time_bucket_expr
=
f
"DATE_FORMAT(timestamp, '{date_format}')"
if
provider_id
:
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
'''
cursor
.
execute
(
f
'''
SELECT
SELECT
{time_bucket_expr} as time_bucket,
{time_bucket_expr} as time_bucket,
...
@@ -524,7 +574,19 @@ class Analytics:
...
@@ -524,7 +574,19 @@ class Analytics:
ORDER BY time_bucket
ORDER BY time_bucket
'''
,
(
provider_id
,
cutoff
.
isoformat
(),
end_time
.
isoformat
()))
'''
,
(
provider_id
,
cutoff
.
isoformat
(),
end_time
.
isoformat
()))
else
:
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
'''
cursor
.
execute
(
f
'''
SELECT
SELECT
{time_bucket_expr} as time_bucket,
{time_bucket_expr} as time_bucket,
...
@@ -706,7 +768,8 @@ class Analytics:
...
@@ -706,7 +768,8 @@ class Analytics:
self
,
self
,
provider_id
:
str
,
provider_id
:
str
,
tokens_used
:
int
,
tokens_used
:
int
,
prompt_tokens
:
Optional
[
int
]
=
None
prompt_tokens
:
Optional
[
int
]
=
None
,
completion_tokens
:
Optional
[
int
]
=
None
)
->
float
:
)
->
float
:
"""
"""
Estimate cost for token usage.
Estimate cost for token usage.
...
@@ -715,6 +778,7 @@ class Analytics:
...
@@ -715,6 +778,7 @@ class Analytics:
provider_id: Provider identifier
provider_id: Provider identifier
tokens_used: Total tokens used
tokens_used: Total tokens used
prompt_tokens: Optional breakdown of prompt tokens
prompt_tokens: Optional breakdown of prompt tokens
completion_tokens: Optional breakdown of completion tokens
Returns:
Returns:
Estimated cost in USD
Estimated cost in USD
...
@@ -722,14 +786,19 @@ class Analytics:
...
@@ -722,14 +786,19 @@ class Analytics:
# Get provider-specific pricing (checks subscription status and custom pricing)
# Get provider-specific pricing (checks subscription status and custom pricing)
provider_pricing
=
self
.
_get_provider_pricing
(
provider_id
)
provider_pricing
=
self
.
_get_provider_pricing
(
provider_id
)
# Calculate cost
# Calculate cost with actual token breakdown if available
if
prompt_tokens
is
not
None
:
if
prompt_tokens
is
not
None
and
completion_tokens
is
not
None
:
completion_tokens
=
tokens_used
-
prompt_tokens
prompt_cost
=
(
prompt_tokens
/
1_000_000
)
*
provider_pricing
.
get
(
'prompt'
,
0
)
prompt_cost
=
(
prompt_tokens
/
1_000_000
)
*
provider_pricing
.
get
(
'prompt'
,
0
)
completion_cost
=
(
completion_tokens
/
1_000_000
)
*
provider_pricing
.
get
(
'completion'
,
0
)
completion_cost
=
(
completion_tokens
/
1_000_000
)
*
provider_pricing
.
get
(
'completion'
,
0
)
return
prompt_cost
+
completion_cost
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
:
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
prompt_tokens_est
=
tokens_used
*
0.25
completion_tokens_est
=
tokens_used
*
0.75
completion_tokens_est
=
tokens_used
*
0.75
prompt_cost
=
(
prompt_tokens_est
/
1_000_000
)
*
provider_pricing
.
get
(
'prompt'
,
0
)
prompt_cost
=
(
prompt_tokens_est
/
1_000_000
)
*
provider_pricing
.
get
(
'prompt'
,
0
)
...
@@ -776,13 +845,24 @@ class Analytics:
...
@@ -776,13 +845,24 @@ class Analytics:
tokens
=
provider
[
'tokens'
]
tokens
=
provider
[
'tokens'
]
total_tokens
=
tokens
[
'TPD'
]
# Use daily tokens for cost estimation
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
total_cost
+=
cost
provider_costs
.
append
({
provider_costs
.
append
({
'provider_id'
:
provider_id
,
'provider_id'
:
provider_id
,
'tokens_today'
:
total_tokens
,
'tokens_today'
:
total_tokens
,
'estimated_cost'
:
cost
'estimated_cost'
:
cost
,
'is_actual'
:
actual_cost
>
0
})
})
return
{
return
{
...
...
aisbf/auth/claude.py
View file @
fe8b625a
...
@@ -27,6 +27,7 @@ import hashlib
...
@@ -27,6 +27,7 @@ import hashlib
import
base64
import
base64
import
webbrowser
import
webbrowser
import
time
import
time
import
asyncio
import
httpx
import
httpx
from
pathlib
import
Path
from
pathlib
import
Path
from
typing
import
Optional
,
Dict
from
typing
import
Optional
,
Dict
...
@@ -223,7 +224,7 @@ class ClaudeAuth:
...
@@ -223,7 +224,7 @@ class ClaudeAuth:
)
)
return
response
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.
Use the refresh token to get a new access token without logging in.
...
@@ -270,7 +271,7 @@ class ClaudeAuth:
...
@@ -270,7 +271,7 @@ class ClaudeAuth:
# Rate limited - wait and retry with exponential backoff
# Rate limited - wait and retry with exponential backoff
wait_time
=
(
2
**
attempt
)
*
5
# 5, 10, 20 seconds
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}"
)
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
continue
else
:
else
:
logger
.
error
(
f
"Token refresh failed: {response.status_code} - {response.text}"
)
logger
.
error
(
f
"Token refresh failed: {response.status_code} - {response.text}"
)
...
@@ -280,14 +281,14 @@ class ClaudeAuth:
...
@@ -280,14 +281,14 @@ class ClaudeAuth:
if
attempt
<
max_retries
-
1
:
if
attempt
<
max_retries
-
1
:
wait_time
=
(
2
**
attempt
)
*
5
wait_time
=
(
2
**
attempt
)
*
5
logger
.
info
(
f
"Retrying in {wait_time} seconds..."
)
logger
.
info
(
f
"Retrying in {wait_time} seconds..."
)
time
.
sleep
(
wait_time
)
await
asyncio
.
sleep
(
wait_time
)
continue
continue
return
False
return
False
logger
.
error
(
f
"Token refresh failed after {max_retries} attempts"
)
logger
.
error
(
f
"Token refresh failed after {max_retries} attempts"
)
return
False
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.
Get a valid access token, refreshing it if necessary.
...
@@ -311,7 +312,7 @@ class ClaudeAuth:
...
@@ -311,7 +312,7 @@ class ClaudeAuth:
# Refresh if less than 5 minutes remain
# Refresh if less than 5 minutes remain
if
time
.
time
()
>
(
self
.
tokens
.
get
(
'expires_at'
,
0
)
-
300
):
if
time
.
time
()
>
(
self
.
tokens
.
get
(
'expires_at'
,
0
)
-
300
):
logger
.
info
(
"Token expiring soon, refreshing..."
)
logger
.
info
(
"Token expiring soon, refreshing..."
)
if
not
self
.
refresh_token
():
if
not
await
self
.
refresh_token
():
if
not
auto_login
:
if
not
auto_login
:
logger
.
error
(
"Token refresh failed and auto_login is disabled"
)
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."
)
raise
Exception
(
"Claude token refresh failed. Please re-authenticate via /dashboard/claude/auth/start or MCP tool."
)
...
@@ -540,7 +541,7 @@ class ClaudeAuth:
...
@@ -540,7 +541,7 @@ class ClaudeAuth:
logger
.
info
(
"OAuth2 login flow completed successfully"
)
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.
Exchange authorization code for access tokens.
Matches CLIProxyAPI implementation exactly.
Matches CLIProxyAPI implementation exactly.
...
@@ -621,7 +622,7 @@ class ClaudeAuth:
...
@@ -621,7 +622,7 @@ class ClaudeAuth:
# Rate limited - wait and retry with exponential backoff
# Rate limited - wait and retry with exponential backoff
wait_time
=
(
2
**
attempt
)
*
5
# 5, 10, 20 seconds
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}"
)
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
continue
else
:
else
:
logger
.
error
(
f
"Token exchange failed: {response.status_code} - {response.text}"
)
logger
.
error
(
f
"Token exchange failed: {response.status_code} - {response.text}"
)
...
@@ -631,7 +632,7 @@ class ClaudeAuth:
...
@@ -631,7 +632,7 @@ class ClaudeAuth:
if
attempt
<
max_retries
-
1
:
if
attempt
<
max_retries
-
1
:
wait_time
=
(
2
**
attempt
)
*
5
wait_time
=
(
2
**
attempt
)
*
5
logger
.
info
(
f
"Retrying in {wait_time} seconds..."
)
logger
.
info
(
f
"Retrying in {wait_time} seconds..."
)
time
.
sleep
(
wait_time
)
await
asyncio
.
sleep
(
wait_time
)
continue
continue
return
False
return
False
...
@@ -652,25 +653,29 @@ class ClaudeAuth:
...
@@ -652,25 +653,29 @@ class ClaudeAuth:
# Example usage
# Example usage
if
__name__
==
"__main__"
:
if
__name__
==
"__main__"
:
import
asyncio
logging
.
basicConfig
(
level
=
logging
.
INFO
)
logging
.
basicConfig
(
level
=
logging
.
INFO
)
auth
=
ClaudeAuth
()
async
def
main
():
token
=
auth
.
get_valid_token
()
auth
=
ClaudeAuth
()
token
=
auth
.
get_valid_token
()
# Use the token for an API call
client
=
httpx
.
Client
()
# Use the token for an API call
response
=
client
.
post
(
async
with
httpx
.
AsyncClient
()
as
client
:
"https://api.anthropic.com/v1/messages"
,
response
=
await
client
.
post
(
headers
=
{
"https://api.anthropic.com/v1/messages"
,
"Authorization"
:
f
"Bearer {token}"
,
headers
=
{
"anthropic-version"
:
"2023-06-01"
,
"Authorization"
:
f
"Bearer {token}"
,
"anthropic-beta"
:
"claude-code-20250219"
,
# Required for subscription usage
"anthropic-version"
:
"2023-06-01"
,
"Content-Type"
:
"application/json"
"anthropic-beta"
:
"claude-code-20250219"
,
# Required for subscription usage
},
"Content-Type"
:
"application/json"
json
=
{
},
"model"
:
"claude-3-7-sonnet-20250219"
,
json
=
{
"max_tokens"
:
1024
,
"model"
:
"claude-3-7-sonnet-20250219"
,
"messages"
:
[{
"role"
:
"user"
,
"content"
:
"How's the weather in the CLI today?"
}]
"max_tokens"
:
1024
,
}
"messages"
:
[{
"role"
:
"user"
,
"content"
:
"How's the weather in the CLI today?"
}]
)
}
print
(
response
.
json
())
)
print
(
response
.
json
())
asyncio
.
run
(
main
())
aisbf/auth/qwen.py
View file @
fe8b625a
...
@@ -152,16 +152,17 @@ class QwenOAuth2:
...
@@ -152,16 +152,17 @@ class QwenOAuth2:
return
code_verifier
,
code_challenge
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.
Acquire a file lock to prevent concurrent token refreshes.
Returns:
Returns:
True if lock acquired, False otherwise.
True if lock acquired, False otherwise.
"""
"""
import
asyncio
lock_id
=
str
(
uuid
.
uuid4
())
lock_id
=
str
(
uuid
.
uuid4
())
interval
=
0.1
interval
=
0.1
for
_
in
range
(
max_attempts
):
for
_
in
range
(
max_attempts
):
try
:
try
:
# Try to create lock file atomically (exclusive mode)
# Try to create lock file atomically (exclusive mode)
...
@@ -173,7 +174,7 @@ class QwenOAuth2:
...
@@ -173,7 +174,7 @@ class QwenOAuth2:
try
:
try
:
stat
=
os
.
stat
(
self
.
lock_file
)
stat
=
os
.
stat
(
self
.
lock_file
)
lock_age
=
time
.
time
()
-
stat
.
st_mtime
lock_age
=
time
.
time
()
-
stat
.
st_mtime
if
lock_age
>
LOCK_TIMEOUT_MS
/
1000
:
if
lock_age
>
LOCK_TIMEOUT_MS
/
1000
:
# Remove stale lock
# Remove stale lock
os
.
unlink
(
self
.
lock_file
)
os
.
unlink
(
self
.
lock_file
)
...
@@ -181,10 +182,10 @@ class QwenOAuth2:
...
@@ -181,10 +182,10 @@ class QwenOAuth2:
except
(
OSError
,
FileNotFoundError
):
except
(
OSError
,
FileNotFoundError
):
# Lock might have been removed by another process
# Lock might have been removed by another process
continue
continue
time
.
sleep
(
interval
)
await
asyncio
.
sleep
(
interval
)
interval
=
min
(
interval
*
1.5
,
2.0
)
# Exponential backoff
interval
=
min
(
interval
*
1.5
,
2.0
)
# Exponential backoff
return
False
return
False
def
_release_lock
(
self
)
->
None
:
def
_release_lock
(
self
)
->
None
:
...
@@ -461,7 +462,7 @@ class QwenOAuth2:
...
@@ -461,7 +462,7 @@ class QwenOAuth2:
logger
.
info
(
"QwenOAuth2: Refreshing access token..."
)
logger
.
info
(
"QwenOAuth2: Refreshing access token..."
)
# Acquire lock to prevent concurrent refreshes
# 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"
)
logger
.
error
(
"QwenOAuth2: Failed to acquire lock for token refresh"
)
return
False
return
False
...
...
aisbf/context.py
View file @
fe8b625a
...
@@ -534,7 +534,7 @@ class ContextManager:
...
@@ -534,7 +534,7 @@ class ContextManager:
"max_tokens"
:
1000
,
"max_tokens"
:
1000
,
"stream"
:
False
"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
):
if
isinstance
(
response
,
dict
):
summary_content
=
response
.
get
(
'choices'
,
[{}])[
0
]
.
get
(
'message'
,
{})
.
get
(
'content'
,
''
)
summary_content
=
response
.
get
(
'choices'
,
[{}])[
0
]
.
get
(
'message'
,
{})
.
get
(
'content'
,
''
)
else
:
else
:
...
@@ -642,7 +642,7 @@ Provide only the relevant information in a concise format."""
...
@@ -642,7 +642,7 @@ Provide only the relevant information in a concise format."""
"max_tokens"
:
2000
,
"max_tokens"
:
2000
,
"stream"
:
False
"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
):
if
isinstance
(
response
,
dict
):
pruned_content
=
response
.
get
(
'choices'
,
[{}])[
0
]
.
get
(
'message'
,
{})
.
get
(
'content'
,
''
)
pruned_content
=
response
.
get
(
'choices'
,
[{}])[
0
]
.
get
(
'message'
,
{})
.
get
(
'content'
,
''
)
else
:
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
...
@@ -27,6 +27,8 @@ from pathlib import Path
from
typing
import
Dict
,
List
,
Optional
,
Tuple
,
Any
from
typing
import
Dict
,
List
,
Optional
,
Tuple
,
Any
from
datetime
import
datetime
,
timedelta
from
datetime
import
datetime
,
timedelta
import
logging
import
logging
import
asyncio
from
concurrent.futures
import
ThreadPoolExecutor
try
:
try
:
import
mysql.connector
as
_mysql_connector
import
mysql.connector
as
_mysql_connector
...
@@ -37,12 +39,23 @@ except ImportError:
...
@@ -37,12 +39,23 @@ except ImportError:
logger
=
logging
.
getLogger
(
__name__
)
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
:
class
DatabaseManager
:
"""
"""
Manages database for persistent tracking of context dimensions and rate limiting.
Manages database for persistent tracking of context dimensions and rate limiting.
Supports both SQLite and MySQL databases.
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
):
def
__init__
(
self
,
db_config
:
Optional
[
Dict
[
str
,
Any
]]
=
None
):
...
@@ -69,7 +82,7 @@ class DatabaseManager:
...
@@ -69,7 +82,7 @@ class DatabaseManager:
self
.
db_config
=
db_config
self
.
db_config
=
db_config
self
.
db_type
=
self
.
db_config
.
get
(
'type'
,
'sqlite'
)
.
lower
()
self
.
db_type
=
self
.
db_config
.
get
(
'type'
,
'sqlite'
)
.
lower
()
self
.
executor
=
get_db_executor
()
if
self
.
db_type
==
'mysql'
:
if
self
.
db_type
==
'mysql'
:
if
not
MYSQL_AVAILABLE
:
if
not
MYSQL_AVAILABLE
:
...
@@ -98,8 +111,59 @@ class DatabaseManager:
...
@@ -98,8 +111,59 @@ class DatabaseManager:
raise
raise
else
:
else
:
raise
ValueError
(
f
"Unsupported database type: {self.db_type}"
)
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
(
def
record_context_dimension
(
self
,
self
,
...
@@ -217,27 +281,44 @@ class DatabaseManager:
...
@@ -217,27 +281,44 @@ class DatabaseManager:
provider_id
:
str
,
provider_id
:
str
,
model_name
:
str
,
model_name
:
str
,
tokens_used
:
int
,
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:
Args:
provider_id: The provider identifier
provider_id: The provider identifier
model_name: The model name
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
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
:
with
self
.
_get_connection
()
as
conn
:
cursor
=
conn
.
cursor
()
cursor
=
conn
.
cursor
()
placeholder
=
'?'
if
self
.
db_type
==
'sqlite'
else
'
%
s'
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
'''
cursor
.
execute
(
f
'''
INSERT INTO token_usage (user_id, provider_id, model_name, tokens_used, timestamp)
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}, CURRENT_TIMESTAMP)
VALUES ({placeholder}, {placeholder}, {placeholder}, {placeholder},
{placeholder}, {placeholder}, {placeholder}, {placeholder}, {placeholder}, {placeholder}, {placeholder},
CURRENT_TIMESTAMP)
'''
,
(
user_id
,
provider_id
,
model_name
,
tokens_used
))
'''
,
(
user_id
,
provider_id
,
model_name
,
tokens_used
,
prompt_tokens
,
completion_tokens
,
actual_cost
,
success
,
latency_int
,
error_type
,
token_id
))
conn
.
commit
()
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
(
def
get_token_usage
(
self
,
self
,
...
@@ -2849,20 +2930,20 @@ def DatabaseManager__initialize_database(self):
...
@@ -2849,20 +2930,20 @@ def DatabaseManager__initialize_database(self):
if
self
.
database_type
==
DatabaseRegistry
.
TYPE_CONFIG
:
if
self
.
database_type
==
DatabaseRegistry
.
TYPE_CONFIG
:
# ONLY CREATE CONFIG TABLES IN CONFIG DATABASE
# ONLY CREATE CONFIG TABLES IN CONFIG DATABASE
# Create context_dimensions table for tracking context usage
# Create context_dimensions table for tracking context usage
cursor
.
execute
(
f
'''
#
cursor.execute(f'''
CREATE TABLE IF NOT EXISTS context_dimensions (
#
CREATE TABLE IF NOT EXISTS context_dimensions (
id INTEGER PRIMARY KEY {auto_increment},
#
id INTEGER PRIMARY KEY {auto_increment},
provider_id VARCHAR(255) NOT NULL,
#
provider_id VARCHAR(255) NOT NULL,
model_name VARCHAR(255) NOT NULL,
#
model_name VARCHAR(255) NOT NULL,
context_size INTEGER,
#
context_size INTEGER,
condense_context INTEGER,
#
condense_context INTEGER,
condense_method TEXT,
#
condense_method TEXT,
effective_context INTEGER DEFAULT 0,
#
effective_context INTEGER DEFAULT 0,
last_updated TIMESTAMP DEFAULT {timestamp_default},
#
last_updated TIMESTAMP DEFAULT {timestamp_default},
UNIQUE(provider_id, model_name)
#
UNIQUE(provider_id, model_name)
)
#
)
'''
)
#
''')
#
# Create token_usage table for tracking rate limiting
# Create token_usage table for tracking rate limiting
cursor
.
execute
(
f
'''
cursor
.
execute
(
f
'''
CREATE TABLE IF NOT EXISTS token_usage (
CREATE TABLE IF NOT EXISTS token_usage (
...
@@ -2871,714 +2952,513 @@ def DatabaseManager__initialize_database(self):
...
@@ -2871,714 +2952,513 @@ def DatabaseManager__initialize_database(self):
provider_id VARCHAR(255) NOT NULL,
provider_id VARCHAR(255) NOT NULL,
model_name VARCHAR(255) NOT NULL,
model_name VARCHAR(255) NOT NULL,
tokens_used INTEGER NOT NULL,
tokens_used INTEGER NOT NULL,
prompt_tokens INTEGER,
completion_tokens INTEGER,
actual_cost DECIMAL(10,6),
timestamp TIMESTAMP DEFAULT {timestamp_default}
timestamp TIMESTAMP DEFAULT {timestamp_default}
)
)
'''
)
'''
)
#
#
#
# Create indexes for better query performance
# Create indexes for better query performance
try
:
# try:
cursor
.
execute
(
'''
# cursor.execute('''
CREATE INDEX IF NOT EXISTS idx_context_provider_model
# CREATE INDEX IF NOT EXISTS idx_context_provider_model
ON context_dimensions(provider_id, model_name)
# ON context_dimensions(provider_id, model_name)
'''
)
# ''')
except
:
# except:
pass
# Index might already exist
# pass # Index might already exist
#
try
:
# try:
cursor
.
execute
(
'''
# cursor.execute('''
CREATE INDEX IF NOT EXISTS idx_token_provider_model
# CREATE INDEX IF NOT EXISTS idx_token_provider_model
ON token_usage(provider_id, model_name)
# ON token_usage(provider_id, model_name)
'''
)
# ''')
except
:
# except:
pass
# pass
#
try
:
# try:
cursor
.
execute
(
'''
# cursor.execute('''
CREATE INDEX IF NOT EXISTS idx_token_timestamp
# CREATE INDEX IF NOT EXISTS idx_token_timestamp
ON token_usage(timestamp)
# ON token_usage(timestamp)
'''
)
# ''')
except
:
# except:
pass
# pass
#
# Create model_embeddings table for caching vectorized model descriptions
# Create model_embeddings table for caching vectorized model descriptions
cursor
.
execute
(
f
'''
# cursor.execute(f'''
CREATE TABLE IF NOT EXISTS model_embeddings (
# CREATE TABLE IF NOT EXISTS model_embeddings (
id INTEGER PRIMARY KEY {auto_increment},
# id INTEGER PRIMARY KEY {auto_increment},
provider_id VARCHAR(255) NOT NULL,
# provider_id VARCHAR(255) NOT NULL,
model_name VARCHAR(255) NOT NULL,
# model_name VARCHAR(255) NOT NULL,
description TEXT,
# description TEXT,
embedding TEXT,
# embedding TEXT,
last_updated TIMESTAMP DEFAULT {timestamp_default},
# last_updated TIMESTAMP DEFAULT {timestamp_default},
UNIQUE(provider_id, model_name)
# UNIQUE(provider_id, model_name)
)
# )
'''
)
# ''')
#
try
:
# try:
cursor
.
execute
(
'''
# cursor.execute('''
CREATE INDEX IF NOT EXISTS idx_model_embeddings_provider_model
# CREATE INDEX IF NOT EXISTS idx_model_embeddings_provider_model
ON model_embeddings(provider_id, model_name)
# ON model_embeddings(provider_id, model_name)
'''
)
# ''')
except
:
# except:
pass
# pass
#
# Create users table for multi-user management
# Create users table for multi-user management
cursor
.
execute
(
f
'''
# cursor.execute(f'''
CREATE TABLE IF NOT EXISTS users (
# CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY {auto_increment},
# id INTEGER PRIMARY KEY {auto_increment},
username VARCHAR(255) UNIQUE NOT NULL,
# username VARCHAR(255) UNIQUE NOT NULL,
email VARCHAR(255) UNIQUE,
# email VARCHAR(255) UNIQUE,
display_name VARCHAR(255),
# display_name VARCHAR(255),
password_hash VARCHAR(255) NOT NULL,
# password_hash VARCHAR(255) NOT NULL,
role VARCHAR(50) DEFAULT 'user',
# role VARCHAR(50) DEFAULT 'user',
created_by VARCHAR(255),
# created_by VARCHAR(255),
created_at TIMESTAMP DEFAULT {timestamp_default},
# created_at TIMESTAMP DEFAULT {timestamp_default},
last_login TIMESTAMP NULL,
# last_login TIMESTAMP NULL,
is_active {boolean_type} DEFAULT 1,
# is_active {boolean_type} DEFAULT 1,
email_verified {boolean_type} DEFAULT 0,
# email_verified {boolean_type} DEFAULT 0,
verification_token VARCHAR(255),
# verification_token VARCHAR(255),
verification_token_expires TIMESTAMP NULL,
# verification_token_expires TIMESTAMP NULL,
last_verification_email_sent TIMESTAMP NULL
# last_verification_email_sent TIMESTAMP NULL
)
# )
'''
)
# ''')
#
# Migration: Add display_name column if it doesn't exist
# Migration: Add display_name column if it doesn't exist
try
:
# try:
# Check if display_name column exists
# Check if display_name column exists
if
self
.
db_type
==
'sqlite'
:
# if self.db_type == 'sqlite':
cursor
.
execute
(
"PRAGMA table_info(users)"
)
# cursor.execute("PRAGMA table_info(users)")
columns
=
[
row
[
1
]
for
row
in
cursor
.
fetchall
()]
# columns = [row[1] for row in cursor.fetchall()]
else
:
# else:
cursor
.
execute
(
"""
# cursor.execute("""
SELECT COLUMN_NAME
# SELECT COLUMN_NAME
FROM INFORMATION_SCHEMA.COLUMNS
# FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_NAME = 'users'
# WHERE TABLE_NAME = 'users'
"""
)
# """)
columns
=
[
row
[
0
]
for
row
in
cursor
.
fetchall
()]
# columns = [row[0] for row in cursor.fetchall()]
#
if
'display_name'
not
in
columns
:
# if 'display_name' not in columns:
logger
.
info
(
"Adding display_name column to users table"
)
# logger.info("Adding display_name column to users table")
cursor
.
execute
(
"ALTER TABLE users ADD COLUMN display_name VARCHAR(255)"
)
# cursor.execute("ALTER TABLE users ADD COLUMN display_name VARCHAR(255)")
conn
.
commit
()
# conn.commit()
#
# Populate display_name for existing users
# Populate display_name for existing users
cursor
.
execute
(
"UPDATE users SET display_name = username WHERE display_name IS NULL"
)
# cursor.execute("UPDATE users SET display_name = username WHERE display_name IS NULL")
conn
.
commit
()
# conn.commit()
logger
.
info
(
"Migration complete: display_name column added and populated"
)
# logger.info("Migration complete: display_name column added and populated")
except
Exception
as
e
:
# except Exception as e:
logger
.
warning
(
f
"Migration warning (display_name): {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 - commented out to fix import
# User-specific configuration tables for multi-user isolation
# cursor.execute(f'''
cursor
.
execute
(
f
'''
# CREATE TABLE IF NOT EXISTS user_providers (
CREATE TABLE IF NOT EXISTS user_providers (
# id INTEGER PRIMARY KEY {auto_increment},
id INTEGER PRIMARY KEY {auto_increment},
# user_id INTEGER NOT NULL,
user_id INTEGER NOT NULL,
# provider_id VARCHAR(255) NOT NULL,
provider_id VARCHAR(255) NOT NULL,
# config TEXT NOT NULL,
config TEXT NOT NULL,
# created_at TIMESTAMP DEFAULT {timestamp_default},
created_at TIMESTAMP DEFAULT {timestamp_default},
# updated_at TIMESTAMP DEFAULT {timestamp_default},
updated_at TIMESTAMP DEFAULT {timestamp_default},
# FOREIGN KEY (user_id) REFERENCES users(id),
FOREIGN KEY (user_id) REFERENCES users(id),
# UNIQUE(user_id, provider_id)
UNIQUE(user_id, provider_id)
# )
)
# ''')
'''
)
#
# cursor.execute(f'''
cursor
.
execute
(
f
'''
# CREATE TABLE IF NOT EXISTS user_rotations (
CREATE TABLE IF NOT EXISTS user_rotations (
# id INTEGER PRIMARY KEY {auto_increment},
id INTEGER PRIMARY KEY {auto_increment},
# user_id INTEGER NOT NULL,
user_id INTEGER NOT NULL,
# rotation_id VARCHAR(255) NOT NULL,
rotation_id VARCHAR(255) NOT NULL,
# config TEXT NOT NULL,
config TEXT NOT NULL,
# created_at TIMESTAMP DEFAULT {timestamp_default},
created_at TIMESTAMP DEFAULT {timestamp_default},
# updated_at TIMESTAMP DEFAULT {timestamp_default},
updated_at TIMESTAMP DEFAULT {timestamp_default},
# FOREIGN KEY (user_id) REFERENCES users(id),
FOREIGN KEY (user_id) REFERENCES users(id),
# UNIQUE(user_id, rotation_id)
UNIQUE(user_id, rotation_id)
# )
)
# ''')
'''
)
#
# cursor.execute(f'''
cursor
.
execute
(
f
'''
# CREATE TABLE IF NOT EXISTS user_autoselects (
CREATE TABLE IF NOT EXISTS user_autoselects (
# id INTEGER PRIMARY KEY {auto_increment},
id INTEGER PRIMARY KEY {auto_increment},
# user_id INTEGER NOT NULL,
user_id INTEGER NOT NULL,
# autoselect_id VARCHAR(255) NOT NULL,
autoselect_id VARCHAR(255) NOT NULL,
# config TEXT NOT NULL,
config TEXT NOT NULL,
# created_at TIMESTAMP DEFAULT {timestamp_default},
created_at TIMESTAMP DEFAULT {timestamp_default},
# updated_at TIMESTAMP DEFAULT {timestamp_default},
updated_at TIMESTAMP DEFAULT {timestamp_default},
# FOREIGN KEY (user_id) REFERENCES users(id),
FOREIGN KEY (user_id) REFERENCES users(id),
# UNIQUE(user_id, autoselect_id)
UNIQUE(user_id, autoselect_id)
# )
)
# ''')
'''
)
#
# cursor.execute(f'''
cursor
.
execute
(
f
'''
# CREATE TABLE IF NOT EXISTS user_prompts (
CREATE TABLE IF NOT EXISTS user_prompts (
# id INTEGER PRIMARY KEY {auto_increment},
id INTEGER PRIMARY KEY {auto_increment},
# user_id INTEGER NOT NULL,
user_id INTEGER NOT NULL,
# prompt_key VARCHAR(255) NOT NULL,
prompt_key VARCHAR(255) NOT NULL,
# content TEXT NOT NULL,
content TEXT NOT NULL,
# created_at TIMESTAMP DEFAULT {timestamp_default},
created_at TIMESTAMP DEFAULT {timestamp_default},
# updated_at TIMESTAMP DEFAULT {timestamp_default},
updated_at TIMESTAMP DEFAULT {timestamp_default},
# FOREIGN KEY (user_id) REFERENCES users(id),
FOREIGN KEY (user_id) REFERENCES users(id),
# UNIQUE(user_id, prompt_key)
UNIQUE(user_id, prompt_key)
# )
)
# ''')
'''
)
#
# cursor.execute(f'''
cursor
.
execute
(
f
'''
# CREATE TABLE IF NOT EXISTS user_api_tokens (
CREATE TABLE IF NOT EXISTS user_api_tokens (
# id INTEGER PRIMARY KEY {auto_increment},
id INTEGER PRIMARY KEY {auto_increment},
# user_id INTEGER NOT NULL,
user_id INTEGER NOT NULL,
# token VARCHAR(255) UNIQUE NOT NULL,
token VARCHAR(255) UNIQUE NOT NULL,
# description TEXT,
description TEXT,
# created_at TIMESTAMP DEFAULT {timestamp_default},
created_at TIMESTAMP DEFAULT {timestamp_default},
# last_used TIMESTAMP NULL,
last_used TIMESTAMP NULL,
# is_active {boolean_type} DEFAULT 1,
is_active {boolean_type} DEFAULT 1,
# FOREIGN KEY (user_id) REFERENCES users(id)
FOREIGN KEY (user_id) REFERENCES users(id)
# )
)
# ''')
'''
)
#
# cursor.execute(f'''
cursor
.
execute
(
f
'''
# CREATE TABLE IF NOT EXISTS user_token_usage (
CREATE TABLE IF NOT EXISTS user_token_usage (
# id INTEGER PRIMARY KEY {auto_increment},
id INTEGER PRIMARY KEY {auto_increment},
# user_id INTEGER NOT NULL,
user_id INTEGER NOT NULL,
# token_id INTEGER,
token_id INTEGER,
# provider_id VARCHAR(255) NOT NULL,
provider_id VARCHAR(255) NOT NULL,
# model_name VARCHAR(255) NOT NULL,
model_name VARCHAR(255) NOT NULL,
# tokens_used INTEGER NOT NULL,
tokens_used INTEGER NOT NULL,
# timestamp TIMESTAMP DEFAULT {timestamp_default},
timestamp TIMESTAMP DEFAULT {timestamp_default},
# FOREIGN KEY (user_id) REFERENCES users(id),
FOREIGN KEY (user_id) REFERENCES users(id),
# FOREIGN KEY (token_id) REFERENCES user_api_tokens(id)
FOREIGN KEY (token_id) REFERENCES user_api_tokens(id)
# )
)
# ''')
'''
)
#
# Create user_auth_files table for storing authentication file metadata
# Create user_auth_files table for storing authentication file metadata
cursor
.
execute
(
f
'''
#
cursor.execute(f'''
CREATE TABLE IF NOT EXISTS user_auth_files (
#
CREATE TABLE IF NOT EXISTS user_auth_files (
id INTEGER PRIMARY KEY {auto_increment},
#
id INTEGER PRIMARY KEY {auto_increment},
user_id INTEGER NOT NULL,
#
user_id INTEGER NOT NULL,
provider_id VARCHAR(255) NOT NULL,
#
provider_id VARCHAR(255) NOT NULL,
file_type VARCHAR(50) NOT NULL,
#
file_type VARCHAR(50) NOT NULL,
original_filename VARCHAR(255) NOT NULL,
#
original_filename VARCHAR(255) NOT NULL,
stored_filename VARCHAR(255) NOT NULL,
#
stored_filename VARCHAR(255) NOT NULL,
file_path TEXT NOT NULL,
#
file_path TEXT NOT NULL,
file_size INTEGER,
#
file_size INTEGER,
mime_type VARCHAR(100),
#
mime_type VARCHAR(100),
created_at TIMESTAMP DEFAULT {timestamp_default},
#
created_at TIMESTAMP DEFAULT {timestamp_default},
updated_at TIMESTAMP DEFAULT {timestamp_default},
#
updated_at TIMESTAMP DEFAULT {timestamp_default},
FOREIGN KEY (user_id) REFERENCES users(id),
#
FOREIGN KEY (user_id) REFERENCES users(id),
UNIQUE(user_id, provider_id, file_type)
#
UNIQUE(user_id, provider_id, file_type)
)
#
)
'''
)
#
''')
#
# Create user_oauth2_credentials table for storing OAuth2 tokens per user/provider
# Create user_oauth2_credentials table for storing OAuth2 tokens per user/provider
cursor
.
execute
(
f
'''
#
cursor.execute(f'''
CREATE TABLE IF NOT EXISTS user_oauth2_credentials (
#
CREATE TABLE IF NOT EXISTS user_oauth2_credentials (
id INTEGER PRIMARY KEY {auto_increment},
#
id INTEGER PRIMARY KEY {auto_increment},
user_id INTEGER NOT NULL,
#
user_id INTEGER NOT NULL,
provider_id VARCHAR(255) NOT NULL,
#
provider_id VARCHAR(255) NOT NULL,
auth_type VARCHAR(50) NOT NULL,
#
auth_type VARCHAR(50) NOT NULL,
credentials TEXT NOT NULL,
#
credentials TEXT NOT NULL,
created_at TIMESTAMP DEFAULT {timestamp_default},
#
created_at TIMESTAMP DEFAULT {timestamp_default},
updated_at TIMESTAMP DEFAULT {timestamp_default},
#
updated_at TIMESTAMP DEFAULT {timestamp_default},
FOREIGN KEY (user_id) REFERENCES users(id),
#
FOREIGN KEY (user_id) REFERENCES users(id),
UNIQUE(user_id, provider_id, auth_type)
#
UNIQUE(user_id, provider_id, auth_type)
)
#
)
'''
)
#
''')
#
# ==============================================
# ==============================================
# UNIVERSAL MIGRATIONS - RUN ON EVERY STARTUP
# UNIVERSAL MIGRATIONS - RUN ON EVERY STARTUP
# ==============================================
# ==============================================
logger
.
info
(
"Running database migrations..."
)
#
logger.info("Running database migrations...")
#
# Migration: Create account_tiers table if missing
# Migration: Create account_tiers table if missing
try
:
#
try:
if
self
.
db_type
==
'sqlite'
:
#
if self.db_type == 'sqlite':
cursor
.
execute
(
"PRAGMA table_info(account_tiers)"
)
#
cursor.execute("PRAGMA table_info(account_tiers)")
if
not
cursor
.
fetchall
():
#
if not cursor.fetchall():
cursor
.
execute
(
f
'''
#
cursor.execute(f'''
CREATE TABLE account_tiers (
#
CREATE TABLE account_tiers (
id INTEGER PRIMARY KEY {auto_increment},
#
id INTEGER PRIMARY KEY {auto_increment},
name VARCHAR(255) UNIQUE NOT NULL,
#
name VARCHAR(255) UNIQUE NOT NULL,
description TEXT,
#
description TEXT,
price_monthly DECIMAL(10,2) DEFAULT 0.00,
#
price_monthly DECIMAL(10,2) DEFAULT 0.00,
price_yearly DECIMAL(10,2) DEFAULT 0.00,
#
price_yearly DECIMAL(10,2) DEFAULT 0.00,
is_default {boolean_type} DEFAULT 0,
#
is_default {boolean_type} DEFAULT 0,
is_active {boolean_type} DEFAULT 1,
#
is_active {boolean_type} DEFAULT 1,
max_requests_per_day INTEGER DEFAULT -1,
#
max_requests_per_day INTEGER DEFAULT -1,
max_requests_per_month INTEGER DEFAULT -1,
#
max_requests_per_month INTEGER DEFAULT -1,
max_providers INTEGER DEFAULT -1,
#
max_providers INTEGER DEFAULT -1,
max_rotations INTEGER DEFAULT -1,
#
max_rotations INTEGER DEFAULT -1,
max_autoselections INTEGER DEFAULT -1,
#
max_autoselections INTEGER DEFAULT -1,
max_rotation_models INTEGER DEFAULT -1,
#
max_rotation_models INTEGER DEFAULT -1,
max_autoselection_models INTEGER DEFAULT -1,
#
max_autoselection_models INTEGER DEFAULT -1,
created_at TIMESTAMP DEFAULT {timestamp_default},
#
created_at TIMESTAMP DEFAULT {timestamp_default},
updated_at TIMESTAMP DEFAULT {timestamp_default}
#
updated_at TIMESTAMP DEFAULT {timestamp_default}
)
#
)
'''
)
#
''')
conn
.
commit
()
#
conn.commit()
logger
.
info
(
"✅ Migration: Created missing account_tiers table"
)
#
logger.info("✅ Migration: Created missing account_tiers table")
except
Exception
as
e
:
#
except Exception as e:
logger
.
warning
(
f
"Migration check for account_tiers table: {e}"
)
#
logger.warning(f"Migration check for account_tiers table: {e}")
#
# Migration: Add missing columns to account_tiers
# Migration: Add missing columns to account_tiers
try
:
#
try:
if
self
.
db_type
==
'sqlite'
:
#
if self.db_type == 'sqlite':
cursor
.
execute
(
"PRAGMA table_info(account_tiers)"
)
#
cursor.execute("PRAGMA table_info(account_tiers)")
existing_columns
=
[
row
[
1
]
for
row
in
cursor
.
fetchall
()]
#
existing_columns = [row[1] for row in cursor.fetchall()]
tier_columns
=
[
#
tier_columns = [
(
'max_requests_per_day'
,
'INTEGER DEFAULT -1'
),
#
('max_requests_per_day', 'INTEGER DEFAULT -1'),
(
'max_requests_per_month'
,
'INTEGER DEFAULT -1'
),
#
('max_requests_per_month', 'INTEGER DEFAULT -1'),
(
'max_providers'
,
'INTEGER DEFAULT -1'
),
#
('max_providers', 'INTEGER DEFAULT -1'),
(
'max_rotations'
,
'INTEGER DEFAULT -1'
),
#
('max_rotations', 'INTEGER DEFAULT -1'),
(
'max_autoselections'
,
'INTEGER DEFAULT -1'
),
#
('max_autoselections', 'INTEGER DEFAULT -1'),
(
'max_rotation_models'
,
'INTEGER DEFAULT -1'
),
#
('max_rotation_models', 'INTEGER DEFAULT -1'),
(
'max_autoselection_models'
,
'INTEGER DEFAULT -1'
),
#
('max_autoselection_models', 'INTEGER DEFAULT -1'),
(
'is_default'
,
f
'{boolean_type} DEFAULT 0'
),
#
('is_default', f'{boolean_type} DEFAULT 0'),
(
'is_active'
,
f
'{boolean_type} DEFAULT 1'
),
#
('is_active', f'{boolean_type} DEFAULT 1'),
(
'is_visible'
,
f
'{boolean_type} DEFAULT 1'
)
#
('is_visible', f'{boolean_type} DEFAULT 1')
]
#
]
col_count
=
0
#
col_count = 0
for
col_name
,
col_def
in
tier_columns
:
#
for col_name, col_def in tier_columns:
if
col_name
not
in
existing_columns
:
#
if col_name not in existing_columns:
cursor
.
execute
(
f
'ALTER TABLE account_tiers ADD COLUMN {col_name} {col_def}'
)
#
cursor.execute(f'ALTER TABLE account_tiers ADD COLUMN {col_name} {col_def}')
col_count
+=
1
#
col_count += 1
if
col_count
>
0
:
#
if col_count > 0:
logger
.
info
(
f
"✅ Migration: Added {col_count} missing columns to account_tiers"
)
#
logger.info(f"✅ Migration: Added {col_count} missing columns to account_tiers")
else
:
#
else:
# MySQL/MariaDB
# MySQL/MariaDB
cursor
.
execute
(
"""
#
cursor.execute("""
SELECT COLUMN_NAME
#
SELECT COLUMN_NAME
FROM INFORMATION_SCHEMA.COLUMNS
#
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_NAME = 'account_tiers'
#
WHERE TABLE_NAME = 'account_tiers'
AND TABLE_SCHEMA = DATABASE()
#
AND TABLE_SCHEMA = DATABASE()
"""
)
#
""")
existing_columns
=
[
row
[
0
]
for
row
in
cursor
.
fetchall
()]
#
existing_columns = [row[0] for row in cursor.fetchall()]
tier_columns
=
[
#
tier_columns = [
(
'max_requests_per_day'
,
'INTEGER DEFAULT -1'
),
#
('max_requests_per_day', 'INTEGER DEFAULT -1'),
(
'max_requests_per_month'
,
'INTEGER DEFAULT -1'
),
#
('max_requests_per_month', 'INTEGER DEFAULT -1'),
(
'max_providers'
,
'INTEGER DEFAULT -1'
),
#
('max_providers', 'INTEGER DEFAULT -1'),
(
'max_rotations'
,
'INTEGER DEFAULT -1'
),
#
('max_rotations', 'INTEGER DEFAULT -1'),
(
'max_autoselections'
,
'INTEGER DEFAULT -1'
),
#
('max_autoselections', 'INTEGER DEFAULT -1'),
(
'max_rotation_models'
,
'INTEGER DEFAULT -1'
),
#
('max_rotation_models', 'INTEGER DEFAULT -1'),
(
'max_autoselection_models'
,
'INTEGER DEFAULT -1'
),
#
('max_autoselection_models', 'INTEGER DEFAULT -1'),
(
'is_default'
,
f
'{boolean_type} DEFAULT 0'
),
#
('is_default', f'{boolean_type} DEFAULT 0'),
(
'is_active'
,
f
'{boolean_type} DEFAULT 1'
),
#
('is_active', f'{boolean_type} DEFAULT 1'),
(
'is_visible'
,
f
'{boolean_type} DEFAULT 1'
)
#
('is_visible', f'{boolean_type} DEFAULT 1')
]
#
]
col_count
=
0
#
col_count = 0
for
col_name
,
col_def
in
tier_columns
:
#
for col_name, col_def in tier_columns:
if
col_name
not
in
existing_columns
:
#
if col_name not in existing_columns:
cursor
.
execute
(
f
'ALTER TABLE account_tiers ADD COLUMN {col_name} {col_def}'
)
#
cursor.execute(f'ALTER TABLE account_tiers ADD COLUMN {col_name} {col_def}')
col_count
+=
1
#
col_count += 1
if
col_count
>
0
:
#
if col_count > 0:
conn
.
commit
()
#
conn.commit()
logger
.
info
(
f
"✅ Migration: Added {col_count} missing columns to account_tiers"
)
#
logger.info(f"✅ Migration: Added {col_count} missing columns to account_tiers")
except
Exception
as
e
:
#
except Exception as e:
logger
.
warning
(
f
"Migration check for account_tiers columns: {e}"
)
#
logger.warning(f"Migration check for account_tiers columns: {e}")
#
# Migration: Ensure default free tier exists
# Migration: Ensure default free tier exists
try
:
#
try:
cursor
.
execute
(
f
'SELECT COUNT(*) FROM account_tiers WHERE is_default = 1'
)
#
cursor.execute(f'SELECT COUNT(*) FROM account_tiers WHERE is_default = 1')
free_tier_count
=
cursor
.
fetchone
()[
0
]
#
free_tier_count = cursor.fetchone()[0]
if
free_tier_count
==
0
:
#
if free_tier_count == 0:
cursor
.
execute
(
f
'''
#
cursor.execute(f'''
INSERT INTO account_tiers
#
INSERT INTO account_tiers
(name, description, price_monthly, price_yearly, is_default, is_active,
#
(name, description, price_monthly, price_yearly, is_default, is_active,
max_requests_per_day, max_requests_per_month, max_providers, max_rotations,
#
max_requests_per_day, max_requests_per_month, max_providers, max_rotations,
max_autoselections, max_rotation_models, max_autoselection_models)
#
max_autoselections, max_rotation_models, max_autoselection_models)
VALUES
#
VALUES
('Free Tier', 'Default free account tier with unlimited access', 0.00, 0.00, 1, 1,
#
('Free Tier', 'Default free account tier with unlimited access', 0.00, 0.00, 1, 1,
-1, -1, -1, -1, -1, -1, -1)
#
-1, -1, -1, -1, -1, -1, -1)
'''
)
#
''')
logger
.
info
(
"✅ Migration: Inserted default free tier"
)
#
logger.info("✅ Migration: Inserted default free tier")
except
Exception
as
e
:
#
except Exception as e:
logger
.
warning
(
f
"Migration check for default free tier: {e}"
)
#
logger.warning(f"Migration check for default free tier: {e}")
#
# Migration: Add tier_id column to users table
# Migration: Add tier_id column to users table
try
:
#
try:
if
self
.
db_type
==
'sqlite'
:
#
if self.db_type == 'sqlite':
cursor
.
execute
(
"PRAGMA table_info(users)"
)
#
cursor.execute("PRAGMA table_info(users)")
columns
=
[
row
[
1
]
for
row
in
cursor
.
fetchall
()]
#
columns = [row[1] for row in cursor.fetchall()]
if
'tier_id'
not
in
columns
:
#
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 tier_id INTEGER DEFAULT 1')
cursor
.
execute
(
'ALTER TABLE users ADD COLUMN subscription_expires TIMESTAMP NULL'
)
#
cursor.execute('ALTER TABLE users ADD COLUMN subscription_expires TIMESTAMP NULL')
logger
.
info
(
"✅ Migration: Added tier_id and subscription_expires columns to users"
)
#
logger.info("✅ Migration: Added tier_id and subscription_expires columns to users")
else
:
#
else:
cursor
.
execute
(
"""
#
cursor.execute("""
SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS
#
SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_NAME = 'users' AND COLUMN_NAME = 'tier_id'
#
WHERE TABLE_NAME = 'users' AND COLUMN_NAME = 'tier_id'
"""
)
#
""")
if
not
cursor
.
fetchone
():
#
if not cursor.fetchone():
cursor
.
execute
(
'ALTER TABLE users ADD COLUMN tier_id INTEGER DEFAULT 1'
)
#
cursor.execute('ALTER TABLE users ADD COLUMN tier_id INTEGER DEFAULT 1')
cursor
.
execute
(
'ALTER TABLE users ADD COLUMN subscription_expires TIMESTAMP NULL'
)
#
cursor.execute('ALTER TABLE users ADD COLUMN subscription_expires TIMESTAMP NULL')
logger
.
info
(
"✅ Migration: Added tier_id and subscription_expires columns to users"
)
#
logger.info("✅ Migration: Added tier_id and subscription_expires columns to users")
except
Exception
as
e
:
#
except Exception as e:
logger
.
warning
(
f
"Migration check for users.tier_id: {e}"
)
#
logger.warning(f"Migration check for users.tier_id: {e}")
#
# Migration: Add password reset token columns to users table
# Migration: Add password reset token columns to users table
try
:
#
try:
if
self
.
db_type
==
'sqlite'
:
#
if self.db_type == 'sqlite':
cursor
.
execute
(
"PRAGMA table_info(users)"
)
#
cursor.execute("PRAGMA table_info(users)")
columns
=
[
row
[
1
]
for
row
in
cursor
.
fetchall
()]
#
columns = [row[1] for row in cursor.fetchall()]
if
'reset_password_token'
not
in
columns
:
#
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 VARCHAR(255)')
cursor
.
execute
(
'ALTER TABLE users ADD COLUMN reset_password_token_expires TIMESTAMP NULL'
)
#
cursor.execute('ALTER TABLE users ADD COLUMN reset_password_token_expires TIMESTAMP NULL')
logger
.
info
(
"✅ Migration: Added password reset token columns to users"
)
#
logger.info("✅ Migration: Added password reset token columns to users")
else
:
#
else:
cursor
.
execute
(
"""
#
cursor.execute("""
SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS
#
SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_NAME = 'users' AND COLUMN_NAME = 'reset_password_token'
#
WHERE TABLE_NAME = 'users' AND COLUMN_NAME = 'reset_password_token'
"""
)
#
""")
if
not
cursor
.
fetchone
():
#
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 VARCHAR(255)')
cursor
.
execute
(
'ALTER TABLE users ADD COLUMN reset_password_token_expires TIMESTAMP NULL'
)
#
cursor.execute('ALTER TABLE users ADD COLUMN reset_password_token_expires TIMESTAMP NULL')
logger
.
info
(
"✅ Migration: Added password reset token columns to users"
)
#
logger.info("✅ Migration: Added password reset token columns to users")
except
Exception
as
e
:
#
except Exception as e:
logger
.
warning
(
f
"Migration check for users.reset_password_token: {e}"
)
#
logger.warning(f"Migration check for users.reset_password_token: {e}")
#
# Migration: Add last_verification_email_sent column to users table
# Migration: Add last_verification_email_sent column to users table
try
:
#
try:
if
self
.
db_type
==
'sqlite'
:
#
if self.db_type == 'sqlite':
cursor
.
execute
(
"PRAGMA table_info(users)"
)
#
cursor.execute("PRAGMA table_info(users)")
columns
=
[
row
[
1
]
for
row
in
cursor
.
fetchall
()]
#
columns = [row[1] for row in cursor.fetchall()]
if
'last_verification_email_sent'
not
in
columns
:
#
if 'last_verification_email_sent' not in columns:
cursor
.
execute
(
'ALTER TABLE users ADD COLUMN last_verification_email_sent TIMESTAMP NULL'
)
#
cursor.execute('ALTER TABLE users ADD COLUMN last_verification_email_sent TIMESTAMP NULL')
logger
.
info
(
"✅ Migration: Added last_verification_email_sent column to users"
)
#
logger.info("✅ Migration: Added last_verification_email_sent column to users")
else
:
#
else:
cursor
.
execute
(
"""
#
cursor.execute("""
SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS
#
SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_NAME = 'users' AND COLUMN_NAME = 'last_verification_email_sent'
#
WHERE TABLE_NAME = 'users' AND COLUMN_NAME = 'last_verification_email_sent'
"""
)
#
""")
if
not
cursor
.
fetchone
():
#
if not cursor.fetchone():
cursor
.
execute
(
'ALTER TABLE users ADD COLUMN last_verification_email_sent TIMESTAMP NULL'
)
#
cursor.execute('ALTER TABLE users ADD COLUMN last_verification_email_sent TIMESTAMP NULL')
logger
.
info
(
"✅ Migration: Added last_verification_email_sent column to users"
)
#
logger.info("✅ Migration: Added last_verification_email_sent column to users")
except
Exception
as
e
:
#
except Exception as e:
logger
.
warning
(
f
"Migration check for users.last_verification_email_sent: {e}"
)
#
logger.warning(f"Migration check for users.last_verification_email_sent: {e}")
#
# Migration: Create payment_methods, user_subscriptions, payment_transactions tables
# Migration: Create payment_methods, user_subscriptions, payment_transactions tables
for
table_name
,
create_sql
in
[
#
for table_name, create_sql in [
(
'payment_methods'
,
f
'''
#
('payment_methods', f'''
CREATE TABLE payment_methods (
#
CREATE TABLE payment_methods (
id INTEGER PRIMARY KEY {auto_increment},
#
id INTEGER PRIMARY KEY {auto_increment},
user_id INTEGER NOT NULL,
#
user_id INTEGER NOT NULL,
type VARCHAR(50) NOT NULL,
#
type VARCHAR(50) NOT NULL,
identifier VARCHAR(255) NOT NULL,
#
identifier VARCHAR(255) NOT NULL,
is_default {boolean_type} DEFAULT 0,
#
is_default {boolean_type} DEFAULT 0,
is_active {boolean_type} DEFAULT 1,
#
is_active {boolean_type} DEFAULT 1,
metadata TEXT,
#
metadata TEXT,
created_at TIMESTAMP DEFAULT {timestamp_default},
#
created_at TIMESTAMP DEFAULT {timestamp_default},
updated_at TIMESTAMP DEFAULT {timestamp_default},
#
updated_at TIMESTAMP DEFAULT {timestamp_default},
FOREIGN KEY (user_id) REFERENCES users(id)
#
FOREIGN KEY (user_id) REFERENCES users(id)
)
#
)
'''
),
#
'''),
(
'admin_settings'
,
f
'''
#
('admin_settings', f'''
CREATE TABLE admin_settings (
#
CREATE TABLE admin_settings (
id INTEGER PRIMARY KEY {auto_increment},
#
id INTEGER PRIMARY KEY {auto_increment},
setting_key VARCHAR(255) UNIQUE NOT NULL,
#
setting_key VARCHAR(255) UNIQUE NOT NULL,
setting_value TEXT,
#
setting_value TEXT,
updated_at TIMESTAMP DEFAULT {timestamp_default}
#
updated_at TIMESTAMP DEFAULT {timestamp_default}
)
#
)
'''
),
#
'''),
(
'user_subscriptions'
,
f
'''
#
('user_subscriptions', f'''
CREATE TABLE user_subscriptions (
#
CREATE TABLE user_subscriptions (
id INTEGER PRIMARY KEY {auto_increment},
#
id INTEGER PRIMARY KEY {auto_increment},
user_id INTEGER NOT NULL,
#
user_id INTEGER NOT NULL,
tier_id INTEGER NOT NULL,
#
tier_id INTEGER NOT NULL,
status VARCHAR(50) DEFAULT 'active',
#
status VARCHAR(50) DEFAULT 'active',
start_date TIMESTAMP DEFAULT {timestamp_default},
#
start_date TIMESTAMP DEFAULT {timestamp_default},
end_date TIMESTAMP NULL,
#
end_date TIMESTAMP NULL,
next_billing_date TIMESTAMP NULL,
#
next_billing_date TIMESTAMP NULL,
trial_end_date TIMESTAMP NULL,
#
trial_end_date TIMESTAMP NULL,
payment_method_id INTEGER,
#
payment_method_id INTEGER,
auto_renew {boolean_type} DEFAULT 1,
#
auto_renew {boolean_type} DEFAULT 1,
created_at TIMESTAMP DEFAULT {timestamp_default},
#
created_at TIMESTAMP DEFAULT {timestamp_default},
updated_at TIMESTAMP DEFAULT {timestamp_default},
#
updated_at TIMESTAMP DEFAULT {timestamp_default},
FOREIGN KEY (user_id) REFERENCES users(id),
#
FOREIGN KEY (user_id) REFERENCES users(id),
FOREIGN KEY (tier_id) REFERENCES account_tiers(id),
#
FOREIGN KEY (tier_id) REFERENCES account_tiers(id),
FOREIGN KEY (payment_method_id) REFERENCES payment_methods(id),
#
FOREIGN KEY (payment_method_id) REFERENCES payment_methods(id),
UNIQUE(user_id, tier_id)
#
UNIQUE(user_id, tier_id)
)
#
)
'''
),
#
'''),
(
'payment_transactions'
,
f
'''
#
('payment_transactions', f'''
CREATE TABLE payment_transactions (
#
CREATE TABLE payment_transactions (
id INTEGER PRIMARY KEY {auto_increment},
#
id INTEGER PRIMARY KEY {auto_increment},
user_id INTEGER NOT NULL,
#
user_id INTEGER NOT NULL,
tier_id INTEGER,
#
tier_id INTEGER,
subscription_id INTEGER,
#
subscription_id INTEGER,
payment_method_id INTEGER,
#
payment_method_id INTEGER,
amount DECIMAL(10,2) NOT NULL,
#
amount DECIMAL(10,2) NOT NULL,
currency VARCHAR(10) DEFAULT 'USD',
#
currency VARCHAR(10) DEFAULT 'USD',
status VARCHAR(50) NOT NULL,
#
status VARCHAR(50) NOT NULL,
transaction_type VARCHAR(50) NOT NULL,
#
transaction_type VARCHAR(50) NOT NULL,
external_transaction_id VARCHAR(255),
#
external_transaction_id VARCHAR(255),
metadata TEXT,
#
metadata TEXT,
created_at TIMESTAMP DEFAULT {timestamp_default},
#
created_at TIMESTAMP DEFAULT {timestamp_default},
completed_at TIMESTAMP NULL,
#
completed_at TIMESTAMP NULL,
FOREIGN KEY (user_id) REFERENCES users(id),
#
FOREIGN KEY (user_id) REFERENCES users(id),
FOREIGN KEY (tier_id) REFERENCES account_tiers(id),
#
FOREIGN KEY (tier_id) REFERENCES account_tiers(id),
FOREIGN KEY (subscription_id) REFERENCES user_subscriptions(id),
#
FOREIGN KEY (subscription_id) REFERENCES user_subscriptions(id),
FOREIGN KEY (payment_method_id) REFERENCES payment_methods(id)
#
FOREIGN KEY (payment_method_id) REFERENCES payment_methods(id)
)
#
)
'''
)
#
''')
]:
#
]:
try
:
#
try:
if
self
.
db_type
==
'sqlite'
:
#
if self.db_type == 'sqlite':
cursor
.
execute
(
f
"PRAGMA table_info({table_name})"
)
#
cursor.execute(f"PRAGMA table_info({table_name})")
if
not
cursor
.
fetchall
():
#
if not cursor.fetchall():
cursor
.
execute
(
create_sql
)
#
cursor.execute(create_sql)
logger
.
info
(
f
"✅ Migration: Created missing {table_name} table"
)
#
logger.info(f"✅ Migration: Created missing {table_name} table")
else
:
#
else:
cursor
.
execute
(
f
"""
#
cursor.execute(f"""
SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES
#
SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES
WHERE TABLE_NAME = '{table_name}'
#
WHERE TABLE_NAME = '{table_name}'
"""
)
#
""")
if
not
cursor
.
fetchone
():
#
if not cursor.fetchone():
cursor
.
execute
(
create_sql
)
#
cursor.execute(create_sql)
logger
.
info
(
f
"✅ Migration: Created missing {table_name} table"
)
#
logger.info(f"✅ Migration: Created missing {table_name} table")
except
Exception
as
e
:
#
except Exception as e:
logger
.
warning
(
f
"Migration check for {table_name} table: {e}"
)
#
logger.warning(f"Migration check for {table_name} table: {e}")
#
conn
.
commit
()
#
conn.commit()
logger
.
info
(
"✅ All database migrations completed"
)
#
logger.info("✅ All database migrations completed")
#
else
:
#
else:
# CACHE DATABASE GETS MINIMAL TABLES ONLY
# CACHE DATABASE GETS MINIMAL TABLES ONLY
cursor
.
execute
(
f
'''
#
cursor.execute(f'''
CREATE TABLE IF NOT EXISTS token_usage (
#
CREATE TABLE IF NOT EXISTS token_usage (
id INTEGER PRIMARY KEY {auto_increment},
#
id INTEGER PRIMARY KEY {auto_increment},
user_id INTEGER,
#
user_id INTEGER,
provider_id VARCHAR(255) NOT NULL,
#
provider_id VARCHAR(255) NOT NULL,
model_name VARCHAR(255) NOT NULL,
#
model_name VARCHAR(255) NOT NULL,
tokens_used INTEGER NOT NULL,
#
tokens_used INTEGER NOT NULL,
timestamp TIMESTAMP DEFAULT {timestamp_default}
#
timestamp TIMESTAMP DEFAULT {timestamp_default}
)
#
)
'''
)
#
''')
#
cursor
.
execute
(
f
'''
#
cursor.execute(f'''
CREATE TABLE IF NOT EXISTS context_dimensions (
#
CREATE TABLE IF NOT EXISTS context_dimensions (
id INTEGER PRIMARY KEY {auto_increment},
#
id INTEGER PRIMARY KEY {auto_increment},
provider_id VARCHAR(255) NOT NULL,
#
provider_id VARCHAR(255) NOT NULL,
model_name VARCHAR(255) NOT NULL,
#
model_name VARCHAR(255) NOT NULL,
context_size INTEGER,
#
context_size INTEGER,
condense_context INTEGER,
#
condense_context INTEGER,
condense_method TEXT,
#
condense_method TEXT,
effective_context INTEGER DEFAULT 0,
#
effective_context INTEGER DEFAULT 0,
last_updated TIMESTAMP DEFAULT {timestamp_default},
#
last_updated TIMESTAMP DEFAULT {timestamp_default},
UNIQUE(provider_id, model_name)
#
UNIQUE(provider_id, model_name)
)
#
)
'''
)
#
''')
#
logger
.
info
(
"⚠️ CACHE DATABASE: Only minimal cache tables created - NO USER TABLES"
)
#
logger.info("⚠️ CACHE DATABASE: Only minimal cache tables created - NO USER TABLES")
conn
.
commit
()
conn
.
commit
()
logger
.
info
(
f
"Database tables initialized successfully for {self.database_type} database"
)
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
):
def
DatabaseManager__create_config_tables
(
self
,
cursor
,
auto_increment
,
timestamp_default
,
boolean_type
):
"""Create all permanent configuration tables (CONFIG DB ONLY)"""
"""Create all permanent configuration tables (CONFIG DB ONLY) - UNUSED METHOD"""
pass
# Method disabled
# 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
):
def
DatabaseManager__create_cache_tables
(
self
,
cursor
,
auto_increment
,
timestamp_default
,
boolean_type
):
"""Create only temporary cache tables (CACHE DB ONLY)"""
"""Create only temporary cache tables (CACHE DB ONLY)"""
...
@@ -3591,6 +3471,9 @@ def DatabaseManager__create_cache_tables(self, cursor, auto_increment, timestamp
...
@@ -3591,6 +3471,9 @@ def DatabaseManager__create_cache_tables(self, cursor, auto_increment, timestamp
provider_id VARCHAR(255) NOT NULL,
provider_id VARCHAR(255) NOT NULL,
model_name VARCHAR(255) NOT NULL,
model_name VARCHAR(255) NOT NULL,
tokens_used INTEGER NOT NULL,
tokens_used INTEGER NOT NULL,
prompt_tokens INTEGER,
completion_tokens INTEGER,
actual_cost DECIMAL(10,6),
timestamp TIMESTAMP DEFAULT {timestamp_default}
timestamp TIMESTAMP DEFAULT {timestamp_default}
)
)
'''
)
'''
)
...
@@ -3818,11 +3701,51 @@ def DatabaseManager__run_config_migrations(self, cursor, auto_increment, timesta
...
@@ -3818,11 +3701,51 @@ def DatabaseManager__run_config_migrations(self, cursor, auto_increment, timesta
except
Exception
as
e
:
except
Exception
as
e
:
logger
.
warning
(
f
"Migration check for {table_name} table: {e}"
)
logger
.
warning
(
f
"Migration check for {table_name} table: {e}"
)
# Migration: Add prompt_tokens and completion_tokens columns to token_usage table
try
:
if
self
.
db_type
==
'sqlite'
:
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 COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_NAME = 'token_usage' AND COLUMN_NAME = 'prompt_tokens'
"""
)
if
not
cursor
.
fetchone
():
cursor
.
execute
(
'ALTER TABLE token_usage ADD COLUMN prompt_tokens INTEGER'
)
logger
.
info
(
"✅ Migration: Added prompt_tokens column to token_usage"
)
cursor
.
execute
(
"""
SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_NAME = 'token_usage' AND COLUMN_NAME = 'completion_tokens'
"""
)
if
not
cursor
.
fetchone
():
cursor
.
execute
(
'ALTER TABLE token_usage ADD COLUMN completion_tokens INTEGER'
)
logger
.
info
(
"✅ Migration: Added completion_tokens column to token_usage"
)
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
(
'ALTER TABLE token_usage ADD COLUMN actual_cost DECIMAL(10,6)'
)
logger
.
info
(
"✅ Migration: Added actual_cost column to token_usage"
)
except
Exception
as
e
:
logger
.
warning
(
f
"Migration check for token_usage columns: {e}"
)
conn
.
commit
()
conn
.
commit
()
logger
.
info
(
"✅ All database migrations completed"
)
logger
.
info
(
"✅ All database migrations completed"
)
# Patch the methods
# Patch the methods
DatabaseManager
.
_initialize_database
=
DatabaseManager__initialize_database
DatabaseManager
.
_initialize_database
=
DatabaseManager__initialize_database
DatabaseManager
.
_create_config_tables
=
DatabaseManager__create_config_tables
DatabaseManager
.
_create_cache_tables
=
DatabaseManager__create_cache_tables
DatabaseManager
.
_create_cache_tables
=
DatabaseManager__create_cache_tables
DatabaseManager
.
_run_config_migrations
=
DatabaseManager__run_config_migrations
DatabaseManager
.
_run_config_migrations
=
DatabaseManager__run_config_migrations
aisbf/handlers.py
View file @
fe8b625a
...
@@ -366,6 +366,9 @@ class RequestHandler:
...
@@ -366,6 +366,9 @@ class RequestHandler:
# Record analytics for authentication failure
# Record analytics for authentication failure
try
:
try
:
analytics
=
get_analytics
()
analytics
=
get_analytics
()
# Calculate latency for auth failure
latency_ms
=
(
time
.
time
()
-
request_start_time
)
*
1000
# Estimate tokens for the request
# Estimate tokens for the request
try
:
try
:
messages
=
request_data
.
get
(
'messages'
,
[])
messages
=
request_data
.
get
(
'messages'
,
[])
...
@@ -378,11 +381,14 @@ class RequestHandler:
...
@@ -378,11 +381,14 @@ class RequestHandler:
provider_id
=
provider_id
,
provider_id
=
provider_id
,
model_name
=
request_data
.
get
(
'model'
,
'unknown'
),
model_name
=
request_data
.
get
(
'model'
,
'unknown'
),
tokens_used
=
estimated_tokens
,
tokens_used
=
estimated_tokens
,
latency_ms
=
0
,
latency_ms
=
latency_ms
,
success
=
False
,
success
=
False
,
error_type
=
'AuthenticationError'
,
error_type
=
'AuthenticationError'
,
user_id
=
getattr
(
request
.
state
,
'user_id'
,
None
),
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
:
except
Exception
as
analytics_error
:
logger
.
warning
(
f
"Analytics recording for auth failure failed: {analytics_error}"
)
logger
.
warning
(
f
"Analytics recording for auth failure failed: {analytics_error}"
)
...
@@ -526,33 +532,48 @@ class RequestHandler:
...
@@ -526,33 +532,48 @@ class RequestHandler:
try
:
try
:
analytics
=
get_analytics
()
analytics
=
get_analytics
()
latency_ms
=
(
time
.
time
()
-
request_start_time
)
*
1000
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
):
if
response
and
isinstance
(
response
,
dict
):
usage
=
response
.
get
(
'usage'
,
{})
usage
=
response
.
get
(
'usage'
,
{})
total_tokens
=
usage
.
get
(
'total_tokens'
,
0
)
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
:
if
total_tokens
==
0
:
try
:
try
:
messages
=
request_data
.
get
(
'messages'
,
[])
messages
=
request_data
.
get
(
'messages'
,
[])
estimated_prompt_tokens
=
count_messages_tokens
(
messages
,
model_name
)
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
max_tokens
=
request_data
.
get
(
'max_tokens'
,
0
)
response_content
=
response
.
get
(
'choices'
,
[{}])[
0
]
.
get
(
'message'
,
{})
.
get
(
'content'
,
''
)
if
max_tokens
>
0
:
if
response_content
:
# Use max_tokens as upper bound for completion
completion_tokens
=
count_messages_tokens
([{
estimated_completion
=
min
(
max_tokens
,
estimated_prompt_tokens
*
2
)
"role"
:
"assistant"
,
"content"
:
response_content
}],
model_name
)
else
:
else
:
# No max_tokens specified, assume completion is similar to prompt
# Fallback to estimation if no content
# but at least 50 tokens for typical responses
max_tokens
=
request_data
.
get
(
'max_tokens'
,
0
)
estimated_completion
=
max
(
estimated_prompt_tokens
,
50
)
if
max_tokens
>
0
:
completion_tokens
=
min
(
max_tokens
,
estimated_prompt_tokens
*
2
)
else
:
completion_tokens
=
max
(
estimated_prompt_tokens
,
50
)
total_tokens
=
estimated_prompt_tokens
+
estimated_completion
total_tokens
=
estimated_prompt_tokens
+
completion_tokens
logger
.
debug
(
f
"Estimated token usage: {total_tokens} (prompt: {estimated_prompt_tokens}, completion: {estimated_completion})"
)
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
:
except
Exception
as
est_error
:
logger
.
debug
(
f
"Token
estimation
failed: {est_error}"
)
logger
.
debug
(
f
"Token
counting
failed: {est_error}"
)
# Use a more realistic default if
estimation
fails
# Use a more realistic default if
counting
fails
total_tokens
=
150
total_tokens
=
150
prompt_tokens
=
0
completion_tokens
=
0
# Always record analytics, even with estimated tokens
# Always record analytics, even with estimated tokens
analytics
.
record_request
(
analytics
.
record_request
(
...
@@ -562,7 +583,10 @@ class RequestHandler:
...
@@ -562,7 +583,10 @@ class RequestHandler:
latency_ms
=
latency_ms
,
latency_ms
=
latency_ms
,
success
=
True
,
success
=
True
,
user_id
=
getattr
(
request
.
state
,
'user_id'
,
None
),
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
:
except
Exception
as
analytics_error
:
logger
.
warning
(
f
"Analytics recording failed: {analytics_error}"
)
logger
.
warning
(
f
"Analytics recording failed: {analytics_error}"
)
...
@@ -582,8 +606,12 @@ class RequestHandler:
...
@@ -582,8 +606,12 @@ class RequestHandler:
messages
=
request_data
.
get
(
'messages'
,
[])
messages
=
request_data
.
get
(
'messages'
,
[])
estimated_tokens
=
count_messages_tokens
(
messages
,
model_name
)
estimated_tokens
=
count_messages_tokens
(
messages
,
model_name
)
total_tokens
=
estimated_tokens
total_tokens
=
estimated_tokens
prompt_tokens
=
estimated_tokens
completion_tokens
=
0
# No completion for failed requests
except
Exception
:
except
Exception
:
total_tokens
=
50
# Minimal estimate for failed requests
total_tokens
=
50
# Minimal estimate for failed requests
prompt_tokens
=
50
completion_tokens
=
0
analytics
.
record_request
(
analytics
.
record_request
(
provider_id
=
provider_id
,
provider_id
=
provider_id
,
...
@@ -593,7 +621,10 @@ class RequestHandler:
...
@@ -593,7 +621,10 @@ class RequestHandler:
success
=
False
,
success
=
False
,
error_type
=
type
(
e
)
.
__name__
,
error_type
=
type
(
e
)
.
__name__
,
user_id
=
getattr
(
request
.
state
,
'user_id'
,
None
),
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
:
except
Exception
as
analytics_error
:
logger
.
warning
(
f
"Analytics recording for failed request failed: {analytics_error}"
)
logger
.
warning
(
f
"Analytics recording for failed request failed: {analytics_error}"
)
...
@@ -667,6 +698,8 @@ class RequestHandler:
...
@@ -667,6 +698,8 @@ class RequestHandler:
import
time
import
time
import
json
import
json
logger
=
logging
.
getLogger
(
__name__
)
logger
=
logging
.
getLogger
(
__name__
)
# Track request start time for latency calculation
request_start_time
=
time
.
time
()
try
:
try
:
# Apply rate limiting
# Apply rate limiting
await
handler
.
apply_rate_limit
()
await
handler
.
apply_rate_limit
()
...
@@ -1200,6 +1233,10 @@ class RequestHandler:
...
@@ -1200,6 +1233,10 @@ class RequestHandler:
# Record analytics for streaming request
# Record analytics for streaming request
try
:
try
:
analytics
=
get_analytics
()
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
# Calculate total tokens from accumulated response
if
accumulated_response_text
:
if
accumulated_response_text
:
completion_tokens
=
count_messages_tokens
([{
"role"
:
"assistant"
,
"content"
:
accumulated_response_text
}],
request_data
[
'model'
])
completion_tokens
=
count_messages_tokens
([{
"role"
:
"assistant"
,
"content"
:
accumulated_response_text
}],
request_data
[
'model'
])
...
@@ -1211,10 +1248,13 @@ class RequestHandler:
...
@@ -1211,10 +1248,13 @@ class RequestHandler:
provider_id
=
provider_id
,
provider_id
=
provider_id
,
model_name
=
request_data
[
'model'
],
model_name
=
request_data
[
'model'
],
tokens_used
=
total_tokens
,
tokens_used
=
total_tokens
,
latency_ms
=
0
,
latency_ms
=
latency_ms
,
success
=
True
,
success
=
True
,
user_id
=
getattr
(
request
.
state
,
'user_id'
,
None
),
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
:
except
Exception
as
analytics_error
:
logger
.
warning
(
f
"Analytics recording for streaming request failed: {analytics_error}"
)
logger
.
warning
(
f
"Analytics recording for streaming request failed: {analytics_error}"
)
...
@@ -1225,23 +1265,34 @@ class RequestHandler:
...
@@ -1225,23 +1265,34 @@ class RequestHandler:
# Record analytics for failed streaming request
# Record analytics for failed streaming request
try
:
try
:
analytics
=
get_analytics
()
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
# Estimate tokens for failed request
try
:
try
:
messages
=
request_data
.
get
(
'messages'
,
[])
messages
=
request_data
.
get
(
'messages'
,
[])
estimated_tokens
=
count_messages_tokens
(
messages
,
request_data
[
'model'
])
estimated_tokens
=
count_messages_tokens
(
messages
,
request_data
[
'model'
])
total_tokens
=
estimated_tokens
total_tokens
=
estimated_tokens
prompt_tokens
=
estimated_tokens
completion_tokens
=
0
except
Exception
:
except
Exception
:
total_tokens
=
50
# Minimal estimate for failed requests
total_tokens
=
50
# Minimal estimate for failed requests
prompt_tokens
=
50
completion_tokens
=
0
analytics
.
record_request
(
analytics
.
record_request
(
provider_id
=
provider_id
,
provider_id
=
provider_id
,
model_name
=
request_data
[
'model'
],
model_name
=
request_data
[
'model'
],
tokens_used
=
total_tokens
,
tokens_used
=
total_tokens
,
latency_ms
=
0
,
latency_ms
=
latency_ms
,
success
=
False
,
success
=
False
,
error_type
=
type
(
e
)
.
__name__
,
error_type
=
type
(
e
)
.
__name__
,
user_id
=
getattr
(
request
.
state
,
'user_id'
,
None
),
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
:
except
Exception
as
analytics_error
:
logger
.
warning
(
f
"Analytics recording for failed streaming request failed: {analytics_error}"
)
logger
.
warning
(
f
"Analytics recording for failed streaming request failed: {analytics_error}"
)
...
@@ -2233,6 +2284,8 @@ class RotationHandler:
...
@@ -2233,6 +2284,8 @@ class RotationHandler:
import
logging
import
logging
import
time
import
time
logger
=
logging
.
getLogger
(
__name__
)
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
"=== RotationHandler.handle_rotation_request START ==="
)
logger
.
info
(
f
"Rotation ID: {rotation_id}"
)
logger
.
info
(
f
"Rotation ID: {rotation_id}"
)
logger
.
info
(
f
"User ID: {self.user_id}"
)
logger
.
info
(
f
"User ID: {self.user_id}"
)
...
@@ -2847,6 +2900,8 @@ class RotationHandler:
...
@@ -2847,6 +2900,8 @@ class RotationHandler:
if
response
and
isinstance
(
response
,
dict
):
if
response
and
isinstance
(
response
,
dict
):
usage
=
response
.
get
(
'usage'
,
{})
usage
=
response
.
get
(
'usage'
,
{})
total_tokens
=
usage
.
get
(
'total_tokens'
,
0
)
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 no token usage provided, estimate it
if
total_tokens
==
0
:
if
total_tokens
==
0
:
...
@@ -2862,21 +2917,32 @@ class RotationHandler:
...
@@ -2862,21 +2917,32 @@ class RotationHandler:
estimated_completion
=
max
(
estimated_prompt_tokens
,
50
)
estimated_completion
=
max
(
estimated_prompt_tokens
,
50
)
total_tokens
=
estimated_prompt_tokens
+
estimated_completion
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}"
)
logger
.
debug
(
f
"Estimated token usage for rotation: {total_tokens}"
)
except
Exception
as
est_error
:
except
Exception
as
est_error
:
logger
.
debug
(
f
"Token estimation failed: {est_error}"
)
logger
.
debug
(
f
"Token estimation failed: {est_error}"
)
total_tokens
=
150
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
# Always record analytics
analytics
.
record_request
(
analytics
.
record_request
(
provider_id
=
provider_id
,
provider_id
=
provider_id
,
model_name
=
model_name
,
model_name
=
model_name
,
tokens_used
=
total_tokens
,
tokens_used
=
total_tokens
,
latency_ms
=
0
,
# Latency tracking would require more extensive changes
latency_ms
=
(
time
.
time
()
-
request_start_time
)
*
1000
,
success
=
True
,
success
=
True
,
rotation_id
=
rotation_id
,
rotation_id
=
rotation_id
,
user_id
=
user_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
:
except
Exception
as
analytics_error
:
logger
.
warning
(
f
"Analytics recording failed: {analytics_error}"
)
logger
.
warning
(
f
"Analytics recording failed: {analytics_error}"
)
...
@@ -2912,24 +2978,35 @@ class RotationHandler:
...
@@ -2912,24 +2978,35 @@ class RotationHandler:
# Record analytics for failed rotation request
# Record analytics for failed rotation request
try
:
try
:
analytics
=
get_analytics
()
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
# Estimate tokens for failed request
try
:
try
:
messages
=
request_data
.
get
(
'messages'
,
[])
messages
=
request_data
.
get
(
'messages'
,
[])
estimated_tokens
=
count_messages_tokens
(
messages
,
rotation_id
)
estimated_tokens
=
count_messages_tokens
(
messages
,
rotation_id
)
total_tokens
=
estimated_tokens
total_tokens
=
estimated_tokens
prompt_tokens
=
estimated_tokens
completion_tokens
=
0
except
Exception
:
except
Exception
:
total_tokens
=
50
# Minimal estimate for failed requests
total_tokens
=
50
# Minimal estimate for failed requests
prompt_tokens
=
50
completion_tokens
=
0
analytics
.
record_request
(
analytics
.
record_request
(
provider_id
=
'rotation'
,
provider_id
=
'rotation'
,
model_name
=
rotation_id
,
model_name
=
rotation_id
,
tokens_used
=
total_tokens
,
tokens_used
=
total_tokens
,
latency_ms
=
0
,
latency_ms
=
latency_ms
,
success
=
False
,
success
=
False
,
error_type
=
'RotationFailure'
,
error_type
=
'RotationFailure'
,
rotation_id
=
rotation_id
,
rotation_id
=
rotation_id
,
user_id
=
user_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
:
except
Exception
as
analytics_error
:
logger
.
warning
(
f
"Analytics recording for failed rotation failed: {analytics_error}"
)
logger
.
warning
(
f
"Analytics recording for failed rotation failed: {analytics_error}"
)
...
@@ -4112,7 +4189,10 @@ class AutoselectHandler:
...
@@ -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
:
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"""
"""Handle an autoselect request"""
import
logging
import
logging
import
time
logger
=
logging
.
getLogger
(
__name__
)
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 REQUEST START ==="
)
logger
.
info
(
f
"Autoselect ID: {autoselect_id}"
)
logger
.
info
(
f
"Autoselect ID: {autoselect_id}"
)
logger
.
info
(
f
"User ID: {self.user_id}"
)
logger
.
info
(
f
"User ID: {self.user_id}"
)
...
@@ -4232,24 +4312,35 @@ class AutoselectHandler:
...
@@ -4232,24 +4312,35 @@ class AutoselectHandler:
logger
.
error
(
f
"Autoselect request failed: {str(e)}"
)
logger
.
error
(
f
"Autoselect request failed: {str(e)}"
)
try
:
try
:
analytics
=
get_analytics
()
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
# Estimate tokens for failed request
try
:
try
:
messages
=
request_data
.
get
(
'messages'
,
[])
messages
=
request_data
.
get
(
'messages'
,
[])
estimated_tokens
=
count_messages_tokens
(
messages
,
autoselect_id
)
estimated_tokens
=
count_messages_tokens
(
messages
,
autoselect_id
)
total_tokens
=
estimated_tokens
total_tokens
=
estimated_tokens
prompt_tokens
=
estimated_tokens
completion_tokens
=
0
except
Exception
:
except
Exception
:
total_tokens
=
50
# Minimal estimate for failed requests
total_tokens
=
50
# Minimal estimate for failed requests
prompt_tokens
=
50
completion_tokens
=
0
analytics
.
record_request
(
analytics
.
record_request
(
provider_id
=
'autoselect'
,
provider_id
=
'autoselect'
,
model_name
=
autoselect_id
,
model_name
=
autoselect_id
,
tokens_used
=
total_tokens
,
tokens_used
=
total_tokens
,
latency_ms
=
0
,
latency_ms
=
latency_ms
,
success
=
False
,
success
=
False
,
error_type
=
type
(
e
)
.
__name__
,
error_type
=
type
(
e
)
.
__name__
,
autoselect_id
=
autoselect_id
,
autoselect_id
=
autoselect_id
,
user_id
=
user_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
:
except
Exception
as
analytics_error
:
logger
.
warning
(
f
"Analytics recording for failed autoselect failed: {analytics_error}"
)
logger
.
warning
(
f
"Analytics recording for failed autoselect failed: {analytics_error}"
)
...
@@ -4276,6 +4367,8 @@ class AutoselectHandler:
...
@@ -4276,6 +4367,8 @@ class AutoselectHandler:
if
response
and
isinstance
(
response
,
dict
):
if
response
and
isinstance
(
response
,
dict
):
usage
=
response
.
get
(
'usage'
,
{})
usage
=
response
.
get
(
'usage'
,
{})
total_tokens
=
usage
.
get
(
'total_tokens'
,
0
)
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 no token usage provided, estimate it
if
total_tokens
==
0
:
if
total_tokens
==
0
:
...
@@ -4292,23 +4385,38 @@ class AutoselectHandler:
...
@@ -4292,23 +4385,38 @@ class AutoselectHandler:
estimated_completion
=
max
(
estimated_prompt_tokens
,
50
)
estimated_completion
=
max
(
estimated_prompt_tokens
,
50
)
total_tokens
=
estimated_prompt_tokens
+
estimated_completion
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}"
)
logger
.
debug
(
f
"Estimated token usage for autoselect: {total_tokens}"
)
except
Exception
as
est_error
:
except
Exception
as
est_error
:
logger
.
debug
(
f
"Token estimation failed: {est_error}"
)
logger
.
debug
(
f
"Token estimation failed: {est_error}"
)
total_tokens
=
150
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
# Always record analytics
# The actual provider/model info is in the response model field
# The actual provider/model info is in the response model field
model_name
=
response
.
get
(
'model'
,
'unknown'
)
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
(
analytics
.
record_request
(
provider_id
=
'autoselect'
,
provider_id
=
'autoselect'
,
model_name
=
model_name
,
model_name
=
model_name
,
tokens_used
=
total_tokens
,
tokens_used
=
total_tokens
,
latency_ms
=
0
,
latency_ms
=
latency_ms
,
success
=
True
,
success
=
True
,
autoselect_id
=
autoselect_id
,
autoselect_id
=
autoselect_id
,
user_id
=
user_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
:
except
Exception
as
analytics_error
:
logger
.
warning
(
f
"Analytics recording failed: {analytics_error}"
)
logger
.
warning
(
f
"Analytics recording failed: {analytics_error}"
)
...
...
aisbf/mcp.py
View file @
fe8b625a
...
@@ -855,12 +855,12 @@ class MCPServer:
...
@@ -855,12 +855,12 @@ class MCPServer:
if
stream
:
if
stream
:
return
{
"error"
:
"Streaming not supported in MCP, use SSE endpoint instead"
}
return
{
"error"
:
"Streaming not supported in MCP, use SSE endpoint instead"
}
else
:
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"
:
elif
provider_id
==
"rotation"
:
handler
=
get_user_handler
(
'rotation'
,
user_id
)
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
):
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"
)
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
:
else
:
handler
=
get_user_handler
(
'request'
,
user_id
)
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
):
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):
...
@@ -159,7 +159,7 @@ class ClaudeProviderHandler(BaseProviderHandler):
logger
.
info
(
"ClaudeProviderHandler: Initializing session for quota tracking"
)
logger
.
info
(
"ClaudeProviderHandler: Initializing session for quota tracking"
)
try
:
try
:
headers
=
self
.
_get_auth_headers
(
stream
=
False
)
headers
=
await
self
.
_get_auth_headers
(
stream
=
False
)
payload
=
{
payload
=
{
'model'
:
'claude-haiku-4-5-20251001'
,
'model'
:
'claude-haiku-4-5-20251001'
,
...
@@ -257,12 +257,12 @@ class ClaudeProviderHandler(BaseProviderHandler):
...
@@ -257,12 +257,12 @@ class ClaudeProviderHandler(BaseProviderHandler):
if
old_util
!=
new_util
:
if
old_util
!=
new_util
:
logger
.
debug
(
f
"ClaudeProviderHandler: Quota utilization updated: {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."""
"""Get or create an Anthropic SDK client configured with OAuth2 auth token."""
import
logging
import
logging
logger
=
logging
.
getLogger
(
__name__
)
logger
=
logging
.
getLogger
(
__name__
)
access_token
=
self
.
auth
.
get_valid_token
()
access_token
=
await
self
.
auth
.
get_valid_token
()
if
not
access_token
:
if
not
access_token
:
logger
.
error
(
"ClaudeProviderHandler: No OAuth2 access token available"
)
logger
.
error
(
"ClaudeProviderHandler: No OAuth2 access token available"
)
...
@@ -277,14 +277,14 @@ class ClaudeProviderHandler(BaseProviderHandler):
...
@@ -277,14 +277,14 @@ class ClaudeProviderHandler(BaseProviderHandler):
logger
.
info
(
"ClaudeProviderHandler: Created SDK client with OAuth2 auth token"
)
logger
.
info
(
"ClaudeProviderHandler: Created SDK client with OAuth2 auth token"
)
return
self
.
_sdk_client
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."""
"""Get HTTP headers with OAuth2 Bearer token."""
import
logging
import
logging
import
uuid
import
uuid
import
platform
import
platform
logger
=
logging
.
getLogger
(
__name__
)
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'
):
if
not
self
.
session_state
.
get
(
'session_id'
):
self
.
session_state
[
'session_id'
]
=
str
(
uuid
.
uuid4
())
self
.
session_state
[
'session_id'
]
=
str
(
uuid
.
uuid4
())
...
@@ -849,7 +849,7 @@ class ClaudeProviderHandler(BaseProviderHandler):
...
@@ -849,7 +849,7 @@ class ClaudeProviderHandler(BaseProviderHandler):
if
anthropic_tool_choice
:
if
anthropic_tool_choice
:
payload
[
'tool_choice'
]
=
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'
api_url
=
'https://api.anthropic.com/v1/messages?beta=true'
logger
.
info
(
f
"ClaudeProviderHandler: Request payload keys: {list(payload.keys())}"
)
logger
.
info
(
f
"ClaudeProviderHandler: Request payload keys: {list(payload.keys())}"
)
...
@@ -1639,8 +1639,8 @@ class ClaudeProviderHandler(BaseProviderHandler):
...
@@ -1639,8 +1639,8 @@ class ClaudeProviderHandler(BaseProviderHandler):
try
:
try
:
logging
.
info
(
"ClaudeProviderHandler: [1/3] Attempting primary API endpoint..."
)
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'
api_endpoint
=
'https://api.anthropic.com/v1/models'
logging
.
info
(
f
"ClaudeProviderHandler: Calling API endpoint: {api_endpoint}"
)
logging
.
info
(
f
"ClaudeProviderHandler: Calling API endpoint: {api_endpoint}"
)
...
...
aisbf/providers/kilo.py
View file @
fe8b625a
...
@@ -169,9 +169,9 @@ class KiloProviderHandler(BaseProviderHandler):
...
@@ -169,9 +169,9 @@ class KiloProviderHandler(BaseProviderHandler):
"status"
:
"authenticated"
,
"status"
:
"authenticated"
,
"token"
:
self
.
api_key
"token"
:
self
.
api_key
}
}
token
=
self
.
oauth2
.
get_valid_token
()
token
=
await
self
.
oauth2
.
get_valid_token
()
if
token
:
if
token
:
logger
.
info
(
"KiloProviderHandler: Using existing OAuth2 token"
)
logger
.
info
(
"KiloProviderHandler: Using existing OAuth2 token"
)
return
{
return
{
...
@@ -182,7 +182,7 @@ class KiloProviderHandler(BaseProviderHandler):
...
@@ -182,7 +182,7 @@ class KiloProviderHandler(BaseProviderHandler):
# Try to reload credentials one more time - this handles the case where credentials
# 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
# were saved by another process/handler instance after this handler was created
self
.
oauth2
.
_load_credentials
()
self
.
oauth2
.
_load_credentials
()
token
=
self
.
oauth2
.
get_valid_token
()
token
=
await
self
.
oauth2
.
get_valid_token
()
if
token
:
if
token
:
logger
.
info
(
"KiloProviderHandler: Found OAuth2 token after reloading credentials"
)
logger
.
info
(
"KiloProviderHandler: Found OAuth2 token after reloading credentials"
)
...
...
aisbf/providers/qwen.py
View file @
fe8b625a
...
@@ -103,7 +103,7 @@ class QwenProviderHandler(BaseProviderHandler):
...
@@ -103,7 +103,7 @@ class QwenProviderHandler(BaseProviderHandler):
logging
.
getLogger
(
__name__
)
.
info
(
f
"QwenProviderHandler: Falling back to file-based credentials for user {self.user_id}"
)
logging
.
getLogger
(
__name__
)
.
info
(
f
"QwenProviderHandler: Falling back to file-based credentials for user {self.user_id}"
)
return
QwenOAuth2
(
credentials_file
=
credentials_file
)
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)."""
"""Get or create an OpenAI SDK client configured with authentication (OAuth2 or API key)."""
import
logging
import
logging
logger
=
logging
.
getLogger
(
__name__
)
logger
=
logging
.
getLogger
(
__name__
)
...
@@ -122,7 +122,7 @@ class QwenProviderHandler(BaseProviderHandler):
...
@@ -122,7 +122,7 @@ class QwenProviderHandler(BaseProviderHandler):
base_url
=
self
.
_get_region_endpoint
(
qwen_config
)
base_url
=
self
.
_get_region_endpoint
(
qwen_config
)
else
:
else
:
# Use OAuth2 authentication
# Use OAuth2 authentication
access_token
=
self
.
auth
.
get_valid_token
()
access_token
=
await
self
.
auth
.
get_valid_token
()
if
not
access_token
:
if
not
access_token
:
logger
.
error
(
"QwenProviderHandler: No OAuth2 access token available"
)
logger
.
error
(
"QwenProviderHandler: No OAuth2 access token available"
)
...
@@ -221,7 +221,7 @@ class QwenProviderHandler(BaseProviderHandler):
...
@@ -221,7 +221,7 @@ class QwenProviderHandler(BaseProviderHandler):
await
self
.
apply_rate_limit
()
await
self
.
apply_rate_limit
()
# Get SDK client with current OAuth token
# Get SDK client with current OAuth token
client
=
self
.
_get_sdk_client
()
client
=
await
self
.
_get_sdk_client
()
# Build request parameters
# Build request parameters
request_params
=
{
request_params
=
{
...
@@ -308,7 +308,7 @@ class QwenProviderHandler(BaseProviderHandler):
...
@@ -308,7 +308,7 @@ class QwenProviderHandler(BaseProviderHandler):
if
refresh_success
:
if
refresh_success
:
logger
.
info
(
"QwenProviderHandler: Token refreshed, retrying request"
)
logger
.
info
(
"QwenProviderHandler: Token refreshed, retrying request"
)
# Retry with new token
# Retry with new token
client
=
self
.
_get_sdk_client
()
client
=
await
self
.
_get_sdk_client
()
if
stream
:
if
stream
:
return
self
.
_handle_streaming_request
(
client
,
request_params
,
model
)
return
self
.
_handle_streaming_request
(
client
,
request_params
,
model
)
...
@@ -472,7 +472,7 @@ class QwenProviderHandler(BaseProviderHandler):
...
@@ -472,7 +472,7 @@ class QwenProviderHandler(BaseProviderHandler):
try
:
try
:
# Get SDK client with API key authentication
# Get SDK client with API key authentication
client
=
self
.
_get_sdk_client
()
client
=
await
self
.
_get_sdk_client
()
# List models using OpenAI SDK
# List models using OpenAI SDK
models_response
=
await
client
.
models
.
list
()
models_response
=
await
client
.
models
.
list
()
...
...
main.py
View file @
fe8b625a
...
@@ -647,6 +647,13 @@ def initialize_app(custom_config_dir=None):
...
@@ -647,6 +647,13 @@ def initialize_app(custom_config_dir=None):
'password'
:
'8c6976e5b5410415bde908bd4dee15dfb167a9c873fc4bb8a81f6f2ab448a918'
'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
_initialized
=
True
logger
.
info
(
"App initialization complete"
)
logger
.
info
(
"App initialization complete"
)
...
@@ -1314,11 +1321,6 @@ async def auth_middleware(request: Request, call_next):
...
@@ -1314,11 +1321,6 @@ async def auth_middleware(request: Request, call_next):
request
.
state
.
is_global_token
=
False
request
.
state
.
is_global_token
=
False
# Store user role - admin users get full access
# Store user role - admin users get full access
request
.
state
.
is_admin
=
(
user_auth
.
get
(
'role'
)
==
'admin'
)
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
:
else
:
return
JSONResponse
(
return
JSONResponse
(
status_code
=
403
,
status_code
=
403
,
...
@@ -1901,6 +1903,42 @@ async def get_subscription_status(request: Request):
...
@@ -1901,6 +1903,42 @@ async def get_subscription_status(request: Request):
status
=
await
payment_service
.
get_subscription_status
(
current_user
[
'id'
])
status
=
await
payment_service
.
get_subscription_status
(
current_user
[
'id'
])
return
{
'subscription'
:
status
}
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
# Dashboard routes
@
app
.
get
(
"/dashboard/analytics"
,
response_class
=
HTMLResponse
)
@
app
.
get
(
"/dashboard/analytics"
,
response_class
=
HTMLResponse
)
async
def
dashboard_analytics
(
async
def
dashboard_analytics
(
...
@@ -1912,7 +1950,8 @@ async def dashboard_analytics(
...
@@ -1912,7 +1950,8 @@ async def dashboard_analytics(
model_filter
:
Optional
[
str
]
=
Query
(
None
),
model_filter
:
Optional
[
str
]
=
Query
(
None
),
rotation_filter
:
Optional
[
str
]
=
Query
(
None
),
rotation_filter
:
Optional
[
str
]
=
Query
(
None
),
autoselect_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"""
"""Token usage analytics dashboard"""
auth_check
=
require_dashboard_auth
(
request
)
auth_check
=
require_dashboard_auth
(
request
)
...
@@ -1950,9 +1989,21 @@ async def dashboard_analytics(
...
@@ -1950,9 +1989,21 @@ async def dashboard_analytics(
is_admin
=
request
.
session
.
get
(
'role'
)
==
'admin'
is_admin
=
request
.
session
.
get
(
'role'
)
==
'admin'
current_user_id
=
request
.
session
.
get
(
'user_id'
)
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
# For non-admin users, force user filter to current user
if
not
is_admin
and
current_user_id
is
not
None
:
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)
# Get all users for filter dropdown (only for admins)
all_users
=
db
.
get_users
()
if
db
and
is_admin
else
[]
all_users
=
db
.
get_users
()
if
db
and
is_admin
else
[]
...
@@ -1972,9 +2023,9 @@ async def dashboard_analytics(
...
@@ -1972,9 +2023,9 @@ async def dashboard_analytics(
# Get provider statistics (with optional filter)
# Get provider statistics (with optional filter)
if
provider_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
:
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)
# Get token usage over time (with optional filters)
token_over_time
=
analytics
.
get_token_usage_over_time
(
token_over_time
=
analytics
.
get_token_usage_over_time
(
...
@@ -1982,7 +2033,7 @@ async def dashboard_analytics(
...
@@ -1982,7 +2033,7 @@ async def dashboard_analytics(
time_range
=
time_range
,
time_range
=
time_range
,
from_datetime
=
from_datetime
,
from_datetime
=
from_datetime
,
to_datetime
=
to_datetime
,
to_datetime
=
to_datetime
,
user_filter
=
user_filter
user_filter
=
user_filter
_int
)
)
# Get model performance (with optional filters)
# Get model performance (with optional filters)
...
@@ -1991,21 +2042,21 @@ async def dashboard_analytics(
...
@@ -1991,21 +2042,21 @@ async def dashboard_analytics(
model_filter
=
model_filter
,
model_filter
=
model_filter
,
rotation_filter
=
rotation_filter
,
rotation_filter
=
rotation_filter
,
autoselect_filter
=
autoselect_filter
,
autoselect_filter
=
autoselect_filter
,
user_filter
=
user_filter
user_filter
=
user_filter
_int
)
)
# Get cost overview
# 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
# 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
# Get date range usage summary
date_range_usage
=
None
date_range_usage
=
None
if
from_datetime
or
to_datetime
:
if
from_datetime
or
to_datetime
:
start
=
from_datetime
or
(
datetime
.
now
()
-
timedelta
(
days
=
1
))
start
=
from_datetime
or
(
datetime
.
now
()
-
timedelta
(
days
=
1
))
end
=
to_datetime
or
datetime
.
now
()
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
(
return
templates
.
TemplateResponse
(
request
=
request
,
request
=
request
,
...
@@ -2032,7 +2083,8 @@ async def dashboard_analytics(
...
@@ -2032,7 +2083,8 @@ async def dashboard_analytics(
"selected_model"
:
model_filter
,
"selected_model"
:
model_filter
,
"selected_rotation"
:
rotation_filter
,
"selected_rotation"
:
rotation_filter
,
"selected_autoselect"
:
autoselect_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:
...
@@ -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)
# For non-remember-me sessions, set expiry to 2 weeks (default session length)
request
.
session
[
'expires_at'
]
=
int
(
time
.
time
())
+
14
*
24
*
60
*
60
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
# Check if email is verified
if
not
user
[
'email_verified'
]:
if
not
user
[
'email_verified'
]:
# Check if account is expired (24 hours old and unverified)
# 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
...
@@ -3236,6 +3295,13 @@ async def oauth2_google_callback(request: Request, code: str = Query(...), state
request
.
session
[
'user_id'
]
=
existing_user
[
'id'
]
request
.
session
[
'user_id'
]
=
existing_user
[
'id'
]
request
.
session
[
'email_verified'
]
=
True
# OAuth2 users have verified emails
request
.
session
[
'email_verified'
]
=
True
# OAuth2 users have verified emails
request
.
session
[
'expires_at'
]
=
int
(
time
.
time
())
+
14
*
24
*
60
*
60
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
:
else
:
# New user - create account automatically (no password required)
# New user - create account automatically (no password required)
if
not
email_verified
:
if
not
email_verified
:
...
@@ -3244,18 +3310,19 @@ async def oauth2_google_callback(request: Request, code: str = Query(...), state
...
@@ -3244,18 +3310,19 @@ async def oauth2_google_callback(request: Request, code: str = Query(...), state
name
=
"dashboard/login.html"
,
name
=
"dashboard/login.html"
,
context
=
{
"request"
:
request
,
"config"
:
config
,
"error"
:
"Google email must be verified to create an account"
}
context
=
{
"request"
:
request
,
"config"
:
config
,
"error"
:
"Google email must be verified to create an account"
}
)
)
# Generate secure random password for OAuth users (never used for login)
# Generate secure random password for OAuth users (never used for login)
random_password
=
secrets
.
token_urlsafe
(
32
)
random_password
=
secrets
.
token_urlsafe
(
32
)
password_hash
=
hashlib
.
sha256
(
random_password
.
encode
())
.
hexdigest
()
password_hash
=
hashlib
.
sha256
(
random_password
.
encode
())
.
hexdigest
()
# Generate clean username from display_name with email fallback
# Generate clean username from display_name with email fallback
google_username
=
db
.
generate_username_from_display_name
(
display_name
,
email
)
google_username
=
db
.
generate_username_from_display_name
(
display_name
,
email
)
final_username
=
db
.
find_unique_username
(
google_username
)
final_username
=
db
.
find_unique_username
(
google_username
)
# Create user with verified email (no verification required)
# Create user with verified email (no verification required)
user_id
=
db
.
create_user
(
final_username
,
password_hash
,
'user'
,
None
,
email
,
True
,
display_name
)
user_id
=
db
.
create_user
(
final_username
,
password_hash
,
'user'
,
None
,
email
,
True
,
display_name
)
# Login the new user
# Login the new user
request
.
session
[
'logged_in'
]
=
True
request
.
session
[
'logged_in'
]
=
True
request
.
session
[
'username'
]
=
final_username
request
.
session
[
'username'
]
=
final_username
...
@@ -3264,6 +3331,13 @@ async def oauth2_google_callback(request: Request, code: str = Query(...), state
...
@@ -3264,6 +3331,13 @@ async def oauth2_google_callback(request: Request, code: str = Query(...), state
request
.
session
[
'user_id'
]
=
user_id
request
.
session
[
'user_id'
]
=
user_id
request
.
session
[
'email_verified'
]
=
True
# OAuth2 users have verified emails
request
.
session
[
'email_verified'
]
=
True
# OAuth2 users have verified emails
request
.
session
[
'expires_at'
]
=
int
(
time
.
time
())
+
14
*
24
*
60
*
60
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
# Cleanup session data
request
.
session
.
pop
(
'oauth2_google'
,
None
)
request
.
session
.
pop
(
'oauth2_google'
,
None
)
...
@@ -3383,6 +3457,13 @@ async def oauth2_github_callback(request: Request, code: str = Query(...), state
...
@@ -3383,6 +3457,13 @@ async def oauth2_github_callback(request: Request, code: str = Query(...), state
request
.
session
[
'user_id'
]
=
existing_user
[
'id'
]
request
.
session
[
'user_id'
]
=
existing_user
[
'id'
]
request
.
session
[
'email_verified'
]
=
True
# OAuth2 users have verified emails
request
.
session
[
'email_verified'
]
=
True
# OAuth2 users have verified emails
request
.
session
[
'expires_at'
]
=
int
(
time
.
time
())
+
14
*
24
*
60
*
60
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
:
else
:
# New user - create account automatically (no password required)
# New user - create account automatically (no password required)
# Generate secure random password for OAuth users (never used for login)
# Generate secure random password for OAuth users (never used for login)
...
@@ -3404,7 +3485,14 @@ async def oauth2_github_callback(request: Request, code: str = Query(...), state
...
@@ -3404,7 +3485,14 @@ async def oauth2_github_callback(request: Request, code: str = Query(...), state
request
.
session
[
'user_id'
]
=
user_id
request
.
session
[
'user_id'
]
=
user_id
request
.
session
[
'email_verified'
]
=
True
# OAuth2 users have verified emails
request
.
session
[
'email_verified'
]
=
True
# OAuth2 users have verified emails
request
.
session
[
'expires_at'
]
=
int
(
time
.
time
())
+
14
*
24
*
60
*
60
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
# Cleanup session data
request
.
session
.
pop
(
'oauth2_github'
,
None
)
request
.
session
.
pop
(
'oauth2_github'
,
None
)
...
@@ -3639,7 +3727,7 @@ async def _auto_detect_provider_models(provider_key: str, provider: dict) -> lis
...
@@ -3639,7 +3727,7 @@ async def _auto_detect_provider_models(provider_key: str, provider: dict) -> lis
kilo_config
=
provider
.
get
(
'kilo_config'
,
{})
kilo_config
=
provider
.
get
(
'kilo_config'
,
{})
credentials_file
=
kilo_config
.
get
(
'credentials_file'
,
'~/.kilo_credentials.json'
)
credentials_file
=
kilo_config
.
get
(
'credentials_file'
,
'~/.kilo_credentials.json'
)
oauth2
=
KiloOAuth2
(
credentials_file
=
credentials_file
,
api_base
=
endpoint
)
oauth2
=
KiloOAuth2
(
credentials_file
=
credentials_file
,
api_base
=
endpoint
)
token
=
oauth2
.
get_valid_token
()
token
=
await
oauth2
.
get_valid_token
()
if
token
:
if
token
:
api_key
=
token
api_key
=
token
logger
.
info
(
f
"Using OAuth2 token for Kilo provider '{provider_key}'"
)
logger
.
info
(
f
"Using OAuth2 token for Kilo provider '{provider_key}'"
)
...
@@ -6992,7 +7080,7 @@ async def dashboard_pricing(request: Request):
...
@@ -6992,7 +7080,7 @@ async def dashboard_pricing(request: Request):
from
aisbf.database
import
get_database
from
aisbf.database
import
get_database
db
=
DatabaseRegistry
.
get_config_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'
))
current_tier
=
db
.
get_user_tier
(
request
.
session
.
get
(
'user_id'
))
# Get enabled payment gateways
# Get enabled payment gateways
...
@@ -7874,12 +7962,13 @@ async def v1_chat_completions(request: Request, body: ChatCompletionRequest):
...
@@ -7874,12 +7962,13 @@ async def v1_chat_completions(request: Request, body: ChatCompletionRequest):
body_dict
[
'model'
]
=
actual_model
body_dict
[
'model'
]
=
actual_model
# Get user-specific handler
# Get user-specific handler
user_id
=
getattr
(
request
.
state
,
'user_id'
,
None
)
user_id
=
getattr
(
request
.
state
,
'user_id'
,
None
)
token_id
=
getattr
(
request
.
state
,
'token_id'
,
None
)
handler
=
get_user_handler
(
'autoselect'
,
user_id
)
handler
=
get_user_handler
(
'autoselect'
,
user_id
)
if
body
.
stream
:
if
body
.
stream
:
return
await
handler
.
handle_autoselect_streaming_request
(
actual_model
,
body_dict
)
return
await
handler
.
handle_autoselect_streaming_request
(
actual_model
,
body_dict
)
else
:
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})
# PATH 2: Check if it's a rotation (format: rotation/{name})
if
provider_id
==
"rotation"
:
if
provider_id
==
"rotation"
:
...
@@ -7891,8 +7980,9 @@ async def v1_chat_completions(request: Request, body: ChatCompletionRequest):
...
@@ -7891,8 +7980,9 @@ async def v1_chat_completions(request: Request, body: ChatCompletionRequest):
body_dict
[
'model'
]
=
actual_model
body_dict
[
'model'
]
=
actual_model
# Get user-specific handler
# Get user-specific handler
user_id
=
getattr
(
request
.
state
,
'user_id'
,
None
)
user_id
=
getattr
(
request
.
state
,
'user_id'
,
None
)
token_id
=
getattr
(
request
.
state
,
'token_id'
,
None
)
handler
=
get_user_handler
(
'rotation'
,
user_id
)
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})
# PATH 1: Direct provider model (format: {provider}/{model})
if
provider_id
not
in
config
.
providers
:
if
provider_id
not
in
config
.
providers
:
...
@@ -8405,11 +8495,12 @@ async def rotation_chat_completions(request: Request, body: ChatCompletionReques
...
@@ -8405,11 +8495,12 @@ async def rotation_chat_completions(request: Request, body: ChatCompletionReques
try
:
try
:
# Get user-specific handler
# Get user-specific handler
user_id
=
getattr
(
request
.
state
,
'user_id'
,
None
)
user_id
=
getattr
(
request
.
state
,
'user_id'
,
None
)
token_id
=
getattr
(
request
.
state
,
'token_id'
,
None
)
handler
=
get_user_handler
(
'rotation'
,
user_id
)
handler
=
get_user_handler
(
'rotation'
,
user_id
)
# The rotation handler handles streaming internally and returns
# The rotation handler handles streaming internally and returns
# a StreamingResponse for streaming requests or a dict for non-streaming
# 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)}"
)
logger
.
debug
(
f
"Rotation response result type: {type(result)}"
)
return
result
return
result
except
Exception
as
e
:
except
Exception
as
e
:
...
@@ -8482,6 +8573,7 @@ async def autoselect_chat_completions(request: Request, body: ChatCompletionRequ
...
@@ -8482,6 +8573,7 @@ async def autoselect_chat_completions(request: Request, body: ChatCompletionRequ
# Get user-specific handler
# Get user-specific handler
user_id
=
getattr
(
request
.
state
,
'user_id'
,
None
)
user_id
=
getattr
(
request
.
state
,
'user_id'
,
None
)
token_id
=
getattr
(
request
.
state
,
'token_id'
,
None
)
handler
=
get_user_handler
(
'autoselect'
,
user_id
)
handler
=
get_user_handler
(
'autoselect'
,
user_id
)
# Check if the model name corresponds to an autoselect configuration
# Check if the model name corresponds to an autoselect configuration
...
@@ -8502,7 +8594,7 @@ async def autoselect_chat_completions(request: Request, body: ChatCompletionRequ
...
@@ -8502,7 +8594,7 @@ async def autoselect_chat_completions(request: Request, body: ChatCompletionRequ
return
await
handler
.
handle_autoselect_streaming_request
(
body
.
model
,
body_dict
)
return
await
handler
.
handle_autoselect_streaming_request
(
body
.
model
,
body_dict
)
else
:
else
:
logger
.
debug
(
"Handling non-streaming autoselect request"
)
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}"
)
logger
.
debug
(
f
"Autoselect response result: {result}"
)
return
result
return
result
except
Exception
as
e
:
except
Exception
as
e
:
...
@@ -8550,6 +8642,7 @@ async def chat_completions(provider_id: str, request: Request, body: ChatComplet
...
@@ -8550,6 +8642,7 @@ async def chat_completions(provider_id: str, request: Request, body: ChatComplet
# Check if it's an autoselect
# 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
):
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"
)
logger
.
debug
(
"Handling autoselect request"
)
token_id
=
getattr
(
request
.
state
,
'token_id'
,
None
)
handler
=
get_user_handler
(
'autoselect'
,
user_id
)
handler
=
get_user_handler
(
'autoselect'
,
user_id
)
try
:
try
:
if
body
.
stream
:
if
body
.
stream
:
...
@@ -8557,7 +8650,7 @@ async def chat_completions(provider_id: str, request: Request, body: ChatComplet
...
@@ -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
)
return
await
handler
.
handle_autoselect_streaming_request
(
provider_id
,
body_dict
)
else
:
else
:
logger
.
debug
(
"Handling non-streaming autoselect request"
)
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}"
)
logger
.
debug
(
f
"Autoselect response result: {result}"
)
return
result
return
result
except
Exception
as
e
:
except
Exception
as
e
:
...
@@ -8568,8 +8661,9 @@ async def chat_completions(provider_id: str, request: Request, body: ChatComplet
...
@@ -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
):
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
.
info
(
f
"Provider ID '{provider_id}' found in rotations"
)
logger
.
debug
(
"Handling rotation request"
)
logger
.
debug
(
"Handling rotation request"
)
token_id
=
getattr
(
request
.
state
,
'token_id'
,
None
)
handler
=
get_user_handler
(
'rotation'
,
user_id
)
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
# Check if it's a provider
handler
=
get_user_handler
(
'request'
,
user_id
)
handler
=
get_user_handler
(
'request'
,
user_id
)
...
@@ -10596,11 +10690,12 @@ async def user_chat_completions_by_username(request: Request, username: str, bod
...
@@ -10596,11 +10690,12 @@ async def user_chat_completions_by_username(request: Request, username: str, bod
detail
=
f
"User autoselect '{actual_model}' not found. Available: {list(handler.user_autoselects.keys())}"
detail
=
f
"User autoselect '{actual_model}' not found. Available: {list(handler.user_autoselects.keys())}"
)
)
body_dict
[
'model'
]
=
actual_model
body_dict
[
'model'
]
=
actual_model
if
body
.
stream
:
if
body
.
stream
:
return
await
handler
.
handle_autoselect_streaming_request
(
actual_model
,
body_dict
)
return
await
handler
.
handle_autoselect_streaming_request
(
actual_model
,
body_dict
)
else
:
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"
:
if
provider_id
==
"user-rotation"
:
handler
=
get_user_handler
(
'rotation'
,
target_user_id
)
handler
=
get_user_handler
(
'rotation'
,
target_user_id
)
...
@@ -10610,7 +10705,8 @@ async def user_chat_completions_by_username(request: Request, username: str, bod
...
@@ -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())}"
detail
=
f
"User rotation '{actual_model}' not found. Available: {list(handler.user_rotations.keys())}"
)
)
body_dict
[
'model'
]
=
actual_model
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"
:
if
provider_id
==
"user-provider"
:
handler
=
get_user_handler
(
'request'
,
target_user_id
)
handler
=
get_user_handler
(
'request'
,
target_user_id
)
...
@@ -10644,12 +10740,13 @@ async def user_chat_completions_by_username(request: Request, username: str, bod
...
@@ -10644,12 +10740,13 @@ async def user_chat_completions_by_username(request: Request, username: str, bod
)
)
handler
=
get_user_handler
(
'autoselect'
,
None
)
handler
=
get_user_handler
(
'autoselect'
,
None
)
body_dict
[
'model'
]
=
actual_model
body_dict
[
'model'
]
=
actual_model
if
body
.
stream
:
if
body
.
stream
:
return
await
handler
.
handle_autoselect_streaming_request
(
actual_model
,
body_dict
)
return
await
handler
.
handle_autoselect_streaming_request
(
actual_model
,
body_dict
)
else
:
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
provider_id
==
"rotation"
:
if
actual_model
not
in
config
.
rotations
:
if
actual_model
not
in
config
.
rotations
:
raise
HTTPException
(
raise
HTTPException
(
...
@@ -10658,7 +10755,8 @@ async def user_chat_completions_by_username(request: Request, username: str, bod
...
@@ -10658,7 +10755,8 @@ async def user_chat_completions_by_username(request: Request, username: str, bod
)
)
handler
=
get_user_handler
(
'rotation'
,
None
)
handler
=
get_user_handler
(
'rotation'
,
None
)
body_dict
[
'model'
]
=
actual_model
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
:
if
provider_id
in
config
.
providers
:
provider_config
=
config
.
get_provider
(
provider_id
)
provider_config
=
config
.
get_provider
(
provider_id
)
...
@@ -10854,11 +10952,12 @@ async def user_chat_completions(request: Request, username: str, body: ChatCompl
...
@@ -10854,11 +10952,12 @@ async def user_chat_completions(request: Request, username: str, body: ChatCompl
detail
=
f
"User autoselect '{actual_model}' not found. Available: {list(handler.user_autoselects.keys())}"
detail
=
f
"User autoselect '{actual_model}' not found. Available: {list(handler.user_autoselects.keys())}"
)
)
body_dict
[
'model'
]
=
actual_model
body_dict
[
'model'
]
=
actual_model
if
body
.
stream
:
if
body
.
stream
:
return
await
handler
.
handle_autoselect_streaming_request
(
actual_model
,
body_dict
)
return
await
handler
.
handle_autoselect_streaming_request
(
actual_model
,
body_dict
)
else
:
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})
# Handle user rotation (format: user-rotation/{name})
if
provider_id
==
"user-rotation"
:
if
provider_id
==
"user-rotation"
:
...
@@ -10869,7 +10968,8 @@ async def user_chat_completions(request: Request, username: str, body: ChatCompl
...
@@ -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())}"
detail
=
f
"User rotation '{actual_model}' not found. Available: {list(handler.user_rotations.keys())}"
)
)
body_dict
[
'model'
]
=
actual_model
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})
# Handle user provider (format: user-provider/{name})
if
provider_id
==
"user-provider"
:
if
provider_id
==
"user-provider"
:
...
@@ -10907,11 +11007,12 @@ async def user_chat_completions(request: Request, username: str, body: ChatCompl
...
@@ -10907,11 +11007,12 @@ async def user_chat_completions(request: Request, username: str, body: ChatCompl
)
)
handler
=
get_user_handler
(
'autoselect'
,
None
)
handler
=
get_user_handler
(
'autoselect'
,
None
)
body_dict
[
'model'
]
=
actual_model
body_dict
[
'model'
]
=
actual_model
if
body
.
stream
:
if
body
.
stream
:
return
await
handler
.
handle_autoselect_streaming_request
(
actual_model
,
body_dict
)
return
await
handler
.
handle_autoselect_streaming_request
(
actual_model
,
body_dict
)
else
:
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
# Handle global rotation
if
provider_id
==
"rotation"
:
if
provider_id
==
"rotation"
:
...
@@ -10922,7 +11023,8 @@ async def user_chat_completions(request: Request, username: str, body: ChatCompl
...
@@ -10922,7 +11023,8 @@ async def user_chat_completions(request: Request, username: str, body: ChatCompl
)
)
handler
=
get_user_handler
(
'rotation'
,
None
)
handler
=
get_user_handler
(
'rotation'
,
None
)
body_dict
[
'model'
]
=
actual_model
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
# Handle global provider
if
provider_id
in
config
.
providers
:
if
provider_id
in
config
.
providers
:
...
@@ -11920,11 +12022,11 @@ async def dashboard_kilo_auth_status(request: Request):
...
@@ -11920,11 +12022,11 @@ async def dashboard_kilo_auth_status(request: Request):
# Config admin or no database credentials: check file
# Config admin or no database credentials: check file
auth
=
KiloOAuth2
(
credentials_file
=
credentials_file
)
auth
=
KiloOAuth2
(
credentials_file
=
credentials_file
)
# Check if authenticated
# Check if authenticated
if
auth
.
is_authenticated
():
if
auth
.
is_authenticated
():
# Try to get a valid token (will refresh if needed)
# 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
:
if
token
:
# Get token expiration info
# Get token expiration info
expires_at
=
auth
.
credentials
.
get
(
'expires'
,
0
)
expires_at
=
auth
.
credentials
.
get
(
'expires'
,
0
)
...
@@ -12240,7 +12342,7 @@ async def dashboard_codex_auth_status(request: Request):
...
@@ -12240,7 +12342,7 @@ async def dashboard_codex_auth_status(request: Request):
# Check if authenticated
# Check if authenticated
if
auth
.
is_authenticated
():
if
auth
.
is_authenticated
():
# Try to get a valid token (will refresh if needed)
# 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
:
if
token
:
# Get user email from ID token
# Get user email from ID token
email
=
auth
.
get_user_email
()
email
=
auth
.
get_user_email
()
...
...
pyproject.toml
View file @
fe8b625a
...
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
...
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
[project]
name
=
"aisbf"
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"
description
=
"AISBF - AI Service Broker Framework || AI Should Be Free - A modular proxy server for managing multiple AI provider integrations"
readme
=
"README.md"
readme
=
"README.md"
license
=
"GPL-3.0-or-later"
license
=
"GPL-3.0-or-later"
...
...
setup.py
View file @
fe8b625a
...
@@ -49,7 +49,7 @@ class InstallCommand(_install):
...
@@ -49,7 +49,7 @@ class InstallCommand(_install):
setup
(
setup
(
name
=
"aisbf"
,
name
=
"aisbf"
,
version
=
"0.99.2
8
"
,
version
=
"0.99.2
9
"
,
author
=
"AISBF Contributors"
,
author
=
"AISBF Contributors"
,
author_email
=
"stefy@nexlab.net"
,
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"
,
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/>.
...
@@ -25,7 +25,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
<style>
<style>
*
{
margin
:
0
;
padding
:
0
;
box-sizing
:
border-box
;
}
*
{
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
;
}
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
{
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
h1
{
font-size
:
24px
;
font-weight
:
600
;
display
:
inline-block
;
}
.header-actions
{
float
:
right
;
}
.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/>.
...
@@ -18,6 +18,21 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
{% block title %}Analytics - AISBF Dashboard{% endblock %}
{% 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 %}
{% block content %}
<h2
style=
"margin-bottom: 30px;"
>
Token Usage Analytics
</h2>
<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/>.
...
@@ -68,7 +83,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
</span>
</span>
{% if date_range_usage %}
{% if date_range_usage %}
<span
style=
"margin-left: 20px; color: #a0a0a0;"
>
<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>
</span>
{% endif %}
{% endif %}
</div>
</div>
...
@@ -127,12 +142,30 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
...
@@ -127,12 +142,30 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
{% if is_admin %}
{% if is_admin %}
<div
style=
"flex: 1; min-width: 150px;"
>
<div
style=
"flex: 1; min-width: 150px;"
>
<label
style=
"display: block; margin-bottom: 5px; color: #a0a0a0; font-size: 14px;"
>
User
</label>
<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>
<option
value=
""
>
All Users
</option>
{% for user in available_users %}
{% 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>
<option
value=
"{{ user.id }}"
{%
if
selected_user =
=
user
.
id
%}
selected
{%
endif
%}
>
{{ user.username }}{% if user.role == 'admin' %} (admin){% endif %}
</option>
{% endfor %}
{% endfor %}
</select>
</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>
</div>
{% endif %}
{% endif %}
...
@@ -174,6 +207,195 @@ document.getElementById('timeRangeSelect').addEventListener('change', function()
...
@@ -174,6 +207,195 @@ document.getElementById('timeRangeSelect').addEventListener('change', function()
customRange
.
style
.
display
=
'none'
;
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>
</script>
{% if recommendations %}
{% if recommendations %}
...
@@ -203,6 +425,9 @@ document.getElementById('timeRangeSelect').addEventListener('change', function()
...
@@ -203,6 +425,9 @@ document.getElementById('timeRangeSelect').addEventListener('change', function()
<th>
Errors
</th>
<th>
Errors
</th>
<th>
Error Rate
</th>
<th>
Error Rate
</th>
<th>
Avg Latency
</th>
<th>
Avg Latency
</th>
<th>
Input Tokens
</th>
<th>
Output Tokens
</th>
<th>
Total Tokens
</th>
<th>
Tokens/Min
</th>
<th>
Tokens/Min
</th>
<th>
Tokens/Hour
</th>
<th>
Tokens/Hour
</th>
<th>
Tokens/Day
</th>
<th>
Tokens/Day
</th>
...
@@ -219,11 +444,50 @@ document.getElementById('timeRangeSelect').addEventListener('change', function()
...
@@ -219,11 +444,50 @@ document.getElementById('timeRangeSelect').addEventListener('change', function()
<td
{%
if
provider
.
avg_latency_ms
>
5000 %}style="color: #fcd34d;"{% endif %}>
<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 %}
{% 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>
<td>
{{ provider.tokens.TPM }}
</td>
<td><strong>
{{ format_tokens(provider.tokens.prompt or 0) }}
</strong></td>
<td>
{{ provider.tokens.TPH }}
</td>
<td><strong>
{{ format_tokens(provider.tokens.completion or 0) }}
</strong></td>
<td>
{{ provider.tokens.TPD }}
</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>
</tr>
{% endfor %}
{% 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>
</table>
{% else %}
{% else %}
<p
style=
"color: #a0a0a0;"
>
No provider statistics available yet. Make API requests to see analytics.
</p>
<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()
...
@@ -246,7 +510,7 @@ document.getElementById('timeRangeSelect').addEventListener('change', function()
<div
style=
"background: #0f3460; padding: 15px; border-radius: 8px;"
>
<div
style=
"background: #0f3460; padding: 15px; border-radius: 8px;"
>
<h4
style=
"font-size: 14px; margin-bottom: 5px;"
>
{{ pc.provider_id }}
</h4>
<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>
<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>
</div>
{% endfor %}
{% endfor %}
</div>
</div>
...
@@ -281,7 +545,7 @@ document.getElementById('timeRangeSelect').addEventListener('change', function()
...
@@ -281,7 +545,7 @@ document.getElementById('timeRangeSelect').addEventListener('change', function()
<td>
{{ model.context_size|default('N/A') }}
</td>
<td>
{{ model.context_size|default('N/A') }}
</td>
<td>
{{ model.condense_context|default('N/A') }}%
</td>
<td>
{{ model.condense_context|default('N/A') }}%
</td>
<td>
{{ model.condense_method|default('None') }}
</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 %}>
<td
{%
if
model
.
error_rate
>
0.1 %}style="color: #f87171;"{% endif %}>
{{ "%.1f"|format(model.error_rate * 100) }}%
{{ "%.1f"|format(model.error_rate * 100) }}%
</td>
</td>
...
...
templates/dashboard/pricing.html
View file @
fe8b625a
...
@@ -21,7 +21,7 @@
...
@@ -21,7 +21,7 @@
<!-- Paid Tiers -->
<!-- Paid Tiers -->
<div
style=
"display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 20px; margin-bottom: 40px;"
>
<div
style=
"display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 20px; margin-bottom: 40px;"
>
{% for tier in tiers %}
{% 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 %}"
>
<div
class=
"pricing-card {% if tier.is_recommended %}recommended{% endif %}"
>
{% if tier.is_recommended %}
{% 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;"
>
<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 @@
...
@@ -2,6 +2,21 @@
{% block title %}User Dashboard - AISBF{% endblock %}
{% 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 %}
{% block content %}
<div
class=
"container"
>
<div
class=
"container"
>
<h1>
User Dashboard
</h1>
<h1>
User Dashboard
</h1>
...
@@ -98,7 +113,7 @@
...
@@ -98,7 +113,7 @@
<div
class=
"stats-grid"
>
<div
class=
"stats-grid"
>
<div
class=
"stat-item"
>
<div
class=
"stat-item"
>
<h3>
Total Tokens Used
</h3>
<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>
<div
class=
"stat-item"
>
<div
class=
"stat-item"
>
<h3>
Requests Today
</h3>
<h3>
Requests Today
</h3>
...
...
templates/dashboard/users.html
View file @
fe8b625a
...
@@ -229,23 +229,60 @@ function updateUserTier(userId, tierId) {
...
@@ -229,23 +229,60 @@ function updateUserTier(userId, tierId) {
.
then
(
response
=>
response
.
json
())
.
then
(
response
=>
response
.
json
())
.
then
(
data
=>
{
.
then
(
data
=>
{
if
(
data
.
success
)
{
if
(
data
.
success
)
{
// Show success message briefly
// Show success notification
const
msg
=
document
.
createElement
(
'div'
);
showNotification
(
'Tier updated successfully'
,
'success'
);
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
);
}
else
{
}
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
location
.
reload
();
// Reload to reset dropdown
}
}
})
})
.
catch
(
error
=>
{
.
catch
(
error
=>
{
alert
(
'Error: '
+
error
);
showNotification
(
'Error: '
+
error
,
'error'
);
location
.
reload
();
// Reload to reset dropdown
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
// Close modal when clicking outside
window
.
onclick
=
function
(
event
)
{
window
.
onclick
=
function
(
event
)
{
const
modal
=
document
.
getElementById
(
'edit-modal'
);
const
modal
=
document
.
getElementById
(
'edit-modal'
);
...
@@ -269,6 +306,43 @@ th {
...
@@ -269,6 +306,43 @@ th {
background
:
#0f3460
;
background
:
#0f3460
;
font-weight
:
600
;
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 */
/* Prevent browser autofill from overriding dark theme */
input
:-webkit-autofill
,
input
:-webkit-autofill
,
input
:-webkit-autofill:hover
,
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