Reorganize project structure: move public files to public/ directory
This commit is contained in:
parent
8de043bfef
commit
38545721a0
253 changed files with 0 additions and 0 deletions
|
|
@ -1,381 +0,0 @@
|
|||
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
|
||||
Loading…
Add table
Add a link
Reference in a new issue