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