modified: CLAUDE.md modified: Makefile new file: languages/awk/Dockerfile new file: languages/awk/uncloseai.awk modified: languages/bash/Dockerfile deleted: languages/bash/examples.sh new file: languages/bash/uncloseai.sh new file: languages/c/curl/Dockerfile new file: languages/c/curl/Makefile new file: languages/c/curl/index.html new file: languages/c/curl/uncloseai.c new file: languages/c/libh2o/Dockerfile new file: languages/c/libh2o/Makefile new file: languages/c/libh2o/uncloseai.c new file: languages/c/nghttp2/Dockerfile new file: languages/c/nghttp2/Makefile new file: languages/c/nghttp2/uncloseai.c new file: languages/clojure/Dockerfile new file: languages/clojure/deps.edn new file: languages/clojure/uncloseai.clj new file: languages/cobol/Dockerfile new file: languages/cobol/discover.sh new file: languages/cobol/hermes.sh new file: languages/cobol/qwen.sh new file: languages/cobol/tts.sh new file: languages/cobol/uncloseai.cob new file: languages/cpp/boost-beast/Dockerfile new file: languages/cpp/boost-beast/Makefile new file: languages/cpp/boost-beast/uncloseai.cpp new file: languages/cpp/cpp-httplib/Dockerfile new file: languages/cpp/cpp-httplib/Makefile new file: languages/cpp/cpp-httplib/uncloseai.cpp new file: languages/cpp/libcurl/Dockerfile new file: languages/cpp/libcurl/Makefile new file: languages/cpp/libcurl/uncloseai.cpp new file: languages/crystal/Dockerfile new file: languages/crystal/uncloseai.cr new file: languages/csharp/Dockerfile new file: languages/csharp/Uncloseai.cs new file: languages/csharp/csharp.csproj new file: languages/dart/Dockerfile new file: languages/dart/bin/uncloseai.dart new file: languages/dart/pubspec.yaml new file: languages/deno/Dockerfile new file: languages/deno/uncloseai.ts new file: languages/elixir/Dockerfile new file: languages/elixir/lib/uncloseai.ex new file: languages/elixir/mix.exs new file: languages/elixir/run.exs new file: languages/erlang/Dockerfile new file: languages/erlang/rebar.config new file: languages/erlang/src/uncloseai.app.src new file: languages/erlang/src/uncloseai.erl new file: languages/fortran/Dockerfile new file: languages/fortran/uncloseai.f90 new file: languages/fsharp/Dockerfile new file: languages/fsharp/Uncloseai.fs new file: languages/fsharp/fsharp.fsproj new file: languages/go/Dockerfile new file: languages/go/README.md new file: languages/go/examples/basic.go new file: languages/go/go.mod new file: languages/go/uncloseai.go new file: languages/go/uncloseai/uncloseai.go new file: languages/haskell/Dockerfile new file: languages/haskell/UncloseAI.hs new file: languages/haskell/uncloseai.cabal new file: languages/java/Dockerfile new file: languages/java/UncloseAI.java new file: languages/javascript/bun/Dockerfile new file: languages/javascript/bun/uncloseai.ts new file: languages/javascript/nodejs/Dockerfile new file: languages/javascript/nodejs/README.md new file: languages/javascript/nodejs/package.json new file: languages/javascript/nodejs/uncloseai.js new file: languages/javascript/typescript/Dockerfile new file: languages/javascript/typescript/package.json new file: languages/javascript/typescript/tsconfig.json new file: languages/javascript/typescript/uncloseai.ts new file: languages/javascript/vanilla/Dockerfile new file: languages/javascript/vanilla/uncloseai.html new file: languages/julia/Dockerfile new file: languages/julia/Project.toml new file: languages/julia/src/uncloseai.jl new file: languages/kotlin/Dockerfile new file: languages/kotlin/build.gradle.kts new file: languages/kotlin/src/main/kotlin/UncloseAI.kt new file: languages/kotlin/uncloseai.kt new file: languages/lua/Dockerfile new file: languages/lua/uncloseai.lua new file: languages/nim/Dockerfile new file: languages/nim/uncloseai.nim new file: languages/ocaml/Dockerfile new file: languages/ocaml/dune new file: languages/ocaml/dune-project new file: languages/ocaml/uncloseai.ml new file: languages/odin/Dockerfile new file: languages/odin/uncloseai.odin new file: languages/perl/Dockerfile new file: languages/perl/uncloseai.pl new file: languages/php/Dockerfile new file: languages/php/uncloseai.php new file: languages/powershell/Dockerfile new file: languages/powershell/uncloseai.ps1 new file: languages/prolog/Dockerfile new file: languages/prolog/uncloseai.pl new file: languages/python/aiohttp/Dockerfile new file: languages/python/aiohttp/requirements.txt new file: languages/python/aiohttp/uncloseai.py modified: languages/python/httpx-async/Dockerfile deleted: languages/python/httpx-async/examples.py new file: languages/python/httpx-async/uncloseai.py modified: languages/python/openai-client/Dockerfile deleted: languages/python/openai-client/examples.py new file: languages/python/openai-client/uncloseai.py modified: languages/python/requests/Dockerfile deleted: languages/python/requests/examples.py new file: languages/python/requests/uncloseai.py new file: languages/r/Dockerfile new file: languages/r/uncloseai.R new file: languages/ruby/Dockerfile new file: languages/ruby/README.md new file: languages/ruby/uncloseai.rb new file: languages/rust/Cargo.toml new file: languages/rust/Dockerfile new file: languages/rust/README.md new file: languages/rust/examples/basic.rs new file: languages/rust/src/lib.rs new file: languages/rust/src/uncloseai.rs new file: languages/scala/Dockerfile new file: languages/scala/build.sbt new file: languages/scala/project/build.properties new file: languages/scala/project/plugins.sbt new file: languages/scala/src/main/scala/UncloseAI.scala new file: languages/tcl/Dockerfile new file: languages/tcl/uncloseai.tcl new file: languages/v/Dockerfile new file: languages/v/uncloseai.v new file: languages/v/v.mod new file: languages/vbnet/Dockerfile new file: languages/vbnet/UncloseAI.vb new file: languages/vbnet/UncloseAI.vbproj modified: languages/zig/Dockerfile modified: languages/zig/build.zig deleted: languages/zig/src/main.zig new file: languages/zig/src/uncloseai.zig
381 lines
11 KiB
Lua
381 lines
11 KiB
Lua
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("✓ Speech file created: speech.mp3 (%d bytes)\n", #audio_data))
|
|
else
|
|
print("✗ TTS Error: " .. tostring(audio_data) .. "\n")
|
|
end
|
|
end
|
|
|
|
print("=== Examples Complete ===")
|
|
end
|
|
|
|
return UncloseAI
|