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
17 changes: 11 additions & 6 deletions backend/app/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,9 +84,16 @@ def effective_config(incoming,user_id):
except (ValueError,ValidationError):
raise HTTPException(422,'Spec overrides conflict with site defaults or model panel') from None
config['advanced_spec']=advanced.model_dump(exclude_unset=True,exclude_none=True)
enforce(config,db.effective_limits(user_id))
return config

def select_compute(config,refs,user_id):
local = any(isinstance(ref,dict) for ref in refs.values())
if local and config['compute_type']=='cpu':
raise HTTPException(422,'CPU evaluations support API models only; Hugging Face models require GPU')
if not local:
config.update(compute_type='cpu',gpu_count=1)
enforce(config,db.effective_limits(user_id))

@app.get('/admin')
def admin_data(actor=Depends(administrator)):
return db.admin_snapshot() | {'policy_schema':Policy.model_json_schema(),'limits_schema':Limits.model_json_schema()}
Expand Down Expand Up @@ -186,8 +193,7 @@ def spec_preview(incoming: EvaluationRequest, user_id=Depends(user)):
import pprint
config = effective_config(incoming,user_id)
config['model_refs'] = resolve_models(incoming, catalog, incoming.hf_token.get_secret_value() or (cfg.hf_token if incoming.funding == 'service' else ''))
if incoming.compute_type == 'cpu' and any(isinstance(ref, dict) for ref in config['model_refs'].values()):
raise HTTPException(422, 'CPU evaluations support API models only; Hugging Face models require GPU')
select_compute(config,config['model_refs'],user_id)
spec = build_spec(config, '.')
return {'spec': spec, 'python': 'RUN_SPEC = ' + pprint.pformat(spec, sort_dicts=False) + '\n'}

Expand Down Expand Up @@ -218,12 +224,11 @@ async def submit(request: Request, user_id=Depends(user), idempotency_key: str =
raise HTTPException(422, 'Supply both your OpenRouter and RunPod API keys')
if incoming.funding == 'own_keys':
await run_in_threadpool(verify_provider_keys, keys)
if incoming.funding == 'service' and db.member(user_id)['role']!='admin' and (incoming.gpu_type != cfg.runpod_gpu_type or incoming.disk_gb != cfg.runpod_disk_gb or incoming.gpu_count != 1 or incoming.compute_type != 'gpu' or incoming.cpu_count != 4 or incoming.cpu_flavor != 'cpu3g' or incoming.volume_gb != 0):
if incoming.funding == 'service' and db.member(user_id)['role']!='admin' and (incoming.gpu_type != cfg.runpod_gpu_type or incoming.disk_gb != cfg.runpod_disk_gb or incoming.gpu_count != 1 or incoming.cpu_count != 4 or incoming.cpu_flavor != 'cpu3g' or incoming.volume_gb != 0):
raise HTTPException(422, 'Custom compute requires your own provider keys')
select_compute(config,refs,user_id)
digest = hashlib.sha256(json.dumps(config, sort_keys=True).encode()).hexdigest()
# Snapshot the catalog: queued jobs do not silently change if administrators update it.
if incoming.compute_type == 'cpu' and any(isinstance(ref, dict) for ref in refs.values()):
raise HTTPException(422, 'CPU evaluations support API models only. Select GPU for Hugging Face models.')
config['model_refs'] = refs
if incoming.funding != 'own_keys': keys = {}
if incoming.hf_token.get_secret_value(): keys['hf_token'] = incoming.hf_token.get_secret_value()
Expand Down
40 changes: 39 additions & 1 deletion backend/tests/test_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -320,7 +320,8 @@ def create(self, job):
assert db.get(job_id)['error_code'] == 'worker_start_timeout'


def test_unavailable_progress_shows_attempt_and_next_retry(service):
def test_unavailable_progress_shows_attempt_and_next_retry(service, monkeypatch):
monkeypatch.setattr('app.api.resolve_models',lambda *args:{'a':{'provider':'hf_local'},'b':'org/b'})
from app.runpod import GPUUnavailable
cfg,db,client=service
job_id=submit(client).json()['id']
Expand Down Expand Up @@ -348,3 +349,40 @@ def test_compute_limits_and_cpu_local_models(service, monkeypatch):
monkeypatch.setattr('app.api.resolve_models', lambda *args: {'a':{'provider':'hf_local'},'b':'org/b'})
result=client.post('/spec-preview',json=payload(compute_type='cpu'),headers={'Authorization':'Bearer '+USER})
assert result.status_code==422 and 'API models only' in result.json()['detail']


@pytest.mark.parametrize('requested_compute',['gpu','cpu'])
def test_api_only_panels_automatically_use_cpu_for_members(service,requested_compute,monkeypatch):
cfg,db,client=service
config=payload(compute_type=requested_compute)
response=submit(client,config)
assert response.status_code==202,response.text
job=db.get(response.json()['id'])
assert job['config']['compute_type']=='cpu'
assert job['config']['gpu_count']==1
assert response.json()['compute_type']=='cpu'
# Preview applies the same choice before compiling the spec.
from app.spec import build_spec as compile_spec
def capture(config,directory):
assert config['compute_type']=='cpu'
return compile_spec(config,directory)
monkeypatch.setattr('app.api.build_spec',capture)
assert client.post('/spec-preview',json=config,headers={'Authorization':'Bearer '+USER}).status_code==200


def test_mixed_panel_keeps_gpu(service,monkeypatch):
_,db,client=service
monkeypatch.setattr('app.api.resolve_models',lambda *args:{'a':{'provider':'hf_local'},'b':'org/b'})
response=submit(client)
assert response.status_code==202
assert db.get(response.json()['id'])['config']['compute_type']=='gpu'


def test_auto_cpu_respects_cpu_limit(service):
from app.governance import PolicyUpdate
_,db,client=service
policy=db.policy();policy['policy']['limits']['max_cpu_count']=2
db.set_policy(USER,PolicyUpdate(**policy))
response=submit(client,payload(compute_type='gpu'))
assert response.status_code==403
assert 'max_cpu_count' in response.json()['detail']
Loading
Loading