uncloseai.com/public/languages/lua/uncloseai.lua

385 lines
11 KiB
Lua

-- PUBLIC DOMAIN - NO LICENSE, NO WARRANTY
-- Copyright 2025 TimeHexOn & foxhop & russell@unturf
-- https://www.permacomputer.com
local http = require("socket.http")
local https = require("ssl.https")
local ltn12 = require("ltn12")
local json = require("cjson")
local socket = require("socket")
-- UncloseAI - Lua client for OpenAI-compatible APIs with streaming support
local UncloseAI = {}
UncloseAI.__index = UncloseAI
function UncloseAI.new(opts)
opts = opts or {}
local self = setmetatable({}, UncloseAI)
self.models = {}
self.tts_endpoints = {}
self.api_key = opts.api_key
self.timeout = opts.timeout or 30
self.debug = opts.debug or false
-- Discover endpoints from environment
local model_endpoints = opts.model_endpoints or self:_discover_env_endpoints("MODEL_ENDPOINT")
local tts_endpoints = opts.tts_endpoints or self:_discover_env_endpoints("TTS_ENDPOINT")
if self.debug then
print(string.format("[DEBUG] Initialized with %d endpoint(s)", #model_endpoints))
end
-- Discover models
self:_discover_models(model_endpoints)
self.tts_endpoints = tts_endpoints
return self
end
function UncloseAI:_discover_env_endpoints(prefix)
local endpoints = {}
for i = 1, 9999 do
local endpoint = os.getenv(prefix .. "_" .. tostring(i))
if not endpoint then break end
table.insert(endpoints, endpoint)
end
return endpoints
end
function UncloseAI:_discover_models(endpoints)
for _, endpoint in ipairs(endpoints) do
if self.debug then
print("[DEBUG] Discovering from: " .. endpoint)
end
local success, err = pcall(function()
local response_body = {}
local res, code = https.request{
url = endpoint .. "/models",
method = "GET",
sink = ltn12.sink.table(response_body)
}
if code == 200 then
local response = json.decode(table.concat(response_body))
if response.data then
for _, model in ipairs(response.data) do
table.insert(self.models, {
id = model.id,
endpoint = endpoint,
max_tokens = model.max_model_len or 8192
})
if self.debug then
print("[DEBUG] Discovered: " .. model.id)
end
end
end
end
end)
if not success and self.debug then
print("[DEBUG] Error: " .. tostring(err))
end
end
end
function UncloseAI:list_models()
local result = {}
for _, model in ipairs(self.models) do
table.insert(result, {
id = model.id,
endpoint = model.endpoint,
max_tokens = model.max_tokens
})
end
return result
end
function UncloseAI:_resolve_model(model)
if #self.models == 0 then
error("No models available")
end
if not model then
return self.models[1]
end
for _, m in ipairs(self.models) do
if m.id == model then
return m
end
end
error("Model '" .. model .. "' not found")
end
function UncloseAI:chat(messages, opts)
opts = opts or {}
local model_info = self:_resolve_model(opts.model)
local max_tokens = opts.max_tokens or 100
local temperature = opts.temperature or 0.7
local payload = {
model = model_info.id,
messages = messages,
max_tokens = max_tokens,
temperature = temperature,
stream = false
}
local request_body = json.encode(payload)
local response_body = {}
local headers = {
["Content-Type"] = "application/json",
["Content-Length"] = tostring(#request_body)
}
if self.api_key then
headers["Authorization"] = "Bearer " .. self.api_key
end
local res, code = https.request{
url = model_info.endpoint .. "/chat/completions",
method = "POST",
headers = headers,
source = ltn12.source.string(request_body),
sink = ltn12.sink.table(response_body)
}
if code == 200 then
return json.decode(table.concat(response_body))
else
error("Request failed with code: " .. tostring(code))
end
end
function UncloseAI:chat_stream(messages, opts, callback)
opts = opts or {}
local model_info = self:_resolve_model(opts.model)
local max_tokens = opts.max_tokens or 500
local temperature = opts.temperature or 0.7
local payload = {
model = model_info.id,
messages = messages,
max_tokens = max_tokens,
temperature = temperature,
stream = true
}
local request_body = json.encode(payload)
-- Parse URL
local protocol, host, port, path = model_info.endpoint:match("^(https?)://([^:/]+):?(%d*)(.*)$")
port = port and tonumber(port) or (protocol == "https" and 443 or 80)
path = (path == "" and "/v1" or path) .. "/chat/completions"
-- Create socket connection
local sock = socket.tcp()
sock:settimeout(self.timeout)
local success, err = pcall(function()
assert(sock:connect(host, port))
-- For HTTPS, wrap socket with SSL
if protocol == "https" then
local ssl = require("ssl")
sock = assert(ssl.wrap(sock, {mode = "client", protocol = "tlsv1_2"}))
assert(sock:dohandshake())
end
-- Send HTTP request
local headers = {
"POST " .. path .. " HTTP/1.1",
"Host: " .. host,
"Content-Type: application/json",
"Content-Length: " .. tostring(#request_body),
"Connection: close"
}
if self.api_key then
table.insert(headers, "Authorization: Bearer " .. self.api_key)
end
local request = table.concat(headers, "\r\n") .. "\r\n\r\n" .. request_body
assert(sock:send(request))
-- Read response headers
local line = sock:receive("*l")
while line and line ~= "" do
line = sock:receive("*l")
end
-- Read streaming response
local buffer = ""
while true do
local chunk, err = sock:receive(1024)
if not chunk then break end
buffer = buffer .. chunk
local lines = {}
for line in buffer:gmatch("([^\n]*)\n") do
table.insert(lines, line)
end
-- Keep incomplete line in buffer
local last_newline = buffer:find("\n[^\n]*$")
if last_newline then
buffer = buffer:sub(last_newline + 1)
end
-- Process complete lines
for i = 1, #lines - 1 do
local line = lines[i]:gsub("\r", "")
if line:match("^data: ") then
local data = line:sub(7)
if data == "[DONE]" then
return
end
local success, chunk_data = pcall(json.decode, data)
if success and chunk_data.choices and chunk_data.choices[1] then
local delta = chunk_data.choices[1].delta
if delta and delta.content then
callback(delta.content)
end
end
end
end
end
end)
sock:close()
if not success and self.debug then
print("[DEBUG] Stream error: " .. tostring(err))
end
end
function UncloseAI:tts(text, opts)
opts = opts or {}
if #self.tts_endpoints == 0 then
error("No TTS endpoints available")
end
local endpoint = self.tts_endpoints[1]
local voice = opts.voice or "alloy"
local model = opts.model or "tts-1"
local response_format = opts.response_format or "mp3"
local payload = {
model = model,
voice = voice,
input = text,
response_format = response_format
}
local request_body = json.encode(payload)
local response_body = {}
local headers = {
["Content-Type"] = "application/json",
["Content-Length"] = tostring(#request_body)
}
if self.api_key then
headers["Authorization"] = "Bearer " .. self.api_key
end
local res, code = https.request{
url = endpoint .. "/audio/speech",
method = "POST",
headers = headers,
source = ltn12.source.string(request_body),
sink = ltn12.sink.table(response_body)
}
if code == 200 then
return table.concat(response_body)
else
error("Request failed with code: " .. tostring(code))
end
end
-- Demo when run as script
if not pcall(debug.getlocal, 4, 1) then
print("=== UncloseAI Lua Client (with Streaming) ===\n")
local client = UncloseAI.new({debug = true})
local models = client:list_models()
if #models == 0 then
print("ERROR: No models discovered. Set environment variables:")
print(" MODEL_ENDPOINT_1, MODEL_ENDPOINT_2, etc.")
os.exit(1)
end
print(string.format("\nDiscovered %d model(s):", #models))
for _, model in ipairs(models) do
print(string.format(" - %s (max_tokens: %d)", model.id, model.max_tokens))
end
print()
-- Non-streaming chat
print("=== Non-Streaming Chat ===")
local success, response = pcall(function()
return client:chat({
{role = "system", content = "You are a helpful AI assistant."},
{role = "user", content = "Explain quantum computing in one sentence."}
}, {max_tokens = 100})
end)
if success then
local content = response.choices[1].message.content
print("Response: " .. content .. "\n")
else
print("Error: " .. tostring(response) .. "\n")
end
-- Streaming chat
print("=== Streaming Chat ===")
local model_id = #models > 1 and models[2].id or nil
print("Model: " .. (model_id or models[1].id))
io.write("Response: ")
io.flush()
local success, err = pcall(function()
client:chat_stream({
{role = "system", content = "You are a coding assistant."},
{role = "user", content = "Write a Lua function to check if a number is prime"}
}, {model = model_id, max_tokens = 200}, function(content)
io.write(content)
io.flush()
end)
end)
if not success then
print("\nError: " .. tostring(err))
end
print("\n")
-- TTS
if #client.tts_endpoints > 0 then
print("=== TTS Speech Generation ===")
local success, audio_data = pcall(function()
return client:tts("Hello from UncloseAI Lua client! This demonstrates streaming support.")
end)
if success then
local file = io.open("speech.mp3", "wb")
file:write(audio_data)
file:close()
print(string.format("[OK] Speech file created: speech.mp3 (%d bytes)\n", #audio_data))
else
print("[ERROR] TTS Error: " .. tostring(audio_data) .. "\n")
end
end
print("=== Examples Complete ===")
end
return UncloseAI