Skip to content

Commit b1eb49a

Browse files
committed
feat: llama-server style logging + SSE CRLF fix
Logging: - srv slot_launch: id 0 | prompt=Xt | prefilling... - srv slot update: id 0 | prefill done | n_tokens=X, t=Y.Ys, Zt/s - srv generate: id 0 | <real-time token stream> - srv log_server_r: response: {full JSON} Both streaming and non-streaming handlers covered. SSE framing: - All SSE chunks now use \r\n\r\n (CRLF) per HTTP spec - Applies to sseChunk, sseUsageChunk, sseToolCallChunk, sseTextChunk, [DONE] SSE spec: already compliant (no further changes needed)
1 parent f394801 commit b1eb49a

1 file changed

Lines changed: 29 additions & 14 deletions

File tree

‎Sources/mlx-server/Server.swift‎

Lines changed: 29 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -887,6 +887,11 @@ func handleChatCompletion(
887887
let promptTokenCount = lmInput.text.tokens.size
888888
let systemHash = systemPromptText.hashValue
889889

890+
// llama-server style: announce prefill start
891+
print("srv slot_launch: id 0 | prompt=\(promptTokenCount)t | prefilling...")
892+
fflush(stdout)
893+
let prefillStart = Date()
894+
890895
// ── Cache-aware generation ──
891896
let stream: AsyncStream<Generation> = try await container.perform { context in
892897
let cache = context.model.newCache(parameters: params)
@@ -940,12 +945,14 @@ func handleChatCompletion(
940945
return handleChatStreaming(
941946
stream: stream, modelId: modelId, stopSequences: stopSequences,
942947
includeUsage: includeUsage, promptTokenCount: promptTokenCount,
943-
jsonMode: jsonMode, semaphore: semaphore, stats: stats, genStart: genStart
948+
jsonMode: jsonMode, semaphore: semaphore, stats: stats,
949+
genStart: genStart, prefillStart: prefillStart
944950
)
945951
} else {
946952
return try await handleChatNonStreaming(
947953
stream: stream, modelId: modelId, stopSequences: stopSequences,
948-
promptTokenCount: promptTokenCount, jsonMode: jsonMode, semaphore: semaphore, stats: stats, genStart: genStart
954+
promptTokenCount: promptTokenCount, jsonMode: jsonMode, semaphore: semaphore,
955+
stats: stats, genStart: genStart, prefillStart: prefillStart
949956
)
950957
}
951958
}
@@ -961,7 +968,8 @@ func handleChatStreaming(
961968
jsonMode: Bool = false,
962969
semaphore: AsyncSemaphore,
963970
stats: ServerStats,
964-
genStart: Date
971+
genStart: Date,
972+
prefillStart: Date
965973
) -> Response {
966974
let (sseStream, cont) = AsyncStream<String>.makeStream()
967975
Task {
@@ -981,9 +989,12 @@ func handleChatStreaming(
981989
if completionTokenCount % 8 == 0 {
982990
try? await Task.sleep(for: .microseconds(50))
983991
}
984-
// Real-time stdout: print token text as it arrives (no newline)
992+
// Real-time stdout: on first token, log prefill completion + start generate line
985993
if firstToken {
986-
print("srv generate: prompt=\(promptTokenCount)t | ", terminator: "")
994+
let prefillDur = Date().timeIntervalSince(prefillStart)
995+
let prefillTokPerSec = prefillDur > 0 ? Double(promptTokenCount) / prefillDur : 0
996+
print("srv slot update: id 0 | prefill done | n_tokens=\(promptTokenCount), t=\(String(format: "%.2f", prefillDur))s, \(String(format: "%.1f", prefillTokPerSec))t/s")
997+
print("srv generate: id 0 | ", terminator: "")
987998
firstToken = false
988999
}
9891000
print(text, terminator: "")
@@ -999,7 +1010,7 @@ func handleChatStreaming(
9991010
if includeUsage {
10001011
cont.yield(sseUsageChunk(modelId: modelId, promptTokens: promptTokenCount, completionTokens: completionTokenCount))
10011012
}
1002-
cont.yield("data: [DONE]\n\n")
1013+
cont.yield("data: [DONE]\r\n\r\n")
10031014
cont.finish()
10041015
stopped = true
10051016
} else {
@@ -1023,7 +1034,7 @@ func handleChatStreaming(
10231034
if includeUsage {
10241035
cont.yield(sseUsageChunk(modelId: modelId, promptTokens: promptTokenCount, completionTokens: completionTokenCount))
10251036
}
1026-
cont.yield("data: [DONE]\n\n")
1037+
cont.yield("data: [DONE]\r\n\r\n")
10271038
cont.finish()
10281039
// llama-server style: print newline then full response JSON
10291040
print("") // end the real-time token stream line
@@ -1072,7 +1083,8 @@ func handleChatNonStreaming(
10721083
jsonMode: Bool = false,
10731084
semaphore: AsyncSemaphore,
10741085
stats: ServerStats,
1075-
genStart: Date
1086+
genStart: Date,
1087+
prefillStart: Date
10761088
) async throws -> Response {
10771089
var fullText = ""
10781090
var completionTokenCount = 0
@@ -1089,9 +1101,12 @@ func handleChatNonStreaming(
10891101
if completionTokenCount % 8 == 0 {
10901102
try? await Task.sleep(for: .microseconds(50))
10911103
}
1092-
// Real-time stdout: print token text as it arrives (no newline)
1104+
// Real-time stdout: on first token, log prefill completion + start generate line
10931105
if firstToken {
1094-
print("srv generate: prompt=\(promptTokenCount)t | ", terminator: "")
1106+
let prefillDur = Date().timeIntervalSince(prefillStart)
1107+
let prefillTokPerSec = prefillDur > 0 ? Double(promptTokenCount) / prefillDur : 0
1108+
print("srv slot update: id 0 | prefill done | n_tokens=\(promptTokenCount), t=\(String(format: "%.2f", prefillDur))s, \(String(format: "%.1f", prefillTokPerSec))t/s")
1109+
print("srv generate: id 0 | ", terminator: "")
10951110
firstToken = false
10961111
}
10971112
print(text, terminator: "")
@@ -1497,7 +1512,7 @@ func sseChunk(modelId: String, delta: String, finishReason: String?) -> String {
14971512
"choices": [choiceObj]
14981513
]
14991514
let data = try! JSONSerialization.data(withJSONObject: chunk)
1500-
return "data: \(String(data: data, encoding: .utf8)!)\n\n"
1515+
return "data: \(String(data: data, encoding: .utf8)!)\r\n\r\n"
15011516
}
15021517

15031518
func sseUsageChunk(modelId: String, promptTokens: Int, completionTokens: Int) -> String {
@@ -1514,7 +1529,7 @@ func sseUsageChunk(modelId: String, promptTokens: Int, completionTokens: Int) ->
15141529
]
15151530
]
15161531
let data = try! JSONSerialization.data(withJSONObject: chunk)
1517-
return "data: \(String(data: data, encoding: .utf8)!)\n\n"
1532+
return "data: \(String(data: data, encoding: .utf8)!)\r\n\r\n"
15181533
}
15191534

15201535
func sseToolCallChunk(modelId: String, index: Int, name: String, arguments: String) -> String {
@@ -1540,7 +1555,7 @@ func sseToolCallChunk(modelId: String, index: Int, name: String, arguments: Stri
15401555
] as [String: Any]]
15411556
]
15421557
let data = try! JSONSerialization.data(withJSONObject: chunk)
1543-
return "data: \(String(data: data, encoding: .utf8)!)\n\n"
1558+
return "data: \(String(data: data, encoding: .utf8)!)\r\n\r\n"
15441559
}
15451560

15461561
func sseTextChunk(modelId: String, text: String, finishReason: String?) -> String {
@@ -1559,7 +1574,7 @@ func sseTextChunk(modelId: String, text: String, finishReason: String?) -> Strin
15591574
"choices": [choiceObj]
15601575
]
15611576
let data = try! JSONSerialization.data(withJSONObject: chunk)
1562-
return "data: \(String(data: data, encoding: .utf8)!)\n\n"
1577+
return "data: \(String(data: data, encoding: .utf8)!)\r\n\r\n"
15631578
}
15641579

15651580
func serializeToolCallArgs(_ args: [String: JSONValue]) -> String {

0 commit comments

Comments
 (0)