-- 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