463 lines
14 KiB
C
463 lines
14 KiB
C
/*
|
|
* UncloseAI C Library using libcurl
|
|
* OpenAI-compatible API client with streaming support
|
|
* Compatible with vLLM, Ollama, and OpenAI-compatible endpoints
|
|
*/
|
|
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <curl/curl.h>
|
|
|
|
#define MAX_ENDPOINTS 100
|
|
#define MAX_MODELS 100
|
|
#define MAX_URL_LEN 512
|
|
#define MAX_MODEL_LEN 256
|
|
#define MAX_CONTENT_LEN 1024
|
|
|
|
// Structure to hold response data
|
|
struct MemoryStruct {
|
|
char *memory;
|
|
size_t size;
|
|
};
|
|
|
|
// Structure to hold discovered model info
|
|
struct ModelInfo {
|
|
char id[MAX_MODEL_LEN];
|
|
char endpoint[MAX_URL_LEN];
|
|
int max_tokens;
|
|
};
|
|
|
|
// UncloseAI Client structure
|
|
typedef struct {
|
|
struct ModelInfo *models;
|
|
int model_count;
|
|
char tts_endpoints[MAX_ENDPOINTS][MAX_URL_LEN];
|
|
int tts_count;
|
|
int timeout;
|
|
} UncloseAIClient;
|
|
|
|
// Callback function type for streaming
|
|
typedef void (*StreamCallback)(const char *content, void *userdata);
|
|
|
|
// Structure for streaming context
|
|
struct StreamContext {
|
|
StreamCallback callback;
|
|
void *userdata;
|
|
char buffer[8192];
|
|
size_t buffer_pos;
|
|
};
|
|
|
|
/*************************************************************
|
|
* LIBRARY API - Callback function to capture response data
|
|
*************************************************************/
|
|
static size_t WriteMemoryCallback(void *contents, size_t size, size_t nmemb, void *userp) {
|
|
size_t realsize = size * nmemb;
|
|
struct MemoryStruct *mem = (struct MemoryStruct *)userp;
|
|
|
|
char *ptr = realloc(mem->memory, mem->size + realsize + 1);
|
|
if(!ptr) {
|
|
printf("Not enough memory (realloc returned NULL)\n");
|
|
return 0;
|
|
}
|
|
|
|
mem->memory = ptr;
|
|
memcpy(&(mem->memory[mem->size]), contents, realsize);
|
|
mem->size += realsize;
|
|
mem->memory[mem->size] = 0;
|
|
|
|
return realsize;
|
|
}
|
|
|
|
/*************************************************************
|
|
* LIBRARY API - Extract content from SSE data chunk
|
|
*************************************************************/
|
|
static void extract_sse_content(const char *data, char *content, size_t max_len) {
|
|
// Look for "content":"..." pattern
|
|
const char *content_marker = "\"content\":\"";
|
|
const char *start = strstr(data, content_marker);
|
|
if(!start) return;
|
|
|
|
start += strlen(content_marker);
|
|
const char *end = start;
|
|
|
|
// Find closing quote, handling escaped quotes
|
|
while(*end && *end != '"') {
|
|
if(*end == '\\' && *(end+1)) {
|
|
end += 2;
|
|
} else {
|
|
end++;
|
|
}
|
|
}
|
|
|
|
size_t len = end - start;
|
|
if(len > max_len - 1) len = max_len - 1;
|
|
strncpy(content, start, len);
|
|
content[len] = '\0';
|
|
}
|
|
|
|
/*************************************************************
|
|
* LIBRARY API - Streaming callback for curl
|
|
*************************************************************/
|
|
static size_t StreamWriteCallback(void *contents, size_t size, size_t nmemb, void *userp) {
|
|
size_t realsize = size * nmemb;
|
|
struct StreamContext *ctx = (struct StreamContext *)userp;
|
|
|
|
// Append to buffer
|
|
char *data = (char *)contents;
|
|
for(size_t i = 0; i < realsize; i++) {
|
|
if(ctx->buffer_pos >= sizeof(ctx->buffer) - 1) {
|
|
// Buffer full, skip
|
|
continue;
|
|
}
|
|
|
|
ctx->buffer[ctx->buffer_pos++] = data[i];
|
|
|
|
// Check for line ending
|
|
if(data[i] == '\n' && ctx->buffer_pos >= 2 &&
|
|
ctx->buffer[ctx->buffer_pos-2] == '\n') {
|
|
ctx->buffer[ctx->buffer_pos] = '\0';
|
|
|
|
// Process SSE line
|
|
if(strncmp(ctx->buffer, "data: ", 6) == 0) {
|
|
const char *json_data = ctx->buffer + 6;
|
|
|
|
// Check for [DONE]
|
|
if(strncmp(json_data, "[DONE]", 6) == 0) {
|
|
ctx->buffer_pos = 0;
|
|
break;
|
|
}
|
|
|
|
// Extract content
|
|
char content[MAX_CONTENT_LEN];
|
|
extract_sse_content(json_data, content, sizeof(content));
|
|
|
|
if(strlen(content) > 0 && ctx->callback) {
|
|
ctx->callback(content, ctx->userdata);
|
|
}
|
|
}
|
|
|
|
ctx->buffer_pos = 0;
|
|
}
|
|
}
|
|
|
|
return realsize;
|
|
}
|
|
|
|
/*************************************************************
|
|
* LIBRARY API - Extract model IDs from JSON
|
|
*************************************************************/
|
|
static void extract_model_ids(UncloseAIClient *client, const char *json, const char *endpoint) {
|
|
const char *search = json;
|
|
const char *id_marker = "\"id\":\"";
|
|
|
|
while((search = strstr(search, id_marker)) != NULL &&
|
|
client->model_count < MAX_MODELS) {
|
|
search += strlen(id_marker);
|
|
const char *end = strchr(search, '"');
|
|
if(end) {
|
|
size_t len = end - search;
|
|
if(len < MAX_MODEL_LEN) {
|
|
// Skip modelperm-* entries
|
|
if(strncmp(search, "modelperm-", 10) == 0) {
|
|
search = end + 1;
|
|
continue;
|
|
}
|
|
|
|
strncpy(client->models[client->model_count].id, search, len);
|
|
client->models[client->model_count].id[len] = '\0';
|
|
strncpy(client->models[client->model_count].endpoint, endpoint, MAX_URL_LEN-1);
|
|
client->models[client->model_count].max_tokens = 8192;
|
|
client->model_count++;
|
|
}
|
|
}
|
|
search = end + 1;
|
|
}
|
|
}
|
|
|
|
/*************************************************************
|
|
* LIBRARY API - Initialize client and discover models
|
|
*************************************************************/
|
|
UncloseAIClient* uncloseai_init(int timeout) {
|
|
UncloseAIClient *client = (UncloseAIClient*)malloc(sizeof(UncloseAIClient));
|
|
if(!client) return NULL;
|
|
|
|
client->models = (struct ModelInfo*)malloc(MAX_MODELS * sizeof(struct ModelInfo));
|
|
if(!client->models) {
|
|
free(client);
|
|
return NULL;
|
|
}
|
|
|
|
client->model_count = 0;
|
|
client->tts_count = 0;
|
|
client->timeout = timeout;
|
|
|
|
printf("Initializing UncloseAI client...\n");
|
|
|
|
// Discover chat/code models
|
|
for(int i = 1; i < 10000; i++) {
|
|
char var_name[32];
|
|
snprintf(var_name, sizeof(var_name), "MODEL_ENDPOINT_%d", i);
|
|
char *endpoint = getenv(var_name);
|
|
if(!endpoint) break;
|
|
|
|
printf("Endpoint %d: %s\n", i, endpoint);
|
|
|
|
// Fetch models
|
|
char url[MAX_URL_LEN];
|
|
snprintf(url, sizeof(url), "%s/models", endpoint);
|
|
|
|
CURL *curl = curl_easy_init();
|
|
if(curl) {
|
|
struct MemoryStruct chunk = {NULL, 0};
|
|
chunk.memory = malloc(1);
|
|
chunk.size = 0;
|
|
|
|
curl_easy_setopt(curl, CURLOPT_URL, url);
|
|
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, WriteMemoryCallback);
|
|
curl_easy_setopt(curl, CURLOPT_WRITEDATA, (void *)&chunk);
|
|
curl_easy_setopt(curl, CURLOPT_TIMEOUT, 10L);
|
|
|
|
CURLcode res = curl_easy_perform(curl);
|
|
|
|
if(res == CURLE_OK && chunk.memory) {
|
|
extract_model_ids(client, chunk.memory, endpoint);
|
|
}
|
|
|
|
free(chunk.memory);
|
|
curl_easy_cleanup(curl);
|
|
}
|
|
}
|
|
|
|
// Discover TTS endpoints
|
|
for(int i = 1; i < 10000; i++) {
|
|
char var_name[32];
|
|
snprintf(var_name, sizeof(var_name), "TTS_ENDPOINT_%d", i);
|
|
char *endpoint = getenv(var_name);
|
|
if(!endpoint) break;
|
|
strncpy(client->tts_endpoints[client->tts_count++], endpoint, MAX_URL_LEN-1);
|
|
}
|
|
|
|
printf("Discovered %d models, %d TTS endpoints\n\n", client->model_count, client->tts_count);
|
|
|
|
return client;
|
|
}
|
|
|
|
/*************************************************************
|
|
* LIBRARY API - Non-streaming chat completion
|
|
*************************************************************/
|
|
int uncloseai_chat(UncloseAIClient *client, int model_idx, const char *prompt,
|
|
struct MemoryStruct *response) {
|
|
if(model_idx >= client->model_count) return -1;
|
|
|
|
char url[MAX_URL_LEN];
|
|
char json[2048];
|
|
|
|
snprintf(url, sizeof(url), "%s/chat/completions",
|
|
client->models[model_idx].endpoint);
|
|
snprintf(json, sizeof(json),
|
|
"{\"model\":\"%s\","
|
|
"\"messages\":[{\"role\":\"user\",\"content\":\"%s\"}],"
|
|
"\"stream\":false,"
|
|
"\"temperature\":0.7,"
|
|
"\"max_tokens\":100}",
|
|
client->models[model_idx].id, prompt);
|
|
|
|
CURL *curl = curl_easy_init();
|
|
if(!curl) return -1;
|
|
|
|
struct curl_slist *headers = NULL;
|
|
headers = curl_slist_append(headers, "Content-Type: application/json");
|
|
|
|
curl_easy_setopt(curl, CURLOPT_URL, url);
|
|
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
|
|
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, json);
|
|
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, WriteMemoryCallback);
|
|
curl_easy_setopt(curl, CURLOPT_WRITEDATA, (void *)response);
|
|
curl_easy_setopt(curl, CURLOPT_TIMEOUT, (long)client->timeout);
|
|
|
|
CURLcode res = curl_easy_perform(curl);
|
|
|
|
curl_slist_free_all(headers);
|
|
curl_easy_cleanup(curl);
|
|
|
|
return (res == CURLE_OK) ? 0 : -1;
|
|
}
|
|
|
|
/*************************************************************
|
|
* LIBRARY API - Streaming chat completion
|
|
*************************************************************/
|
|
int uncloseai_chat_stream(UncloseAIClient *client, int model_idx, const char *prompt,
|
|
StreamCallback callback, void *userdata) {
|
|
if(model_idx >= client->model_count) return -1;
|
|
|
|
char url[MAX_URL_LEN];
|
|
char json[2048];
|
|
|
|
snprintf(url, sizeof(url), "%s/chat/completions",
|
|
client->models[model_idx].endpoint);
|
|
snprintf(json, sizeof(json),
|
|
"{\"model\":\"%s\","
|
|
"\"messages\":[{\"role\":\"user\",\"content\":\"%s\"}],"
|
|
"\"stream\":true,"
|
|
"\"temperature\":0.7,"
|
|
"\"max_tokens\":500}",
|
|
client->models[model_idx].id, prompt);
|
|
|
|
CURL *curl = curl_easy_init();
|
|
if(!curl) return -1;
|
|
|
|
struct StreamContext ctx;
|
|
ctx.callback = callback;
|
|
ctx.userdata = userdata;
|
|
ctx.buffer_pos = 0;
|
|
|
|
struct curl_slist *headers = NULL;
|
|
headers = curl_slist_append(headers, "Content-Type: application/json");
|
|
|
|
curl_easy_setopt(curl, CURLOPT_URL, url);
|
|
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
|
|
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, json);
|
|
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, StreamWriteCallback);
|
|
curl_easy_setopt(curl, CURLOPT_WRITEDATA, (void *)&ctx);
|
|
curl_easy_setopt(curl, CURLOPT_TIMEOUT, (long)client->timeout);
|
|
|
|
CURLcode res = curl_easy_perform(curl);
|
|
|
|
curl_slist_free_all(headers);
|
|
curl_easy_cleanup(curl);
|
|
|
|
return (res == CURLE_OK) ? 0 : -1;
|
|
}
|
|
|
|
/*************************************************************
|
|
* LIBRARY API - Text-to-speech generation
|
|
*************************************************************/
|
|
int uncloseai_tts(UncloseAIClient *client, const char *text, const char *voice,
|
|
const char *output_file) {
|
|
if(client->tts_count == 0) return -1;
|
|
|
|
char url[MAX_URL_LEN];
|
|
char json[2048];
|
|
|
|
snprintf(url, sizeof(url), "%s/audio/speech", client->tts_endpoints[0]);
|
|
snprintf(json, sizeof(json),
|
|
"{\"model\":\"tts-1\","
|
|
"\"voice\":\"%s\","
|
|
"\"input\":\"%s\"}",
|
|
voice, text);
|
|
|
|
CURL *curl = curl_easy_init();
|
|
if(!curl) return -1;
|
|
|
|
struct MemoryStruct chunk = {NULL, 0};
|
|
chunk.memory = malloc(1);
|
|
chunk.size = 0;
|
|
|
|
struct curl_slist *headers = NULL;
|
|
headers = curl_slist_append(headers, "Content-Type: application/json");
|
|
|
|
curl_easy_setopt(curl, CURLOPT_URL, url);
|
|
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
|
|
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, json);
|
|
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, WriteMemoryCallback);
|
|
curl_easy_setopt(curl, CURLOPT_WRITEDATA, (void *)&chunk);
|
|
curl_easy_setopt(curl, CURLOPT_TIMEOUT, (long)client->timeout);
|
|
|
|
CURLcode res = curl_easy_perform(curl);
|
|
|
|
int result = -1;
|
|
if(res == CURLE_OK) {
|
|
FILE *fp = fopen(output_file, "wb");
|
|
if(fp) {
|
|
fwrite(chunk.memory, 1, chunk.size, fp);
|
|
fclose(fp);
|
|
result = 0;
|
|
}
|
|
}
|
|
|
|
free(chunk.memory);
|
|
curl_slist_free_all(headers);
|
|
curl_easy_cleanup(curl);
|
|
|
|
return result;
|
|
}
|
|
|
|
/*************************************************************
|
|
* LIBRARY API - Free client resources
|
|
*************************************************************/
|
|
void uncloseai_free(UncloseAIClient *client) {
|
|
if(client) {
|
|
if(client->models) free(client->models);
|
|
free(client);
|
|
}
|
|
}
|
|
|
|
/*************************************************************
|
|
* DEMO PROGRAM - Shows library usage
|
|
*************************************************************/
|
|
|
|
// Callback for streaming
|
|
void stream_callback(const char *content, void *userdata) {
|
|
printf("%s", content);
|
|
fflush(stdout);
|
|
}
|
|
|
|
int main(void) {
|
|
printf("=== UncloseAI C Client (with Streaming) ===\n\n");
|
|
|
|
curl_global_init(CURL_GLOBAL_ALL);
|
|
|
|
// Initialize client
|
|
UncloseAIClient *client = uncloseai_init(30);
|
|
if(!client || client->model_count == 0) {
|
|
printf("ERROR: No models discovered\n");
|
|
curl_global_cleanup();
|
|
return 1;
|
|
}
|
|
|
|
// Non-streaming chat example
|
|
printf("=== Non-Streaming Chat ===\n");
|
|
printf("Model: %s\n", client->models[0].id);
|
|
|
|
struct MemoryStruct response = {NULL, 0};
|
|
response.memory = malloc(1);
|
|
response.size = 0;
|
|
|
|
if(uncloseai_chat(client, 0, "Explain quantum computing in one sentence",
|
|
&response) == 0) {
|
|
printf("Response: (%zu bytes received)\n", response.size);
|
|
}
|
|
free(response.memory);
|
|
printf("\n");
|
|
|
|
// Streaming chat example
|
|
int model_idx = (client->model_count >= 2) ? 1 : 0;
|
|
printf("=== Streaming Chat ===\n");
|
|
printf("Model: %s\n", client->models[model_idx].id);
|
|
printf("Response: ");
|
|
|
|
uncloseai_chat_stream(client, model_idx,
|
|
"Write a hello world program in C",
|
|
stream_callback, NULL);
|
|
printf("\n\n");
|
|
|
|
// TTS example
|
|
if(client->tts_count > 0) {
|
|
printf("=== TTS Speech Generation ===\n");
|
|
printf("Model: tts-1\n");
|
|
|
|
if(uncloseai_tts(client, "Hello from UncloseAI C client!",
|
|
"alloy", "/tmp/speech.mp3") == 0) {
|
|
printf("Audio saved to /tmp/speech.mp3\n");
|
|
} else {
|
|
printf("TTS failed\n");
|
|
}
|
|
}
|
|
|
|
printf("\n=== Examples Complete ===\n");
|
|
|
|
uncloseai_free(client);
|
|
curl_global_cleanup();
|
|
return 0;
|
|
}
|