Commit b52224b8 authored by Your Name's avatar Your Name

Bump version to 0.99.8

Fixes:
- Kilo provider model prefetch at startup for OAuth2 credentials
- Rotations save endpoint 500 error (form parameter mismatch, config shadowing)
- Autoselect save endpoint config shadowing bug
- Analytics page Jinja2 TemplateSyntaxError (missing closing parenthesis)
- FormData JSON serialization in validation error handler
parent cb80847f
......@@ -54,7 +54,7 @@ from .auth.qwen import QwenOAuth2
from .handlers import RequestHandler, RotationHandler, AutoselectHandler
from .utils import count_messages_tokens, split_messages_into_chunks, get_max_request_tokens_for_model
__version__ = "0.99.5"
__version__ = "0.99.8"
__all__ = [
# Config
"config",
......
......@@ -953,8 +953,13 @@ async def startup_event():
kilo_config = getattr(provider_config, 'kilo_config', None)
credentials_file = None
api_base = getattr(provider_config, 'endpoint', 'https://api.kilo.ai')
if kilo_config and isinstance(kilo_config, dict):
credentials_file = kilo_config.get('credentials_file')
# Override api_base from kilo_config if present
if 'api_base' in kilo_config and kilo_config['api_base']:
api_base = kilo_config['api_base']
oauth2 = KiloOAuth2(credentials_file=credentials_file, api_base=api_base)
if oauth2.is_authenticated():
has_valid_auth = True
......@@ -1216,9 +1221,21 @@ async def validation_exception_handler(request: Request, exc: RequestValidationE
logger.error(f"Validation error details: {exc.errors()}")
logger.error(f"=== END VALIDATION ERROR ===")
# Convert FormData to plain dict for JSON serialization
body_data = None
if hasattr(exc, 'body'):
if isinstance(exc.body, dict):
body_data = exc.body
elif hasattr(exc.body, '_dict'):
body_data = exc.body._dict
elif hasattr(exc.body, 'items'):
body_data = dict(exc.body.items())
else:
body_data = str(exc.body)
return JSONResponse(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
content={"detail": exc.errors(), "body": exc.body}
content={"detail": exc.errors(), "body": body_data}
)
# CORS middleware
......@@ -1912,7 +1929,7 @@ async def dashboard_rotations(request: Request):
)
@app.post("/dashboard/rotations")
async def dashboard_rotations_save(request: Request, config_data: str = Form(...)):
async def dashboard_rotations_save(request: Request, config: str = Form(...)):
"""Save rotations configuration"""
auth_check = require_dashboard_auth(request)
if auth_check:
......@@ -1923,7 +1940,7 @@ async def dashboard_rotations_save(request: Request, config_data: str = Form(...
is_config_admin = current_user_id is None
try:
rotations_data = json.loads(config_data)
rotations_data = json.loads(config)
# Apply defaults: if condense_method is set but condense_context is not, default to 80
if 'rotations' in rotations_data:
......@@ -1954,19 +1971,21 @@ async def dashboard_rotations_save(request: Request, config_data: str = Form(...
logger.info(f"Saved {len(rotations)} rotation(s) to database for user {current_user_id}")
available_providers = list(config.providers.keys()) if config else []
# Get global config safely
from aisbf.config import config as global_config
available_providers = list(global_config.providers.keys()) if global_config else []
return templates.TemplateResponse(
request=request,
name="dashboard/rotations.html",
context={
"request": request,
"session": request.session,
"rotations_json": json.dumps(rotations_data),
"available_providers": json.dumps(available_providers),
"success": "Configuration saved successfully! Restart server for changes to take effect."
}
)
request=request,
name="dashboard/rotations.html",
context={
"request": request,
"session": request.session,
"rotations_json": json.dumps(rotations_data),
"available_providers": json.dumps(available_providers),
"success": "Configuration saved successfully! Restart server for changes to take effect."
}
)
except json.JSONDecodeError as e:
# Reload current config on error
current_user_id = request.session.get('user_id')
......@@ -2111,19 +2130,21 @@ async def dashboard_autoselect_save(request: Request, config: str = Form(...)):
logger.info(f"Saved {len(autoselect_data)} autoselect(s) to database for user {current_user_id}")
available_rotations = list(config.rotations.keys()) if config else []
# Get global config safely
from aisbf.config import config as global_config
available_rotations = list(global_config.rotations.keys()) if global_config else []
return templates.TemplateResponse(
request=request,
name="dashboard/autoselect.html",
context={
"request": request,
"session": request.session,
"autoselect_json": json.dumps(autoselect_data),
"available_rotations": json.dumps(available_rotations),
"success": "Configuration saved successfully! Restart server for changes to take effect."
}
)
request=request,
name="dashboard/autoselect.html",
context={
"request": request,
"session": request.session,
"autoselect_json": json.dumps(autoselect_data),
"available_rotations": json.dumps(available_rotations),
"success": "Configuration saved successfully! Restart server for changes to take effect."
}
)
except json.JSONDecodeError as e:
# Reload current config on error
current_user_id = request.session.get('user_id')
......
......@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "aisbf"
version = "0.99.6"
version = "0.99.8"
description = "AISBF - AI Service Broker Framework || AI Should Be Free - A modular proxy server for managing multiple AI provider integrations"
readme = "README.md"
license = "GPL-3.0-or-later"
......@@ -53,4 +53,4 @@ packages = ["aisbf", "aisbf.auth", "aisbf.providers", "aisbf.providers.kiro"]
py-modules = ["cli"]
[tool.setuptools.package-data]
aisbf = ["*.json", "streaming_optimization.py"]
\ No newline at end of file
aisbf = ["*.json", "streaming_optimization.py"]
......@@ -49,7 +49,7 @@ class InstallCommand(_install):
setup(
name="aisbf",
version="0.99.7",
version="0.99.8",
author="AISBF Contributors",
author_email="stefy@nexlab.net",
description="AISBF - AI Service Broker Framework || AI Should Be Free - A modular proxy server for managing multiple AI provider integrations",
......
......@@ -24,7 +24,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
<!-- Date Range Filter Section -->
<div style="background: #1a1a2e; padding: 20px; border-radius: 8px; margin-bottom: 30px;">
<h3 style="margin-bottom: 15px;">Filter by Date Range</h3>
<form method="get" action="{{ url_for(request, "/dashboard/analytics" style="display: flex; flex-wrap: wrap; gap: 15px; align-items: flex-end;">
<form method="get" action="{{ url_for(request, "/dashboard/analytics") }}" style="display: flex; flex-wrap: wrap; gap: 15px; align-items: flex-end;">
<div style="flex: 1; min-width: 200px;">
<label style="display: block; margin-bottom: 5px; color: #a0a0a0; font-size: 14px;">Time Range</label>
<select name="time_range" id="timeRangeSelect" style="width: 100%; padding: 10px; border-radius: 4px; background: #0f3460; color: white; border: 1px solid #2a4a7a;">
......@@ -78,7 +78,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
<!-- Filter by Provider/Model/Rotation/Autoselect -->
<div style="background: #1a1a2e; padding: 20px; border-radius: 8px; margin-bottom: 30px;">
<h3 style="margin-bottom: 15px;">Filter by Provider, Model, Rotation, Autoselect, or User</h3>
<form method="get" action="{{ url_for(request, "/dashboard/analytics" style="display: flex; flex-wrap: wrap; gap: 15px; align-items: flex-end;">
<form method="get" action="{{ url_for(request, "/dashboard/analytics") }}" style="display: flex; flex-wrap: wrap; gap: 15px; align-items: flex-end;">
<!-- Preserve date filter parameters -->
<input type="hidden" name="time_range" value="{{ selected_time_range }}">
{% if from_date %}<input type="hidden" name="from_date" value="{{ from_date }}">{% endif %}
......@@ -142,7 +142,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
{% if selected_provider or selected_model or selected_rotation or selected_autoselect or selected_user %}
<div>
<a href="{{ url_for(request, "/dashboard/analytics?time_range={{ selected_time_range }}{% if from_date %}&from_date={{ from_date }}{% endif %}{% if to_date %}&to_date={{ to_date }}{% endif %}" style="padding: 10px 20px; background: #7f8c8d; color: white; border: none; border-radius: 4px; text-decoration: none; display: inline-block;">
<a href="{{ url_for(request, "/dashboard/analytics?time_range=%s%s%s"|format(selected_time_range, "&from_date=" + from_date if from_date else "", "&to_date=" + to_date if to_date else "")) }}" style="padding: 10px 20px; background: #7f8c8d; color: white; border: none; border-radius: 4px; text-decoration: none; display: inline-block;">
Clear Filters
</a>
</div>
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment