Add owned OpenAI endpoint and coordinated native model switching

This commit is contained in:
Mikei386
2026-09-28 20:24:12 +02:00
parent 8d03a9a979
commit ff5d36252a
25 changed files with 952 additions and 177 deletions
+28 -56
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
"""Athena Deck prototype: loopback API, owned demo process, read-only telemetry."""
"""Athena Deck prototype: loopback API, owned model processes, read-only telemetry."""
import argparse
import os
import hashlib
@@ -13,6 +13,8 @@ from runtime import Runtime
from image_runtime import ImageRuntime
from image_test import ImageTests
from docker_support import DockerSupport
from inference import LlamaWorker,Scheduler
from endpoint import Endpoint
from urllib.parse import urlsplit, parse_qs
from network.client import NetworkClient
from network.config import parse_config, ConfigError
@@ -29,49 +31,6 @@ from pathlib import Path
ROOT = Path(__file__).resolve().parent
class DemoService:
def __init__(self):
self.lock = threading.RLock()
self.process = None
self.port = None
def status(self):
with self.lock:
running = self.process is not None and self.process.poll() is None
reachable = False
if running:
try:
with urllib.request.urlopen(f'http://127.0.0.1:{self.port}/health', timeout=.5) as response:
reachable = json.load(response).get('service') == 'athena-deck-demo'
except (OSError, ValueError):
pass
return dict(state='running' if running else 'stopped', reachable=reachable, pid=self.process.pid if running else None, port=self.port if running else None, location='Deck · isolierter Demo-Prozess')
def start(self):
with self.lock:
if self.process is None or self.process.poll() is not None:
with socket.socket() as sock:
sock.bind(('127.0.0.1', 0))
sock.listen(8)
self.port = sock.getsockname()[1]
self.process = subprocess.Popen([sys.executable, str(ROOT/'demo.py'), str(sock.fileno())], pass_fds=(sock.fileno(),), stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
for _ in range(30):
if self.status()['reachable']:
break
time.sleep(.05)
return self.status()
def stop(self):
with self.lock:
if self.process is not None and self.process.poll() is None:
self.process.terminate()
try:
self.process.wait(timeout=3)
except subprocess.TimeoutExpired:
self.process.kill()
self.process.wait()
return self.status()
class HardwareProvider:
def __init__(self):
self.lock = threading.Lock()
@@ -110,7 +69,6 @@ class Server(ThreadingHTTPServer):
self.image_tests.runtime = self.image_runtime
self.profiles.image_runtime_ready=lambda:self.image_tests.status()["runtime_installed"]
self.docker = DockerSupport()
self.demo = DemoService()
self.hardware = HardwareProvider()
self.started = time.time()
self.network = NetworkClient()
@@ -122,6 +80,14 @@ class Server(ThreadingHTTPServer):
if os.environ.get('DECK_REQUIRE_SETUP') == '1' and self.credentials.read() is None:
self.server_close()
raise RuntimeError('Serverzugang muss vor dem Start eingerichtet werden.')
self.worker=LlamaWorker(self.catalog.root.parent/'llama-worker',self.catalog,self.runtime)
self.scheduler=Scheduler(self.worker)
self.profiles.chat_blockers=self.worker.blockers
self.image_tests.acquire=self.scheduler.image_reservation
self.endpoint=Endpoint(self.catalog.root.parent,self.profiles,self.worker,self.scheduler,self.image_tests,self.credentials,self.server_port)
if self.endpoint.config['autostart']:
try:self.endpoint.start()
except ValueError as exc:self.endpoint.error=str(exc)
self.sessions = {}
self.login_attempts = []
self.auth_lock = threading.Lock()
@@ -158,7 +124,7 @@ class Handler(BaseHTTPRequestHandler):
return secrets.compare_digest(actual,record['api_token_hash'])
def token_route(self):
return (self.command == 'GET' and self.path in ('/api/v1/status','/api/v1/hardware','/api/v1/demo')) or (self.command == 'POST' and self.path in ('/api/v1/demo/start','/api/v1/demo/stop'))
return self.command == 'GET' and self.path in ('/api/v1/status','/api/v1/hardware')
def session_token(self):
try:
@@ -273,12 +239,15 @@ class Handler(BaseHTTPRequestHandler):
return self.respond({'error':'Anmeldung erforderlich.'},401)
if not self.authenticated() and self.path == '/':
return self.respond((ROOT/'login.html').read_bytes(), mime='text/html; charset=utf-8')
routes = {'/docker-ui.js': ('docker-ui.js','text/javascript'), '/': ('index.html', 'text/html; charset=utf-8'), '/app.js': ('app.js', 'text/javascript'), '/style.css': ('style.css', 'text/css'), '/network-ui.js': ('network-ui.js', 'text/javascript'), '/access-ui.js': ('access-ui.js', 'text/javascript'), '/studio.js': ('studio.js', 'text/javascript'), '/catalog-ui.js': ('catalog-ui.js','text/javascript'), '/runtime-ui.js': ('runtime-ui.js','text/javascript'), '/profiles-ui.js': ('profiles-ui.js','text/javascript'), '/image-test-ui.js': ('image-test-ui.js','text/javascript')}
routes = {'/endpoint-ui.js':('endpoint-ui.js','text/javascript'),'/docker-ui.js': ('docker-ui.js','text/javascript'), '/': ('index.html', 'text/html; charset=utf-8'), '/app.js': ('app.js', 'text/javascript'), '/style.css': ('style.css', 'text/css'), '/network-ui.js': ('network-ui.js', 'text/javascript'), '/access-ui.js': ('access-ui.js', 'text/javascript'), '/studio.js': ('studio.js', 'text/javascript'), '/catalog-ui.js': ('catalog-ui.js','text/javascript'), '/runtime-ui.js': ('runtime-ui.js','text/javascript'), '/profiles-ui.js': ('profiles-ui.js','text/javascript'), '/image-test-ui.js': ('image-test-ui.js','text/javascript')}
if self.path in routes:
name, mime = routes[self.path]
return self.respond((ROOT/name).read_bytes(), mime=mime)
if self.path == '/api/v1/status':
return self.respond(dict(name='Athena Deck', version='0.6.0', state='ready', uptime_seconds=round(time.time()-self.server.started), mode='isolated', location=os.environ.get('DECK_LOCATION', 'Athena · Debian-Server'), demo=self.server.demo.status()))
endpoint=self.server.endpoint.status()
public_endpoint={key:endpoint[key] for key in ('state','port','counts','active_requests')}
return self.respond(dict(name='Athena Deck', version='0.7.0', state='ready', uptime_seconds=round(time.time()-self.server.started), mode='isolated', location=os.environ.get('DECK_LOCATION', 'Athena · Debian-Server'), endpoint=public_endpoint))
if self.path == '/api/v1/endpoint':return self.respond(self.server.endpoint.status())
if self.path == '/api/v1/docker':return self.respond(self.server.docker.status())
if self.path == '/api/v1/image-runtime':return self.respond(self.server.image_runtime.status())
if self.path == '/api/v1/image-tests':return self.respond(self.server.image_tests.status())
@@ -317,8 +286,6 @@ class Handler(BaseHTTPRequestHandler):
return self.respond({'error':str(exc) if isinstance(exc,ValueError) else 'Katalog nicht erreichbar.'},400)
if self.path == '/api/v1/network':
return self.respond(self.server.network.status(self.ingress()))
if self.path == '/api/v1/demo':
return self.respond(self.server.demo.status())
return self.respond({'error': 'Not found'}, 404)
def do_POST(self):
@@ -367,6 +334,15 @@ class Handler(BaseHTTPRequestHandler):
raise ValueError('Ungültige Laufzeitaktion.')
except ValueError as exc:return self.respond({'error':str(exc)},400)
except (OSError, subprocess.SubprocessError):return self.respond({'error':'Laufzeitaktion fehlgeschlagen; Speicher und Werkzeuge prüfen.'},503)
if self.path in ('/api/v1/endpoint/start','/api/v1/endpoint/stop','/api/v1/endpoint/config','/api/v1/endpoint/profile'):
try:
data=self.read_json();ep=self.server.endpoint
if self.path.endswith('/config'):return self.respond(ep.configure(data))
if self.path.endswith('/profile'):return self.respond(ep.enable(data))
if data:raise ValueError('Keine Parameter erwartet.')
return self.respond(ep.start() if self.path.endswith('/start') else ep.stop())
except ValueError as exc:return self.respond({'error':str(exc)},400)
except OSError:return self.respond({'error':'Endpunkt konnte nicht konfiguriert werden.'},503)
if self.path in ('/api/v1/image-runtime/install','/api/v1/image-runtime/cancel'):
try:
if self.read_json():raise ValueError('Keine Parameter erwartet.')
@@ -431,11 +407,7 @@ class Handler(BaseHTTPRequestHandler):
except (ValueError, OSError, subprocess.SubprocessError) as exc:
message = str(exc) if isinstance(exc, ValueError) else 'Netzwerkmodul nicht erreichbar.'
return self.respond({'error':message},400)
if self.path not in ('/api/v1/demo/start', '/api/v1/demo/stop'):
return self.respond({'error': 'Not found'}, 404)
action = self.server.demo.start if self.path.endswith('/start') else self.server.demo.stop
result = action()
self.respond(result, 503 if self.path.endswith('/start') and not result['reachable'] else 200)
return self.respond({'error':'Not found'},404)
def main():
@@ -451,11 +423,11 @@ def main():
try:
server.serve_forever()
finally:
server.endpoint.close()
server.image_runtime.stop()
server.image_tests.stop()
server.runtime.stop()
server.catalog.stop()
server.demo.stop()
server.server_close()
if __name__ == '__main__':