Add owned OpenAI endpoint and coordinated native model switching
This commit is contained in:
@@ -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__':
|
||||
|
||||
Reference in New Issue
Block a user