From f7e1e6b17f666a5e2ccfdd98b62f87c6b7931118 Mon Sep 17 00:00:00 2001 From: Russell Ballestrini Date: Wed, 15 Oct 2025 15:57:30 -0400 Subject: [PATCH] move raw HTTP implementations to http subdirectories for Go, Ruby, Java, and C# --- languages/csharp/http/Dockerfile | 18 ++ languages/csharp/http/Uncloseai.cs | 306 ++++++++++++++++++ languages/csharp/http/csharp.csproj | 7 + languages/go/http/Dockerfile | 28 ++ languages/go/http/go.mod | 3 + languages/go/http/uncloseai.go | 465 ++++++++++++++++++++++++++++ languages/java/http/Dockerfile | 9 + languages/java/http/UncloseAI.java | 366 ++++++++++++++++++++++ languages/ruby/http/Dockerfile | 10 + languages/ruby/http/uncloseai.rb | 225 ++++++++++++++ 10 files changed, 1437 insertions(+) create mode 100644 languages/csharp/http/Dockerfile create mode 100644 languages/csharp/http/Uncloseai.cs create mode 100644 languages/csharp/http/csharp.csproj create mode 100644 languages/go/http/Dockerfile create mode 100644 languages/go/http/go.mod create mode 100644 languages/go/http/uncloseai.go create mode 100644 languages/java/http/Dockerfile create mode 100644 languages/java/http/UncloseAI.java create mode 100644 languages/ruby/http/Dockerfile create mode 100644 languages/ruby/http/uncloseai.rb diff --git a/languages/csharp/http/Dockerfile b/languages/csharp/http/Dockerfile new file mode 100644 index 0000000..9a0a960 --- /dev/null +++ b/languages/csharp/http/Dockerfile @@ -0,0 +1,18 @@ +# .NET 9.0 C# (checked 2025-10-13: mcr.microsoft.com/dotnet/sdk:9.0 is latest stable) +FROM mcr.microsoft.com/dotnet/sdk:9.0-alpine AS builder + +WORKDIR /app +COPY csharp.csproj . +COPY Uncloseai.cs . + +# Build the application +RUN dotnet build -c Release -o out + +FROM mcr.microsoft.com/dotnet/runtime:9.0-alpine + +RUN apk add --no-cache ca-certificates + +WORKDIR /app +COPY --from=builder /app/out . + +CMD ["dotnet", "csharp.dll"] diff --git a/languages/csharp/http/Uncloseai.cs b/languages/csharp/http/Uncloseai.cs new file mode 100644 index 0000000..327f1f7 --- /dev/null +++ b/languages/csharp/http/Uncloseai.cs @@ -0,0 +1,306 @@ +using System; +using System.Net.Http; +using System.Text; +using System.Text.Json; +using System.Threading.Tasks; +using System.IO; +using System.Collections.Generic; + +class ModelInfo +{ + public string Id { get; set; } = ""; + public string Endpoint { get; set; } = ""; + public int MaxTokens { get; set; } = 8192; +} + +class Uncloseai +{ + static readonly HttpClient client = new HttpClient(); + static readonly List models = new List(); + static readonly List ttsEndpoints = new List(); + + static async Task DiscoverModelsFromEndpoint(string endpoint) + { + Console.WriteLine($"Discovering models from: {endpoint}"); + try + { + var response = await client.GetAsync($"{endpoint}/models"); + var body = await response.Content.ReadAsStringAsync(); + + using var doc = JsonDocument.Parse(body); + var root = doc.RootElement; + + if (root.TryGetProperty("data", out var data)) + { + foreach (var model in data.EnumerateArray()) + { + var modelId = model.GetProperty("id").GetString() ?? ""; + var maxTokens = 8192; + if (model.TryGetProperty("max_model_len", out var maxModelLen)) + { + maxTokens = maxModelLen.GetInt32(); + } + + models.Add(new ModelInfo + { + Id = modelId, + Endpoint = endpoint, + MaxTokens = maxTokens + }); + Console.WriteLine($" - Discovered: {modelId}"); + } + } + } + catch (Exception ex) + { + Console.WriteLine($" Error: {ex.Message}"); + } + } + + static async Task DiscoverAllModels() + { + Console.WriteLine("=== Model Discovery ==="); + + // Discover chat/code models + for (int i = 1; i <= 9999; i++) + { + var endpoint = Environment.GetEnvironmentVariable($"MODEL_ENDPOINT_{i}"); + if (string.IsNullOrEmpty(endpoint)) break; + await DiscoverModelsFromEndpoint(endpoint); + } + + // Discover TTS endpoints + for (int i = 1; i <= 9999; i++) + { + var endpoint = Environment.GetEnvironmentVariable($"TTS_ENDPOINT_{i}"); + if (string.IsNullOrEmpty(endpoint)) break; + Console.WriteLine($"Discovering TTS from: {endpoint}"); + ttsEndpoints.Add(endpoint); + } + + Console.WriteLine($"\n{models.Count} model(s) discovered"); + Console.WriteLine($"{ttsEndpoints.Count} TTS endpoint(s) discovered\n"); + } + + static async Task MakeChatRequest(int modelIdx, string systemMsg, string userMsg, int maxTokens) + { + var model = models[modelIdx]; + var url = $"{model.Endpoint}/chat/completions"; + + var payload = new + { + model = model.Id, + messages = new[] + { + new { role = "system", content = systemMsg }, + new { role = "user", content = userMsg } + }, + max_tokens = maxTokens + }; + + var json = JsonSerializer.Serialize(payload); + var content = new StringContent(json, Encoding.UTF8, "application/json"); + + var response = await client.PostAsync(url, content); + var body = await response.Content.ReadAsStringAsync(); + + using var doc = JsonDocument.Parse(body); + var root = doc.RootElement; + return root.GetProperty("choices")[0].GetProperty("message").GetProperty("content").GetString() ?? ""; + } + + static async Task MakeChatStreamRequest(int modelIdx, string systemMsg, string userMsg, int maxTokens) + { + var model = models[modelIdx]; + var url = $"{model.Endpoint}/chat/completions"; + + var payload = new + { + model = model.Id, + messages = new[] + { + new { role = "system", content = systemMsg }, + new { role = "user", content = userMsg } + }, + max_tokens = maxTokens, + stream = true + }; + + var json = JsonSerializer.Serialize(payload); + var content = new StringContent(json, Encoding.UTF8, "application/json"); + + using var request = new HttpRequestMessage(HttpMethod.Post, url); + request.Content = content; + + using var response = await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead); + using var stream = await response.Content.ReadAsStreamAsync(); + using var reader = new StreamReader(stream); + + Console.Write("Response: "); + + while (!reader.EndOfStream) + { + var line = await reader.ReadLineAsync(); + if (string.IsNullOrEmpty(line)) continue; + + if (line.StartsWith("data: ")) + { + var data = line.Substring(6); + if (data == "[DONE]") break; + + try + { + using var doc = JsonDocument.Parse(data); + var root = doc.RootElement; + if (root.TryGetProperty("choices", out var choices) && choices.GetArrayLength() > 0) + { + var choice = choices[0]; + if (choice.TryGetProperty("delta", out var delta)) + { + if (delta.TryGetProperty("content", out var contentProp)) + { + var contentStr = contentProp.GetString(); + if (!string.IsNullOrEmpty(contentStr)) + { + Console.Write(contentStr); + } + } + } + } + } + catch + { + // Ignore parse errors + } + } + } + + Console.WriteLine(); + } + + static async Task MakeTtsRequest(string text) + { + if (ttsEndpoints.Count == 0) + return "ERROR: No TTS endpoints available"; + + var endpoint = ttsEndpoints[0]; + var url = $"{endpoint}/audio/speech"; + + var payload = new + { + model = "tts-1", + voice = "alloy", + input = text + }; + + var json = JsonSerializer.Serialize(payload); + var content = new StringContent(json, Encoding.UTF8, "application/json"); + + var response = await client.PostAsync(url, content); + var audioData = await response.Content.ReadAsByteArrayAsync(); + + await File.WriteAllBytesAsync("output.mp3", audioData); + return $"Audio saved to output.mp3 ({audioData.Length} bytes)"; + } + + static async Task HermesExample() + { + Console.WriteLine("\n=== Non-Streaming Chat (using first discovered model) ==="); + if (models.Count == 0) + { + Console.WriteLine("ERROR: No models available"); + return; + } + + var model = models[0]; + Console.WriteLine($"Model: {model.Id}"); + Console.WriteLine($"Endpoint: {model.Endpoint}\n"); + + try + { + var response = await MakeChatRequest( + 0, + "You are Hermes, a helpful AI assistant from Nous Research.", + "Explain quantum computing in one sentence.", + 100 + ); + Console.WriteLine($"Response: {response}"); + } + catch (Exception ex) + { + Console.WriteLine($"Error: {ex.Message}"); + } + } + + static async Task QwenStreamExample() + { + Console.WriteLine("\n=== Streaming Chat (using second or first model) ==="); + if (models.Count == 0) + { + Console.WriteLine("ERROR: No models available"); + return; + } + + var modelIdx = models.Count >= 2 ? 1 : 0; + var model = models[modelIdx]; + Console.WriteLine($"Model: {model.Id}"); + Console.WriteLine($"Endpoint: {model.Endpoint}\n"); + + try + { + await MakeChatStreamRequest( + modelIdx, + "You are Qwen, a coding assistant specialized in software development.", + "Write a hello world function in C#.", + 200 + ); + Console.WriteLine(); + } + catch (Exception ex) + { + Console.WriteLine($"Error: {ex.Message}"); + } + } + + static async Task TtsExample() + { + Console.WriteLine("\n=== TTS Speech Generation Example ==="); + if (ttsEndpoints.Count == 0) + { + Console.WriteLine("ERROR: No TTS endpoints available"); + return; + } + + Console.WriteLine($"Endpoint: {ttsEndpoints[0]}\n"); + + try + { + var result = await MakeTtsRequest("Hello from C#! This is a text to speech example."); + Console.WriteLine(result); + } + catch (Exception ex) + { + Console.WriteLine($"Error: {ex.Message}"); + } + } + + static async Task Main(string[] args) + { + Console.WriteLine("C# AI API Examples (Dynamic Model Discovery)"); + Console.WriteLine("============================================="); + Console.WriteLine(); + + await DiscoverAllModels(); + + if (models.Count == 0) + { + Console.WriteLine("ERROR: No models discovered. Check environment variables:"); + Console.WriteLine(" MODEL_ENDPOINT_1, MODEL_ENDPOINT_2, etc."); + Environment.Exit(1); + } + + await HermesExample(); + await QwenStreamExample(); + await TtsExample(); + } +} diff --git a/languages/csharp/http/csharp.csproj b/languages/csharp/http/csharp.csproj new file mode 100644 index 0000000..39e869b --- /dev/null +++ b/languages/csharp/http/csharp.csproj @@ -0,0 +1,7 @@ + + + Exe + net9.0 + enable + + diff --git a/languages/go/http/Dockerfile b/languages/go/http/Dockerfile new file mode 100644 index 0000000..adbea49 --- /dev/null +++ b/languages/go/http/Dockerfile @@ -0,0 +1,28 @@ +# Pin to specific Go version (checked 2025-10-13: golang:1.23-alpine is latest stable) +FROM golang:1.23-alpine AS builder + +# Install ca-certificates for HTTPS requests +RUN apk --no-cache add ca-certificates + +WORKDIR /app + +# Copy module files +COPY go.mod . +COPY uncloseai/ ./uncloseai/ +COPY examples/ ./examples/ + +# Build the examples +RUN go build -o basic examples/basic.go + +# Use minimal alpine image for runtime +FROM alpine:3.21 + +# Install ca-certificates for HTTPS +RUN apk --no-cache add ca-certificates + +WORKDIR /app + +COPY --from=builder /app/basic . + +# Default: run basic example +CMD ["./basic"] diff --git a/languages/go/http/go.mod b/languages/go/http/go.mod new file mode 100644 index 0000000..c3c7c61 --- /dev/null +++ b/languages/go/http/go.mod @@ -0,0 +1,3 @@ +module uncloseai.com + +go 1.23 diff --git a/languages/go/http/uncloseai.go b/languages/go/http/uncloseai.go new file mode 100644 index 0000000..4da1ec7 --- /dev/null +++ b/languages/go/http/uncloseai.go @@ -0,0 +1,465 @@ +package main + +import ( + "bufio" + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "os" + "strings" + "time" +) + +// ModelInfo contains metadata about a discovered model +type ModelInfo struct { + ID string `json:"id"` + Endpoint string `json:"endpoint"` + MaxTokens int `json:"max_tokens"` +} + +// Message represents a chat message +type Message struct { + Role string `json:"role"` + Content string `json:"content"` +} + +// ChatResponse is the response from a non-streaming chat completion +type ChatResponse struct { + Model string `json:"model"` + Choices []struct { + Message Message `json:"message"` + } `json:"choices"` +} + +// StreamChunk represents a chunk from streaming chat +type StreamChunk struct { + Model string `json:"model"` + Choices []struct { + Delta struct { + Content string `json:"content"` + } `json:"delta"` + } `json:"choices"` +} + +// UncloseAI is the main client for OpenAI-compatible APIs +type UncloseAI struct { + models []ModelInfo + ttsEndpoints []string + apiKey string + timeout time.Duration + httpClient *http.Client +} + +// NewUncloseAI creates a new client with auto-discovery from environment variables +func NewUncloseAI() (*UncloseAI, error) { + return NewUncloseAIWithOptions(nil, nil, "") +} + +// NewUncloseAIWithOptions creates a new client with custom endpoints +func NewUncloseAIWithOptions(modelEndpoints, ttsEndpoints []string, apiKey string) (*UncloseAI, error) { + client := &UncloseAI{ + models: make([]ModelInfo, 0), + ttsEndpoints: make([]string, 0), + apiKey: apiKey, + timeout: 30 * time.Second, + httpClient: &http.Client{Timeout: 30 * time.Second}, + } + + // Discover endpoints from environment if not provided + if modelEndpoints == nil { + modelEndpoints = discoverEnvEndpoints("MODEL_ENDPOINT") + } + if ttsEndpoints == nil { + ttsEndpoints = discoverEnvEndpoints("TTS_ENDPOINT") + } + + // Discover models from each endpoint + for _, endpoint := range modelEndpoints { + if err := client.discoverModelsFromEndpoint(endpoint); err != nil { + fmt.Printf("Warning: Failed to discover models from %s: %v\n", endpoint, err) + } + } + + client.ttsEndpoints = ttsEndpoints + + return client, nil +} + +// discoverEnvEndpoints finds endpoints from environment variables like PREFIX_1, PREFIX_2, ... +func discoverEnvEndpoints(prefix string) []string { + endpoints := make([]string, 0) + for i := 1; i <= 9999; i++ { + endpoint := os.Getenv(fmt.Sprintf("%s_%d", prefix, i)) + if endpoint == "" { + break + } + endpoints = append(endpoints, endpoint) + } + return endpoints +} + +// discoverModelsFromEndpoint discovers available models from an endpoint +func (c *UncloseAI) discoverModelsFromEndpoint(endpoint string) error { + req, err := http.NewRequest("GET", endpoint+"/models", nil) + if err != nil { + return err + } + + if c.apiKey != "" { + req.Header.Set("Authorization", "Bearer "+c.apiKey) + } + + resp, err := c.httpClient.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("unexpected status: %d", resp.StatusCode) + } + + var result struct { + Data []struct { + ID string `json:"id"` + MaxModelLen int `json:"max_model_len"` + } `json:"data"` + } + + if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { + return err + } + + for _, model := range result.Data { + maxTokens := model.MaxModelLen + if maxTokens == 0 { + maxTokens = 8192 + } + c.models = append(c.models, ModelInfo{ + ID: model.ID, + Endpoint: endpoint, + MaxTokens: maxTokens, + }) + } + + return nil +} + +// ListModels returns all discovered models +func (c *UncloseAI) ListModels() []ModelInfo { + return c.models +} + +// Chat performs non-streaming chat completion +func (c *UncloseAI) Chat(ctx context.Context, messages []Message, model string, maxTokens int, temperature float64) (*ChatResponse, error) { + modelInfo, err := c.getModelInfo(model) + if err != nil { + return nil, err + } + + payload := map[string]interface{}{ + "model": modelInfo.ID, + "messages": messages, + "max_tokens": maxTokens, + "temperature": temperature, + "stream": false, + } + + body, err := json.Marshal(payload) + if err != nil { + return nil, err + } + + req, err := http.NewRequestWithContext(ctx, "POST", modelInfo.Endpoint+"/chat/completions", bytes.NewBuffer(body)) + if err != nil { + return nil, err + } + + req.Header.Set("Content-Type", "application/json") + if c.apiKey != "" { + req.Header.Set("Authorization", "Bearer "+c.apiKey) + } + + resp, err := c.httpClient.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + bodyBytes, _ := io.ReadAll(resp.Body) + return nil, fmt.Errorf("unexpected status %d: %s", resp.StatusCode, string(bodyBytes)) + } + + var chatResp ChatResponse + if err := json.NewDecoder(resp.Body).Decode(&chatResp); err != nil { + return nil, err + } + + return &chatResp, nil +} + +// ChatStream performs streaming chat completion, returning a channel of chunks +func (c *UncloseAI) ChatStream(ctx context.Context, messages []Message, model string, maxTokens int, temperature float64) (<-chan StreamChunk, <-chan error) { + chunkChan := make(chan StreamChunk) + errChan := make(chan error, 1) + + go func() { + defer close(chunkChan) + defer close(errChan) + + modelInfo, err := c.getModelInfo(model) + if err != nil { + errChan <- err + return + } + + payload := map[string]interface{}{ + "model": modelInfo.ID, + "messages": messages, + "max_tokens": maxTokens, + "temperature": temperature, + "stream": true, + } + + body, err := json.Marshal(payload) + if err != nil { + errChan <- err + return + } + + req, err := http.NewRequestWithContext(ctx, "POST", modelInfo.Endpoint+"/chat/completions", bytes.NewBuffer(body)) + if err != nil { + errChan <- err + return + } + + req.Header.Set("Content-Type", "application/json") + if c.apiKey != "" { + req.Header.Set("Authorization", "Bearer "+c.apiKey) + } + + resp, err := c.httpClient.Do(req) + if err != nil { + errChan <- err + return + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + bodyBytes, _ := io.ReadAll(resp.Body) + errChan <- fmt.Errorf("unexpected status %d: %s", resp.StatusCode, string(bodyBytes)) + return + } + + // Parse SSE stream + scanner := bufio.NewScanner(resp.Body) + for scanner.Scan() { + line := scanner.Text() + + // SSE format: "data: {...}" + if strings.HasPrefix(line, "data: ") { + data := strings.TrimPrefix(line, "data: ") + + // Check for stream termination + if strings.TrimSpace(data) == "[DONE]" { + break + } + + var chunk StreamChunk + if err := json.Unmarshal([]byte(data), &chunk); err != nil { + continue // Skip malformed chunks + } + + select { + case chunkChan <- chunk: + case <-ctx.Done(): + return + } + } + } + + if err := scanner.Err(); err != nil { + errChan <- err + } + }() + + return chunkChan, errChan +} + +// TTS generates speech from text +func (c *UncloseAI) TTS(text, voice, model string) ([]byte, error) { + if len(c.ttsEndpoints) == 0 { + return nil, fmt.Errorf("no TTS endpoints available") + } + + endpoint := c.ttsEndpoints[0] + + payload := map[string]interface{}{ + "model": model, + "voice": voice, + "input": text, + } + + body, err := json.Marshal(payload) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("POST", endpoint+"/audio/speech", bytes.NewBuffer(body)) + if err != nil { + return nil, err + } + + req.Header.Set("Content-Type", "application/json") + if c.apiKey != "" { + req.Header.Set("Authorization", "Bearer "+c.apiKey) + } + + resp, err := c.httpClient.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("unexpected status: %d", resp.StatusCode) + } + + return io.ReadAll(resp.Body) +} + +// getModelInfo retrieves model info by ID or returns first available +func (c *UncloseAI) getModelInfo(modelID string) (*ModelInfo, error) { + if len(c.models) == 0 { + return nil, fmt.Errorf("no models available") + } + + if modelID == "" { + return &c.models[0], nil + } + + for i := range c.models { + if c.models[i].ID == modelID { + return &c.models[i], nil + } + } + + return nil, fmt.Errorf("model '%s' not found", modelID) +} + +// Demo usage +func main() { + fmt.Println("=== UncloseAI Go Client (with Streaming) ===\n") + + // Initialize client with auto-discovery + client, err := NewUncloseAI() + if err != nil { + fmt.Printf("Error initializing client: %v\n", err) + os.Exit(1) + } + + models := client.ListModels() + if len(models) == 0 { + fmt.Println("ERROR: No models discovered. Set environment variables:") + fmt.Println(" MODEL_ENDPOINT_1, MODEL_ENDPOINT_2, etc.") + os.Exit(1) + } + + fmt.Printf("Discovered %d model(s)\n", len(models)) + for _, m := range models { + fmt.Printf(" - %s (max_tokens: %d)\n", m.ID, m.MaxTokens) + } + fmt.Println() + + ctx := context.Background() + + // Non-streaming chat example + fmt.Println("=== Non-Streaming Chat ===") + response, err := client.Chat( + ctx, + []Message{ + {Role: "system", Content: "You are a helpful AI assistant."}, + {Role: "user", Content: "Explain quantum computing in one sentence."}, + }, + "", // Use first available model + 100, + 0.7, + ) + + if err != nil { + fmt.Printf("Error: %v\n", err) + } else { + fmt.Printf("Model: %s\n", response.Model) + fmt.Printf("Response: %s\n\n", response.Choices[0].Message.Content) + } + + // Streaming chat example + fmt.Println("=== Streaming Chat ===") + modelID := "" + if len(models) > 1 { + modelID = models[1].ID + } + + fmt.Printf("Model: %s\n", modelID) + fmt.Print("Response: ") + + chunkChan, errChan := client.ChatStream( + ctx, + []Message{ + {Role: "system", Content: "You are a coding assistant."}, + {Role: "user", Content: "Write a Go function to check if a number is prime"}, + }, + modelID, + 200, + 0.7, + ) + + for { + select { + case chunk, ok := <-chunkChan: + if !ok { + goto done + } + if len(chunk.Choices) > 0 { + content := chunk.Choices[0].Delta.Content + if content != "" { + fmt.Print(content) + } + } + case err := <-errChan: + if err != nil { + fmt.Printf("\nError: %v\n", err) + } + goto done + } + } + +done: + fmt.Println("\n") + + // TTS example + if len(client.ttsEndpoints) > 0 { + fmt.Println("=== TTS Speech Generation ===") + audioData, err := client.TTS( + "Hello from UncloseAI Go client! This demonstrates text to speech with streaming support.", + "alloy", + "tts-1", + ) + + if err != nil { + fmt.Printf("Error: %v\n", err) + } else { + if err := os.WriteFile("speech.mp3", audioData, 0644); err != nil { + fmt.Printf("[ERROR] Failed to write speech file: %v\n", err) + } else { + fmt.Printf("[OK] Speech file created: speech.mp3 (%d bytes)\n\n", len(audioData)) + } + } + } + + fmt.Println("=== Examples Complete ===") +} diff --git a/languages/java/http/Dockerfile b/languages/java/http/Dockerfile new file mode 100644 index 0000000..7c51cbd --- /dev/null +++ b/languages/java/http/Dockerfile @@ -0,0 +1,9 @@ +FROM openjdk:17-jdk-slim + +WORKDIR /app + +COPY UncloseAI.java . + +RUN javac UncloseAI.java + +CMD ["java", "UncloseAI"] diff --git a/languages/java/http/UncloseAI.java b/languages/java/http/UncloseAI.java new file mode 100644 index 0000000..b397b43 --- /dev/null +++ b/languages/java/http/UncloseAI.java @@ -0,0 +1,366 @@ +import java.io.*; +import java.net.*; +import java.nio.charset.StandardCharsets; +import java.nio.file.*; +import java.util.*; +import java.util.function.Consumer; + +class ModelInfo { + String id; + String endpoint; + int maxTokens; + + ModelInfo(String id, String endpoint, int maxTokens) { + this.id = id; + this.endpoint = endpoint; + this.maxTokens = maxTokens; + } +} + +public class UncloseAI { + private List models = new ArrayList<>(); + private List ttsEndpoints = new ArrayList<>(); + private String apiKey; + private int timeout = 30000; + private boolean debug = false; + + public UncloseAI() { + this(null, null, null, 30000, false); + } + + public UncloseAI(List endpoints, List ttsEndpoints, String apiKey, int timeout, boolean debug) { + this.apiKey = apiKey; + this.timeout = timeout; + this.debug = debug; + + if (endpoints == null) { + endpoints = discoverEndpointsFromEnv("MODEL_ENDPOINT"); + } + if (ttsEndpoints == null) { + ttsEndpoints = discoverEndpointsFromEnv("TTS_ENDPOINT"); + } + + if (debug) { + System.out.println("[DEBUG] Initialized with " + endpoints.size() + " endpoint(s)"); + } + + discoverModels(endpoints); + this.ttsEndpoints = ttsEndpoints; + } + + public List listModels() { + return new ArrayList<>(models); + } + + public String chat(List> messages, String model, int maxTokens) throws IOException { + ModelInfo modelInfo = resolveModel(model); + String jsonRequest = buildChatRequest(modelInfo.id, messages, maxTokens, false); + String response = postJSON(modelInfo.endpoint + "/chat/completions", jsonRequest); + return extractContent(response); + } + + public void chatStream(List> messages, String model, int maxTokens, Consumer callback) throws IOException { + ModelInfo modelInfo = resolveModel(model); + String jsonRequest = buildChatRequest(modelInfo.id, messages, maxTokens, true); + + URL url = new URL(modelInfo.endpoint + "/chat/completions"); + HttpURLConnection conn = (HttpURLConnection) url.openConnection(); + conn.setRequestMethod("POST"); + conn.setRequestProperty("Content-Type", "application/json"); + conn.setDoOutput(true); + conn.setConnectTimeout(timeout); + conn.setReadTimeout(timeout); + + try (OutputStream os = conn.getOutputStream()) { + os.write(jsonRequest.getBytes(StandardCharsets.UTF_8)); + } + + try (BufferedReader br = new BufferedReader( + new InputStreamReader(conn.getInputStream(), StandardCharsets.UTF_8))) { + String line; + while ((line = br.readLine()) != null) { + if (line.startsWith("data: ")) { + String data = line.substring(6).trim(); + if ("[DONE]".equals(data)) { + break; + } + String content = extractStreamContent(data); + if (content != null && !content.isEmpty()) { + callback.accept(content); + } + } + } + } + } + + public byte[] tts(String text, String voice, String model) throws IOException { + if (ttsEndpoints.isEmpty()) { + throw new IOException("No TTS endpoints available"); + } + + String jsonRequest = String.format( + "{\"model\":\"%s\",\"voice\":\"%s\",\"input\":\"%s\"}", + model, voice, text.replace("\"", "\\\"") + ); + + return postJSONBinary(ttsEndpoints.get(0) + "/audio/speech", jsonRequest); + } + + private List discoverEndpointsFromEnv(String prefix) { + List endpoints = new ArrayList<>(); + for (int i = 1; i < 10000; i++) { + String endpoint = System.getenv(prefix + "_" + i); + if (endpoint == null || endpoint.isEmpty()) { + break; + } + endpoints.add(endpoint); + } + return endpoints; + } + + private void discoverModels(List endpoints) { + for (String endpoint : endpoints) { + if (debug) { + System.out.println("[DEBUG] Discovering from: " + endpoint); + } + + try { + String response = getJSON(endpoint + "/models"); + parseModels(response, endpoint); + } catch (Exception e) { + if (debug) { + System.out.println("[DEBUG] Error: " + e.getMessage()); + } + } + } + } + + private void parseModels(String jsonResponse, String endpoint) { + int dataIndex = jsonResponse.indexOf("\"data\":["); + if (dataIndex == -1) return; + + String dataSection = jsonResponse.substring(dataIndex + 8); + int pos = 0; + while (pos < dataSection.length()) { + int idIndex = dataSection.indexOf("\"id\":\"", pos); + if (idIndex == -1) break; + + int idStart = idIndex + 6; + int idEnd = dataSection.indexOf("\"", idStart); + String modelId = dataSection.substring(idStart, idEnd); + + if (modelId.startsWith("modelperm-")) { + pos = idEnd + 1; + continue; + } + + int maxTokens = 8192; + int maxLenIndex = dataSection.indexOf("\"max_model_len\":", idEnd); + if (maxLenIndex != -1 && maxLenIndex < dataSection.indexOf("}", idEnd)) { + int maxLenStart = maxLenIndex + 16; + int maxLenEnd = dataSection.indexOf(",", maxLenStart); + if (maxLenEnd == -1) maxLenEnd = dataSection.indexOf("}", maxLenStart); + if (maxLenEnd != -1) { + try { + maxTokens = Integer.parseInt(dataSection.substring(maxLenStart, maxLenEnd).trim()); + } catch (NumberFormatException ignored) {} + } + } + + models.add(new ModelInfo(modelId, endpoint, maxTokens)); + if (debug) { + System.out.println("[DEBUG] Discovered: " + modelId); + } + + pos = idEnd + 1; + } + } + + private ModelInfo resolveModel(String model) throws IOException { + if (models.isEmpty()) { + throw new IOException("No models available"); + } + if (model == null || model.isEmpty()) { + return models.get(0); + } + for (ModelInfo m : models) { + if (m.id.equals(model)) { + return m; + } + } + throw new IOException("Model '" + model + "' not found"); + } + + private String buildChatRequest(String modelId, List> messages, int maxTokens, boolean stream) { + StringBuilder sb = new StringBuilder(); + sb.append("{\"model\":\"").append(modelId).append("\","); + sb.append("\"messages\":["); + for (int i = 0; i < messages.size(); i++) { + if (i > 0) sb.append(","); + Map msg = messages.get(i); + sb.append("{\"role\":\"").append(msg.get("role")).append("\","); + sb.append("\"content\":\"").append(msg.get("content").replace("\"", "\\\"")).append("\"}"); + } + sb.append("],\"max_tokens\":").append(maxTokens); + if (stream) { + sb.append(",\"stream\":true"); + } + sb.append("}"); + return sb.toString(); + } + + private String getJSON(String urlString) throws IOException { + URL url = new URL(urlString); + HttpURLConnection conn = (HttpURLConnection) url.openConnection(); + conn.setRequestMethod("GET"); + conn.setConnectTimeout(10000); + conn.setReadTimeout(10000); + + try (BufferedReader br = new BufferedReader( + new InputStreamReader(conn.getInputStream(), StandardCharsets.UTF_8))) { + StringBuilder response = new StringBuilder(); + String line; + while ((line = br.readLine()) != null) { + response.append(line.trim()); + } + return response.toString(); + } + } + + private String postJSON(String urlString, String jsonRequest) throws IOException { + URL url = new URL(urlString); + HttpURLConnection conn = (HttpURLConnection) url.openConnection(); + conn.setRequestMethod("POST"); + conn.setRequestProperty("Content-Type", "application/json"); + conn.setDoOutput(true); + conn.setConnectTimeout(timeout); + conn.setReadTimeout(timeout); + + try (OutputStream os = conn.getOutputStream()) { + os.write(jsonRequest.getBytes(StandardCharsets.UTF_8)); + } + + try (BufferedReader br = new BufferedReader( + new InputStreamReader(conn.getInputStream(), StandardCharsets.UTF_8))) { + StringBuilder response = new StringBuilder(); + String line; + while ((line = br.readLine()) != null) { + response.append(line.trim()); + } + return response.toString(); + } + } + + private byte[] postJSONBinary(String urlString, String jsonRequest) throws IOException { + URL url = new URL(urlString); + HttpURLConnection conn = (HttpURLConnection) url.openConnection(); + conn.setRequestMethod("POST"); + conn.setRequestProperty("Content-Type", "application/json"); + conn.setDoOutput(true); + conn.setConnectTimeout(timeout); + conn.setReadTimeout(timeout); + + try (OutputStream os = conn.getOutputStream()) { + os.write(jsonRequest.getBytes(StandardCharsets.UTF_8)); + } + + try (InputStream is = conn.getInputStream()) { + ByteArrayOutputStream buffer = new ByteArrayOutputStream(); + byte[] data = new byte[1024]; + int nRead; + while ((nRead = is.read(data, 0, data.length)) != -1) { + buffer.write(data, 0, nRead); + } + return buffer.toByteArray(); + } + } + + private String extractContent(String jsonResponse) { + int contentIndex = jsonResponse.indexOf("\"content\":\""); + if (contentIndex == -1) return jsonResponse; + + int startIndex = contentIndex + 11; + int endIndex = jsonResponse.indexOf("\"", startIndex); + while (endIndex > 0 && jsonResponse.charAt(endIndex - 1) == '\\') { + endIndex = jsonResponse.indexOf("\"", endIndex + 1); + } + if (endIndex == -1) return jsonResponse.substring(startIndex); + + String content = jsonResponse.substring(startIndex, endIndex); + return content.replace("\\n", "\n").replace("\\\"", "\"").replace("\\\\", "\\"); + } + + private String extractStreamContent(String jsonChunk) { + int contentIndex = jsonChunk.indexOf("\"content\":\""); + if (contentIndex == -1) return null; + + int startIndex = contentIndex + 11; + int endIndex = jsonChunk.indexOf("\"", startIndex); + if (endIndex == -1) return null; + + return jsonChunk.substring(startIndex, endIndex) + .replace("\\n", "\n").replace("\\\"", "\"").replace("\\\\", "\\"); + } + + // Demo when run as application + public static void main(String[] args) { + System.out.println("=== UncloseAI Java Client (with Streaming) ===\n"); + + UncloseAI client = new UncloseAI(null, null, null, 30000, true); + + if (client.models.isEmpty()) { + System.out.println("ERROR: No models discovered. Set environment variables:"); + System.out.println(" MODEL_ENDPOINT_1, MODEL_ENDPOINT_2, etc."); + System.exit(1); + } + + System.out.println("\nDiscovered " + client.models.size() + " model(s):"); + for (ModelInfo m : client.models) { + System.out.println(" - " + m.id + " (max_tokens: " + m.maxTokens + ")"); + } + System.out.println(); + + // Non-streaming chat + System.out.println("=== Non-Streaming Chat ==="); + try { + List> messages = Arrays.asList( + new HashMap() {{ put("role", "system"); put("content", "You are a helpful AI assistant."); }}, + new HashMap() {{ put("role", "user"); put("content", "Explain quantum computing in one sentence."); }} + ); + String response = client.chat(messages, null, 100); + System.out.println("Response: " + response + "\n"); + } catch (IOException e) { + System.out.println("Error: " + e.getMessage() + "\n"); + } + + // Streaming chat + System.out.println("=== Streaming Chat ==="); + String modelId = client.models.size() > 1 ? client.models.get(1).id : null; + System.out.println("Model: " + (modelId != null ? modelId : client.models.get(0).id)); + System.out.print("Response: "); + try { + List> messages = Arrays.asList( + new HashMap() {{ put("role", "system"); put("content", "You are a coding assistant."); }}, + new HashMap() {{ put("role", "user"); put("content", "Write a Java function to check if a number is prime"); }} + ); + client.chatStream(messages, modelId, 200, content -> System.out.print(content)); + System.out.println("\n"); + } catch (IOException e) { + System.out.println("\nError: " + e.getMessage() + "\n"); + } + + // TTS + if (!client.ttsEndpoints.isEmpty()) { + System.out.println("=== TTS Speech Generation ==="); + try { + byte[] audio = client.tts("Hello from UncloseAI Java client! This demonstrates streaming support.", "alloy", "tts-1"); + Files.write(Paths.get("speech.mp3"), audio); + System.out.println("[OK] Speech file created: speech.mp3 (" + audio.length + " bytes)\n"); + } catch (IOException e) { + System.out.println("[ERROR] TTS Error: " + e.getMessage() + "\n"); + } + } + + System.out.println("=== Examples Complete ==="); + } +} diff --git a/languages/ruby/http/Dockerfile b/languages/ruby/http/Dockerfile new file mode 100644 index 0000000..a18340e --- /dev/null +++ b/languages/ruby/http/Dockerfile @@ -0,0 +1,10 @@ +# Ruby 3.3 (checked 2025-10-13: ruby:3.3-alpine is latest stable) +FROM ruby:3.3-alpine + +RUN apk add --no-cache ca-certificates + +WORKDIR /app +COPY uncloseai.rb . +RUN chmod +x uncloseai.rb + +CMD ["ruby", "uncloseai.rb"] diff --git a/languages/ruby/http/uncloseai.rb b/languages/ruby/http/uncloseai.rb new file mode 100644 index 0000000..a2715c8 --- /dev/null +++ b/languages/ruby/http/uncloseai.rb @@ -0,0 +1,225 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +require 'net/http' +require 'uri' +require 'json' + +# uncloseai. - Ruby client for OpenAI-compatible APIs with streaming support +class uncloseai + attr_reader :models, :tts_endpoints + + def initialize(endpoints: nil, tts_endpoints: nil, api_key: nil, timeout: 30, debug: false) + @api_key = api_key + @timeout = timeout + @debug = debug + @models = [] + @tts_endpoints = [] + + endpoints ||= discover_endpoints_from_env('MODEL_ENDPOINT') + tts_endpoints ||= discover_endpoints_from_env('TTS_ENDPOINT') + + puts "[DEBUG] Initialized with #{endpoints.length} endpoint(s)" if @debug + + discover_models(endpoints) + @tts_endpoints = tts_endpoints + end + + def list_models + @models + end + + def chat(messages, model: nil, max_tokens: 100, temperature: 0.7, **kwargs) + model_info = resolve_model(model) + + payload = { + model: model_info[:id], + messages: messages, + max_tokens: max_tokens, + temperature: temperature, + **kwargs + } + + response = http_request("#{model_info[:endpoint]}/chat/completions", :post, payload) + JSON.parse(response) + end + + def chat_stream(messages, model: nil, max_tokens: 500, temperature: 0.7, **kwargs) + model_info = resolve_model(model) + + payload = { + model: model_info[:id], + messages: messages, + max_tokens: max_tokens, + temperature: temperature, + stream: true, + **kwargs + } + + uri = URI.parse("#{model_info[:endpoint]}/chat/completions") + http = Net::HTTP.new(uri.host, uri.port) + http.use_ssl = uri.scheme == 'https' + http.read_timeout = @timeout + + request = Net::HTTP::Post.new(uri.request_uri) + request['Content-Type'] = 'application/json' + request['Authorization'] = "Bearer #{@api_key}" if @api_key + request.body = payload.to_json + + buffer = '' + http.request(request) do |response| + raise "HTTP #{response.code}" unless response.code.to_i == 200 + + response.read_body do |chunk| + buffer += chunk + lines = buffer.split("\n") + buffer = lines.pop || '' + + lines.each do |line| + next unless line.start_with?('data: ') + + data = line[6..-1].strip + break if data == '[DONE]' + + begin + parsed = JSON.parse(data) + yield parsed + rescue JSON::ParserError => e + puts "[DEBUG] Parse error: #{e.message}" if @debug + end + end + end + end + end + + def tts(text, voice: 'alloy', model: 'tts-1') + raise 'No TTS endpoints available' if @tts_endpoints.empty? + + payload = { + model: model, + voice: voice, + input: text + } + + http_request("#{@tts_endpoints[0]}/audio/speech", :post, payload) + end + + private + + def discover_endpoints_from_env(prefix) + endpoints = [] + (1..9999).each do |i| + endpoint = ENV["#{prefix}_#{i}"] + break unless endpoint + endpoints << endpoint + end + endpoints + end + + def discover_models(endpoints) + endpoints.each do |endpoint| + puts "[DEBUG] Discovering from: #{endpoint}" if @debug + + begin + response = http_request("#{endpoint}/models", :get) + data = JSON.parse(response) + + data['data'].each do |model| + @models << { + id: model['id'], + endpoint: endpoint, + max_tokens: model['max_model_len'] || 8192 + } + puts "[DEBUG] Discovered: #{model['id']}" if @debug + end + rescue => e + puts "[DEBUG] Error: #{e.message}" if @debug + end + end + end + + def resolve_model(model) + raise 'No models available' if @models.empty? + return @models[0] if model.nil? + + found = @models.find { |m| m[:id] == model } + raise "Model '#{model}' not found" unless found + found + end + + def http_request(url, method, payload = nil) + uri = URI.parse(url) + http = Net::HTTP.new(uri.host, uri.port) + http.use_ssl = uri.scheme == 'https' + http.read_timeout = @timeout + + request = case method + when :get + Net::HTTP::Get.new(uri.request_uri) + when :post + req = Net::HTTP::Post.new(uri.request_uri) + req['Content-Type'] = 'application/json' + req.body = payload.to_json if payload + req + end + + request['Authorization'] = "Bearer #{@api_key}" if @api_key + + response = http.request(request) + raise "HTTP #{response.code}" unless response.code.to_i == 200 + response.body + end +end + +# Demo when run as script +if __FILE__ == $0 + puts "=== uncloseai. Ruby Client (with Streaming) ===\n" + + client = uncloseai.new(debug: true) + + if client.models.empty? + puts "ERROR: No models discovered. Set environment variables:" + puts " MODEL_ENDPOINT_1, MODEL_ENDPOINT_2, etc." + exit 1 + end + + puts "\nDiscovered #{client.models.length} model(s):" + client.models.each do |m| + puts " - #{m[:id]} (max_tokens: #{m[:max_tokens]})" + end + puts + + # Non-streaming chat + puts "=== Non-Streaming Chat ===" + response = client.chat([ + { role: 'system', content: 'You are a helpful AI assistant.' }, + { role: 'user', content: 'Explain quantum computing in one sentence.' } + ]) + puts "Response: #{response['choices'][0]['message']['content']}\n\n" + + # Streaming chat + puts "=== Streaming Chat ===" + model_id = client.models.length > 1 ? client.models[1][:id] : nil + puts "Model: #{model_id || client.models[0][:id]}" + print "Response: " + + client.chat_stream([ + { role: 'system', content: 'You are a coding assistant.' }, + { role: 'user', content: 'Write a Ruby function to check if a number is prime' } + ], model: model_id, max_tokens: 200) do |chunk| + content = chunk.dig('choices', 0, 'delta', 'content') + print content if content + end + + puts "\n\n" + + # TTS + if client.tts_endpoints.any? + puts "=== TTS Speech Generation ===" + audio_data = client.tts('Hello from uncloseai. Ruby client! This demonstrates streaming support.') + File.binwrite('speech.mp3', audio_data) + puts "[OK] Speech file created: speech.mp3 (#{audio_data.bytesize} bytes)\n\n" + end + + puts "=== Examples Complete ===" +end