Separate stable API profile IDs from display names with legacy aliases

This commit is contained in:
Mikei386 committed 2026-10-03 23:35:30 +02:00
1 parent 0cffcf4295
commit a908313c8f
7 files changed
+88 -11

No files matched your search

+27 -2
View File
@@ -1,3 +1,4 @@
import unicodedata
"""Server-persisted library-backed profiles with validated, typed parameters."""
import json
import re
@@ -137,6 +138,20 @@ def audio_package_recipe(model,source):
return {f'runtime_file_{i}':dict(label='Laufzeit-Paket: '+f,files=[f]) for i,f in enumerate(extras)} or None
return None
def api_slug(name):
text=unicodedata.normalize('NFKD',name.replace('ß','ss')).encode('ascii','ignore').decode().lower()
return re.sub(r'[^a-z0-9]+','-',text).strip('-')[:64].rstrip('-') or 'model'
def migrate_api_ids(rows):
used={r['api_id'] for r in rows if r.get('api_id')}
for row in rows:
if not row.get('api_id'):
base=api_slug(row['name']);candidate=base;i=2
while candidate in used or any(candidate in [r['name'],*r.get('api_aliases',[])] for r in rows if r is not row):
suffix='-'+str(i);candidate=base[:64-len(suffix)]+suffix;i+=1
row['api_id']=candidate;used.add(candidate)
row.setdefault('api_aliases',[row['name']])
class Profiles:
def _recipe(self,model):return component_recipe(model) or getattr(self,'runtime_recipe',lambda m:None)(model)
def __init__(self,path,catalog):
@@ -145,6 +160,10 @@ class Profiles:
self.prompt_enhancer_ready=lambda task:False
self.video_blockers=None;self.chat_blockers=None;self.tts_blockers=None;self.stt_blockers=None
self.rows=json.loads(self.path.read_text()) if self.path.exists() else []
original=json.dumps(self.rows)
migrate_api_ids(self.rows)
if self.path.exists() and json.dumps(self.rows)!=original:
temp=self.path.with_suffix('.identity.tmp');temp.write_text(json.dumps(self.rows));temp.replace(self.path)
# First baselines may be captured while a pre-Preset server still runs.
# Merge on startup so its old in-memory saves cannot discard that baseline.
pending=self.path.with_name('profile-presets-pending.json')
@@ -203,7 +222,7 @@ class Profiles:
p['runnable']=not p['blockers'];p['state']='ready' if p['runnable'] else 'configured'
return {'profiles':[p for p in rows if p['kind']!='video'],'schemas':{k:v for k,v in SCHEMAS.items() if k!='video'}}
def save(self,data):
if set(data)!={'id','revision','name','kind','model_id','parameters'}:raise ValueError('Ungültige Profilfelder.')
if set(data)-{'api_id'}!={'id','revision','name','kind','model_id','parameters'}:raise ValueError('Ungültige Profilfelder.')
kind=data['kind'];name=data['name'];params=data['parameters']
if kind=='video':raise ValueError('Videoprofile werden nicht mehr in Deck verwaltet. Originale LTX-Oberfläche verwenden.')
if not isinstance(kind,str) or kind not in SCHEMAS or not isinstance(name,str) or not (2<=len(name)<=64 and name[0].isalnum() and name[-1].isalnum() and re.fullmatch(r'[\w .-]+',name)):raise ValueError('API-Name: 2–64 Zeichen; Buchstaben, Ziffern, Leerzeichen, Punkt, Bindestrich oder Unterstrich. Anfang und Ende müssen Buchstabe oder Ziffer sein.')
@@ -231,8 +250,14 @@ class Profiles:
if data['id'] is not None and not existing:raise ValueError('Profil nicht gefunden.')
if data['revision']!=(existing['revision'] if existing else 0):raise ValueError('Profil wurde zwischenzeitlich geändert. Ansicht neu laden.')
if any(p['name']==name and p is not existing for p in self.rows):raise ValueError('API-Profilname bereits vergeben.')
api_id=data.get('api_id') or (existing.get('api_id') if existing else api_slug(name))
if not isinstance(api_id,str) or not re.fullmatch(r'[a-zA-Z0-9][a-zA-Z0-9_-]{0,63}',api_id):raise ValueError('API-ID: 1–64 Buchstaben, Ziffern, Bindestrich oder Unterstrich, ohne Leerzeichen.')
aliases=list(existing.get('api_aliases',[]) if existing else [])
if existing:
aliases=list(dict.fromkeys(aliases+[existing['name'],existing['api_id']]))
if any(set([api_id,name,*aliases]) & set([p.get('api_id',p['name']),p['name'],*p.get('api_aliases',[])]) for p in self.rows if p is not existing):raise ValueError('API-ID oder Alias bereits vergeben.')
components=existing.get('components',{}) if existing and existing['model_id']==model['id'] else {}
row=dict(presets=existing.get('presets',[]) if existing else [],components=components,prompt_enhancer=existing.get('prompt_enhancer',dict(PROMPT_ENHANCER_DEFAULTS)) if existing and existing['model_id']==model['id'] and kind=='image' else dict(PROMPT_ENHANCER_DEFAULTS) if kind=='image' else None,id=existing['id'] if existing else uuid.uuid4().hex,revision=data['revision']+1,name=name,kind=kind,model_id=model['id'],parameters=params,updated_at=time.time())
row=dict(api_id=api_id,api_aliases=aliases,presets=existing.get('presets',[]) if existing else [],components=components,prompt_enhancer=existing.get('prompt_enhancer',dict(PROMPT_ENHANCER_DEFAULTS)) if existing and existing['model_id']==model['id'] and kind=='image' else dict(PROMPT_ENHANCER_DEFAULTS) if kind=='image' else None,id=existing['id'] if existing else uuid.uuid4().hex,revision=data['revision']+1,name=name,kind=kind,model_id=model['id'],parameters=params,updated_at=time.time())
rows=[row if p is existing else p for p in self.rows] if existing else self.rows+[row]
self.path.parent.mkdir(parents=True,exist_ok=True,mode=0o700)
temp=self.path.with_suffix('.tmp');temp.write_text(json.dumps(rows));temp.replace(self.path);self.rows=rows