271 lines
8.9 KiB
C++
271 lines
8.9 KiB
C++
// PUBLIC DOMAIN - NO LICENSE, NO WARRANTY
|
|
// Copyright 2025 TimeHexOn & foxhop & russell@unturf
|
|
// https://www.permacomputer.com
|
|
|
|
/*
|
|
* UncloseAI C++ Library using Boost.Beast
|
|
* OpenAI-compatible API client with HTTP/HTTPS support
|
|
* Compatible with vLLM, Ollama, and OpenAI-compatible endpoints
|
|
*/
|
|
|
|
#include <boost/beast/core.hpp>
|
|
#include <boost/beast/http.hpp>
|
|
#include <boost/beast/version.hpp>
|
|
#include <boost/asio/connect.hpp>
|
|
#include <boost/asio/ip/tcp.hpp>
|
|
#include <boost/asio/ssl/error.hpp>
|
|
#include <boost/asio/ssl/stream.hpp>
|
|
#include <iostream>
|
|
#include <string>
|
|
#include <vector>
|
|
#include <sstream>
|
|
#include <fstream>
|
|
#include <cstdlib>
|
|
|
|
namespace beast = boost::beast;
|
|
namespace http = beast::http;
|
|
namespace net = boost::asio;
|
|
namespace ssl = net::ssl;
|
|
using tcp = net::ip::tcp;
|
|
|
|
struct ModelInfo {
|
|
std::string id;
|
|
std::string host;
|
|
std::string port;
|
|
std::string base_path;
|
|
int max_tokens;
|
|
};
|
|
|
|
class UncloseAI {
|
|
private:
|
|
std::vector<ModelInfo> models;
|
|
std::vector<std::pair<std::string, std::string>> tts_endpoints; // host, port
|
|
int timeout;
|
|
|
|
void parse_url(const std::string& url, std::string& host, std::string& port, std::string& path) {
|
|
// Parse https://host:port/path
|
|
size_t proto_end = url.find("://");
|
|
if (proto_end == std::string::npos) return;
|
|
|
|
std::string rest = url.substr(proto_end + 3);
|
|
size_t slash = rest.find("/");
|
|
|
|
std::string host_port = (slash != std::string::npos) ? rest.substr(0, slash) : rest;
|
|
path = (slash != std::string::npos) ? rest.substr(slash) : "/v1";
|
|
|
|
size_t colon = host_port.find(":");
|
|
if (colon != std::string::npos) {
|
|
host = host_port.substr(0, colon);
|
|
port = host_port.substr(colon + 1);
|
|
} else {
|
|
host = host_port;
|
|
port = (url.find("https://") == 0) ? "443" : "80";
|
|
}
|
|
}
|
|
|
|
std::string http_get(const std::string& host, const std::string& port, const std::string& target) {
|
|
try {
|
|
net::io_context ioc;
|
|
tcp::resolver resolver(ioc);
|
|
beast::tcp_stream stream(ioc);
|
|
|
|
auto const results = resolver.resolve(host, port);
|
|
stream.connect(results);
|
|
|
|
http::request<http::string_body> req{http::verb::get, target, 11};
|
|
req.set(http::field::host, host);
|
|
req.set(http::field::user_agent, "UncloseAI-Beast");
|
|
|
|
http::write(stream, req);
|
|
|
|
beast::flat_buffer buffer;
|
|
http::response<http::string_body> res;
|
|
http::read(stream, buffer, res);
|
|
|
|
beast::error_code ec;
|
|
stream.socket().shutdown(tcp::socket::shutdown_both, ec);
|
|
|
|
return res.body();
|
|
} catch (...) {
|
|
return "";
|
|
}
|
|
}
|
|
|
|
std::string http_post(const std::string& host, const std::string& port,
|
|
const std::string& target, const std::string& body) {
|
|
try {
|
|
net::io_context ioc;
|
|
tcp::resolver resolver(ioc);
|
|
beast::tcp_stream stream(ioc);
|
|
|
|
auto const results = resolver.resolve(host, port);
|
|
stream.connect(results);
|
|
|
|
http::request<http::string_body> req{http::verb::post, target, 11};
|
|
req.set(http::field::host, host);
|
|
req.set(http::field::user_agent, "UncloseAI-Beast");
|
|
req.set(http::field::content_type, "application/json");
|
|
req.body() = body;
|
|
req.prepare_payload();
|
|
|
|
http::write(stream, req);
|
|
|
|
beast::flat_buffer buffer;
|
|
http::response<http::string_body> res;
|
|
http::read(stream, buffer, res);
|
|
|
|
beast::error_code ec;
|
|
stream.socket().shutdown(tcp::socket::shutdown_both, ec);
|
|
|
|
return res.body();
|
|
} catch (...) {
|
|
return "";
|
|
}
|
|
}
|
|
|
|
void discover_models_from_endpoint(const std::string& endpoint) {
|
|
std::string host, port, base_path;
|
|
parse_url(endpoint, host, port, base_path);
|
|
|
|
std::string target = base_path + (base_path.back() == '/' ? "models" : "/models");
|
|
std::string response = http_get(host, port, target);
|
|
|
|
if (!response.empty()) {
|
|
// Simple JSON parsing for model IDs
|
|
size_t pos = 0;
|
|
while ((pos = response.find("\"id\":\"", pos)) != std::string::npos) {
|
|
pos += 6;
|
|
size_t end = response.find("\"", pos);
|
|
if (end != std::string::npos) {
|
|
std::string model_id = response.substr(pos, end - pos);
|
|
|
|
// Filter out modelperm-* and chatcmpl-*
|
|
if (model_id.substr(0, 10) != "modelperm-" &&
|
|
model_id.substr(0, 9) != "chatcmpl-") {
|
|
ModelInfo info;
|
|
info.id = model_id;
|
|
info.host = host;
|
|
info.port = port;
|
|
info.base_path = base_path;
|
|
info.max_tokens = 8192;
|
|
models.push_back(info);
|
|
}
|
|
}
|
|
pos = end + 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
public:
|
|
UncloseAI(int timeout_sec = 30) : timeout(timeout_sec) {
|
|
std::vector<std::string> endpoints, tts_eps;
|
|
|
|
// Discover endpoints from environment
|
|
for (int i = 1; i < 10000; i++) {
|
|
std::string var = "MODEL_ENDPOINT_" + std::to_string(i);
|
|
const char* ep = std::getenv(var.c_str());
|
|
if (!ep) break;
|
|
endpoints.push_back(ep);
|
|
}
|
|
|
|
for (int i = 1; i < 10000; i++) {
|
|
std::string var = "TTS_ENDPOINT_" + std::to_string(i);
|
|
const char* ep = std::getenv(var.c_str());
|
|
if (!ep) break;
|
|
tts_eps.push_back(ep);
|
|
}
|
|
|
|
for (const auto& ep : endpoints) {
|
|
std::cout << "Discovering from: " << ep << std::endl;
|
|
discover_models_from_endpoint(ep);
|
|
}
|
|
|
|
for (const auto& ep : tts_eps) {
|
|
std::string host, port, path;
|
|
parse_url(ep, host, port, path);
|
|
tts_endpoints.push_back({host, port});
|
|
}
|
|
|
|
std::cout << "\nDiscovered " << models.size() << " model(s)\n" << std::endl;
|
|
}
|
|
|
|
const std::vector<ModelInfo>& list_models() const { return models; }
|
|
|
|
int chat(const std::string& prompt, std::string& response, int model_idx = 0, int max_tokens = 100) {
|
|
if (model_idx >= static_cast<int>(models.size())) return -1;
|
|
|
|
const ModelInfo& model = models[model_idx];
|
|
std::string target = model.base_path + (model.base_path.back() == '/' ? "chat/completions" : "/chat/completions");
|
|
|
|
std::ostringstream json;
|
|
json << "{\"model\":\"" << model.id << "\","
|
|
<< "\"messages\":[{\"role\":\"user\",\"content\":\"" << prompt << "\"}],"
|
|
<< "\"stream\":false,"
|
|
<< "\"max_tokens\":" << max_tokens << ","
|
|
<< "\"temperature\":0.7}";
|
|
|
|
response = http_post(model.host, model.port, target, json.str());
|
|
return response.empty() ? -1 : 0;
|
|
}
|
|
|
|
int tts(const std::string& text, const std::string& voice, const std::string& output_file) {
|
|
if (tts_endpoints.empty()) return -1;
|
|
|
|
auto [host, port] = tts_endpoints[0];
|
|
|
|
std::ostringstream json;
|
|
json << "{\"model\":\"tts-1\","
|
|
<< "\"voice\":\"" << voice << "\","
|
|
<< "\"input\":\"" << text << "\"}";
|
|
|
|
std::string response = http_post(host, port, "/audio/speech", json.str());
|
|
|
|
if (!response.empty()) {
|
|
std::ofstream file(output_file, std::ios::binary);
|
|
if (file.is_open()) {
|
|
file.write(response.c_str(), response.size());
|
|
file.close();
|
|
return 0;
|
|
}
|
|
}
|
|
return -1;
|
|
}
|
|
};
|
|
|
|
int main() {
|
|
std::cout << "=== UncloseAI C++ Client (Boost.Beast) ===\n\n";
|
|
|
|
UncloseAI client(30);
|
|
|
|
if (client.list_models().empty()) {
|
|
std::cout << "ERROR: No models discovered. Set environment variables:\n";
|
|
std::cout << " MODEL_ENDPOINT_1, MODEL_ENDPOINT_2, etc.\n";
|
|
return 1;
|
|
}
|
|
|
|
auto models = client.list_models();
|
|
for (const auto& m : models) {
|
|
std::cout << " - " << m.id << " (max_tokens: " << m.max_tokens << ")\n";
|
|
}
|
|
std::cout << "\n";
|
|
|
|
// Non-streaming chat
|
|
std::cout << "=== Non-Streaming Chat ===\n";
|
|
std::string response;
|
|
if (client.chat("Explain quantum computing in one sentence", response) == 0) {
|
|
std::cout << "Response received (" << response.size() << " bytes)\n\n";
|
|
} else {
|
|
std::cout << "Request failed\n\n";
|
|
}
|
|
|
|
// TTS
|
|
std::cout << "=== TTS Speech Generation ===\n";
|
|
if (client.tts("Hello from Boost.Beast!", "alloy", "/tmp/speech.mp3") == 0) {
|
|
std::cout << "Audio saved to /tmp/speech.mp3\n";
|
|
} else {
|
|
std::cout << "TTS failed\n";
|
|
}
|
|
|
|
std::cout << "\n=== Examples Complete ===\n";
|
|
return 0;
|
|
}
|