uncloseai.com/public/languages/nim/uncloseai.nim

309 lines
8.2 KiB
Nim

# PUBLIC DOMAIN - NO LICENSE, NO WARRANTY
# Copyright 2025 TimeHexOn & foxhop & russell@unturf
# https://www.permacomputer.com
import httpclient, json, strformat, os, strutils, asyncdispatch, streams
# UncloseAI - Nim client for OpenAI-compatible APIs with streaming support
type
ModelInfo* = object
id*: string
endpoint*: string
maxTokens*: int
ChatMessage* = object
role*: string
content*: string
UncloseAI* = ref object
models: seq[ModelInfo]
ttsEndpoints: seq[string]
apiKey: string
timeout: int
debug: bool
# Forward declarations
proc discoverEnvEndpoints(self: UncloseAI, prefix: string): seq[string]
proc discoverModels(self: UncloseAI, endpoints: seq[string])
proc newUncloseAI*(
modelEndpoints: seq[string] = @[],
ttsEndpoints: seq[string] = @[],
apiKey: string = "",
timeout: int = 30000,
debug: bool = false
): UncloseAI =
result = UncloseAI(
models: @[],
ttsEndpoints: @[],
apiKey: apiKey,
timeout: timeout,
debug: debug
)
# Discover endpoints from environment
let modelEnds = if modelEndpoints.len > 0: modelEndpoints else: result.discoverEnvEndpoints("MODEL_ENDPOINT")
let ttsEnds = if ttsEndpoints.len > 0: ttsEndpoints else: result.discoverEnvEndpoints("TTS_ENDPOINT")
if result.debug:
echo fmt"[DEBUG] Initialized with {modelEnds.len} endpoint(s)"
result.discoverModels(modelEnds)
result.ttsEndpoints = ttsEnds
proc discoverEnvEndpoints(self: UncloseAI, prefix: string): seq[string] =
result = @[]
for i in 1..<10000:
let endpoint = getEnv(prefix & "_" & $i)
if endpoint == "":
break
result.add(endpoint)
proc discoverModels(self: UncloseAI, endpoints: seq[string]) =
for endpoint in endpoints:
if self.debug:
echo "[DEBUG] Discovering from: ", endpoint
try:
let client = newHttpClient(timeout = 10000)
let response = client.getContent(endpoint & "/models")
let jsonData = parseJson(response)
for model in jsonData["data"]:
let modelId = model["id"].getStr()
# Skip permission entries
if modelId.startsWith("modelperm-") or modelId.startsWith("chatcmpl-"):
continue
let maxTokens = if model.hasKey("max_model_len"):
model["max_model_len"].getInt()
else:
8192
self.models.add(ModelInfo(
id: modelId,
endpoint: endpoint,
maxTokens: maxTokens
))
if self.debug:
echo "[DEBUG] Discovered: ", modelId
except:
if self.debug:
echo "[DEBUG] Error: ", getCurrentExceptionMsg()
proc listModels*(self: UncloseAI): seq[ModelInfo] =
return self.models
proc resolveModel(self: UncloseAI, model: string): ModelInfo =
if self.models.len == 0:
raise newException(ValueError, "No models available")
if model == "":
return self.models[0]
for m in self.models:
if m.id == model:
return m
raise newException(ValueError, fmt"Model '{model}' not found")
proc chat*(
self: UncloseAI,
messages: seq[ChatMessage],
model: string = "",
maxTokens: int = 100,
temperature: float = 0.7
): JsonNode =
let modelInfo = self.resolveModel(model)
var messagesJson = newJArray()
for msg in messages:
messagesJson.add(%* {"role": msg.role, "content": msg.content})
let payload = %* {
"model": modelInfo.id,
"messages": messagesJson,
"max_tokens": maxTokens,
"temperature": temperature,
"stream": false
}
let client = newHttpClient(timeout = self.timeout)
client.headers = newHttpHeaders({"Content-Type": "application/json"})
if self.apiKey != "":
client.headers["Authorization"] = "Bearer " & self.apiKey
let response = client.request(
modelInfo.endpoint & "/chat/completions",
httpMethod = HttpPost,
body = $payload
)
return parseJson(response.body)
proc chatStream*(
self: UncloseAI,
messages: seq[ChatMessage],
model: string = "",
maxTokens: int = 500,
temperature: float = 0.7,
callback: proc(content: string)
) =
let modelInfo = self.resolveModel(model)
var messagesJson = newJArray()
for msg in messages:
messagesJson.add(%* {"role": msg.role, "content": msg.content})
let payload = %* {
"model": modelInfo.id,
"messages": messagesJson,
"max_tokens": maxTokens,
"temperature": temperature,
"stream": true
}
let client = newHttpClient(timeout = self.timeout)
client.headers = newHttpHeaders({
"Content-Type": "application/json",
"Accept": "text/event-stream"
})
if self.apiKey != "":
client.headers["Authorization"] = "Bearer " & self.apiKey
try:
let response = client.request(
modelInfo.endpoint & "/chat/completions",
httpMethod = HttpPost,
body = $payload
)
# Parse streaming response
var buffer = ""
for line in response.bodyStream.lines:
let trimmed = line.strip()
if trimmed.startsWith("data: "):
let data = trimmed[6..^1].strip()
if data == "[DONE]":
break
try:
let chunk = parseJson(data)
if chunk.hasKey("choices") and chunk["choices"].len > 0:
let delta = chunk["choices"][0]["delta"]
if delta.hasKey("content"):
let content = delta["content"].getStr()
if content.len > 0:
callback(content)
except:
if self.debug:
echo "[DEBUG] Parse error: ", getCurrentExceptionMsg()
except:
if self.debug:
echo "[DEBUG] Stream error: ", getCurrentExceptionMsg()
proc tts*(
self: UncloseAI,
text: string,
voice: string = "alloy",
model: string = "tts-1",
responseFormat: string = "mp3"
): string =
if self.ttsEndpoints.len == 0:
raise newException(ValueError, "No TTS endpoints available")
let endpoint = self.ttsEndpoints[0]
let payload = %* {
"model": model,
"voice": voice,
"input": text,
"response_format": responseFormat
}
let client = newHttpClient(timeout = self.timeout)
client.headers = newHttpHeaders({"Content-Type": "application/json"})
if self.apiKey != "":
client.headers["Authorization"] = "Bearer " & self.apiKey
let response = client.request(
endpoint & "/audio/speech",
httpMethod = HttpPost,
body = $payload
)
return response.body
# Demo when run as main module
when isMainModule:
echo "=== UncloseAI Nim Client (with Streaming) ===\n"
let client = newUncloseAI(debug = true)
let models = client.listModels()
if models.len == 0:
echo "ERROR: No models discovered. Set environment variables:"
echo " MODEL_ENDPOINT_1, MODEL_ENDPOINT_2, etc."
quit(1)
echo fmt"\nDiscovered {models.len} model(s):"
for model in models:
echo fmt" - {model.id} (max_tokens: {model.maxTokens})"
echo ""
# Non-streaming chat
echo "=== Non-Streaming Chat ==="
try:
let response = client.chat(
@[
ChatMessage(role: "system", content: "You are a helpful AI assistant."),
ChatMessage(role: "user", content: "Explain quantum computing in one sentence.")
],
maxTokens = 100
)
let content = response["choices"][0]["message"]["content"].getStr()
echo "Response: ", content, "\n"
except:
echo "Error: ", getCurrentExceptionMsg(), "\n"
# Streaming chat
echo "=== Streaming Chat ==="
let modelId = if models.len > 1: models[1].id else: ""
echo "Model: ", if modelId != "": modelId else: models[0].id
stdout.write("Response: ")
stdout.flushFile()
try:
client.chatStream(
@[
ChatMessage(role: "system", content: "You are a coding assistant."),
ChatMessage(role: "user", content: "Write a Nim function to check if a number is prime")
],
model = modelId,
maxTokens = 200,
callback = proc(content: string) =
stdout.write(content)
stdout.flushFile()
)
echo "\n"
except:
echo "\nError: ", getCurrentExceptionMsg(), "\n"
# TTS
if client.ttsEndpoints.len > 0:
echo "=== TTS Speech Generation ==="
try:
let audioData = client.tts("Hello from UncloseAI Nim client! This demonstrates streaming support.")
writeFile("speech.mp3", audioData)
echo fmt"[OK] Speech file created: speech.mp3 ({audioData.len} bytes)\n"
except:
echo "[ERROR] TTS Error: ", getCurrentExceptionMsg(), "\n"
echo "=== Examples Complete ==="