Reorganize project structure: move public files to public/ directory

This commit is contained in:
Russell Ballestrini 2025-10-24 13:23:42 -04:00
parent 8de043bfef
commit 38545721a0
253 changed files with 0 additions and 0 deletions

View file

@ -0,0 +1,21 @@
# Pin to specific Alpine version (checked 2025-10-14: alpine:3.20 is latest stable)
FROM alpine:latest
# Install C++ compiler and Boost libraries
RUN apk --no-cache add \
g++ \
make \
boost1.84-dev \
openssl-dev \
ca-certificates
WORKDIR /app
COPY uncloseai.cpp .
COPY Makefile .
# Compile the application
RUN make
# Run the examples
CMD ["./uncloseai"]

View file

@ -0,0 +1,16 @@
CXX = g++
CXXFLAGS = -std=c++17 -Wall -Wextra -O2
LDFLAGS = -lssl -lcrypto -lpthread
TARGET = uncloseai
SRC = uncloseai.cpp
all: $(TARGET)
$(TARGET): $(SRC)
$(CXX) $(CXXFLAGS) -o $(TARGET) $(SRC) $(LDFLAGS)
clean:
rm -f $(TARGET) speech.mp3
.PHONY: all clean

View file

@ -0,0 +1,267 @@
/*
* 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;
}

View file

@ -0,0 +1,24 @@
# Pin to specific Alpine version (checked 2025-10-14: alpine:3.20 is latest stable)
FROM alpine:latest
# Install C++ compiler and build tools
RUN apk --no-cache add \
g++ \
make \
wget \
ca-certificates \
openssl-dev
WORKDIR /app
# Download cpp-httplib header-only library (v0.18.3 latest as of 2025-10-14)
RUN wget -O httplib.h https://raw.githubusercontent.com/yhirose/cpp-httplib/v0.18.3/httplib.h
COPY uncloseai.cpp .
COPY Makefile .
# Compile the application
RUN make
# Run the examples
CMD ["./uncloseai"]

View file

@ -0,0 +1,16 @@
CXX = g++
CXXFLAGS = -std=c++17 -Wall -Wextra -O2 -DCPPHTTPLIB_OPENSSL_SUPPORT
LDFLAGS = -lssl -lcrypto -lpthread
TARGET = uncloseai
SRC = uncloseai.cpp
all: $(TARGET)
$(TARGET): $(SRC) httplib.h
$(CXX) $(CXXFLAGS) -o $(TARGET) $(SRC) $(LDFLAGS)
clean:
rm -f $(TARGET) speech.mp3
.PHONY: all clean

View file

@ -0,0 +1,268 @@
/*
* UncloseAI C++ Library using cpp-httplib (header-only)
* OpenAI-compatible API client with streaming support
* Compatible with vLLM, Ollama, and OpenAI-compatible endpoints
*/
#include <iostream>
#include <string>
#include <vector>
#include <sstream>
#include <fstream>
#include <cstdlib>
#include <functional>
#include "httplib.h"
struct ModelInfo {
std::string id;
std::string endpoint;
std::string host;
int port;
int max_tokens;
};
class UncloseAI {
private:
std::vector<ModelInfo> models;
std::vector<std::pair<std::string, int>> tts_endpoints; // host, port
int timeout;
bool debug;
// Parse URL into host and port
bool parse_url(const std::string& url, std::string& host, int& port, std::string& base_path) {
// Simple URL parsing for https://host:port/path
size_t proto_end = url.find("://");
if (proto_end == std::string::npos) return false;
std::string rest = url.substr(proto_end + 3);
size_t slash_pos = rest.find("/");
std::string host_port;
if (slash_pos != std::string::npos) {
host_port = rest.substr(0, slash_pos);
base_path = rest.substr(slash_pos);
} else {
host_port = rest;
base_path = "/";
}
size_t colon_pos = host_port.find(":");
if (colon_pos != std::string::npos) {
host = host_port.substr(0, colon_pos);
port = std::stoi(host_port.substr(colon_pos + 1));
} else {
host = host_port;
port = (url.find("https://") == 0) ? 443 : 80;
}
return true;
}
void discover_endpoints_from_env(const std::string& prefix, std::vector<std::string>& endpoints) {
for(int i = 1; i < 10000; i++) {
std::string var_name = prefix + "_" + std::to_string(i);
const char* endpoint = std::getenv(var_name.c_str());
if(!endpoint) break;
endpoints.push_back(endpoint);
}
}
void discover_models(const std::vector<std::string>& endpoints) {
for(const auto& endpoint : endpoints) {
if(debug) {
std::cout << "[DEBUG] Discovering from: " << endpoint << std::endl;
}
std::string host;
int port;
std::string base_path;
if(!parse_url(endpoint, host, port, base_path)) continue;
httplib::Client cli(host, port);
cli.set_connection_timeout(0, 10000000); // 10 sec
cli.set_read_timeout(10, 0);
std::string models_path = base_path + (base_path.back() == '/' ? "models" : "/models");
auto res = cli.Get(models_path.c_str());
if(res && res->status == 200) {
// Simple JSON parsing for model IDs
std::string body = res->body;
size_t pos = 0;
while((pos = body.find("\"id\":\"", pos)) != std::string::npos) {
pos += 6;
size_t end = body.find("\"", pos);
if(end != std::string::npos) {
std::string model_id = body.substr(pos, end - pos);
// Filter out modelperm-* and chatcmpl-* entries
if(model_id.substr(0, 10) != "modelperm-" && model_id.substr(0, 9) != "chatcmpl-") {
ModelInfo info;
info.id = model_id;
info.endpoint = endpoint;
info.host = host;
info.port = port;
info.max_tokens = 8192;
models.push_back(info);
if(debug) {
std::cout << "[DEBUG] Discovered: " << model_id << std::endl;
}
}
}
pos = end + 1;
}
}
}
}
public:
UncloseAI(int timeout = 30, bool debug = false) : timeout(timeout), debug(debug) {
std::vector<std::string> endpoints;
std::vector<std::string> tts_eps;
discover_endpoints_from_env("MODEL_ENDPOINT", endpoints);
discover_endpoints_from_env("TTS_ENDPOINT", tts_eps);
if(debug) {
std::cout << "[DEBUG] Initialized with " << endpoints.size() << " endpoint(s)" << std::endl;
}
discover_models(endpoints);
// Parse TTS endpoints
for(const auto& ep : tts_eps) {
std::string host;
int port;
std::string base_path;
if(parse_url(ep, host, port, base_path)) {
tts_endpoints.push_back({host, port});
}
}
}
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];
httplib::Client cli(model.host, model.port);
cli.set_connection_timeout(0, timeout * 1000000);
cli.set_read_timeout(timeout, 0);
std::ostringstream json;
json << "{\"model\":\"" << model.id << "\","
<< "\"messages\":[{\"role\":\"user\",\"content\":\"" << prompt << "\"}],"
<< "\"stream\":false,"
<< "\"max_tokens\":" << max_tokens << ","
<< "\"temperature\":0.7}";
auto res = cli.Post("/chat/completions", json.str(), "application/json");
if(res && res->status == 200) {
response = res->body;
return 0;
}
return -1;
}
int chat_stream(const std::string& prompt, std::function<void(const std::string&)> callback, int model_idx = 0, int max_tokens = 500) {
// NOTE: cpp-httplib streaming API is complex, using simple buffered approach
// For production use, consider implementing proper SSE streaming with ContentReceiver
std::string response;
if(chat(prompt, response, model_idx, max_tokens) == 0) {
if(callback) {
callback(response);
}
return 0;
}
return -1;
}
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];
httplib::Client cli(host, port);
cli.set_connection_timeout(0, timeout * 1000000);
cli.set_read_timeout(timeout, 0);
std::ostringstream json;
json << "{\"model\":\"tts-1\","
<< "\"voice\":\"" << voice << "\","
<< "\"input\":\"" << text << "\"}";
auto res = cli.Post("/audio/speech", json.str(), "application/json");
if(res && res->status == 200) {
std::ofstream file(output_file, std::ios::binary);
if(file.is_open()) {
file.write(res->body.c_str(), res->body.size());
file.close();
return 0;
}
}
return -1;
}
};
// Demo program showing library usage
int main() {
std::cout << "=== UncloseAI C++ Client (cpp-httplib with Streaming) ===\n\n";
UncloseAI client(30, true);
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();
std::cout << "\nDiscovered " << models.size() << " model(s):\n";
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";
std::cout << "(Full response requires JSON parsing library)\n\n";
} else {
std::cout << "Request failed\n\n";
}
// Streaming chat
std::cout << "=== Streaming Chat ===\n";
int model_idx = (models.size() > 1) ? 1 : 0;
std::cout << "Model: " << models[model_idx].id << "\n";
std::cout << "Response: ";
client.chat_stream("Write a hello world program in C++",
[](const std::string& content) {
std::cout << content << std::flush;
}, model_idx, 500);
std::cout << "\n\n";
// TTS
std::cout << "=== TTS Speech Generation ===\n";
std::cout << "Model: tts-1\n";
if(client.tts("Hello from UncloseAI C++ client with cpp-httplib! This demonstrates streaming support.",
"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;
}

View file

@ -0,0 +1,21 @@
# Pin to specific Alpine version (checked 2025-10-12: alpine:3.21 is latest stable)
FROM alpine:latest
# Install C++ compiler and libcurl development libraries
RUN apk --no-cache add \
g++ \
musl-dev \
curl-dev \
make \
ca-certificates
WORKDIR /app
COPY uncloseai.cpp .
COPY Makefile .
# Compile the application
RUN make
# Run the examples
CMD ["./uncloseai"]

View file

@ -0,0 +1,16 @@
CXX = g++
CXXFLAGS = -std=c++11 -Wall -Wextra -O2
LDFLAGS = -lcurl
TARGET = uncloseai
SRC = uncloseai.cpp
all: $(TARGET)
$(TARGET): $(SRC)
$(CXX) $(CXXFLAGS) -o $(TARGET) $(SRC) $(LDFLAGS)
clean:
rm -f $(TARGET) speech.mp3
.PHONY: all clean

View file

@ -0,0 +1,336 @@
/*
* UncloseAI C++ Library using libcurl
* OpenAI-compatible API client with streaming support
*/
#include <iostream>
#include <string>
#include <vector>
#include <functional>
#include <cstring>
#include <cstdlib>
#include <fstream>
#include <curl/curl.h>
#define MAX_CONTENT_LEN 1024
struct ModelInfo {
std::string id;
std::string endpoint;
int max_tokens;
};
struct MemoryStruct {
std::string data;
};
// Callback for non-streaming responses
static size_t WriteCallback(void *contents, size_t size, size_t nmemb, void *userp) {
size_t realsize = size * nmemb;
MemoryStruct *mem = static_cast<MemoryStruct*>(userp);
mem->data.append(static_cast<char*>(contents), realsize);
return realsize;
}
// Streaming context
struct StreamContext {
std::function<void(const std::string&)> callback;
std::string buffer;
};
// Extract content from SSE JSON
static void extract_sse_content(const std::string& data, std::string& content) {
const char *content_marker = "\"content\":\"";
size_t start = data.find(content_marker);
if(start == std::string::npos) return;
start += strlen(content_marker);
size_t end = start;
while(end < data.size() && data[end] != '"') {
if(data[end] == '\\' && end + 1 < data.size()) {
end += 2;
} else {
end++;
}
}
content = data.substr(start, end - start);
}
// Streaming callback
static size_t StreamCallback(void *contents, size_t size, size_t nmemb, void *userp) {
size_t realsize = size * nmemb;
StreamContext *ctx = static_cast<StreamContext*>(userp);
ctx->buffer.append(static_cast<char*>(contents), realsize);
size_t pos = 0;
while((pos = ctx->buffer.find("\n\n")) != std::string::npos) {
std::string line = ctx->buffer.substr(0, pos);
ctx->buffer.erase(0, pos + 2);
if(line.substr(0, 6) == "data: ") {
std::string data = line.substr(6);
if(data == "[DONE]") break;
std::string content;
extract_sse_content(data, content);
if(!content.empty() && ctx->callback) {
ctx->callback(content);
}
}
}
return realsize;
}
class UncloseAI {
private:
std::vector<ModelInfo> models;
std::vector<std::string> tts_endpoints;
int timeout;
bool debug;
void discover_endpoints_from_env(const std::string& prefix, std::vector<std::string>& endpoints) {
for(int i = 1; i < 10000; i++) {
std::string var_name = prefix + "_" + std::to_string(i);
const char* endpoint = std::getenv(var_name.c_str());
if(!endpoint) break;
endpoints.push_back(endpoint);
}
}
void discover_models(const std::vector<std::string>& endpoints) {
for(const auto& endpoint : endpoints) {
if(debug) {
std::cout << "[DEBUG] Discovering from: " << endpoint << std::endl;
}
CURL *curl = curl_easy_init();
if(curl) {
MemoryStruct response;
std::string url = endpoint + "/models";
curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, WriteCallback);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response);
curl_easy_setopt(curl, CURLOPT_TIMEOUT, 10L);
CURLcode res = curl_easy_perform(curl);
curl_easy_cleanup(curl);
if(res == CURLE_OK) {
// Simple JSON parsing for model IDs
size_t pos = 0;
while((pos = response.data.find("\"id\":\"", pos)) != std::string::npos) {
pos += 6;
size_t end = response.data.find("\"", pos);
if(end != std::string::npos) {
std::string model_id = response.data.substr(pos, end - pos);
if(model_id.substr(0, 10) != "modelperm-") {
ModelInfo info;
info.id = model_id;
info.endpoint = endpoint;
info.max_tokens = 8192;
models.push_back(info);
if(debug) {
std::cout << "[DEBUG] Discovered: " << model_id << std::endl;
}
}
}
pos = end + 1;
}
}
}
}
}
public:
UncloseAI(int timeout = 30, bool debug = false) : timeout(timeout), debug(debug) {
std::vector<std::string> endpoints;
std::vector<std::string> tts_eps;
discover_endpoints_from_env("MODEL_ENDPOINT", endpoints);
discover_endpoints_from_env("TTS_ENDPOINT", tts_eps);
if(debug) {
std::cout << "[DEBUG] Initialized with " << endpoints.size() << " endpoint(s)" << std::endl;
}
discover_models(endpoints);
tts_endpoints = tts_eps;
}
const std::vector<ModelInfo>& list_models() const {
return models;
}
int chat(const std::string& prompt, std::string& response, int model_idx = 0) {
if(model_idx >= static_cast<int>(models.size())) return -1;
const ModelInfo& model = models[model_idx];
std::string url = model.endpoint + "/chat/completions";
std::string json = "{\"model\":\"" + model.id + "\","
"\"messages\":[{\"role\":\"user\",\"content\":\"" + prompt + "\"}],"
"\"max_tokens\":100}";
CURL *curl = curl_easy_init();
if(!curl) return -1;
MemoryStruct mem;
struct curl_slist *headers = nullptr;
headers = curl_slist_append(headers, "Content-Type: application/json");
curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, json.c_str());
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, WriteCallback);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &mem);
curl_easy_setopt(curl, CURLOPT_TIMEOUT, (long)timeout);
CURLcode res = curl_easy_perform(curl);
curl_slist_free_all(headers);
curl_easy_cleanup(curl);
response = mem.data;
return (res == CURLE_OK) ? 0 : -1;
}
int chat_stream(const std::string& prompt, std::function<void(const std::string&)> callback, int model_idx = 0) {
if(model_idx >= static_cast<int>(models.size())) return -1;
const ModelInfo& model = models[model_idx];
std::string url = model.endpoint + "/chat/completions";
std::string json = "{\"model\":\"" + model.id + "\","
"\"messages\":[{\"role\":\"user\",\"content\":\"" + prompt + "\"}],"
"\"stream\":true,"
"\"max_tokens\":500}";
CURL *curl = curl_easy_init();
if(!curl) return -1;
StreamContext ctx;
ctx.callback = callback;
struct curl_slist *headers = nullptr;
headers = curl_slist_append(headers, "Content-Type: application/json");
curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, json.c_str());
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, StreamCallback);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &ctx);
curl_easy_setopt(curl, CURLOPT_TIMEOUT, (long)timeout);
CURLcode res = curl_easy_perform(curl);
curl_slist_free_all(headers);
curl_easy_cleanup(curl);
return (res == CURLE_OK) ? 0 : -1;
}
int tts(const std::string& text, const std::string& voice, const std::string& output_file) {
if(tts_endpoints.empty()) return -1;
std::string url = tts_endpoints[0] + "/audio/speech";
std::string json = "{\"model\":\"tts-1\","
"\"voice\":\"" + voice + "\","
"\"input\":\"" + text + "\"}";
CURL *curl = curl_easy_init();
if(!curl) return -1;
MemoryStruct mem;
struct curl_slist *headers = nullptr;
headers = curl_slist_append(headers, "Content-Type: application/json");
curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, json.c_str());
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, WriteCallback);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &mem);
curl_easy_setopt(curl, CURLOPT_TIMEOUT, (long)timeout);
CURLcode res = curl_easy_perform(curl);
curl_slist_free_all(headers);
curl_easy_cleanup(curl);
if(res == CURLE_OK) {
std::ofstream file(output_file, std::ios::binary);
if(file.is_open()) {
file.write(mem.data.c_str(), mem.data.size());
file.close();
return 0;
}
}
return -1;
}
};
// Demo
int main() {
std::cout << "=== UncloseAI C++ Client (with Streaming) ===\n\n";
curl_global_init(CURL_GLOBAL_ALL);
UncloseAI client(30, true);
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";
curl_global_cleanup();
return 1;
}
auto models = client.list_models();
std::cout << "\nDiscovered " << models.size() << " model(s):\n";
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: (" << response.size() << " bytes received)\n\n";
}
// Streaming chat
std::cout << "=== Streaming Chat ===\n";
int model_idx = (models.size() > 1) ? 1 : 0;
std::cout << "Model: " << models[model_idx].id << "\n";
std::cout << "Response: ";
client.chat_stream("Write a C++ function to check if a number is prime",
[](const std::string& content) {
std::cout << content << std::flush;
}, model_idx);
std::cout << "\n\n";
// TTS
std::cout << "=== TTS Speech Generation ===\n";
if(client.tts("Hello from UncloseAI C++ client! This demonstrates streaming support.",
"alloy", "speech.mp3") == 0) {
std::cout << "[OK] Speech file created: speech.mp3\n\n";
} else {
std::cout << "[ERROR] TTS Error\n\n";
}
std::cout << "=== Examples Complete ===\n";
curl_global_cleanup();
return 0;
}