Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 1 addition & 4 deletions backend/app/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -193,10 +193,7 @@ def spec_preview(incoming: EvaluationRequest, user_id=Depends(user)):

@app.get('/account')
def account(user_id=Depends(user)):
member=db.member(user_id)
return db.balance(user_id) | member | {'enabled':member['status']=='approved' and db.balance(user_id)['enabled'],
'limits':db.effective_limits(user_id),'submissions_enabled':db.policy()['policy']['submissions_enabled'],
'credit_unit':'execution seconds; not currency'}
return db.account_snapshot(user_id)

@app.post('/evaluations', status_code=202)
async def submit(request: Request, user_id=Depends(user), idempotency_key: str = Header(alias='Idempotency-Key')):
Expand Down
21 changes: 21 additions & 0 deletions backend/app/governance.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,12 @@ def policy(self):

def ensure_member(self,user_id,email='',username='',bootstrap=False):
from .db import members,accounts,policy,ledger
# Most authenticated requests only need to read an existing member.
# Avoid serializing all users behind the global policy row lock.
with self.engine.connect() as c:
existing=c.execute(select(members).where(members.c.user_id==user_id)).mappings().first()
if existing and (not email or existing['email']==email) and (not username or existing['username']==username[:100]) and (not bootstrap or (existing['role']=='admin' and existing['status']=='approved')):
return dict(existing)
with self.engine.begin() as c:
p=c.execute(select(policy).where(policy.c.id==1).with_for_update()).mappings().one()
row=c.execute(select(members).where(members.c.user_id==user_id).with_for_update()).mappings().first()
Expand All @@ -126,6 +132,21 @@ def ensure_member(self,user_id,email='',username='',bootstrap=False):
if bootstrap: c.execute(update(accounts).where(accounts.c.user_id==user_id).values(enabled=True))
return dict(c.execute(select(members).where(members.c.user_id==user_id)).mappings().one())

def account_snapshot(self,user_id):
from .db import members,accounts,policy
with self.engine.connect() as c:
row=c.execute(select(members,accounts.c.credits,accounts.c.enabled,policy.c.data.label('site_policy'))
.outerjoin(accounts,accounts.c.user_id==members.c.user_id)
.join(policy,policy.c.id==1).where(members.c.user_id==user_id)).mappings().one()
result=dict(row)
site=result.pop('site_policy')
result['credits']=result['credits'] or 0
result['enabled']=bool(result['enabled']) and result['status']=='approved'
result['limits']=Limits.model_validate(result['limits'] or site['limits']).model_dump()
result['submissions_enabled']=site['submissions_enabled']
result['credit_unit']='execution seconds; not currency'
return result

def member(self,user_id):
from .db import members
with self.engine.connect() as c:
Expand Down
2 changes: 1 addition & 1 deletion backend/app/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ class EvaluationRequest(BaseModel):
cpu_count: Literal[2, 4, 8, 16, 32] = 4
cpu_flavor: Literal['cpu3c', 'cpu3g'] = 'cpu3g'
volume_gb: int = Field(default=0, ge=0, le=1000)
gpu_type: Literal['NVIDIA A40', 'NVIDIA RTX A6000', 'NVIDIA GeForce RTX 4090', 'NVIDIA A100 80GB PCIe', 'NVIDIA H100 80GB HBM3'] = 'NVIDIA A40'
gpu_type: Literal['NVIDIA A40', 'NVIDIA RTX A6000', 'NVIDIA GeForce RTX 4090', 'NVIDIA A100 80GB PCIe', 'NVIDIA A100-SXM4-80GB', 'NVIDIA H100 80GB HBM3'] = 'NVIDIA A40'
disk_gb: int = Field(default=100, ge=50, le=1000)
engine: Literal['native', 'inspect'] = 'native'
name: Annotated[str, StringConstraints(strip_whitespace=True, min_length=1, max_length=120)]
Expand Down
27 changes: 27 additions & 0 deletions backend/tests/test_governance.py
Original file line number Diff line number Diff line change
Expand Up @@ -194,3 +194,30 @@ def test_optional_credit_budget_and_large_panel(service):
assert job['config']['max_runtime_seconds']==10000
assert job['reserved_credits']==10000
assert db.balance(USER)['credits']==0


def test_existing_account_check_is_read_only_and_permissions_stay_fresh(service):
from sqlalchemy import event
_,db,api,h=setup_admin(service)
statements=[]
def capture(conn,cursor,statement,parameters,context,executemany):
statements.append(statement)
event.listen(db.engine,'before_cursor_execute',capture)
try:
result=api.get('/account',headers=h)
finally:
event.remove(db.engine,'before_cursor_execute',capture)
assert result.status_code==200
statements=[s for s in statements if not s.startswith('BEGIN')]
assert len(statements)==2
assert all(s.lstrip().upper().startswith('SELECT') for s in statements)
db.set_member(USER,OTHER,MemberUpdate(version=db.member(OTHER)['version'],status='suspended'))
result=api.get('/account',headers={'Authorization':'Bearer '+OTHER}).json()
assert result['status']=='suspended'
assert result['enabled'] is False


def test_a100_sxm_request_supported():
from app.models import EvaluationRequest
request=EvaluationRequest.model_validate(payload(gpu_type='NVIDIA A100-SXM4-80GB'))
assert request.gpu_type=='NVIDIA A100-SXM4-80GB'
4 changes: 3 additions & 1 deletion next-src/src/app/pages.css
Original file line number Diff line number Diff line change
Expand Up @@ -826,9 +826,11 @@ body:has(.arena-home) .page-hills {

/* Liquid nav highlight: the pill glides between items (see Header) */
.va-header .va-nav {
position: relative;
isolation: isolate;
}
@media (min-width: 1101px) {
.va-header .va-nav { position: relative; }
}
.va-nav-blob {
position: absolute;
left: 0;
Expand Down
2 changes: 1 addition & 1 deletion next-src/src/app/research.css
Original file line number Diff line number Diff line change
Expand Up @@ -646,7 +646,7 @@ a:focus-visible, button:focus-visible, summary:focus-visible, select:focus-visib
@media(max-width:1100px) {
.va-header { grid-template-columns: minmax(0,1fr) auto; grid-template-areas: "brand actions"; }
.va-header .va-brand { justify-self: start; }
.va-header .va-nav { display: none; position: absolute; right: 0; top: calc(100% + 10px); width: min(300px,100%); padding: 10px; border-radius: 20px; box-shadow: 0 12px 36px rgba(25,30,25,.14); }
.va-header .va-nav { display: none; position: absolute; grid-area: auto; right: 0; top: calc(100% + 10px); width: min(300px,100%); padding: 10px; border-radius: 20px; box-shadow: 0 12px 36px rgba(25,30,25,.14); }
.va-header .va-nav.is-open { display: flex; flex-direction: column; align-items: stretch; flex-wrap: nowrap; }
.va-header .va-nav a { font-size: 14px; padding: 12px 16px; border-radius: 12px; }
.va-menu-toggle { display: inline-flex; }
Expand Down
2 changes: 1 addition & 1 deletion next-src/src/components/EvaluationRunner.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ type Model = { id: string; label: string; provider?: string };
type CustomModel = { id: string; provider: string; repo_id: string; kind: string; subfolder: string; base_model_id: string };
const presetNames = Object.keys(CONSTITUTIONS_DATA).sort();
const label = (s: string) => s.replaceAll('_', ' ').replace(/^./, c => c.toUpperCase());
const gpuTypes = ['NVIDIA A40', 'NVIDIA RTX A6000', 'NVIDIA GeForce RTX 4090', 'NVIDIA A100 80GB PCIe', 'NVIDIA H100 80GB HBM3'];
const gpuTypes = ['NVIDIA A40', 'NVIDIA RTX A6000', 'NVIDIA GeForce RTX 4090', 'NVIDIA A100 80GB PCIe', 'NVIDIA A100-SXM4-80GB', 'NVIDIA H100 80GB HBM3'];

export function EvaluationRunner() {
const auth = evaluationAuth();
Expand Down
Loading