Skip to content

Commit 2dce45c

Browse files
simbasimba
authored andcommitted
feat: add Aegis integration events
- Emit JSON-line ready event on stdout after server starts: {"event":"ready","port":5413,"model":"...","engine":"mlx","vision":false} - Add approximate token counting in non-streaming responses (prompt chars/4 heuristic + completion chunk count) - Note: Aegis detects process exit for stopped state automatically
1 parent 6bd9224 commit 2dce45c

File tree

1 file changed

+27
-1
lines changed

1 file changed

+27
-1
lines changed

Sources/mlx-server/main.swift

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -172,16 +172,23 @@ struct MLXServer: AsyncParsableCommand {
172172
} else {
173173
// Non-streaming: collect all chunks
174174
var fullText = ""
175+
var completionTokenCount = 0
175176
for await generation in stream {
176177
switch generation {
177178
case .chunk(let text):
178179
fullText += text
180+
completionTokenCount += 1
179181
case .info, .toolCall:
180182
break
181183
}
182184
}
183185
await semaphore.signal()
184186

187+
// Approximate prompt tokens (chars / 4 is a reasonable heuristic for most tokenizers)
188+
let promptText = chatReq.messages.map { $0.content }.joined(separator: " ")
189+
let estimatedPromptTokens = max(1, promptText.count / 4)
190+
let totalTokens = estimatedPromptTokens + completionTokenCount
191+
185192
let resp = ChatCompletionResponse(
186193
id: "chatcmpl-\(UUID().uuidString)",
187194
model: modelId,
@@ -193,7 +200,7 @@ struct MLXServer: AsyncParsableCommand {
193200
finishReason: "stop"
194201
)
195202
],
196-
usage: TokenUsage(promptTokens: 0, completionTokens: 0, totalTokens: 0)
203+
usage: TokenUsage(promptTokens: estimatedPromptTokens, completionTokens: completionTokenCount, totalTokens: totalTokens)
197204
)
198205
let encoded = try JSONEncoder().encode(resp)
199206
return Response(
@@ -211,6 +218,25 @@ struct MLXServer: AsyncParsableCommand {
211218
)
212219

213220
print("[mlx-server] ✅ Ready. Listening on http://\(host):\(port)")
221+
222+
// ── Emit machine-readable ready event for Aegis integration ──
223+
let readyEvent: [String: Any] = [
224+
"event": "ready",
225+
"port": port,
226+
"model": modelId,
227+
"engine": "mlx",
228+
"vision": false
229+
]
230+
if let data = try? JSONSerialization.data(withJSONObject: readyEvent),
231+
let json = String(data: data, encoding: .utf8) {
232+
print(json)
233+
fflush(stdout)
234+
}
235+
236+
// ── Handle SIGTERM/SIGINT for graceful shutdown ──
237+
// Note: C signal handlers can't safely do complex I/O.
238+
// Aegis detects process exit and fires onEngineStopped() automatically.
239+
214240
try await app.runService()
215241
}
216242
}

0 commit comments

Comments
 (0)