From 18a055ee2c821152853fec64a4ab73387aca89cf Mon Sep 17 00:00:00 2001 From: Shroominic Date: Mon, 22 Sep 2025 12:58:53 +0100 Subject: [PATCH] add models.json batch paste --- routstr/core/admin.py | 134 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 134 insertions(+) diff --git a/routstr/core/admin.py b/routstr/core/admin.py index 7961797..0b1245e 100644 --- a/routstr/core/admin.py +++ b/routstr/core/admin.py @@ -988,6 +988,10 @@ DASHBOARD_MODELS_JS: str = """ if (event.target == createModal) { closeCreateModel(); } + const batchModal = document.getElementById('model-batch-modal'); + if (event.target == batchModal) { + closeBatchModal(); + } } function openCreateModel() { @@ -1089,6 +1093,71 @@ DASHBOARD_MODELS_JS: str = """ btn.textContent = '➕ Create'; } } + + function openBatchModal() { + const modal = document.getElementById('model-batch-modal'); + if (!modal) { alert('Batch modal not found'); return; } + const err = document.getElementById('batch-error'); + if (err) { err.style.display = 'none'; err.textContent = ''; } + const textarea = document.getElementById('batch-json'); + if (textarea && !textarea.value.trim()) { + const sample = { + models: [ + { + id: 'provider/model-id', + name: 'Model Name', + description: 'Description', + created: Math.floor(Date.now()/1000), + context_length: 0, + architecture: { modality: 'text', input_modalities: ['text'], output_modalities: ['text'], tokenizer: '', instruct_type: null }, + pricing: { prompt: 0.0, completion: 0.0, request: 0.0, image: 0.0, web_search: 0.0, internal_reasoning: 0.0 }, + per_request_limits: null, + top_provider: null + } + ] + }; + textarea.value = JSON.stringify(sample, null, 2); + } + modal.style.display = 'block'; + } + + function closeBatchModal() { + const modal = document.getElementById('model-batch-modal'); + if (modal) modal.style.display = 'none'; + } + + async function performBatchAdd() { + const textarea = document.getElementById('batch-json'); + const err = document.getElementById('batch-error'); + const btn = document.getElementById('batch-submit-btn'); + if (err) { err.style.display = 'none'; err.textContent = ''; } + if (btn) { btn.disabled = true; btn.textContent = 'Adding…'; } + try { + if (!textarea) throw new Error('Input not found'); + const data = JSON.parse(textarea.value); + if (!data || !Array.isArray(data.models) || data.models.length === 0) { + throw new Error('Payload must include a non-empty "models" array'); + } + const resp = await fetch('/admin/api/models/batch', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + credentials: 'same-origin', + body: JSON.stringify(data) + }); + if (!resp.ok) { + let errText = 'Failed to add models'; + try { const e = await resp.json(); if (e && e.detail) errText = typeof e.detail === 'string' ? e.detail : JSON.stringify(e.detail); } catch(_) {} + throw new Error(errText); + } + closeBatchModal(); + await fetchModels(); + } catch (e) { + if (err) { err.style.display = 'block'; err.textContent = e.message || String(e); } + else { alert(e.message || String(e)); } + } finally { + if (btn) { btn.disabled = false; btn.textContent = '➕ Add Models'; } + } + } """ @@ -1126,6 +1195,7 @@ def models_page() -> str:
+
@@ -1204,6 +1274,20 @@ def models_page() -> str: + + """ @@ -1261,6 +1345,56 @@ async def create_model_admin_api(payload: Model) -> dict[str, object]: return created_model.dict() if created_model else {"id": payload.id} # type: ignore +@admin_router.post("/api/models/batch", dependencies=[Depends(require_admin_api)]) +async def batch_create_models(payload: dict[str, object]) -> dict[str, int]: + models = payload.get("models") + if not isinstance(models, list) or not models: + raise HTTPException( + status_code=400, detail="Payload must include non-empty 'models' array" + ) + created = 0 + skipped = 0 + async with create_session() as session: + for m in models: + try: + model = Model(**m) # type: ignore[arg-type] + except Exception: + skipped += 1 + continue + exists = await session.get(ModelRow, model.id) + if exists: + skipped += 1 + continue + pricing_dict = model.pricing.dict() + for k in ("max_prompt_cost", "max_completion_cost", "max_cost"): + pricing_dict.pop(k, None) + row = ModelRow( + id=model.id, + name=model.name, + description=model.description, + created=int(model.created), + context_length=int(model.context_length), + architecture=json.dumps(model.architecture.dict()), + pricing=json.dumps(pricing_dict), + sats_pricing=None, + per_request_limits=( + json.dumps(model.per_request_limits) + if model.per_request_limits is not None + else None + ), + top_provider=( + json.dumps(model.top_provider.dict()) + if model.top_provider + else None + ), + ) + session.add(row) + created += 1 + if created: + await session.commit() + return {"created": created, "skipped": skipped} + + @admin_router.get( "/api/models/{model_id:path}", dependencies=[Depends(require_admin_api)] )