move raw HTTP implementations to http subdirectories for Go, Ruby, Java, and C#

This commit is contained in:
Russell Ballestrini 2025-10-15 15:57:30 -04:00
parent 8841bacb0b
commit f7e1e6b17f
10 changed files with 1437 additions and 0 deletions

View file

@ -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"]

View file

@ -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<ModelInfo> models = new List<ModelInfo>();
static readonly List<string> ttsEndpoints = new List<string>();
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<string> 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<string> 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();
}
}

View file

@ -0,0 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net9.0</TargetFramework>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>

View file

@ -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"]

3
languages/go/http/go.mod Normal file
View file

@ -0,0 +1,3 @@
module uncloseai.com
go 1.23

View file

@ -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 ===")
}

View file

@ -0,0 +1,9 @@
FROM openjdk:17-jdk-slim
WORKDIR /app
COPY UncloseAI.java .
RUN javac UncloseAI.java
CMD ["java", "UncloseAI"]

View file

@ -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<ModelInfo> models = new ArrayList<>();
private List<String> 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<String> endpoints, List<String> 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<ModelInfo> listModels() {
return new ArrayList<>(models);
}
public String chat(List<Map<String, String>> 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<Map<String, String>> messages, String model, int maxTokens, Consumer<String> 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<String> discoverEndpointsFromEnv(String prefix) {
List<String> 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<String> 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<Map<String, String>> 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<String, String> 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<Map<String, String>> messages = Arrays.asList(
new HashMap<String, String>() {{ put("role", "system"); put("content", "You are a helpful AI assistant."); }},
new HashMap<String, String>() {{ 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<Map<String, String>> messages = Arrays.asList(
new HashMap<String, String>() {{ put("role", "system"); put("content", "You are a coding assistant."); }},
new HashMap<String, String>() {{ 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 ===");
}
}

View file

@ -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"]

View file

@ -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