AQIT
0.1.0
Toggle main menu visibility
Loading...
Searching...
No Matches
model_daemon.py
Go to the documentation of this file.
1
# Copyright (c) 2025-present Aquin Labs Private Limited. All Rights Reserved.
2
# This file is part of the Aquin Engine. Unauthorized copying, modification,
3
# distribution, or use of this file, via any medium, is strictly prohibited.
4
# Proprietary and confidential. See LICENSE for terms.
5
6
"""
7
Client + lifecycle for the persistent model daemon (aquin.engine.daemon_main).
8
9
The daemon keeps one model resident in VRAM across separate `aquin` invocations.
10
This module is the thin client the CLI uses to start it, check it, switch models,
11
warm SAEs, dispatch tools, and stop it. Everything here is best-effort: if the
12
daemon is unavailable, callers fall back to loading in-process.
13
"""
14
from
__future__
import
annotations
15
16
import
json
17
import
os
18
import
subprocess
19
import
sys
20
import
time
21
import
urllib.error
22
import
urllib.request
23
from
pathlib
import
Path
24
from
typing
import
Any
25
26
from
aquin.engine.engine_info
import
(
27
DEFAULT_ENGINE_HOST,
28
resolve_engine_port,
29
set_daemon_state,
30
)
31
32
_HOST = DEFAULT_ENGINE_HOST
33
_LOG_PATH = Path.home() /
".aquin"
/
"daemon.log"
34
35
# Generous ceiling for a large model load / long tool run.
36
DEFAULT_TIMEOUT = 1800
37
FAST_TIMEOUT = 0.2
38
39
40
def
_port
() -> int:
41
return
resolve_engine_port()
42
43
44
def
_base
() -> str:
45
return
f
"http://{_HOST}:{_port()}"
46
47
48
def
_url
(path: str) -> str:
49
return
f
"{_base()}{path}"
50
51
52
def
_get
(path: str, timeout: float = 1.5) -> tuple[int |
None
, Any]:
53
try
:
54
with
urllib.request.urlopen(
_url
(path), timeout=timeout)
as
resp:
55
raw = resp.read().decode()
or
"{}"
56
return
resp.status, json.loads(raw)
57
except
Exception:
58
return
None
,
None
59
60
61
def
_post
(path: str, body: dict |
None
, timeout: float = DEFAULT_TIMEOUT) -> tuple[int |
None
, Any]:
62
data = json.dumps(body
or
{}).encode()
63
req = urllib.request.Request(
64
_url
(path), data=data, headers={
"Content-Type"
:
"application/json"
}, method=
"POST"
,
65
)
66
try
:
67
with
urllib.request.urlopen(req, timeout=timeout)
as
resp:
68
raw = resp.read().decode()
or
"{}"
69
return
resp.status, json.loads(raw)
70
except
urllib.error.HTTPError
as
exc:
71
try
:
72
return
exc.code, json.loads(exc.read().decode()
or
"{}"
)
73
except
Exception:
74
return
exc.code,
None
75
except
Exception:
76
return
None
,
None
77
78
79
def
health
(timeout: float = 1.5) -> dict |
None
:
80
"""Return the daemon health payload, or None if no daemon is answering."""
81
status, data =
_get
(
"/health"
, timeout=timeout)
82
if
status == 200
and
isinstance(data, dict)
and
data.get(
"daemon"
):
83
return
data
84
return
None
85
86
87
def
health_fast
() -> dict | None:
88
"""Quick probe for status screens — fails fast when daemon is down."""
89
return
health
(timeout=FAST_TIMEOUT)
90
91
92
def
is_running
() -> bool:
93
return
health
()
is
not
None
94
95
96
def
loaded_model
() -> str | None:
97
h =
health
()
98
return
h.get(
"model_id"
)
if
h
else
None
99
100
101
def
_port_occupied
() -> bool:
102
"""Something is listening on the port (may be a non-daemon chat server)."""
103
status, _ =
_get
(
"/health"
, timeout=0.6)
104
return
status
is
not
None
105
106
107
def
_write_state
(pid: int) ->
None
:
108
try
:
109
set_daemon_state(
110
status=
"running"
,
111
pid=pid,
112
port=
_port
(),
113
started_at=time.strftime(
"%Y-%m-%dT%H:%M:%SZ"
, time.gmtime()),
114
model_id=
None
,
115
)
116
except
Exception:
117
pass
118
119
120
def
_clear_state
() -> None:
121
try
:
122
set_daemon_state(status=
"stopped"
, pid=
None
, started_at=
None
, model_id=
None
)
123
except
Exception:
124
pass
125
126
127
def
_spawn
() -> bool:
128
"""Spawn the daemon as a detached background process. Returns spawn success."""
129
try
:
130
_LOG_PATH.parent.mkdir(parents=
True
, exist_ok=
True
)
131
logf = open(_LOG_PATH,
"ab"
)
132
except
Exception:
133
logf = subprocess.DEVNULL
# type: ignore[assignment]
134
135
creationflags = 0
136
popen_kwargs: dict[str, Any] = {}
137
if
os.name ==
"nt"
:
138
# DETACHED_PROCESS (0x00000008) + new process group so it outlives this shell.
139
creationflags = subprocess.CREATE_NEW_PROCESS_GROUP | 0x00000008
140
else
:
141
popen_kwargs[
"start_new_session"
] =
True
142
143
port =
_port
()
144
try
:
145
proc = subprocess.Popen(
146
[sys.executable,
"-m"
,
"aquin.engine.daemon_main"
,
"--port"
, str(port)],
147
stdout=logf,
148
stderr=logf,
149
stdin=subprocess.DEVNULL,
150
creationflags=creationflags,
151
close_fds=
True
,
152
**popen_kwargs,
153
)
154
except
Exception:
155
return
False
156
_write_state
(proc.pid)
157
return
True
158
159
160
def
ensure_running
(wait_seconds: float = 40.0) -> bool:
161
"""
162
Make sure the daemon HTTP server is up. Returns True if it is (or becomes)
163
reachable. Does NOT load a model; use switch_model for that.
164
"""
165
if
is_running
():
166
return
True
167
# Another server (e.g. `aquin chat`) may hold the port; do not fight it.
168
if
_port_occupied
():
169
return
False
170
if
not
_spawn
():
171
return
False
172
deadline = time.time() + wait_seconds
173
while
time.time() < deadline:
174
if
is_running
():
175
return
True
176
time.sleep(0.5)
177
return
False
178
179
180
def
switch_model
(
181
model_id: str,
182
timeout: float = DEFAULT_TIMEOUT,
183
*,
184
on_tick: Any =
None
,
185
) -> dict |
None
:
186
"""Load model_id in the daemon, evicting any previously resident model."""
187
status, data =
_post
(
"/model/switch"
, {
"model_id"
: model_id}, timeout=10.0)
188
if
status != 200
or
not
isinstance(data, dict):
189
return
{
190
"ok"
:
False
,
191
"error"
:
"background engine did not respond to model switch request"
,
192
}
193
if
not
data.get(
"ok"
):
194
return
data
195
if
data.get(
"status"
) ==
"ready"
:
196
h =
health
(timeout=2.0)
or
{}
197
return
{
"ok"
:
True
,
"model_id"
: data.get(
"model_id"
)
or
model_id,
"vram"
: h.get(
"vram"
)}
198
199
from
aquin.load_model_display
import
watch_daemon_load
200
201
target_model = data.get(
"model_id"
)
or
model_id
202
return
watch_daemon_load(
203
target_model=target_model,
204
poll=
lambda
:
health
(timeout=2.0),
205
timeout=timeout,
206
on_tick=on_tick,
207
)
208
209
210
def
warm_sae
(model_id: str, layer: int, timeout: float = 600) -> dict |
None
:
211
status, data =
_post
(
"/sae/load"
, {
"model_id"
: model_id,
"layer"
: layer}, timeout=timeout)
212
if
status == 200
and
isinstance(data, dict):
213
return
data
214
return
None
215
216
217
def
dispatch
(name: str, args: dict, ctx: dict, timeout: float = DEFAULT_TIMEOUT) -> dict |
None
:
218
"""Run a tool in the daemon. Returns {"ok": bool, "result"|"error": ...} or None."""
219
status, data =
_post
(
"/dispatch"
, {
"name"
: name,
"args"
: args,
"ctx"
: ctx}, timeout=timeout)
220
if
status == 200
and
isinstance(data, dict):
221
return
data
222
return
None
223
224
225
def
unload
() -> bool:
226
"""Free the daemon's VRAM (keeps the daemon process alive)."""
227
status, _ =
_post
(
"/unload"
, {}, timeout=120)
228
return
status == 200
229
230
231
def
prompt
(
232
text: str,
233
*,
234
model_id: str |
None
=
None
,
235
max_new_tokens: int = 200,
236
temperature: float = 0.7,
237
timeout: float = DEFAULT_TIMEOUT,
238
) -> dict |
None
:
239
"""Generate a completion from the daemon's resident model."""
240
body: dict[str, Any] = {
241
"prompt"
: text,
242
"max_new_tokens"
: max_new_tokens,
243
"temperature"
: temperature,
244
}
245
if
model_id:
246
body[
"model_id"
] = model_id
247
status, data =
_post
(
"/prompt"
, body, timeout=timeout)
248
if
status == 200
and
isinstance(data, dict):
249
return
data
250
return
None
251
252
253
def
stop
() -> bool:
254
"""Ask the daemon to shut down and clear local state."""
255
status, _ =
_post
(
"/shutdown"
, {}, timeout=10)
256
_clear_state
()
257
return
status == 200
aquin.engine.engine_info
Definition
engine_info.py:1
aquin.engine.model_daemon._get
tuple[int|None, Any] _get(str path, float timeout=1.5)
Definition
model_daemon.py:56
aquin.engine.model_daemon._port_occupied
bool _port_occupied()
Definition
model_daemon.py:105
aquin.engine.model_daemon._write_state
None _write_state(int pid)
Definition
model_daemon.py:111
aquin.engine.model_daemon.stop
bool stop()
Definition
model_daemon.py:257
aquin.engine.model_daemon.unload
bool unload()
Definition
model_daemon.py:229
aquin.engine.model_daemon._clear_state
None _clear_state()
Definition
model_daemon.py:124
aquin.engine.model_daemon.ensure_running
bool ensure_running(float wait_seconds=40.0)
Definition
model_daemon.py:164
aquin.engine.model_daemon._port
int _port()
Definition
model_daemon.py:44
aquin.engine.model_daemon.loaded_model
str|None loaded_model()
Definition
model_daemon.py:100
aquin.engine.model_daemon.health
dict|None health(float timeout=1.5)
Definition
model_daemon.py:83
aquin.engine.model_daemon.prompt
dict|None prompt(str text, *, str|None model_id=None, int max_new_tokens=200, float temperature=0.7, float timeout=DEFAULT_TIMEOUT)
Definition
model_daemon.py:242
aquin.engine.model_daemon._spawn
bool _spawn()
Definition
model_daemon.py:131
aquin.engine.model_daemon.switch_model
dict|None switch_model(str model_id, float timeout=DEFAULT_TIMEOUT, *, Any on_tick=None)
Definition
model_daemon.py:189
aquin.engine.model_daemon.warm_sae
dict|None warm_sae(str model_id, int layer, float timeout=600)
Definition
model_daemon.py:214
aquin.engine.model_daemon._base
str _base()
Definition
model_daemon.py:48
aquin.engine.model_daemon.health_fast
dict|None health_fast()
Definition
model_daemon.py:91
aquin.engine.model_daemon.dispatch
dict|None dispatch(str name, dict args, dict ctx, float timeout=DEFAULT_TIMEOUT)
Definition
model_daemon.py:221
aquin.engine.model_daemon.is_running
bool is_running()
Definition
model_daemon.py:96
aquin.engine.model_daemon._post
tuple[int|None, Any] _post(str path, dict|None body, float timeout=DEFAULT_TIMEOUT)
Definition
model_daemon.py:65
aquin.engine.model_daemon._url
str _url(str path)
Definition
model_daemon.py:52
aquin.load_model_display
Definition
load_model_display.py:1
aquin
engine
model_daemon.py
AQIT · Aquin Labs Private Limited · Apache 2.0 · Generated by
1.18.0