From bdf7875f3154acd5d1b9be77e3a8bd9f6372a75a Mon Sep 17 00:00:00 2001 From: Russell Ballestrini Date: Wed, 15 Oct 2025 15:47:23 -0400 Subject: [PATCH] reorganize openai clients: rename python/openai-client to python/openai, add javascript/openai/nodejs and elixir/openai --- languages/elixir/openai/Dockerfile | 17 + languages/elixir/openai/mix.exs | 25 ++ languages/elixir/openai/uncloseai.exs | 86 +++++ languages/javascript/openai/nodejs/Dockerfile | 13 + .../javascript/openai/nodejs/package.json | 27 ++ .../javascript/openai/nodejs/uncloseai.js | 65 ++++ languages/python/openai/Dockerfile | 13 + languages/python/openai/requirements.txt | 1 + languages/python/openai/uncloseai.py | 305 ++++++++++++++++++ 9 files changed, 552 insertions(+) create mode 100644 languages/elixir/openai/Dockerfile create mode 100644 languages/elixir/openai/mix.exs create mode 100644 languages/elixir/openai/uncloseai.exs create mode 100644 languages/javascript/openai/nodejs/Dockerfile create mode 100644 languages/javascript/openai/nodejs/package.json create mode 100644 languages/javascript/openai/nodejs/uncloseai.js create mode 100644 languages/python/openai/Dockerfile create mode 100644 languages/python/openai/requirements.txt create mode 100644 languages/python/openai/uncloseai.py diff --git a/languages/elixir/openai/Dockerfile b/languages/elixir/openai/Dockerfile new file mode 100644 index 0000000..08c1653 --- /dev/null +++ b/languages/elixir/openai/Dockerfile @@ -0,0 +1,17 @@ +# Use official Elixir image +FROM elixir:1.17-alpine + +WORKDIR /app + +# Install hex and rebar +RUN mix local.hex --force && \ + mix local.rebar --force + +# Copy mix files and install dependencies +COPY mix.exs ./ +RUN mix deps.get && mix deps.compile + +# Copy application code +COPY uncloseai.exs ./ + +CMD ["elixir", "uncloseai.exs"] diff --git a/languages/elixir/openai/mix.exs b/languages/elixir/openai/mix.exs new file mode 100644 index 0000000..479ff7d --- /dev/null +++ b/languages/elixir/openai/mix.exs @@ -0,0 +1,25 @@ +defmodule UncloseaiOpenai.MixProject do + use Mix.Project + + def project do + [ + app: :uncloseai_openai, + version: "0.1.0", + elixir: "~> 1.17", + start_permanent: Mix.env() == :prod, + deps: deps() + ] + end + + def application do + [ + extra_applications: [:logger] + ] + end + + defp deps do + [ + {:openai_ex, "~> 0.9.17"} + ] + end +end diff --git a/languages/elixir/openai/uncloseai.exs b/languages/elixir/openai/uncloseai.exs new file mode 100644 index 0000000..b233f06 --- /dev/null +++ b/languages/elixir/openai/uncloseai.exs @@ -0,0 +1,86 @@ +#!/usr/bin/env elixir + +Mix.install([{:openai_ex, "~> 0.9.17"}]) + +IO.puts("=== UncloseAI Elixir Client (OpenAI Ex Library) ===\n") + +# Non-streaming chat with Hermes +IO.puts("=== Non-Streaming Chat (Hermes) ===") + +hermes = OpenaiEx.new("dummy-key") + |> OpenaiEx.with_base_url("https://hermes.ai.unturf.com/v1") + +hermes_req = OpenaiEx.ChatCompletion.new( + model: "adamo1139/Hermes-3-Llama-3.1-8B-FP8-Dynamic", + messages: [ + %{role: "user", content: "Give a Python Fizzbuzz solution in one line of code?"} + ], + temperature: 0.5, + max_tokens: 150 +) + +case OpenaiEx.ChatCompletion.create(hermes, hermes_req) do + {:ok, response} -> + content = response.choices |> List.first() |> Map.get(:message) |> Map.get(:content) + IO.puts("Response: #{content}\n") + {:error, error} -> + IO.puts("Error: #{inspect(error)}\n") +end + +# Streaming chat with Qwen +IO.puts("=== Streaming Chat (Qwen) ===") + +qwen = OpenaiEx.new("dummy-key") + |> OpenaiEx.with_base_url("https://qwen.ai.unturf.com/v1") + +qwen_req = OpenaiEx.ChatCompletion.new( + model: "hf.co/unsloth/Qwen3-Coder-30B-A3B-Instruct-GGUF:Q4_K_M", + messages: [ + %{role: "user", content: "Give a Python Fizzbuzz solution in one line of code?"} + ], + temperature: 0.5, + max_tokens: 150, + stream: true +) + +IO.write("Response: ") + +OpenaiEx.ChatCompletion.create(qwen, qwen_req) +|> Stream.each(fn + {:data, chunk} -> + case chunk do + %{choices: [%{delta: %{content: content}} | _]} when not is_nil(content) -> + IO.write(content) + _ -> + :ok + end + _ -> + :ok +end) +|> Stream.run() + +IO.puts("\n") + +# TTS example +IO.puts("=== TTS Speech Generation ===") + +tts = OpenaiEx.new("YOLO") + |> OpenaiEx.with_base_url("https://speech.ai.unturf.com/v1") + +tts_req = OpenaiEx.Audio.Speech.new( + model: "tts-1", + voice: "alloy", + input: "I think so therefore, Today is a wonderful day to grow something people love!", + speed: 0.9 +) + +case OpenaiEx.Audio.Speech.create(tts, tts_req) do + {:ok, audio_data} -> + File.write!("speech.mp3", audio_data) + {:ok, file_info} = File.stat("speech.mp3") + IO.puts("[OK] Speech file created: speech.mp3 (#{file_info.size} bytes)\n") + {:error, error} -> + IO.puts("Error: #{inspect(error)}\n") +end + +IO.puts("=== Examples Complete ===") diff --git a/languages/javascript/openai/nodejs/Dockerfile b/languages/javascript/openai/nodejs/Dockerfile new file mode 100644 index 0000000..cce3123 --- /dev/null +++ b/languages/javascript/openai/nodejs/Dockerfile @@ -0,0 +1,13 @@ +# Use official Node.js image (checked 2025-10-15: node:23-alpine is latest stable) +FROM node:23-alpine + +WORKDIR /app + +# Copy package files and install dependencies +COPY package.json ./ +RUN npm install + +# Copy application code +COPY uncloseai.js ./ + +CMD ["node", "uncloseai.js"] diff --git a/languages/javascript/openai/nodejs/package.json b/languages/javascript/openai/nodejs/package.json new file mode 100644 index 0000000..c474419 --- /dev/null +++ b/languages/javascript/openai/nodejs/package.json @@ -0,0 +1,27 @@ +{ + "name": "uncloseai-nodejs-openai", + "version": "1.0.0", + "description": "Node.js client using official OpenAI SDK for uncloseai endpoints", + "type": "module", + "main": "uncloseai.js", + "scripts": { + "start": "node uncloseai.js" + }, + "keywords": [ + "ai", + "openai", + "uncloseai", + "llm", + "chat", + "tts", + "streaming" + ], + "author": "UncloseAI", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "dependencies": { + "openai": "^4.77.0" + } +} diff --git a/languages/javascript/openai/nodejs/uncloseai.js b/languages/javascript/openai/nodejs/uncloseai.js new file mode 100644 index 0000000..64d0382 --- /dev/null +++ b/languages/javascript/openai/nodejs/uncloseai.js @@ -0,0 +1,65 @@ +import OpenAI from 'openai'; +import fs from 'fs'; + +console.log('=== UncloseAI Node.js Client (Official OpenAI SDK) ===\n'); + +// Non-streaming chat with Hermes +console.log('=== Non-Streaming Chat (Hermes) ==='); +const hermesClient = new OpenAI({ + apiKey: 'dummy-key', + baseURL: 'https://hermes.ai.unturf.com/v1' +}); + +const hermesResponse = await hermesClient.chat.completions.create({ + model: 'adamo1139/Hermes-3-Llama-3.1-8B-FP8-Dynamic', + messages: [ + { role: 'user', content: 'Give a Python Fizzbuzz solution in one line of code?' } + ], + temperature: 0.5, + max_tokens: 150 +}); + +console.log(`Response: ${hermesResponse.choices[0].message.content}\n`); + +// Streaming chat with Qwen +console.log('=== Streaming Chat (Qwen) ==='); +const qwenClient = new OpenAI({ + apiKey: 'dummy-key', + baseURL: 'https://qwen.ai.unturf.com/v1' +}); + +const qwenStream = await qwenClient.chat.completions.create({ + model: 'hf.co/unsloth/Qwen3-Coder-30B-A3B-Instruct-GGUF:Q4_K_M', + messages: [ + { role: 'user', content: 'Give a Python Fizzbuzz solution in one line of code?' } + ], + temperature: 0.5, + max_tokens: 150, + stream: true +}); + +process.stdout.write('Response: '); +for await (const chunk of qwenStream) { + process.stdout.write(chunk.choices[0]?.delta?.content || ''); +} +console.log('\n'); + +// TTS example +console.log('=== TTS Speech Generation ==='); +const ttsClient = new OpenAI({ + apiKey: 'YOLO', + baseURL: 'https://speech.ai.unturf.com/v1' +}); + +const mp3 = await ttsClient.audio.speech.create({ + model: 'tts-1', + voice: 'alloy', + input: 'I think so therefore, Today is a wonderful day to grow something people love!', + speed: 0.9 +}); + +const buffer = Buffer.from(await mp3.arrayBuffer()); +fs.writeFileSync('speech.mp3', buffer); +console.log(`[OK] Speech file created: speech.mp3 (${buffer.length} bytes)\n`); + +console.log('=== Examples Complete ==='); diff --git a/languages/python/openai/Dockerfile b/languages/python/openai/Dockerfile new file mode 100644 index 0000000..c885bf6 --- /dev/null +++ b/languages/python/openai/Dockerfile @@ -0,0 +1,13 @@ +# Pin to specific Python version (checked 2025-10-12: python:3.13-alpine is latest stable) +FROM python:3.13-alpine + +WORKDIR /app + +COPY requirements.txt /app/requirements.txt +COPY uncloseai.py /app/uncloseai.py + +RUN chmod +x /app/uncloseai.py + +RUN pip3 install --no-cache-dir -r /app/requirements.txt + +CMD ["python3", "/app/uncloseai.py"] diff --git a/languages/python/openai/requirements.txt b/languages/python/openai/requirements.txt new file mode 100644 index 0000000..f429274 --- /dev/null +++ b/languages/python/openai/requirements.txt @@ -0,0 +1 @@ +openai==2.3.0 diff --git a/languages/python/openai/uncloseai.py b/languages/python/openai/uncloseai.py new file mode 100644 index 0000000..c5c6c54 --- /dev/null +++ b/languages/python/openai/uncloseai.py @@ -0,0 +1,305 @@ +#!/usr/bin/env python3 +""" +uncloseai. - Python Client using OpenAI SDK +A Python client library for OpenAI-compatible APIs with streaming support +Compatible with vLLM, Ollama, and OpenAI-compatible endpoints +""" + +from openai import OpenAI +import os +import requests +from typing import List, Dict, Optional, Iterator + + +class uncloseai: + """Client for OpenAI-compatible API endpoints using OpenAI SDK""" + + def __init__( + self, + model_endpoints: Optional[List[str]] = None, + tts_endpoints: Optional[List[str]] = None, + api_key: str = "dummy-key", + timeout: int = 30 + ): + """ + Initialize uncloseai. client with automatic model discovery + + Args: + model_endpoints: List of model endpoint URLs (defaults to MODEL_ENDPOINT_* env vars) + tts_endpoints: List of TTS endpoint URLs (defaults to TTS_ENDPOINT_* env vars) + api_key: API key for authentication (default: "dummy-key") + timeout: Request timeout in seconds + """ + self.timeout = timeout + self.api_key = api_key + self.models: List[Dict] = [] + self.tts_endpoints: List[str] = [] + + # Discover endpoints from environment or use provided + if model_endpoints is None: + model_endpoints = self._discover_env_endpoints("MODEL_ENDPOINT") + if tts_endpoints is None: + tts_endpoints = self._discover_env_endpoints("TTS_ENDPOINT") + + # Discover models from each endpoint + for endpoint in model_endpoints: + self._discover_models_from_endpoint(endpoint) + + self.tts_endpoints = tts_endpoints + + def _discover_env_endpoints(self, prefix: str) -> List[str]: + """Discover endpoints from environment variables like PREFIX_1, PREFIX_2, ...""" + endpoints = [] + for i in range(1, 10000): + endpoint = os.getenv(f"{prefix}_{i}") + if not endpoint: + break + endpoints.append(endpoint) + return endpoints + + def _discover_models_from_endpoint(self, endpoint: str) -> None: + """Discover available models from an endpoint""" + try: + response = requests.get(f"{endpoint}/models", timeout=10) + if response.status_code == 200: + data = response.json() + for model in data.get("data", []): + model_id = model["id"] + + # Filter out modelperm-* and chatcmpl-* entries + if model_id.startswith("modelperm-") or model_id.startswith("chatcmpl-"): + continue + + self.models.append({ + "id": model_id, + "endpoint": endpoint, + "max_tokens": model.get("max_model_len", 8192) + }) + except Exception: + # Silently skip failed endpoints + pass + + def list_models(self) -> List[Dict]: + """Return list of discovered models with their metadata""" + return self.models.copy() + + def chat( + self, + messages: List[Dict[str, str]], + model: Optional[str] = None, + max_tokens: int = 100, + temperature: float = 0.7, + **kwargs + ) -> Dict: + """ + Non-streaming chat completion + + Args: + messages: List of message dicts with 'role' and 'content' + model: Model ID (defaults to first available model) + max_tokens: Maximum tokens in response + temperature: Sampling temperature + **kwargs: Additional parameters to pass to the API + + Returns: + Response dict with 'choices' containing the completion + """ + model_info = self._get_model_info(model) + + client = OpenAI( + base_url=f"{model_info['endpoint']}/v1", + api_key=self.api_key, + timeout=self.timeout + ) + + response = client.chat.completions.create( + model=model_info["id"], + messages=messages, + max_tokens=max_tokens, + temperature=temperature, + **kwargs + ) + + # Convert OpenAI response to dict format + return { + "id": response.id, + "model": response.model, + "choices": [ + { + "index": choice.index, + "message": { + "role": choice.message.role, + "content": choice.message.content + }, + "finish_reason": choice.finish_reason + } + for choice in response.choices + ], + "usage": { + "prompt_tokens": response.usage.prompt_tokens, + "completion_tokens": response.usage.completion_tokens, + "total_tokens": response.usage.total_tokens + } + } + + def chat_stream( + self, + messages: List[Dict[str, str]], + model: Optional[str] = None, + max_tokens: int = 500, + temperature: float = 0.7, + **kwargs + ) -> Iterator[str]: + """ + Streaming chat completion using OpenAI SDK + + Args: + messages: List of message dicts with 'role' and 'content' + model: Model ID (defaults to first available model) + max_tokens: Maximum tokens in response + temperature: Sampling temperature + **kwargs: Additional parameters to pass to the API + + Yields: + Content strings as they arrive + """ + model_info = self._get_model_info(model) + + client = OpenAI( + base_url=f"{model_info['endpoint']}/v1", + api_key=self.api_key, + timeout=self.timeout + ) + + stream = client.chat.completions.create( + model=model_info["id"], + messages=messages, + max_tokens=max_tokens, + temperature=temperature, + stream=True, + **kwargs + ) + + for chunk in stream: + if chunk.choices[0].delta.content: + yield chunk.choices[0].delta.content + + def tts( + self, + text: str, + voice: str = "alloy", + model: str = "tts-1", + output_file: str = "speech.mp3" + ) -> str: + """ + Generate speech from text + + Args: + text: Input text to convert to speech + voice: Voice name (alloy, echo, fable, onyx, nova, shimmer) + model: TTS model (tts-1 or tts-1-hd) + output_file: Path to save the audio file + + Returns: + Path to the saved audio file + """ + if not self.tts_endpoints: + raise ValueError("No TTS endpoints available") + + endpoint = self.tts_endpoints[0] + + client = OpenAI( + base_url=f"{endpoint}/v1", + api_key=self.api_key, + timeout=self.timeout + ) + + with client.audio.speech.with_streaming_response.create( + model=model, + voice=voice, + input=text + ) as response: + response.stream_to_file(output_file) + + return output_file + + def _get_model_info(self, model: Optional[str] = None) -> Dict: + """Get model info by ID or return first available model""" + if not self.models: + raise ValueError("No models available. Check endpoint configuration.") + + if model is None: + return self.models[0] + + for m in self.models: + if m["id"] == model: + return m + + raise ValueError(f"Model '{model}' not found in discovered models") + + +# Demo usage when run as script +if __name__ == "__main__": + print("=== uncloseai. Python Client (OpenAI SDK) ===\n") + + # Initialize client (auto-discovers from environment) + client = uncloseai() + + if not client.models: + print("ERROR: No models discovered. Set environment variables:") + print(" MODEL_ENDPOINT_1, MODEL_ENDPOINT_2, etc.") + exit(1) + + print(f"Discovered {len(client.models)} model(s)") + for model in client.models: + print(f" - {model['id']} (max_tokens: {model['max_tokens']})") + print() + + # Non-streaming chat example + print("=== Non-Streaming Chat ===") + response = client.chat( + messages=[ + {"role": "system", "content": "You are a helpful AI assistant."}, + {"role": "user", "content": "Explain quantum computing in one sentence."} + ], + max_tokens=100 + ) + print(f"Model: {response['model']}") + print(f"Response: {response['choices'][0]['message']['content']}\n") + + # Streaming chat example + print("=== Streaming Chat ===") + if len(client.models) > 1: + model_id = client.models[1]["id"] + else: + model_id = None + + print(f"Model: {model_id or client.models[0]['id']}") + print("Response: ", end="", flush=True) + + for content in client.chat_stream( + messages=[ + {"role": "system", "content": "You are a coding assistant."}, + {"role": "user", "content": "Write a Python function to check if a number is prime"} + ], + model=model_id, + max_tokens=200 + ): + print(content, end="", flush=True) + + print("\n") + + # TTS example + if client.tts_endpoints: + print("=== TTS Speech Generation ===") + output_path = client.tts( + text="Hello from uncloseai. Python client with OpenAI SDK! This demonstrates text to speech with streaming support.", + voice="alloy", + output_file="speech.mp3" + ) + + if os.path.exists(output_path): + file_size = os.path.getsize(output_path) + print(f"[OK] Speech file created: {output_path} ({file_size} bytes)\n") + + print("=== Examples Complete ===")