forked from retoor/devplacepy
feat: restrict backup archive download to primary admin and hide admin-hidden projects from other admins
- Add `get_admin_uids()` and `get_primary_admin_uid()` to database.py for resolving the earliest-created admin - Modify `can_view_project()` in content.py so a project hidden by an admin is invisible to other admins (both web UI and REST API) - Update `_download_url()` and `_backup_payload()` in admin/backups.py to accept a `can_download` flag, gating the download endpoint with `is_primary_admin()` - Remove `role` from `_user_facts()` in docs_live.py to avoid leaking admin status in live docs - Update doc summaries in docs_api.py to reflect the new admin-visibility and backup-download semantics
This commit is contained in:
@@ -130,9 +130,13 @@ def build_analytics(
|
||||
) -> dict:
|
||||
if GATEWAY_LEDGER not in db.tables:
|
||||
return empty_payload(hours)
|
||||
hours = max(1, min(hours, MAX_WINDOW_HOURS))
|
||||
now = _now()
|
||||
cutoff = _iso(now - timedelta(hours=hours))
|
||||
if hours <= 0:
|
||||
hours = 0
|
||||
cutoff = "0000-01-01T00:00:00+00:00"
|
||||
else:
|
||||
hours = max(1, min(hours, MAX_WINDOW_HOURS))
|
||||
cutoff = _iso(now - timedelta(hours=hours))
|
||||
rows = _ledger_rows(cutoff)
|
||||
if not rows:
|
||||
return empty_payload(hours)
|
||||
|
||||
@@ -27,6 +27,7 @@ PRICE_OUTPUT_PER_M_DEFAULT = 0.28
|
||||
VISION_PRICE_INPUT_PER_M_DEFAULT = 0.0
|
||||
VISION_PRICE_OUTPUT_PER_M_DEFAULT = 0.0
|
||||
EMBED_PRICE_INPUT_PER_M_DEFAULT = 0.01
|
||||
RSEARCH_COST_PER_CALL_DEFAULT = 0.0
|
||||
|
||||
USAGE_RETENTION_HOURS_DEFAULT = 720
|
||||
|
||||
|
||||
@@ -267,6 +267,15 @@ class GatewayService(BaseService):
|
||||
help="Fallback only; used when the embeddings upstream returns no native cost.",
|
||||
group="Pricing",
|
||||
),
|
||||
ConfigField(
|
||||
"gateway_rsearch_cost_per_call",
|
||||
"rsearch cost / call ($)",
|
||||
type="float",
|
||||
default=config.RSEARCH_COST_PER_CALL_DEFAULT,
|
||||
minimum=0,
|
||||
help="Flat cost attributed to each external rsearch call (web search / AI answer / chat / image describe), recorded under backend 'rsearch' so external AI spend appears in AI usage.",
|
||||
group="Pricing",
|
||||
),
|
||||
ConfigField(
|
||||
"gateway_max_retries",
|
||||
"Max retries",
|
||||
|
||||
@@ -313,6 +313,90 @@ class GatewayUsageLedger:
|
||||
logger.warning("gateway usage record failed: %s", exc)
|
||||
return None
|
||||
|
||||
def record_external(
|
||||
self,
|
||||
*,
|
||||
owner_kind: str,
|
||||
owner_id: str,
|
||||
backend: str,
|
||||
endpoint: str,
|
||||
model: str,
|
||||
cost_usd: float,
|
||||
success: bool,
|
||||
status_code: int,
|
||||
latency_ms: float = 0.0,
|
||||
) -> Optional[dict]:
|
||||
try:
|
||||
row = {
|
||||
"created_at": _iso(_now()),
|
||||
"owner_kind": owner_kind or "unknown",
|
||||
"owner_id": owner_id or "unknown",
|
||||
"backend": backend,
|
||||
"endpoint": endpoint or "",
|
||||
"requested_model": model or "",
|
||||
"model": model or "",
|
||||
"status_code": int(status_code or 0),
|
||||
"success": 1 if success else 0,
|
||||
"error_category": None,
|
||||
"upstream_latency_ms": float(latency_ms or 0),
|
||||
"gateway_overhead_ms": 0.0,
|
||||
"queue_wait_ms": 0.0,
|
||||
"connect_ms": 0.0,
|
||||
"total_latency_ms": float(latency_ms or 0),
|
||||
"prompt_tokens": 0,
|
||||
"completion_tokens": 0,
|
||||
"cache_hit_tokens": 0,
|
||||
"cache_miss_tokens": 0,
|
||||
"reasoning_tokens": 0,
|
||||
"total_tokens": 0,
|
||||
"tokens_per_second": 0.0,
|
||||
"context_window": None,
|
||||
"context_utilization": None,
|
||||
"cost_usd": round(float(cost_usd or 0), 8),
|
||||
"input_cost_usd": 0.0,
|
||||
"output_cost_usd": 0.0,
|
||||
"native_cost": 0,
|
||||
"stream_requested": 0,
|
||||
"temperature": None,
|
||||
"top_p": None,
|
||||
"max_tokens": None,
|
||||
"has_tools": 0,
|
||||
"retries_attempted": 0,
|
||||
"retry_succeeded": 0,
|
||||
"circuit_open": 0,
|
||||
"user_agent": "",
|
||||
}
|
||||
get_table(GATEWAY_LEDGER).insert(row)
|
||||
self._audit_external(row)
|
||||
return row
|
||||
except Exception as exc:
|
||||
logger.warning("gateway external usage record failed: %s", exc)
|
||||
return None
|
||||
|
||||
def _audit_external(self, row: dict) -> None:
|
||||
from devplacepy.services.audit import record as audit
|
||||
|
||||
owner_kind = row.get("owner_kind") or "unknown"
|
||||
owner_id = row.get("owner_id") or "unknown"
|
||||
actor_kind, actor_uid, actor_role = audit_actor_for(owner_kind, owner_id)
|
||||
audit.record_system(
|
||||
"ai.gateway.call",
|
||||
actor_kind=actor_kind,
|
||||
actor_uid=actor_uid,
|
||||
actor_role=actor_role,
|
||||
origin="api",
|
||||
result="success" if row.get("success") else "failure",
|
||||
summary=f"external AI call by {owner_kind}/{owner_id} ({row.get('backend')})",
|
||||
metadata={
|
||||
"backend": row.get("backend"),
|
||||
"endpoint": row.get("endpoint"),
|
||||
"cost_usd": row.get("cost_usd"),
|
||||
"status_code": row.get("status_code"),
|
||||
"owner_kind": owner_kind,
|
||||
"owner_id": owner_id,
|
||||
},
|
||||
)
|
||||
|
||||
def _audit(self, raw: dict, norm: dict, cost_usd: float) -> None:
|
||||
from devplacepy.services.audit import record as audit
|
||||
|
||||
@@ -365,3 +449,26 @@ class GatewayUsageLedger:
|
||||
get_table(GATEWAY_CONCURRENCY).delete(created_at={"<": cutoff})
|
||||
)
|
||||
return ledger_removed, samples_removed
|
||||
|
||||
|
||||
def record_rsearch_call(
|
||||
owner_kind: str, owner_id: str, endpoint: str, success: bool, status_code: int
|
||||
) -> None:
|
||||
try:
|
||||
from devplacepy.services.manager import service_manager
|
||||
|
||||
service = service_manager.get_service("openai")
|
||||
cfg = service.get_config() if service is not None else {}
|
||||
cost = float(cfg.get("gateway_rsearch_cost_per_call", 0.0) or 0.0)
|
||||
GatewayUsageLedger().record_external(
|
||||
owner_kind=owner_kind or "system",
|
||||
owner_id=owner_id or "",
|
||||
backend="rsearch",
|
||||
endpoint=endpoint or "/search",
|
||||
model="rsearch",
|
||||
cost_usd=cost,
|
||||
success=success,
|
||||
status_code=status_code,
|
||||
)
|
||||
except Exception as exc:
|
||||
logger.warning("rsearch usage ledger failed: %s", exc)
|
||||
|
||||
Reference in New Issue
Block a user