Hi, I have trying to use nim for Deepseek API, but got suck with handling SSE response. I have a working example for the non-stream version, but I want to also support the stream version. can anyone help?
dpsk.nim ( this one only works with "stream": false )
import httpclient, json, os, strutils
const
DeepSeekBaseUrl = "https://api.deepseek.com/v1"
DefaultModel = "deepseek-chat"
type
DeepSeekClient* = ref object
apiKey*: string
model*: string
client*: HttpClient
proc newDeepSeekClient*(apiKey: string, model = DefaultModel): DeepSeekClient =
new result
result.apiKey = apiKey
result.model = model
result.client = newHttpClient(headers = newHttpHeaders({
"Content-Type": "application/json",
"Authorization": "Bearer " & apiKey
}))
proc chat*(chat: DeepSeekClient, messages: JsonNode): JsonNode =
let payload = %*{
"model": chat.model,
"messages": messages,
"stream": false
}
let response = chat.client.post(
DeepSeekBaseUrl & "/chat/completions",
body = $payload
)
return response.body.parseJson()
proc getMessageText*(response: JsonNode): string =
result = response["choices"][0]["message"]["content"].getStr()
when isMainModule:
let apiKey = getEnv("DEEPSEEK_API_KEY")
if apiKey.len == 0:
echo "Please set DEEPSEEK_API_KEY environment variable"
quit(1)
var dpsk = newDeepSeekClient(apiKey)
var messages = %*[{"role": "system", "content": "You are a helpful assistant."}]
while true:
stdout.write("\nYou: ")
let userInput = stdin.readLine()
if toLowerAscii(userInput) == "exit" or toLowerAscii(userInput) == "quit":
break
# Add user message to history
messages.add(%*{"role": "user", "content": userInput})
# Display streaming response
stdout.write("\nDeepSeek: ")
let response = dpsk.chat(messages)
# write response token-wise
for t in response.getMessageText.tokenize():
stdout.write(t[0])
stdout.flushFile()
sleep(100)
echo "" it will probably not help you, I made a SSE client once based on Harpoon, @juancarlospaco, but it shows the protocol (half way down the iterator). It is the only Nim http client where I got it working.
https://gist.github.com/ingoogni/459c79707065a8492651c0130954e5db
I have update the original code. added timer for chat() and chatStream(), it seems that bodyStream*: Stream did not get the response immediately...
import httpclient, json, os, strutils
import times
import streams
const
DeepSeekBaseUrl = "https://api.deepseek.com/v1"
DefaultModel = "deepseek-chat"
type
# DeepSeekClient* = ref object
DeepSeekClient* = object
apiKey*: string
model*: string
client*: HttpClient
proc newDeepSeekClient*(apiKey: string, model = DefaultModel): DeepSeekClient =
# new result
result.apiKey = apiKey
result.model = model
result.client = newHttpClient(headers = newHttpHeaders({
"Content-Type": "application/json",
"Authorization": "Bearer " & apiKey
}))
proc chat*(chat: DeepSeekClient, messages: JsonNode): JsonNode =
let payload = %*{
"model": chat.model,
"messages": messages,
"stream": false
}
let response = chat.client.post(
DeepSeekBaseUrl & "/chat/completions",
body = $payload
)
return response.body.parseJson()
proc chatStream*(chat: DeepSeekClient, messages: JsonNode): Stream =
let payload = %*{
"model": chat.model,
"messages": messages,
"stream": true
}
let response = chat.client.post(
DeepSeekBaseUrl & "/chat/completions",
body = $payload
)
return response.bodyStream
proc echoStream*(s: Stream) =
while not s.atEnd:
let line = s.readLine()
if line.startsWith("data: "):
let payload = line[6..^1].strip()
echo getTime().toUnix, " | ", payload
elif line.len > 0:
echo "Raw stream line: ", line
# Maintain responsiveness
# sleep(50)
proc getMsgText*(response: JsonNode): string =
result = response["choices"][0]["message"]["content"].getStr()
proc getMsgID*(response: JsonNode): string =
result = response["id"].getStr()
template timeIt*(body: untyped): untyped =
let start = getTime()
body
let finish = getTime()
let duration = finish - start
echo "Time taken: ", duration.inMilliseconds, "ms"
# nim c -r -d:ssl dpsk.nim
when isMainModule:
let apiKey = getEnv("DEEPSEEK_API_KEY")
if apiKey.len == 0:
echo "Please set DEEPSEEK_API_KEY environment variable"
quit(1)
var dpsk = newDeepSeekClient(apiKey)
var messages = %*[{"role": "system", "content": "You are a helpful assistant."}]
while true:
stdout.write("\nYou: ")
let userInput = stdin.readLine()
if toLowerAscii(userInput) == "exit" or toLowerAscii(userInput) == "quit":
break
# Add user message to history
messages.add(%*{"role": "user", "content": userInput})
# Display streaming response
# stdout.write("\nDeepSeek: ")
timeIt:
let response = dpsk.chat(messages)
echo response.getMsgID
timeIt:
dpsk.chatStream(messages).echoStream
I have also tried curl method, it does work. reply is write to terminal one by one, code below
import json, os, strutils
import times
import streams
import osproc
const
DeepSeekBaseUrl = "https://api.deepseek.com/v1"
DefaultModel = "deepseek-chat"
proc chatStreamCurl*(messages: JsonNode, apiKey: string) =
let payload = %*{
"model": DefaultModel,
"messages": messages,
"stream": true
}
let curlCmd = @[
"curl", "-sS", "-N", # -N disables buffering
"-X", "POST",
"-H", "Authorization: Bearer " & apiKey,
"-H", "Content-Type: application/json",
"-H", "Accept: text/event-stream",
"-d", $payload,
DeepSeekBaseUrl & "/chat/completions"
]
var p = startProcess(command=curlCmd[0], args=curlCmd[1..^1], options={poUsePath})
let output = p.outputStream
var buffer: string
while not output.atEnd:
let line = output.readLine()
if line.startsWith("data: "):
let payload = line[6..^1].strip()
if payload == "[DONE]": break
try:
let jsonNode = parseJson(payload)
let content = jsonNode["choices"][0]["delta"]["content"].getStr()
buffer.add(content)
stdout.write(content)
stdout.flushFile()
except:
echo "Error processing chunk: ", getCurrentExceptionMsg()
echo "\n\nFinal response: ", buffer
p.close()
template timeIt*(body: untyped): untyped =
let start = getTime()
body
let finish = getTime()
let duration = finish - start
echo "Time taken: ", duration.inMilliseconds, "ms"
when isMainModule:
let apiKey = getEnv("DEEPSEEK_API_KEY")
if apiKey.len == 0:
echo "Please set DEEPSEEK_API_KEY environment variable"
quit(1)
var messages = %*[{"role": "system", "content": "You are a helpful assistant."}]
while true:
stdout.write("\nYou: ")
let userInput = stdin.readLine()
if toLowerAscii(userInput) == "exit" or toLowerAscii(userInput) == "quit":
break
# Add user message to history
messages.add(%*{"role": "user", "content": userInput})
timeIt:
chatStreamCurl(messages, apiKey) Stumbled across this thread because I am trying to write a local AI agent and ran into the same stream issues. After some tinkering I ended up with some code that seems to work - so seems that the behavior of the httpclient was fixed... (and yes the code is not idiomatic or beautiful - just a quick and dirty test)
import std/json, httpclient, asyncdispatch
const
OllamaAPIURL = "http://localhost:11434/api/chat"
modelName = "mistral"
stream = true
proc main() {.async.} =
var client = newAsyncHTTPClient()
var requestBody = %* {
"model" : modelName,
"messages" : [
{"role" : "system", "content": "You are a usefull helper"},
{"role" : "user", "content": "Compare Marc Aurelius Philosophy to Schoppenhauer"},
],
"stream" : stream
}
let response = await client.post(OllamaAPIURL, $requestBody)
if response.status == $Http200:
while true:
let (hasContent, data) = waitfor response.bodyStream.read()
if hasContent:
let jdata = parseJson(data)
let content = jdata["message"]["content"]
write(stdout, content.getStr())
flushfile(stdout)
if response.bodyStream.finished:
write(stdout, "\n")
break
else:
echo "Error: "
waitfor main()
# sorry for the wrong format
import std/[json, httpclient, asyncdispatch, strutils, os]
const
OllamaAPIURL = "http://localhost:11434/api/chat"
modelName = "mistral"
proc main() {.async.} =
var client = newAsyncHTTPClient()
let requestBody = %* {
"model": modelName,
"messages": @[
%* {"role": "system", "content": "You are a useful helper"},
%* {"role": "user", "content": "Compare Robert Musil philosophy to Schopenhauer"}
],
"stream": true
}
let response = await client.post(OllamaAPIURL, $requestBody)
if response.code == Http200:
var buffer = ""
while true:
let (ok, chunk) = await response.bodyStream.read()
if not ok: break
buffer.add(chunk)
# Process complete lines
while '\n' in buffer:
let idx = buffer.find('\n')
let line = buffer[0 .. idx - 1]
buffer.delete(0, idx + 1)
let trimmed = line.strip()
if trimmed.len == 0: continue
if trimmed == "[DONE]":
break
try:
let jdata = parseJson(trimmed)
if jdata.hasKey("message") and jdata["message"].hasKey("content"):
let content = jdata["message"]["content"].getStr()
write(stdout, content)
flushFile(stdout)
except JsonParsingError, KeyError, ValueError:
stderr.write("⚠ Invalid JSON: ", trimmed, "\n")
echo "" # final newline
else:
let body = await response.body()
echo "HTTP error ", response.code, ": ", body
when isMainModule:
waitFor main()