move raw HTTP implementations to http subdirectories for Go, Ruby, Java, and C#
This commit is contained in:
parent
8841bacb0b
commit
f7e1e6b17f
10 changed files with 1437 additions and 0 deletions
18
languages/csharp/http/Dockerfile
Normal file
18
languages/csharp/http/Dockerfile
Normal 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"]
|
||||
306
languages/csharp/http/Uncloseai.cs
Normal file
306
languages/csharp/http/Uncloseai.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
7
languages/csharp/http/csharp.csproj
Normal file
7
languages/csharp/http/csharp.csproj
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
28
languages/go/http/Dockerfile
Normal file
28
languages/go/http/Dockerfile
Normal 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
3
languages/go/http/go.mod
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
module uncloseai.com
|
||||
|
||||
go 1.23
|
||||
465
languages/go/http/uncloseai.go
Normal file
465
languages/go/http/uncloseai.go
Normal 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 ===")
|
||||
}
|
||||
9
languages/java/http/Dockerfile
Normal file
9
languages/java/http/Dockerfile
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
FROM openjdk:17-jdk-slim
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY UncloseAI.java .
|
||||
|
||||
RUN javac UncloseAI.java
|
||||
|
||||
CMD ["java", "UncloseAI"]
|
||||
366
languages/java/http/UncloseAI.java
Normal file
366
languages/java/http/UncloseAI.java
Normal 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 ===");
|
||||
}
|
||||
}
|
||||
10
languages/ruby/http/Dockerfile
Normal file
10
languages/ruby/http/Dockerfile
Normal 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"]
|
||||
225
languages/ruby/http/uncloseai.rb
Normal file
225
languages/ruby/http/uncloseai.rb
Normal 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
|
||||
Loading…
Add table
Add a link
Reference in a new issue