Minimal template fixes

parent 6130a6db
......@@ -18,5 +18,6 @@ recursive-include templates *.css
recursive-include templates *.js
recursive-include static *.zip
recursive-include static *.js
recursive-include static *.css
recursive-include static/i18n *.json
recursive-include static/extension *.js *.json *.html *.md *.png *.svg
......@@ -962,6 +962,8 @@ class Analytics:
Returns:
List of model performance data
"""
start, end, _ = self._normalize_time_window('custom' if from_datetime or to_datetime else '24h', from_datetime, to_datetime)
# Always query token_usage for provider/model combinations within the time range
# context_dimensions is used only for metadata (context_size, condense settings)
with self.db._get_connection() as conn:
......@@ -975,12 +977,10 @@ class Analytics:
'''
params = []
if from_datetime:
query += f' AND timestamp >= {placeholder}'
params.append(self._format_timestamp(from_datetime))
if to_datetime:
query += f' AND timestamp <= {placeholder}'
params.append(self._format_timestamp(to_datetime))
query += f' AND timestamp >= {placeholder}'
params.append(self._format_timestamp(start))
query += f' AND timestamp <= {placeholder}'
params.append(self._format_timestamp(end))
if user_filter == -1:
query += ' AND user_id IS NULL'
elif user_filter is not None:
......@@ -1003,7 +1003,15 @@ class Analytics:
query += ' ORDER BY provider_id, model_name'
cursor.execute(query, params)
active_combos = {(row[0], row[1]): {'rotation_id': row[2], 'autoselect_id': row[3]} for row in cursor.fetchall()}
active_combos = [
{
'provider_id': row[0],
'model_name': row[1],
'rotation_id': row[2],
'autoselect_id': row[3],
}
for row in cursor.fetchall()
]
# Build context_dims lookup from context_dimensions table (for metadata only)
raw_dims = self.db.get_all_context_dimensions(user_filter=user_filter)
......@@ -1011,7 +1019,9 @@ class Analytics:
# Build context_dims from active token_usage combinations, enriched with context metadata
context_dims = []
for (pid, mname), extra in active_combos.items():
for extra in active_combos:
pid = extra['provider_id']
mname = extra['model_name']
meta = dim_lookup.get((pid, mname), {})
context_dims.append({
'provider_id': pid,
......@@ -1053,8 +1063,8 @@ class Analytics:
continue
combo_stats = next((item for item in self.get_all_providers_stats(
from_datetime,
to_datetime,
start,
end,
user_filter=user_filter,
provider_filter=provider_id,
model_filter=model_name,
......@@ -1237,8 +1247,8 @@ class Analytics:
start, end, _ = self._normalize_time_window('custom' if from_datetime or to_datetime else '24h', from_datetime, to_datetime)
provider_stats = self.get_all_providers_stats(
from_datetime,
to_datetime,
start,
end,
user_filter=user_filter,
provider_filter=provider_filter,
model_filter=model_filter,
......@@ -1271,9 +1281,6 @@ class Analytics:
if not tier_info:
continue
if tier_info.get('source') == 'default':
continue
cached_usage = None
try:
cached_row = self.db.get_provider_usage(None if user_filter == -1 else user_filter, provider_id)
......@@ -1290,6 +1297,10 @@ class Analytics:
if free_limit <= 0:
continue
premium_price = float(tier_info.get('premium_monthly_cost', 0) or 0)
if premium_price <= 0:
continue
if limit_type == 'tokens':
usage_amount = usage.get('tokens_used', 0) or 0
else:
......@@ -1302,9 +1313,11 @@ class Analytics:
if extra_free_tiers <= 0:
continue
if tier_info.get('source') == 'default' and usage_amount > free_limit * 10:
continue
saved_tokens = usage['tokens_used'] * extra_free_tiers
equivalent_token_savings += saved_tokens
premium_price = float(tier_info.get('premium_monthly_cost', 0) or 0)
premium_equivalent_cost = premium_price * extra_free_tiers
provider_equivalents.append({
'provider_id': provider_id,
......
......@@ -6,7 +6,7 @@ import time
import asyncio
from typing import Optional
from fastapi import APIRouter, HTTPException, Query, WebSocket, WebSocketDisconnect
from fastapi import APIRouter, HTTPException, Query, WebSocket, WebSocketDisconnect, Body
from fastapi.responses import JSONResponse
from aisbf.coderai_broker import broker
......@@ -17,6 +17,26 @@ router = APIRouter()
logger = logging.getLogger(__name__)
def _coderai_register_payload(
provider_id: str,
client_id: str,
username: str,
owner_user_id: Optional[int],
scope_name: str,
) -> dict:
return {
"v": 1,
"event": "registered",
"provider_id": provider_id,
"client_id": client_id,
"username": username,
"scope_name": scope_name,
"accepted": True,
"owner_user_id": owner_user_id,
"expires_at": int(time.time()) + 86400,
}
async def _coderai_broker_websocket_impl(websocket: WebSocket, scope_name: str):
provider_id = websocket.query_params.get("provider_id") or websocket.headers.get("x-coderai-provider-id") or "coderai"
client_id = websocket.query_params.get("client_id") or websocket.headers.get("x-coderai-client-id") or f"anon-{int(time.time())}"
......@@ -26,21 +46,14 @@ async def _coderai_broker_websocket_impl(websocket: WebSocket, scope_name: str):
if not valid:
await websocket.close(code=1008, reason=error or "registration rejected")
return
await websocket.accept()
expected_scope = scope_name
session = await broker.register(websocket, provider_id, client_id, metadata={"source": "websocket", "owner_user_id": owner_user_id, "username": username, "scope_name": expected_scope, "proxy_scheme": websocket.url.scheme})
await websocket.accept()
expected_scope = scope_name
session = await broker.register(websocket, provider_id, client_id, metadata={"source": "websocket", "owner_user_id": owner_user_id, "username": username, "scope_name": expected_scope, "proxy_scheme": websocket.url.scheme})
try:
await websocket.send_text(json.dumps({
"v": 1,
"event": "registered",
"session_id": session.session_id,
"provider_id": session.provider_id,
"client_id": session.client_id,
"username": username,
"scope_name": expected_scope,
"accepted": True,
}))
payload = _coderai_register_payload(session.provider_id, session.client_id, username, owner_user_id, expected_scope)
payload["session_id"] = session.session_id
await websocket.send_text(json.dumps(payload))
while True:
try:
raw = await asyncio.wait_for(websocket.receive_text(), timeout=1.0)
......@@ -78,14 +91,8 @@ async def _coderai_broker_websocket_impl(websocket: WebSocket, scope_name: str):
"request_id": message.get("request_id"),
"status": "ok",
"payload": {
"accepted": True,
**_coderai_register_payload(session.provider_id, session.client_id, username, owner_user_id, expected_scope),
"session_id": session.session_id,
"provider_id": session.provider_id,
"client_id": session.client_id,
"owner_user_id": owner_user_id,
"username": username,
"scope_name": expected_scope,
"expires_at": int(time.time()) + 86400,
},
}))
continue
......@@ -126,6 +133,30 @@ async def coderai_broker_websocket_user(websocket: WebSocket, username: str):
await _coderai_broker_websocket_impl(websocket, username)
@router.post("/api/coderai/register")
async def coderai_register_global(body: dict = Body(default={})): # nosec B008
provider_id = body.get("provider_id") or "coderai"
client_id = body.get("client_id") or f"anon-{int(time.time())}"
username = body.get("username") or "global"
presented_token = body.get("registration_token")
valid, owner_user_id, _provider_config, error = validate_coderai_registration_token(provider_id, presented_token, username=username)
if not valid:
raise HTTPException(status_code=403, detail=error or "registration rejected")
return _coderai_register_payload(provider_id, client_id, username, owner_user_id, "global")
@router.post("/api/u/{username}/coderai/register")
async def coderai_register_user(username: str, body: dict = Body(default={})): # nosec B008
provider_id = body.get("provider_id") or "coderai"
client_id = body.get("client_id") or f"anon-{int(time.time())}"
presented_token = body.get("registration_token")
effective_username = body.get("username") or username
valid, owner_user_id, _provider_config, error = validate_coderai_registration_token(provider_id, presented_token, username=effective_username)
if not valid:
raise HTTPException(status_code=403, detail=error or "registration rejected")
return _coderai_register_payload(provider_id, client_id, effective_username, owner_user_id, effective_username)
@router.get("/api/coderai/broker/sessions")
async def coderai_broker_sessions():
return {"sessions": await broker.list_sessions()}
......
This diff is collapsed.
......@@ -595,7 +595,7 @@ async def dashboard_studio(request: Request):
"session": request.session,
"__version__": __version__,
"studio_bootstrap_json": json.dumps(catalog),
"studio_root_path_json": json.dumps("/api/v1") if is_config_admin else json.dumps(f"/api/u/{request.session.get('username', '')}"),
"studio_root_path_json": json.dumps("/dashboard/api/studio") if is_config_admin else json.dumps(f"/dashboard/api/studio/u/{request.session.get('username', '')}"),
"studio_username_json": json.dumps(request.session.get("username", "")),
"studio_is_global_admin_json": json.dumps(is_config_admin),
"studio_system_prompt_json": json.dumps(studio_service.load_studio_system_prompt(scope, current_user_id)),
......
......@@ -47,7 +47,7 @@ class StudioService:
"category": "chat",
"endpoint": "/chat/completions",
"roles": [
{"key": "model", "label": "Chat model", "capabilities": ["text_generation"]},
{"key": "model", "label": "Chat model", "capabilities": ["text_generation", "chat"]},
],
},
{
......@@ -350,7 +350,7 @@ class StudioService:
"endpoint": "/pipelines/audio-understand",
"roles": [
{"key": "audio_model", "label": "Audio model", "capabilities": ["speech_to_text"]},
{"key": "text_model", "label": "Reasoning model", "capabilities": ["text_generation"], "optional": True},
{"key": "text_model", "label": "Reasoning model", "capabilities": ["text_generation", "chat"], "optional": True},
],
},
{
......@@ -749,9 +749,22 @@ class StudioService:
return record
def list_pipelines(self, scope: str, owner_id: Optional[int]) -> List[Dict[str, Any]]:
admin_rows = self._read_pipelines_json(self._admin_pipelines_path())
if self._uses_database(scope, owner_id):
return self._db().list_studio_pipelines(owner_id)
return self._read_pipelines_json(self._admin_pipelines_path())
user_rows = self._db().list_studio_pipelines(owner_id)
merged: List[Dict[str, Any]] = []
seen_ids = set()
for row in user_rows:
pipeline_id = row.get("id")
if pipeline_id:
seen_ids.add(pipeline_id)
merged.append(row)
for row in admin_rows:
if row.get("id") in seen_ids:
continue
merged.append(row)
return merged
return admin_rows
def delete_pipeline(self, scope: str, owner_id: Optional[int], pipeline_id: str) -> bool:
if self._uses_database(scope, owner_id):
......@@ -766,7 +779,9 @@ class StudioService:
def get_pipeline(self, scope: str, owner_id: Optional[int], pipeline_id: str) -> Optional[Dict[str, Any]]:
if self._uses_database(scope, owner_id):
return self._db().get_studio_pipeline(owner_id, pipeline_id)
user_pipeline = self._db().get_studio_pipeline(owner_id, pipeline_id)
if user_pipeline:
return user_pipeline
rows = self.list_pipelines(scope, owner_id)
for row in rows:
if row.get("id") == pipeline_id:
......@@ -1142,7 +1157,7 @@ class StudioService:
{"type": "img-edit", "label": "Image edit", "params": [["model", "text", "Model", ""], ["image", "ref", "Image ref", "{{input}}"], ["prompt", "textarea", "Prompt", "Enhance this image"]]},
{"type": "img-faceswap", "label": "Face swap", "params": [["model", "text", "Model", ""], ["source_face", "ref", "Source face", "{{input}}"], ["target", "ref", "Target", "{{step0.url}}"], ["target_type", "select:image|video", "Target type", "image"]]},
{"type": "vid-t2v", "label": "Text to video", "params": [["model", "text", "Model", ""], ["prompt", "textarea", "Prompt", "{{input}}"]]},
{"type": "vid-dub", "label": "Video dub", "params": [["video_model", "text", "Video model", ""], ["stt_model", "text", "STT model", ""], ["tts_model", "text", "TTS model", ""], ["video", "ref", "Video ref", "{{input}}"], ["source_lang", "text", "Source language", ""], ["target_lang", "text", "Target language", "en"], ["burn_subtitles", "checkbox", "Burn subtitles", false]]},
{"type": "vid-dub", "label": "Video dub", "params": [["video_model", "text", "Video model", ""], ["stt_model", "text", "STT model", ""], ["tts_model", "text", "TTS model", ""], ["video", "ref", "Video ref", "{{input}}"], ["source_lang", "text", "Source language", ""], ["target_lang", "text", "Target language", "en"], ["burn_subtitles", "checkbox", "Burn subtitles", False]]},
{"type": "aud-gen", "label": "Audio generate", "params": [["model", "text", "Model", ""], ["prompt", "textarea", "Prompt", "{{input}}"]]},
{"type": "aud-tts", "label": "Text to speech", "params": [["model", "text", "Model", ""], ["input", "textarea", "Input text", "{{input}}"], ["voice", "text", "Voice", "alloy"]]},
{"type": "aud-stt", "label": "Transcribe", "params": [["model", "text", "Model", ""], ["file", "ref", "Audio ref", "{{input}}"]]},
......
......@@ -341,6 +341,10 @@ setup(
'static/aisbf-oauth2-extension.zip',
'static/i18n.js',
]),
('share/aisbf/static/dashboard', [
'static/dashboard/studio.css',
'static/dashboard/studio.js',
]),
('share/aisbf/static/i18n', [
'static/i18n/af.json',
'static/i18n/ar.json',
......
This diff is collapsed.
......@@ -342,7 +342,7 @@ function escHtml(s) {
if (query.length === 0) {
// Show all users option
resultsDiv.innerHTML = '<div class="user-result-item" data-user-id="" data-username=window.i18n.t('analytics.all_users') style="padding: 10px; cursor: pointer; border-bottom: 1px solid #2a4a7a;">All Users</div>';
resultsDiv.innerHTML = '<div class="user-result-item" data-user-id="" data-username="" 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() {
......@@ -369,7 +369,7 @@ function escHtml(s) {
// Handle focus
searchInput.addEventListener('focus', function() {
if (this.value.trim().length === 0) {
resultsDiv.innerHTML = '<div class="user-result-item" data-user-id="" data-username=window.i18n.t('analytics.all_users') style="padding: 10px; cursor: pointer; border-bottom: 1px solid #2a4a7a;">All Users</div>';
resultsDiv.innerHTML = '<div class="user-result-item" data-user-id="" data-username="" 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() {
......
......@@ -67,6 +67,10 @@ const BASE_PATH = {{ (request.scope.get('root_path', '') or '') | tojson }};
function _providerIdFromModel(modelId) {
// model IDs in autoselect are like "provider_id/model_name"
if (!modelId) return null;
if (typeof modelId === 'object') {
modelId = modelId.model_id || modelId.id || modelId.name || '';
}
if (typeof modelId !== 'string') return null;
const parts = modelId.split('/');
return parts.length > 1 ? parts[0] : null;
}
......@@ -141,7 +145,7 @@ async function _refreshAutoselectUsage() {
const pids = new Set();
Object.values(autoselectConfig).forEach(as => {
(as.available_models || []).forEach(m => {
const pid = _providerIdFromModel(m.id || m);
const pid = _providerIdFromModel(m.model_id || m.id || m);
if (_providerSupportsUsage(pid)) pids.add(pid);
});
});
......@@ -337,6 +341,8 @@ function renderAutoselectList() {
}
}
document.addEventListener('i18n:ready', renderAutoselectList);
function toggleAutoselect(key) {
if (expandedAutoselects.has(key)) {
expandedAutoselects.delete(key);
......
......@@ -335,6 +335,8 @@ function renderRotationsList() {
}
}
document.addEventListener('i18n:ready', renderRotationsList);
function toggleRotation(key) {
if (expandedRotations.has(key)) {
expandedRotations.delete(key);
......
......@@ -192,6 +192,10 @@ const BASE_PATH = {{ (request.scope.get('root_path', '') or '') | tojson }};
function _providerIdFromModel(modelId) {
if (!modelId) return null;
if (typeof modelId === 'object') {
modelId = modelId.model_id || modelId.id || modelId.name || '';
}
if (typeof modelId !== 'string') return null;
const parts = modelId.split('/');
return parts.length > 1 ? parts[0] : null;
}
......@@ -347,6 +351,8 @@ function renderAutoselectList() {
}
}
document.addEventListener('i18n:ready', renderAutoselectList);
function toggleAutoselect(key) {
if (expandedAutoselects.has(key)) {
expandedAutoselects.delete(key);
......
......@@ -327,6 +327,8 @@ function renderRotationsList() {
}
}
document.addEventListener('i18n:ready', renderRotationsList);
function toggleRotation(key) {
if (expandedRotations.has(key)) {
expandedRotations.delete(key);
......
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