349 lines
10 KiB
C
349 lines
10 KiB
C
// PUBLIC DOMAIN - NO LICENSE, NO WARRANTY
|
|
// Copyright 2025 TimeHexOn & foxhop & russell@unturf
|
|
// https://www.permacomputer.com
|
|
|
|
/*
|
|
* UncloseAI C Client using libsoup (GNOME HTTP Library)
|
|
* 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 <libsoup/soup.h>
|
|
|
|
#define MAX_ENDPOINTS 100
|
|
#define MAX_MODELS 100
|
|
#define MAX_URL_LEN 512
|
|
#define MAX_MODEL_LEN 256
|
|
|
|
// Structure to hold discovered model info
|
|
struct ModelInfo {
|
|
char id[MAX_MODEL_LEN];
|
|
char endpoint[MAX_URL_LEN];
|
|
int max_tokens;
|
|
};
|
|
|
|
struct ModelInfo models[MAX_MODELS];
|
|
int model_count = 0;
|
|
|
|
char tts_endpoints[MAX_ENDPOINTS][MAX_URL_LEN];
|
|
int tts_count = 0;
|
|
|
|
// Simple JSON string extractor (finds "id":"value" patterns)
|
|
void extract_model_ids(const char *json, const char *endpoint) {
|
|
const char *search = json;
|
|
const char *id_marker = "\"id\":\"";
|
|
|
|
while((search = strstr(search, id_marker)) != NULL && 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) {
|
|
strncpy(models[model_count].id, search, len);
|
|
models[model_count].id[len] = '\0';
|
|
|
|
// Filter out modelperm-* and chatcmpl-* entries
|
|
if(strncmp(models[model_count].id, "modelperm-", 10) == 0 ||
|
|
strncmp(models[model_count].id, "chatcmpl-", 9) == 0) {
|
|
search = end + 1;
|
|
continue;
|
|
}
|
|
|
|
strncpy(models[model_count].endpoint, endpoint, MAX_URL_LEN-1);
|
|
models[model_count].max_tokens = 8192; // Default
|
|
printf(" - Discovered: %s\n", models[model_count].id);
|
|
model_count++;
|
|
}
|
|
}
|
|
search = end + 1;
|
|
}
|
|
}
|
|
|
|
// Discover models from an endpoint
|
|
void discover_models_from_endpoint(SoupSession *session, const char *endpoint) {
|
|
char url[MAX_URL_LEN];
|
|
snprintf(url, sizeof(url), "%s/models", endpoint);
|
|
|
|
printf("Discovering models from: %s\n", endpoint);
|
|
|
|
SoupMessage *msg = soup_message_new("GET", url);
|
|
if(!msg) return;
|
|
|
|
GError *error = NULL;
|
|
GInputStream *stream = soup_session_send(session, msg, NULL, &error);
|
|
|
|
if(error) {
|
|
g_error_free(error);
|
|
g_object_unref(msg);
|
|
return;
|
|
}
|
|
|
|
// Read response body
|
|
GString *body = g_string_new(NULL);
|
|
char buffer[4096];
|
|
gssize read;
|
|
|
|
while((read = g_input_stream_read(stream, buffer, sizeof(buffer)-1, NULL, NULL)) > 0) {
|
|
buffer[read] = '\0';
|
|
g_string_append(body, buffer);
|
|
}
|
|
|
|
extract_model_ids(body->str, endpoint);
|
|
|
|
g_string_free(body, TRUE);
|
|
g_object_unref(stream);
|
|
g_object_unref(msg);
|
|
}
|
|
|
|
// Discover all models from environment variables
|
|
void discover_all_models(SoupSession *session) {
|
|
printf("=== Model Discovery ===\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;
|
|
discover_models_from_endpoint(session, endpoint);
|
|
}
|
|
|
|
// 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;
|
|
printf("Discovering TTS from: %s\n", endpoint);
|
|
strncpy(tts_endpoints[tts_count++], endpoint, MAX_URL_LEN-1);
|
|
}
|
|
|
|
printf("\nTotal models discovered: %d\n", model_count);
|
|
printf("Total TTS endpoints: %d\n\n", tts_count);
|
|
}
|
|
|
|
// Make a non-streaming chat request
|
|
int chat_request(SoupSession *session, int model_idx, const char *prompt) {
|
|
char url[MAX_URL_LEN];
|
|
char json[2048];
|
|
|
|
snprintf(url, sizeof(url), "%s/chat/completions", models[model_idx].endpoint);
|
|
snprintf(json, sizeof(json),
|
|
"{\"model\":\"%s\","
|
|
"\"messages\":[{\"role\":\"user\",\"content\":\"%s\"}],"
|
|
"\"stream\":false,"
|
|
"\"temperature\":0.7,"
|
|
"\"max_tokens\":100}",
|
|
models[model_idx].id, prompt);
|
|
|
|
SoupMessage *msg = soup_message_new("POST", url);
|
|
if(!msg) return -1;
|
|
|
|
soup_message_set_request(msg, "application/json", SOUP_MEMORY_COPY, json, strlen(json));
|
|
|
|
GError *error = NULL;
|
|
GInputStream *stream = soup_session_send(session, msg, NULL, &error);
|
|
|
|
if(error) {
|
|
g_error_free(error);
|
|
g_object_unref(msg);
|
|
return -1;
|
|
}
|
|
|
|
// Read response
|
|
char buffer[4096];
|
|
gssize read = g_input_stream_read(stream, buffer, sizeof(buffer)-1, NULL, NULL);
|
|
if(read > 0) {
|
|
printf("Response received (%ld bytes)\n", read);
|
|
printf("(Full response requires JSON parsing library)\n");
|
|
}
|
|
|
|
g_object_unref(stream);
|
|
g_object_unref(msg);
|
|
return 0;
|
|
}
|
|
|
|
// Streaming context for SSE parsing
|
|
typedef struct {
|
|
char buffer[4096];
|
|
size_t buffer_pos;
|
|
} StreamContext;
|
|
|
|
// Extract content from SSE data line
|
|
void extract_sse_content(const char *json_data, char *content, size_t content_size) {
|
|
const char *content_marker = "\"content\":\"";
|
|
const char *found = strstr(json_data, content_marker);
|
|
if(found) {
|
|
found += strlen(content_marker);
|
|
const char *end = strchr(found, '"');
|
|
if(end) {
|
|
size_t len = end - found;
|
|
if(len < content_size) {
|
|
strncpy(content, found, len);
|
|
content[len] = '\0';
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Process streaming chunk
|
|
void process_stream_chunk(const char *data, size_t len, StreamContext *ctx) {
|
|
for(size_t i = 0; i < len; i++) {
|
|
if(data[i] == '\n') {
|
|
ctx->buffer[ctx->buffer_pos] = '\0';
|
|
|
|
// Process SSE line
|
|
if(strncmp(ctx->buffer, "data: ", 6) == 0) {
|
|
const char *json_data = ctx->buffer + 6;
|
|
if(strcmp(json_data, "[DONE]") == 0) {
|
|
return; // Stop streaming
|
|
}
|
|
|
|
char content[1024] = {0};
|
|
extract_sse_content(json_data, content, sizeof(content));
|
|
if(strlen(content) > 0) {
|
|
printf("%s", content);
|
|
fflush(stdout);
|
|
}
|
|
}
|
|
|
|
ctx->buffer_pos = 0;
|
|
} else {
|
|
if(ctx->buffer_pos < sizeof(ctx->buffer) - 1) {
|
|
ctx->buffer[ctx->buffer_pos++] = data[i];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Streaming chat request
|
|
int chat_stream_request(SoupSession *session, int model_idx, const char *prompt) {
|
|
char url[MAX_URL_LEN];
|
|
char json[2048];
|
|
|
|
snprintf(url, sizeof(url), "%s/chat/completions", models[model_idx].endpoint);
|
|
snprintf(json, sizeof(json),
|
|
"{\"model\":\"%s\","
|
|
"\"messages\":[{\"role\":\"user\",\"content\":\"%s\"}],"
|
|
"\"stream\":true,"
|
|
"\"temperature\":0.7,"
|
|
"\"max_tokens\":500}",
|
|
models[model_idx].id, prompt);
|
|
|
|
SoupMessage *msg = soup_message_new("POST", url);
|
|
if(!msg) return -1;
|
|
|
|
soup_message_set_request(msg, "application/json", SOUP_MEMORY_COPY, json, strlen(json));
|
|
|
|
GError *error = NULL;
|
|
GInputStream *stream = soup_session_send(session, msg, NULL, &error);
|
|
|
|
if(error) {
|
|
g_error_free(error);
|
|
g_object_unref(msg);
|
|
return -1;
|
|
}
|
|
|
|
// Read and process streaming chunks
|
|
StreamContext ctx = {{0}, 0};
|
|
char buffer[1024];
|
|
gssize read;
|
|
|
|
while((read = g_input_stream_read(stream, buffer, sizeof(buffer), NULL, NULL)) > 0) {
|
|
process_stream_chunk(buffer, read, &ctx);
|
|
}
|
|
|
|
g_object_unref(stream);
|
|
g_object_unref(msg);
|
|
return 0;
|
|
}
|
|
|
|
int main(void) {
|
|
printf("=== UncloseAI C Client (libsoup/GNOME) ===\n\n");
|
|
|
|
// Initialize libsoup session
|
|
SoupSession *session = soup_session_new();
|
|
|
|
discover_all_models(session);
|
|
|
|
if(model_count == 0) {
|
|
printf("ERROR: No models discovered\n");
|
|
g_object_unref(session);
|
|
return 1;
|
|
}
|
|
|
|
// Non-streaming chat example
|
|
printf("=== Non-Streaming Chat ===\n");
|
|
printf("Model: %s\n", models[0].id);
|
|
|
|
if(chat_request(session, 0, "Explain quantum computing in one sentence") != 0) {
|
|
printf("Request failed\n");
|
|
}
|
|
|
|
printf("\n");
|
|
|
|
// Streaming chat example
|
|
int model_idx = (model_count >= 2) ? 1 : 0;
|
|
printf("=== Streaming Chat ===\n");
|
|
printf("Model: %s\n", models[model_idx].id);
|
|
printf("Response: ");
|
|
|
|
if(chat_stream_request(session, model_idx, "Write a hello world program in C") != 0) {
|
|
printf("\nStreaming request failed\n");
|
|
}
|
|
|
|
printf("\n\n");
|
|
|
|
// TTS example
|
|
if(tts_count > 0) {
|
|
printf("=== TTS Speech Generation ===\n");
|
|
printf("Model: tts-1\n");
|
|
|
|
char tts_url[MAX_URL_LEN];
|
|
snprintf(tts_url, sizeof(tts_url), "%s/audio/speech", tts_endpoints[0]);
|
|
|
|
const char *tts_json = "{"
|
|
"\"model\":\"tts-1\","
|
|
"\"voice\":\"alloy\","
|
|
"\"input\":\"Hello from UncloseAI libsoup client!\""
|
|
"}";
|
|
|
|
SoupMessage *tts_msg = soup_message_new("POST", tts_url);
|
|
if(tts_msg) {
|
|
soup_message_set_request(tts_msg, "application/json", SOUP_MEMORY_COPY,
|
|
tts_json, strlen(tts_json));
|
|
|
|
GError *error = NULL;
|
|
GInputStream *stream = soup_session_send(session, tts_msg, NULL, &error);
|
|
|
|
if(!error) {
|
|
FILE *fp = fopen("/tmp/speech.mp3", "wb");
|
|
if(fp) {
|
|
char buffer[4096];
|
|
gssize read;
|
|
while((read = g_input_stream_read(stream, buffer, sizeof(buffer), NULL, NULL)) > 0) {
|
|
fwrite(buffer, 1, read, fp);
|
|
}
|
|
fclose(fp);
|
|
printf("Audio saved to /tmp/speech.mp3\n");
|
|
} else {
|
|
printf("TTS failed: could not write file\n");
|
|
}
|
|
g_object_unref(stream);
|
|
} else {
|
|
printf("TTS failed: %s\n", error->message);
|
|
g_error_free(error);
|
|
}
|
|
|
|
g_object_unref(tts_msg);
|
|
}
|
|
}
|
|
|
|
printf("\n=== Examples Complete ===\n");
|
|
|
|
g_object_unref(session);
|
|
return 0;
|
|
}
|