yex
This commit is contained in:
@@ -11,7 +11,7 @@ from fastapi.responses import JSONResponse
|
||||
|
||||
from devplacepy.database import get_int_setting
|
||||
from devplacepy.services.base import BaseService, ConfigField
|
||||
from devplacepy.services.openai_gateway import config
|
||||
from devplacepy.services.openai_gateway import config, quota
|
||||
from devplacepy.services.openai_gateway.analytics import summary_metrics
|
||||
from devplacepy.services.openai_gateway.gateway import GatewayRuntime
|
||||
from devplacepy.services.openai_gateway.routing import model_store
|
||||
@@ -262,6 +262,58 @@ class GatewayService(BaseService):
|
||||
"with this key. Clear it and restart to rotate.",
|
||||
group="Access",
|
||||
),
|
||||
ConfigField(
|
||||
quota.FIELD_DEFAULT_USER,
|
||||
"Default per-user daily cap ($)",
|
||||
type="float",
|
||||
default=1.0,
|
||||
minimum=0,
|
||||
help="Rolling 24h cap applied to a signed-in member with no matching quota rule. "
|
||||
"0 = unlimited. Overridable per user/app-reference on /admin/gateway.",
|
||||
group="Quota",
|
||||
),
|
||||
ConfigField(
|
||||
quota.FIELD_DEFAULT_ADMIN,
|
||||
"Default per-admin daily cap ($)",
|
||||
type="float",
|
||||
default=0.0,
|
||||
minimum=0,
|
||||
help="Rolling 24h cap applied to an administrator with no matching quota rule. "
|
||||
"0 = unlimited (the default - admins are exempt unless a rule says otherwise).",
|
||||
group="Quota",
|
||||
),
|
||||
ConfigField(
|
||||
quota.FIELD_DEFAULT_GUEST,
|
||||
"Default per-guest daily cap ($)",
|
||||
type="float",
|
||||
default=0.05,
|
||||
minimum=0,
|
||||
help="Rolling 24h cap applied to an unauthenticated caller with no matching quota "
|
||||
"rule (only reachable when authentication is not required). 0 = unlimited.",
|
||||
group="Quota",
|
||||
),
|
||||
ConfigField(
|
||||
quota.FIELD_DEFAULT_INTERNAL,
|
||||
"Default per-internal-key daily cap ($)",
|
||||
type="float",
|
||||
default=0.0,
|
||||
minimum=0,
|
||||
help="Rolling 24h cap applied to calls authenticated with the auto-generated internal "
|
||||
"key (DevPlace's own services: news, bots, Devii guests, correction/modifier). "
|
||||
"0 = unlimited (the default - do not cap this without a matching quota rule, or "
|
||||
"internal platform traffic will start failing).",
|
||||
group="Quota",
|
||||
),
|
||||
ConfigField(
|
||||
quota.FIELD_DEFAULT_KEY,
|
||||
"Default per-access-key daily cap ($)",
|
||||
type="float",
|
||||
default=0.0,
|
||||
minimum=0,
|
||||
help="Rolling 24h cap applied to calls authenticated with the static access key. "
|
||||
"0 = unlimited by default (it is an admin-provisioned trusted secret).",
|
||||
group="Quota",
|
||||
),
|
||||
ConfigField(
|
||||
"gateway_price_cache_hit_per_m",
|
||||
"Chat price cache-hit / 1M ($)",
|
||||
@@ -463,6 +515,37 @@ class GatewayService(BaseService):
|
||||
return (kind, user.get("uid") or "unknown")
|
||||
return ("anonymous", "anonymous")
|
||||
|
||||
def _audit_quota_exceeded(
|
||||
self,
|
||||
owner_kind: str,
|
||||
owner_id: str,
|
||||
app_reference: str,
|
||||
spent: float,
|
||||
limit: float,
|
||||
rule,
|
||||
) -> None:
|
||||
from devplacepy.services.audit import record as audit
|
||||
from devplacepy.services.openai_gateway.usage import audit_actor_for
|
||||
|
||||
actor_kind, actor_uid, actor_role = audit_actor_for(owner_kind, owner_id)
|
||||
audit.record_system(
|
||||
"ai.quota.exceeded",
|
||||
actor_kind=actor_kind,
|
||||
actor_uid=actor_uid,
|
||||
actor_role=actor_role,
|
||||
origin="api",
|
||||
result="denied",
|
||||
summary=f"AI gateway call by {owner_kind}/{owner_id} blocked - 24h quota reached",
|
||||
metadata={
|
||||
"owner_kind": owner_kind,
|
||||
"owner_id": owner_id,
|
||||
"app_reference": app_reference,
|
||||
"spent_usd": round(spent, 6),
|
||||
"limit_usd": limit,
|
||||
"rule_uid": rule.uid if rule else None,
|
||||
},
|
||||
)
|
||||
|
||||
def _models_response(self) -> JSONResponse:
|
||||
created = int(time.time())
|
||||
seen: set = set()
|
||||
@@ -501,6 +584,17 @@ class GatewayService(BaseService):
|
||||
)
|
||||
if subpath == "models" and request.method == "GET":
|
||||
return self._models_response()
|
||||
limit, scope, rule = quota.resolve(owner[0], owner[1], app_reference, cfg)
|
||||
if limit > 0:
|
||||
spent = quota.spent_24h(*scope)
|
||||
if spent >= limit:
|
||||
self.log(
|
||||
f"Rejected {owner[0]}:{owner[1]} app={app_reference}: "
|
||||
f"quota exceeded (${spent:.4f}/${limit:.4f}"
|
||||
f"{' rule ' + rule.uid if rule else ' default'})"
|
||||
)
|
||||
self._audit_quota_exceeded(owner[0], owner[1], app_reference, spent, limit, rule)
|
||||
raise HTTPException(status_code=429, detail="AI gateway daily quota exceeded")
|
||||
if subpath == "chat/completions" and request.method == "POST":
|
||||
try:
|
||||
body = await request.json()
|
||||
|
||||
Reference in New Issue
Block a user