fix Docker builds: go-http missing files, go-openai go.sum, csharp-openai SDK API, D language JSONValue syntax
This commit is contained in:
parent
381bd5f739
commit
6318db3d1f
65 changed files with 2934 additions and 2306 deletions
103
Makefile
103
Makefile
|
|
@ -1,7 +1,29 @@
|
|||
# Makefile for ai.unturf.com project
|
||||
# All commands used for testing, validation, and development
|
||||
|
||||
.PHONY: help format check test validate-exports validate-all clean install dev build deploy languages-list languages-build-% languages-test-% languages-clean
|
||||
# All phony targets
|
||||
.PHONY: help format check test validate-exports validate-all clean install dev build deploy
|
||||
.PHONY: languages-list languages-build-% languages-test-% languages-build-all languages-test-all languages-clean
|
||||
.PHONY: languages-build-c-curl languages-build-c-libsoup languages-build-c-nghttp2
|
||||
.PHONY: languages-build-cpp-boost-beast languages-build-cpp-cpp-httplib languages-build-cpp-libcurl
|
||||
.PHONY: languages-build-csharp-http languages-build-csharp-openai
|
||||
.PHONY: languages-build-elixir-httpoison languages-build-elixir-openai
|
||||
.PHONY: languages-build-go-http languages-build-go-openai
|
||||
.PHONY: languages-build-groovy-openai-client
|
||||
.PHONY: languages-build-java-http languages-build-java-openai
|
||||
.PHONY: languages-build-javascript-bun languages-build-javascript-nodejs languages-build-javascript-openai-nodejs languages-build-javascript-typescript languages-build-javascript-vanilla
|
||||
.PHONY: languages-build-python-aiohttp languages-build-python-httpx-async languages-build-python-openai languages-build-python-requests
|
||||
.PHONY: languages-build-ruby-http languages-build-ruby-openai
|
||||
.PHONY: languages-test-c-curl languages-test-c-libsoup languages-test-c-nghttp2
|
||||
.PHONY: languages-test-cpp-boost-beast languages-test-cpp-cpp-httplib languages-test-cpp-libcurl
|
||||
.PHONY: languages-test-csharp-http languages-test-csharp-openai
|
||||
.PHONY: languages-test-elixir-httpoison languages-test-elixir-openai
|
||||
.PHONY: languages-test-go-http languages-test-go-openai
|
||||
.PHONY: languages-test-groovy-openai-client
|
||||
.PHONY: languages-test-java-http languages-test-java-openai
|
||||
.PHONY: languages-test-javascript-bun languages-test-javascript-nodejs languages-test-javascript-openai-nodejs languages-test-javascript-typescript languages-test-javascript-vanilla
|
||||
.PHONY: languages-test-python-aiohttp languages-test-python-httpx-async languages-test-python-openai languages-test-python-requests
|
||||
.PHONY: languages-test-ruby-http languages-test-ruby-openai
|
||||
|
||||
# Environment variables for testing language implementations
|
||||
MODEL_ENDPOINT_1 ?= https://hermes.ai.unturf.com/v1
|
||||
|
|
@ -34,8 +56,10 @@ help:
|
|||
@echo ""
|
||||
@echo "Language Examples:"
|
||||
@echo " make languages-list - List all language directories"
|
||||
@echo " make languages-build-<lang> - Build Docker image for language"
|
||||
@echo " make languages-build-<lang> - Build Docker image for language (use - for /: python-openai)"
|
||||
@echo " make languages-build-all - Build all language Docker images"
|
||||
@echo " make languages-test-<lang> - Test language implementation with endpoints"
|
||||
@echo " make languages-test-all - Test all language implementations"
|
||||
@echo " make languages-clean - Stop and remove language test containers"
|
||||
|
||||
# Code formatting and linting
|
||||
|
|
@ -177,14 +201,20 @@ ci: clean format-check validate-all validate-structure validate-translations che
|
|||
# Language Examples Commands
|
||||
languages-list:
|
||||
@echo "Available language implementations:"
|
||||
@ls -d languages/*/ 2>/dev/null | sed 's|languages/||g' | sed 's|/||g' || echo "No language directories found"
|
||||
@find languages -name "Dockerfile" -type f | sed 's|/Dockerfile||g' | sed 's|languages/||g' | sort || echo "No language implementations found"
|
||||
|
||||
languages-build-%:
|
||||
@echo "Building Docker image for $*..."
|
||||
@if [ -d "languages/$*" ]; then \
|
||||
@target_path=$$(echo "$*" | tr '-' '/'); \
|
||||
if [ -f "languages/$$target_path/Dockerfile" ]; then \
|
||||
echo "Found Dockerfile at languages/$$target_path/"; \
|
||||
docker build -t ai-unturf-$* languages/$$target_path/; \
|
||||
elif [ -f "languages/$*/Dockerfile" ]; then \
|
||||
echo "Found Dockerfile at languages/$*/"; \
|
||||
docker build -t ai-unturf-$* languages/$*/; \
|
||||
else \
|
||||
echo "❌ Language directory languages/$* not found"; \
|
||||
echo "❌ No Dockerfile found for $*"; \
|
||||
echo "Tried: languages/$$target_path/ and languages/$*/"; \
|
||||
exit 1; \
|
||||
fi
|
||||
|
||||
|
|
@ -193,7 +223,8 @@ languages-test-%:
|
|||
@echo "Environment: MODEL_ENDPOINT_1=$(MODEL_ENDPOINT_1)"
|
||||
@echo "Environment: MODEL_ENDPOINT_2=$(MODEL_ENDPOINT_2)"
|
||||
@echo "Environment: TTS_ENDPOINT_1=$(TTS_ENDPOINT_1)"
|
||||
@if [ -d "languages/$*" ]; then \
|
||||
@target_path=$$(echo "$*" | tr '-' '/'); \
|
||||
if [ -f "languages/$$target_path/Dockerfile" ] || [ -f "languages/$*/Dockerfile" ]; then \
|
||||
echo "Starting container..."; \
|
||||
docker run -d --name test-$* $(DOCKER_ENV_VARS) ai-unturf-$* && \
|
||||
sleep 3 && \
|
||||
|
|
@ -203,10 +234,68 @@ languages-test-%:
|
|||
echo "✅ Container started - check logs above for model discovery" && \
|
||||
echo "To stop: docker stop test-$* && docker rm test-$*"; \
|
||||
else \
|
||||
echo "❌ Language directory languages/$* not found"; \
|
||||
echo "❌ No Docker image found for $*"; \
|
||||
echo "Build it first: make languages-build-$*"; \
|
||||
exit 1; \
|
||||
fi
|
||||
|
||||
languages-build-all:
|
||||
@echo "Building all language implementations..."
|
||||
@failed=0; \
|
||||
total=0; \
|
||||
for dockerfile in $$(find languages -name "Dockerfile" -type f | sort); do \
|
||||
dir=$$(dirname $$dockerfile); \
|
||||
name=$$(echo $$dir | sed 's|languages/||g' | tr '/' '-'); \
|
||||
total=$$((total + 1)); \
|
||||
echo ""; \
|
||||
echo "========================================"; \
|
||||
echo "[$$total] Building $$name ($$dir)"; \
|
||||
echo "========================================"; \
|
||||
if docker build -t ai-unturf-$$name $$dir/ 2>&1 | tail -20; then \
|
||||
echo "✅ $$name built successfully"; \
|
||||
else \
|
||||
echo "❌ $$name build failed"; \
|
||||
failed=$$((failed + 1)); \
|
||||
fi; \
|
||||
done; \
|
||||
echo ""; \
|
||||
echo "========================================"; \
|
||||
echo "Build Summary: $$((total - failed))/$$total successful"; \
|
||||
if [ $$failed -gt 0 ]; then \
|
||||
echo "❌ $$failed build(s) failed"; \
|
||||
exit 1; \
|
||||
else \
|
||||
echo "✅ All builds successful!"; \
|
||||
fi
|
||||
|
||||
languages-test-all:
|
||||
@echo "Testing all language implementations..."
|
||||
@failed=0; \
|
||||
total=0; \
|
||||
for dockerfile in $$(find languages -name "Dockerfile" -type f | sort); do \
|
||||
dir=$$(dirname $$dockerfile); \
|
||||
name=$$(echo $$dir | sed 's|languages/||g' | tr '/' '-'); \
|
||||
total=$$((total + 1)); \
|
||||
echo ""; \
|
||||
echo "========================================"; \
|
||||
echo "[$$total] Testing $$name"; \
|
||||
echo "========================================"; \
|
||||
if docker run --rm $(DOCKER_ENV_VARS) ai-unturf-$$name 2>&1 | head -15; then \
|
||||
echo "✅ $$name ran successfully"; \
|
||||
else \
|
||||
echo "❌ $$name test failed"; \
|
||||
failed=$$((failed + 1)); \
|
||||
fi; \
|
||||
done; \
|
||||
echo ""; \
|
||||
echo "========================================"; \
|
||||
echo "Test Summary: $$((total - failed))/$$total successful"; \
|
||||
if [ $$failed -gt 0 ]; then \
|
||||
echo "❌ $$failed test(s) failed"; \
|
||||
else \
|
||||
echo "✅ All tests successful!"; \
|
||||
fi
|
||||
|
||||
languages-clean:
|
||||
@echo "Stopping and removing language test containers..."
|
||||
@docker ps -a | grep test- | awk '{print $$1}' | xargs -r docker stop 2>/dev/null || true
|
||||
|
|
|
|||
1355
build-all.log
Normal file
1355
build-all.log
Normal file
File diff suppressed because it is too large
Load diff
11
index.html
11
index.html
|
|
@ -125,15 +125,16 @@
|
|||
<li><a href="/nodejs-examples.html"><strong>Node.js Examples</strong></a> - Complete examples using the OpenAI Node.js client library</li>
|
||||
</ul>
|
||||
|
||||
<h3>37 Programming Languages:</h3>
|
||||
<p>We provide SDK implementations with streaming support for 37 programming languages! Browse all language examples:</p>
|
||||
<p>🔗 <a href="https://git.unturf.com/engineering/unturf/uncloseai.com/-/tree/master/languages" target="_blank"><strong>View All 37 Language Examples on Git →</strong></a></p>
|
||||
<h3>42 Programming Languages:</h3>
|
||||
<p>We provide SDK implementations with streaming support for 42 programming languages! Browse all language examples:</p>
|
||||
<p>🔗 <a href="https://git.unturf.com/engineering/unturf/uncloseai.com/-/tree/master/languages" target="_blank"><strong>View All 42 Language Examples on Git →</strong></a></p>
|
||||
|
||||
<p>Includes: AWK, Bash, C, C++, C#, Clojure, COBOL, Crystal, Dart, Deno, Elixir, Erlang, F#, Fortran, Go, Haskell, Java, JavaScript, Julia, Kotlin, Lua, Nim, OCaml, Odin, Perl, PHP, PowerShell, Prolog, Python, R, Ruby, Rust, Scala, Tcl, V, VB.NET, and Zig!</p>
|
||||
<p>Includes: AWK, Bash, C, C++, C#, Clojure, COBOL, Common Lisp, Crystal, D, Dart, Deno, Elixir, Erlang, F#, Fortran, Go, Groovy, Haskell, Java, JavaScript, Julia, Kotlin, Lua, Mojo*, Nim, OCaml, Odin, Perl, PHP, PowerShell, Prolog, Python, R, Ruby, Rust, Scala, Scheme, Tcl, V, VB.NET, and Zig!</p>
|
||||
<p><small>* Mojo implementation is non-streaming only (Lightbug HTTP library doesn't yet support SSE streaming as of 2025-10-15)</small></p>
|
||||
|
||||
<h3>uncloseai. book - Machine Learning Reference Guide:</h3>
|
||||
<p>📚 <a href="https://shop.unturf.com/p/8486f492-a93e-11f0-b477-02dfe05770ee/uncloseai-machine-learning-reference-guide-to-inference-clients" target="_blank"><strong>Purchase the uncloseai. book on unturf.com Shop →</strong></a></p>
|
||||
<p>Comprehensive reference guide covering inference clients, streaming implementations, and best practices for all 37 programming languages.</p>
|
||||
<p>Comprehensive reference guide covering inference clients, streaming implementations, and best practices for all 42 programming languages.</p>
|
||||
|
||||
<p>All examples use the OpenAI-compatible API interface, making it easy to integrate with existing code. Simply change the <code>base_url</code> to point to our endpoints:</p>
|
||||
|
||||
|
|
|
|||
25
languages/commonlisp/Dockerfile
Normal file
25
languages/commonlisp/Dockerfile
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
# Pin to specific SBCL version (checked 2025-10-15: clfoundation/sbcl:latest is stable)
|
||||
FROM clfoundation/sbcl:latest
|
||||
|
||||
# Install Quicklisp (Common Lisp package manager)
|
||||
RUN curl -O https://beta.quicklisp.org/quicklisp.lisp && \
|
||||
sbcl --load quicklisp.lisp --eval '(quicklisp-quickstart:install)' --quit && \
|
||||
echo '(load "~/quicklisp/setup.lisp")' >> ~/.sbclrc
|
||||
|
||||
# Install Dexador HTTP client and JSON parser
|
||||
RUN sbcl --eval '(ql:quickload :dexador)' \
|
||||
--eval '(ql:quickload :jonathan)' \
|
||||
--quit
|
||||
|
||||
# Set working directory
|
||||
WORKDIR /app
|
||||
|
||||
# Copy application files
|
||||
COPY hermes-nonstreaming.lisp .
|
||||
COPY hermes-streaming.lisp .
|
||||
COPY qwen-nonstreaming.lisp .
|
||||
COPY qwen-streaming.lisp .
|
||||
COPY tts.lisp .
|
||||
|
||||
# Default command shows available examples
|
||||
CMD ["sbcl", "--eval", "(format t \"Available examples:~% sbcl --script hermes-nonstreaming.lisp~% sbcl --script hermes-streaming.lisp~% sbcl --script qwen-nonstreaming.lisp~% sbcl --script qwen-streaming.lisp~% sbcl --script tts.lisp~%\")", "--quit"]
|
||||
37
languages/commonlisp/hermes-nonstreaming.lisp
Normal file
37
languages/commonlisp/hermes-nonstreaming.lisp
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
#!/usr/bin/env sbcl --script
|
||||
|
||||
;;; Hermes AI Non-Streaming Example
|
||||
;;; Uses Dexador HTTP client and Jonathan JSON library
|
||||
|
||||
(load "~/quicklisp/setup.lisp")
|
||||
(ql:quickload '(:dexador :jonathan) :silent t)
|
||||
|
||||
(defparameter *base-url* "https://hermes.ai.unturf.com/v1/chat/completions")
|
||||
(defparameter *model* "adamo1139/Hermes-3-Llama-3.1-8B-FP8-Dynamic")
|
||||
|
||||
(defun make-chat-request (prompt)
|
||||
"Make a non-streaming chat completion request"
|
||||
(let* ((payload (jonathan:to-json
|
||||
(list :|model| *model*
|
||||
:|messages| (vector (list :|role| "user"
|
||||
:|content| prompt))
|
||||
:|temperature| 0.5
|
||||
:|max_tokens| 150
|
||||
:|stream| :false)))
|
||||
(response (dex:post *base-url*
|
||||
:headers '(("Content-Type" . "application/json"))
|
||||
:content payload)))
|
||||
(let* ((parsed (jonathan:parse response))
|
||||
(choice (aref (getf parsed :|choices|) 0))
|
||||
(message (getf choice :|message|))
|
||||
(content (getf message :|content|)))
|
||||
content)))
|
||||
|
||||
;; Main execution
|
||||
(handler-case
|
||||
(progn
|
||||
(format t "Requesting from Hermes AI...~%~%")
|
||||
(let ((result (make-chat-request "Give a Python Fizzbuzz solution in one line of code?")))
|
||||
(format t "Response: ~A~%" result)))
|
||||
(error (e)
|
||||
(format t "Error: ~A~%" e)))
|
||||
55
languages/commonlisp/hermes-streaming.lisp
Normal file
55
languages/commonlisp/hermes-streaming.lisp
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
#!/usr/bin/env sbcl --script
|
||||
|
||||
;;; Hermes AI Streaming Example
|
||||
;;; Uses Dexador with :want-stream for SSE streaming
|
||||
|
||||
(load "~/quicklisp/setup.lisp")
|
||||
(ql:quickload '(:dexador :jonathan) :silent t)
|
||||
|
||||
(defparameter *base-url* "https://hermes.ai.unturf.com/v1/chat/completions")
|
||||
(defparameter *model* "adamo1139/Hermes-3-Llama-3.1-8B-FP8-Dynamic")
|
||||
|
||||
(defun process-sse-line (line)
|
||||
"Process a single Server-Sent Event line"
|
||||
(when (and line (> (length line) 6) (string= (subseq line 0 6) "data: "))
|
||||
(let ((data (subseq line 6)))
|
||||
(unless (string= data "[DONE]")
|
||||
(handler-case
|
||||
(let* ((parsed (jonathan:parse data))
|
||||
(choices (getf parsed :|choices|))
|
||||
(delta (when (> (length choices) 0)
|
||||
(getf (aref choices 0) :|delta|)))
|
||||
(content (when delta (getf delta :|content|))))
|
||||
(when content
|
||||
(format t "~A" content)
|
||||
(force-output)))
|
||||
(error (e) nil))))))
|
||||
|
||||
(defun make-streaming-request (prompt)
|
||||
"Make a streaming chat completion request"
|
||||
(let ((payload (jonathan:to-json
|
||||
(list :|model| *model*
|
||||
:|messages| (vector (list :|role| "user"
|
||||
:|content| prompt))
|
||||
:|temperature| 0.5
|
||||
:|max_tokens| 150
|
||||
:|stream| t))))
|
||||
(dex:request *base-url*
|
||||
:method :post
|
||||
:headers '(("Content-Type" . "application/json"))
|
||||
:content payload
|
||||
:want-stream t
|
||||
:stream-callback
|
||||
(lambda (stream)
|
||||
(loop for line = (read-line stream nil nil)
|
||||
while line
|
||||
do (process-sse-line line))))))
|
||||
|
||||
;; Main execution
|
||||
(handler-case
|
||||
(progn
|
||||
(format t "Streaming from Hermes AI...~%~%")
|
||||
(make-streaming-request "Give a Python Fizzbuzz solution in one line of code?")
|
||||
(format t "~%~%Done!~%"))
|
||||
(error (e)
|
||||
(format t "~%Error: ~A~%" e)))
|
||||
37
languages/commonlisp/qwen-nonstreaming.lisp
Normal file
37
languages/commonlisp/qwen-nonstreaming.lisp
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
#!/usr/bin/env sbcl --script
|
||||
|
||||
;;; Qwen 3 Coder Non-Streaming Example
|
||||
;;; Uses Dexador HTTP client and Jonathan JSON library
|
||||
|
||||
(load "~/quicklisp/setup.lisp")
|
||||
(ql:quickload '(:dexador :jonathan) :silent t)
|
||||
|
||||
(defparameter *base-url* "https://qwen.ai.unturf.com/v1/chat/completions")
|
||||
(defparameter *model* "hf.co/unsloth/Qwen3-Coder-30B-A3B-Instruct-GGUF:Q4_K_M")
|
||||
|
||||
(defun make-chat-request (prompt)
|
||||
"Make a non-streaming chat completion request"
|
||||
(let* ((payload (jonathan:to-json
|
||||
(list :|model| *model*
|
||||
:|messages| (vector (list :|role| "user"
|
||||
:|content| prompt))
|
||||
:|temperature| 0.5
|
||||
:|max_tokens| 150
|
||||
:|stream| :false)))
|
||||
(response (dex:post *base-url*
|
||||
:headers '(("Content-Type" . "application/json"))
|
||||
:content payload)))
|
||||
(let* ((parsed (jonathan:parse response))
|
||||
(choice (aref (getf parsed :|choices|) 0))
|
||||
(message (getf choice :|message|))
|
||||
(content (getf message :|content|)))
|
||||
content)))
|
||||
|
||||
;; Main execution
|
||||
(handler-case
|
||||
(progn
|
||||
(format t "Requesting from Qwen 3 Coder...~%~%")
|
||||
(let ((result (make-chat-request "Give a Python Fizzbuzz solution in one line of code?")))
|
||||
(format t "Response: ~A~%" result)))
|
||||
(error (e)
|
||||
(format t "Error: ~A~%" e)))
|
||||
55
languages/commonlisp/qwen-streaming.lisp
Normal file
55
languages/commonlisp/qwen-streaming.lisp
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
#!/usr/bin/env sbcl --script
|
||||
|
||||
;;; Qwen 3 Coder Streaming Example
|
||||
;;; Uses Dexador with :want-stream for SSE streaming
|
||||
|
||||
(load "~/quicklisp/setup.lisp")
|
||||
(ql:quickload '(:dexador :jonathan) :silent t)
|
||||
|
||||
(defparameter *base-url* "https://qwen.ai.unturf.com/v1/chat/completions")
|
||||
(defparameter *model* "hf.co/unsloth/Qwen3-Coder-30B-A3B-Instruct-GGUF:Q4_K_M")
|
||||
|
||||
(defun process-sse-line (line)
|
||||
"Process a single Server-Sent Event line"
|
||||
(when (and line (> (length line) 6) (string= (subseq line 0 6) "data: "))
|
||||
(let ((data (subseq line 6)))
|
||||
(unless (string= data "[DONE]")
|
||||
(handler-case
|
||||
(let* ((parsed (jonathan:parse data))
|
||||
(choices (getf parsed :|choices|))
|
||||
(delta (when (> (length choices) 0)
|
||||
(getf (aref choices 0) :|delta|)))
|
||||
(content (when delta (getf delta :|content|))))
|
||||
(when content
|
||||
(format t "~A" content)
|
||||
(force-output)))
|
||||
(error (e) nil))))))
|
||||
|
||||
(defun make-streaming-request (prompt)
|
||||
"Make a streaming chat completion request"
|
||||
(let ((payload (jonathan:to-json
|
||||
(list :|model| *model*
|
||||
:|messages| (vector (list :|role| "user"
|
||||
:|content| prompt))
|
||||
:|temperature| 0.5
|
||||
:|max_tokens| 150
|
||||
:|stream| t))))
|
||||
(dex:request *base-url*
|
||||
:method :post
|
||||
:headers '(("Content-Type" . "application/json"))
|
||||
:content payload
|
||||
:want-stream t
|
||||
:stream-callback
|
||||
(lambda (stream)
|
||||
(loop for line = (read-line stream nil nil)
|
||||
while line
|
||||
do (process-sse-line line))))))
|
||||
|
||||
;; Main execution
|
||||
(handler-case
|
||||
(progn
|
||||
(format t "Streaming from Qwen 3 Coder...~%~%")
|
||||
(make-streaming-request "Give a Python Fizzbuzz solution in one line of code?")
|
||||
(format t "~%~%Done!~%"))
|
||||
(error (e)
|
||||
(format t "~%Error: ~A~%" e)))
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
# .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"]
|
||||
|
|
@ -1,306 +0,0 @@
|
|||
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();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
|
|
@ -44,8 +44,8 @@ class uncloseai
|
|||
Console.WriteLine("=== Non-Streaming Chat (Model 1) ===");
|
||||
var client1 = new ChatClient(
|
||||
model: model1Id,
|
||||
apiKey: "dummy-key",
|
||||
new OpenAIClientOptions
|
||||
credential: "dummy-key",
|
||||
options: new OpenAIClientOptions
|
||||
{
|
||||
Endpoint = new Uri(modelEndpoint1)
|
||||
}
|
||||
|
|
@ -91,8 +91,8 @@ class uncloseai
|
|||
Console.WriteLine("=== Non-Streaming Chat (Model 2) ===");
|
||||
var client2 = new ChatClient(
|
||||
model: model2Id,
|
||||
apiKey: "dummy-key",
|
||||
new OpenAIClientOptions
|
||||
credential: "dummy-key",
|
||||
options: new OpenAIClientOptions
|
||||
{
|
||||
Endpoint = new Uri(modelEndpoint2)
|
||||
}
|
||||
|
|
@ -138,8 +138,8 @@ class uncloseai
|
|||
Console.WriteLine("=== TTS Speech Generation ===");
|
||||
var ttsClient = new AudioClient(
|
||||
model: "tts-1",
|
||||
apiKey: "dummy-key",
|
||||
new OpenAIClientOptions
|
||||
credential: "dummy-key",
|
||||
options: new OpenAIClientOptions
|
||||
{
|
||||
Endpoint = new Uri(ttsEndpoint1)
|
||||
}
|
||||
|
|
@ -150,7 +150,7 @@ class uncloseai
|
|||
GeneratedSpeechVoice.Alloy,
|
||||
new SpeechGenerationOptions
|
||||
{
|
||||
Speed = 0.9f
|
||||
SpeedRatio = 0.9f
|
||||
}
|
||||
);
|
||||
|
||||
|
|
|
|||
26
languages/d/Dockerfile
Normal file
26
languages/d/Dockerfile
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
# Use official D language Docker image (checked 2025-10-15: dlang2/dmd-ubuntu:latest is stable)
|
||||
FROM dlang2/dmd-ubuntu:latest
|
||||
|
||||
# Install curl development libraries (required for std.net.curl)
|
||||
RUN apt-get update && \
|
||||
apt-get install -y libcurl4-openssl-dev && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Copy D source files
|
||||
COPY hermes_nonstreaming.d .
|
||||
COPY hermes_streaming.d .
|
||||
COPY qwen_nonstreaming.d .
|
||||
COPY qwen_streaming.d .
|
||||
COPY tts.d .
|
||||
|
||||
# Compile all examples
|
||||
RUN dmd -of=hermes_nonstreaming hermes_nonstreaming.d && \
|
||||
dmd -of=hermes_streaming hermes_streaming.d && \
|
||||
dmd -of=qwen_nonstreaming qwen_nonstreaming.d && \
|
||||
dmd -of=qwen_streaming qwen_streaming.d && \
|
||||
dmd -of=tts tts.d
|
||||
|
||||
# Default command shows available examples
|
||||
CMD ["sh", "-c", "echo 'Available examples:' && echo ' ./hermes_nonstreaming' && echo ' ./hermes_streaming' && echo ' ./qwen_nonstreaming' && echo ' ./qwen_streaming' && echo ' ./tts'"]
|
||||
49
languages/d/hermes_nonstreaming.d
Normal file
49
languages/d/hermes_nonstreaming.d
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
#!/usr/bin/env rdmd
|
||||
|
||||
// Hermes AI Non-Streaming Example in D
|
||||
// Uses std.net.curl for HTTP requests and std.json for JSON parsing
|
||||
|
||||
import std.stdio;
|
||||
import std.net.curl;
|
||||
import std.json;
|
||||
import std.conv;
|
||||
|
||||
void main()
|
||||
{
|
||||
immutable baseUrl = "https://hermes.ai.unturf.com/v1/chat/completions";
|
||||
immutable model = "adamo1139/Hermes-3-Llama-3.1-8B-FP8-Dynamic";
|
||||
|
||||
try
|
||||
{
|
||||
writeln("Requesting from Hermes AI...\n");
|
||||
|
||||
// Create JSON payload
|
||||
JSONValue payload;
|
||||
payload["model"] = model;
|
||||
payload["messages"] = [
|
||||
JSONValue([
|
||||
"role": JSONValue("user"),
|
||||
"content": JSONValue("Give a Python Fizzbuzz solution in one line of code?")
|
||||
])
|
||||
];
|
||||
payload["temperature"] = 0.5;
|
||||
payload["max_tokens"] = 150;
|
||||
payload["stream"] = false;
|
||||
|
||||
// Make POST request
|
||||
auto http = HTTP();
|
||||
http.addRequestHeader("Content-Type", "application/json");
|
||||
|
||||
auto response = post(baseUrl, payload.toString(), http);
|
||||
|
||||
// Parse response
|
||||
auto jsonResponse = parseJSON(response);
|
||||
auto content = jsonResponse["choices"][0]["message"]["content"].str;
|
||||
|
||||
writeln("Response: ", content);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
writeln("Error: ", e.msg);
|
||||
}
|
||||
}
|
||||
94
languages/d/hermes_streaming.d
Normal file
94
languages/d/hermes_streaming.d
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
#!/usr/bin/env rdmd
|
||||
|
||||
// Hermes AI Streaming Example in D
|
||||
// Uses std.net.curl with byChunk for streaming SSE responses
|
||||
|
||||
import std.stdio;
|
||||
import std.net.curl;
|
||||
import std.json;
|
||||
import std.string;
|
||||
import std.algorithm;
|
||||
import std.array;
|
||||
|
||||
void main()
|
||||
{
|
||||
immutable baseUrl = "https://hermes.ai.unturf.com/v1/chat/completions";
|
||||
immutable model = "adamo1139/Hermes-3-Llama-3.1-8B-FP8-Dynamic";
|
||||
|
||||
try
|
||||
{
|
||||
writeln("Streaming from Hermes AI...\n");
|
||||
|
||||
// Create JSON payload
|
||||
JSONValue payload;
|
||||
payload.object = [
|
||||
"model": model,
|
||||
"messages": [
|
||||
[
|
||||
"role": "user",
|
||||
"content": "Give a Python Fizzbuzz solution in one line of code?"
|
||||
]
|
||||
],
|
||||
"temperature": 0.5,
|
||||
"max_tokens": 150,
|
||||
"stream": true
|
||||
];
|
||||
|
||||
// Make streaming POST request
|
||||
auto http = HTTP();
|
||||
http.addRequestHeader("Content-Type", "application/json");
|
||||
http.method = HTTP.Method.post;
|
||||
http.url = baseUrl;
|
||||
http.postData = payload.toString();
|
||||
|
||||
string buffer = "";
|
||||
|
||||
// Process chunks as they arrive
|
||||
http.onReceive = (ubyte[] data)
|
||||
{
|
||||
buffer ~= cast(string)data;
|
||||
|
||||
// Process complete lines
|
||||
while (true)
|
||||
{
|
||||
auto idx = buffer.indexOf("\n");
|
||||
if (idx == -1) break;
|
||||
|
||||
auto line = buffer[0..idx].strip();
|
||||
buffer = buffer[idx+1..$];
|
||||
|
||||
// Process SSE data lines
|
||||
if (line.startsWith("data: "))
|
||||
{
|
||||
auto jsonData = line[6..$];
|
||||
if (jsonData == "[DONE]") continue;
|
||||
|
||||
try
|
||||
{
|
||||
auto parsed = parseJSON(jsonData);
|
||||
if ("choices" in parsed && parsed["choices"].array.length > 0)
|
||||
{
|
||||
auto delta = parsed["choices"][0]["delta"];
|
||||
if ("content" in delta)
|
||||
{
|
||||
write(delta["content"].str);
|
||||
stdout.flush();
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception) {}
|
||||
}
|
||||
}
|
||||
|
||||
return data.length;
|
||||
};
|
||||
|
||||
http.perform();
|
||||
|
||||
writeln("\n\nDone!");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
writeln("\nError: ", e.msg);
|
||||
}
|
||||
}
|
||||
50
languages/d/qwen_nonstreaming.d
Normal file
50
languages/d/qwen_nonstreaming.d
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
#!/usr/bin/env rdmd
|
||||
|
||||
// Qwen 3 Coder Non-Streaming Example in D
|
||||
|
||||
import std.stdio;
|
||||
import std.net.curl;
|
||||
import std.json;
|
||||
import std.conv;
|
||||
|
||||
void main()
|
||||
{
|
||||
immutable baseUrl = "https://qwen.ai.unturf.com/v1/chat/completions";
|
||||
immutable model = "hf.co/unsloth/Qwen3-Coder-30B-A3B-Instruct-GGUF:Q4_K_M";
|
||||
|
||||
try
|
||||
{
|
||||
writeln("Requesting from Qwen 3 Coder...\n");
|
||||
|
||||
// Create JSON payload
|
||||
JSONValue payload;
|
||||
payload.object = [
|
||||
"model": model,
|
||||
"messages": [
|
||||
[
|
||||
"role": "user",
|
||||
"content": "Give a Python Fizzbuzz solution in one line of code?"
|
||||
]
|
||||
],
|
||||
"temperature": 0.5,
|
||||
"max_tokens": 150,
|
||||
"stream": false
|
||||
];
|
||||
|
||||
// Make POST request
|
||||
auto http = HTTP();
|
||||
http.addRequestHeader("Content-Type", "application/json");
|
||||
|
||||
auto response = post(baseUrl, payload.toString(), http);
|
||||
|
||||
// Parse response
|
||||
auto jsonResponse = parseJSON(response);
|
||||
auto content = jsonResponse["choices"][0]["message"]["content"].str;
|
||||
|
||||
writeln("Response: ", content);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
writeln("Error: ", e.msg);
|
||||
}
|
||||
}
|
||||
93
languages/d/qwen_streaming.d
Normal file
93
languages/d/qwen_streaming.d
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
#!/usr/bin/env rdmd
|
||||
|
||||
// Qwen 3 Coder Streaming Example in D
|
||||
|
||||
import std.stdio;
|
||||
import std.net.curl;
|
||||
import std.json;
|
||||
import std.string;
|
||||
import std.algorithm;
|
||||
import std.array;
|
||||
|
||||
void main()
|
||||
{
|
||||
immutable baseUrl = "https://qwen.ai.unturf.com/v1/chat/completions";
|
||||
immutable model = "hf.co/unsloth/Qwen3-Coder-30B-A3B-Instruct-GGUF:Q4_K_M";
|
||||
|
||||
try
|
||||
{
|
||||
writeln("Streaming from Qwen 3 Coder...\n");
|
||||
|
||||
// Create JSON payload
|
||||
JSONValue payload;
|
||||
payload.object = [
|
||||
"model": model,
|
||||
"messages": [
|
||||
[
|
||||
"role": "user",
|
||||
"content": "Give a Python Fizzbuzz solution in one line of code?"
|
||||
]
|
||||
],
|
||||
"temperature": 0.5,
|
||||
"max_tokens": 150,
|
||||
"stream": true
|
||||
];
|
||||
|
||||
// Make streaming POST request
|
||||
auto http = HTTP();
|
||||
http.addRequestHeader("Content-Type", "application/json");
|
||||
http.method = HTTP.Method.post;
|
||||
http.url = baseUrl;
|
||||
http.postData = payload.toString();
|
||||
|
||||
string buffer = "";
|
||||
|
||||
// Process chunks as they arrive
|
||||
http.onReceive = (ubyte[] data)
|
||||
{
|
||||
buffer ~= cast(string)data;
|
||||
|
||||
// Process complete lines
|
||||
while (true)
|
||||
{
|
||||
auto idx = buffer.indexOf("\n");
|
||||
if (idx == -1) break;
|
||||
|
||||
auto line = buffer[0..idx].strip();
|
||||
buffer = buffer[idx+1..$];
|
||||
|
||||
// Process SSE data lines
|
||||
if (line.startsWith("data: "))
|
||||
{
|
||||
auto jsonData = line[6..$];
|
||||
if (jsonData == "[DONE]") continue;
|
||||
|
||||
try
|
||||
{
|
||||
auto parsed = parseJSON(jsonData);
|
||||
if ("choices" in parsed && parsed["choices"].array.length > 0)
|
||||
{
|
||||
auto delta = parsed["choices"][0]["delta"];
|
||||
if ("content" in delta)
|
||||
{
|
||||
write(delta["content"].str);
|
||||
stdout.flush();
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception) {}
|
||||
}
|
||||
}
|
||||
|
||||
return data.length;
|
||||
};
|
||||
|
||||
http.perform();
|
||||
|
||||
writeln("\n\nDone!");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
writeln("\nError: ", e.msg);
|
||||
}
|
||||
}
|
||||
43
languages/d/tts.d
Normal file
43
languages/d/tts.d
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
#!/usr/bin/env rdmd
|
||||
|
||||
// Text-to-Speech Example in D
|
||||
|
||||
import std.stdio;
|
||||
import std.net.curl;
|
||||
import std.json;
|
||||
import std.file;
|
||||
|
||||
void main()
|
||||
{
|
||||
immutable baseUrl = "https://speech.ai.unturf.com/v1/audio/speech";
|
||||
immutable outputFile = "speech.mp3";
|
||||
|
||||
try
|
||||
{
|
||||
writeln("Generating speech from TTS...\n");
|
||||
|
||||
// Create JSON payload
|
||||
JSONValue payload;
|
||||
payload.object = [
|
||||
"model": "tts-1",
|
||||
"voice": "alloy",
|
||||
"speed": 0.9,
|
||||
"input": "I think so therefore, Today is a wonderful day to grow something people love!"
|
||||
];
|
||||
|
||||
// Make POST request and get binary response
|
||||
auto http = HTTP();
|
||||
http.addRequestHeader("Content-Type", "application/json");
|
||||
|
||||
auto response = post(baseUrl, payload.toString(), http);
|
||||
|
||||
// Write binary data to file
|
||||
std.file.write(outputFile, response);
|
||||
|
||||
writeln("Speech saved to: ", outputFile);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
writeln("Error: ", e.msg);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,37 +0,0 @@
|
|||
FROM elixir:1.17-alpine AS build
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Install hex and rebar
|
||||
RUN mix local.hex --force && \
|
||||
mix local.rebar --force
|
||||
|
||||
# Copy mix files for dependency resolution
|
||||
COPY mix.exs mix.lock* ./
|
||||
RUN mix deps.get --only prod
|
||||
|
||||
# Copy source code
|
||||
COPY lib ./lib
|
||||
COPY run.exs ./
|
||||
|
||||
# Compile the project (don't run yet)
|
||||
RUN MIX_ENV=prod mix compile
|
||||
|
||||
# Runtime stage
|
||||
FROM elixir:1.17-alpine
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Install hex and rebar in runtime
|
||||
RUN mix local.hex --force && \
|
||||
mix local.rebar --force
|
||||
|
||||
# Copy built application from build stage
|
||||
COPY --from=build /app/_build /app/_build
|
||||
COPY --from=build /app/deps /app/deps
|
||||
COPY --from=build /app/lib /app/lib
|
||||
COPY --from=build /app/run.exs /app/run.exs
|
||||
COPY --from=build /app/mix.exs /app/mix.exs
|
||||
|
||||
# Run the application
|
||||
CMD ["elixir", "run.exs"]
|
||||
|
|
@ -1,252 +0,0 @@
|
|||
defmodule UncloseAI do
|
||||
@moduledoc """
|
||||
UncloseAI Elixir Library
|
||||
OpenAI-compatible API client with streaming support
|
||||
Compatible with vLLM, Ollama, and OpenAI-compatible endpoints
|
||||
"""
|
||||
|
||||
defmodule ModelInfo do
|
||||
@moduledoc "Struct to hold model information"
|
||||
defstruct [:id, :endpoint, :max_tokens]
|
||||
end
|
||||
|
||||
defmodule Client do
|
||||
@moduledoc "Client struct to hold discovered models and endpoints"
|
||||
defstruct models: [], tts_endpoints: [], timeout: 30_000
|
||||
|
||||
@doc """
|
||||
Initialize a new UncloseAI client with auto-discovery
|
||||
"""
|
||||
def new(opts \\ []) do
|
||||
timeout = Keyword.get(opts, :timeout, 30_000)
|
||||
IO.puts("Initializing UncloseAI client...")
|
||||
|
||||
# Discover chat/code models
|
||||
models =
|
||||
Stream.iterate(1, &(&1 + 1))
|
||||
|> Stream.take_while(fn i ->
|
||||
System.get_env("MODEL_ENDPOINT_#{i}") != nil
|
||||
end)
|
||||
|> Stream.map(fn i ->
|
||||
endpoint = System.get_env("MODEL_ENDPOINT_#{i}")
|
||||
IO.puts("Endpoint #{i}: #{endpoint}")
|
||||
discover_models_from_endpoint(endpoint)
|
||||
end)
|
||||
|> Enum.to_list()
|
||||
|> List.flatten()
|
||||
|
||||
# Discover TTS endpoints
|
||||
tts_endpoints =
|
||||
Stream.iterate(1, &(&1 + 1))
|
||||
|> Stream.take_while(fn i ->
|
||||
System.get_env("TTS_ENDPOINT_#{i}") != nil
|
||||
end)
|
||||
|> Stream.map(fn i ->
|
||||
System.get_env("TTS_ENDPOINT_#{i}")
|
||||
end)
|
||||
|> Enum.to_list()
|
||||
|
||||
IO.puts("Discovered #{length(models)} models, #{length(tts_endpoints)} TTS endpoints\n")
|
||||
|
||||
%Client{models: models, tts_endpoints: tts_endpoints, timeout: timeout}
|
||||
end
|
||||
|
||||
defp discover_models_from_endpoint(endpoint) do
|
||||
case HTTPoison.get("#{endpoint}/models", [], recv_timeout: 10_000) do
|
||||
{:ok, %HTTPoison.Response{body: body}} ->
|
||||
data = Jason.decode!(body)
|
||||
|
||||
case data["data"] do
|
||||
nil ->
|
||||
[]
|
||||
|
||||
models ->
|
||||
Enum.filter(models, fn model ->
|
||||
# Skip modelperm-* entries
|
||||
not String.starts_with?(model["id"], "modelperm-")
|
||||
end)
|
||||
|> Enum.map(fn model ->
|
||||
model_id = model["id"]
|
||||
max_tokens = model["max_model_len"] || 8192
|
||||
%ModelInfo{id: model_id, endpoint: endpoint, max_tokens: max_tokens}
|
||||
end)
|
||||
end
|
||||
|
||||
{:error, _reason} ->
|
||||
# Silently skip failed endpoints
|
||||
[]
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Non-streaming chat completion
|
||||
"""
|
||||
def chat(client, messages, opts \\ []) do
|
||||
model_idx = Keyword.get(opts, :model_idx, 0)
|
||||
max_tokens = Keyword.get(opts, :max_tokens, 100)
|
||||
temperature = Keyword.get(opts, :temperature, 0.7)
|
||||
|
||||
model = Enum.at(client.models, model_idx)
|
||||
|
||||
if model == nil do
|
||||
{:error, "Invalid model index"}
|
||||
else
|
||||
url = "#{model.endpoint}/chat/completions"
|
||||
|
||||
request = %{
|
||||
model: model.id,
|
||||
messages: messages,
|
||||
stream: false,
|
||||
max_tokens: max_tokens,
|
||||
temperature: temperature
|
||||
}
|
||||
|
||||
case HTTPoison.post(
|
||||
url,
|
||||
Jason.encode!(request),
|
||||
[{"Content-Type", "application/json"}],
|
||||
recv_timeout: client.timeout
|
||||
) do
|
||||
{:ok, %HTTPoison.Response{body: body}} ->
|
||||
data = Jason.decode!(body)
|
||||
{:ok, get_in(data, ["choices", Access.at(0), "message", "content"])}
|
||||
|
||||
{:error, reason} ->
|
||||
{:error, reason}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Streaming chat completion - returns a Stream that yields content chunks
|
||||
"""
|
||||
def chat_stream(client, messages, opts \\ []) do
|
||||
model_idx = Keyword.get(opts, :model_idx, 0)
|
||||
max_tokens = Keyword.get(opts, :max_tokens, 500)
|
||||
temperature = Keyword.get(opts, :temperature, 0.7)
|
||||
|
||||
model = Enum.at(client.models, model_idx)
|
||||
|
||||
if model == nil do
|
||||
raise "Invalid model index"
|
||||
end
|
||||
|
||||
url = "#{model.endpoint}/chat/completions"
|
||||
|
||||
request = %{
|
||||
model: model.id,
|
||||
messages: messages,
|
||||
stream: true,
|
||||
max_tokens: max_tokens,
|
||||
temperature: temperature
|
||||
}
|
||||
|
||||
# Use HTTPoison stream with async response handling
|
||||
Stream.resource(
|
||||
fn ->
|
||||
{:ok, response} =
|
||||
HTTPoison.post(
|
||||
url,
|
||||
Jason.encode!(request),
|
||||
[{"Content-Type", "application/json"}],
|
||||
stream_to: self(),
|
||||
async: :once,
|
||||
recv_timeout: client.timeout
|
||||
)
|
||||
|
||||
{response, ""}
|
||||
end,
|
||||
fn {response, buffer} ->
|
||||
receive do
|
||||
%HTTPoison.AsyncStatus{} ->
|
||||
HTTPoison.stream_next(response)
|
||||
{[], {response, buffer}}
|
||||
|
||||
%HTTPoison.AsyncHeaders{} ->
|
||||
HTTPoison.stream_next(response)
|
||||
{[], {response, buffer}}
|
||||
|
||||
%HTTPoison.AsyncChunk{chunk: chunk} ->
|
||||
# Append chunk to buffer and process lines
|
||||
new_buffer = buffer <> chunk
|
||||
{lines, remaining} = extract_lines(new_buffer)
|
||||
|
||||
contents =
|
||||
lines
|
||||
|> Enum.filter(&String.starts_with?(&1, "data: "))
|
||||
|> Enum.map(&String.slice(&1, 6..-1))
|
||||
|> Enum.reject(&(&1 == "[DONE]"))
|
||||
|> Enum.map(&extract_content/1)
|
||||
|> Enum.reject(&is_nil/1)
|
||||
|
||||
HTTPoison.stream_next(response)
|
||||
{contents, {response, remaining}}
|
||||
|
||||
%HTTPoison.AsyncEnd{} ->
|
||||
{:halt, {response, buffer}}
|
||||
after
|
||||
client.timeout ->
|
||||
{:halt, {response, buffer}}
|
||||
end
|
||||
end,
|
||||
fn {response, _buffer} ->
|
||||
:hackney.close(response.id)
|
||||
end
|
||||
)
|
||||
end
|
||||
|
||||
defp extract_lines(buffer) do
|
||||
lines = String.split(buffer, "\n")
|
||||
|
||||
case List.last(lines) do
|
||||
"" -> {Enum.drop(lines, -1), ""}
|
||||
partial -> {Enum.drop(lines, -1), partial}
|
||||
end
|
||||
end
|
||||
|
||||
defp extract_content(data) do
|
||||
case Jason.decode(data) do
|
||||
{:ok, json} ->
|
||||
get_in(json, ["choices", Access.at(0), "delta", "content"])
|
||||
|
||||
{:error, _} ->
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Text-to-speech generation
|
||||
"""
|
||||
def tts(client, text, opts \\ []) do
|
||||
voice = Keyword.get(opts, :voice, "alloy")
|
||||
output_file = Keyword.get(opts, :output_file, "/tmp/speech.mp3")
|
||||
|
||||
if Enum.empty?(client.tts_endpoints) do
|
||||
{:error, "No TTS endpoints available"}
|
||||
else
|
||||
endpoint = List.first(client.tts_endpoints)
|
||||
url = "#{endpoint}/audio/speech"
|
||||
|
||||
request = %{
|
||||
model: "tts-1",
|
||||
voice: voice,
|
||||
input: text
|
||||
}
|
||||
|
||||
case HTTPoison.post(
|
||||
url,
|
||||
Jason.encode!(request),
|
||||
[{"Content-Type", "application/json"}],
|
||||
recv_timeout: client.timeout
|
||||
) do
|
||||
{:ok, %HTTPoison.Response{body: body}} ->
|
||||
File.write!(output_file, body)
|
||||
{:ok, output_file}
|
||||
|
||||
{:error, reason} ->
|
||||
{:error, reason}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
defmodule AIExamples.MixProject do
|
||||
use Mix.Project
|
||||
|
||||
def project do
|
||||
[
|
||||
app: :ai_examples,
|
||||
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
|
||||
[
|
||||
{:httpoison, "~> 2.2"},
|
||||
{:jason, "~> 1.4"}
|
||||
]
|
||||
end
|
||||
end
|
||||
|
|
@ -1,62 +0,0 @@
|
|||
#!/usr/bin/env elixir
|
||||
|
||||
# Load dependencies
|
||||
Mix.install([
|
||||
{:httpoison, "~> 2.2"},
|
||||
{:jason, "~> 1.4"}
|
||||
])
|
||||
|
||||
# Load the module
|
||||
Code.require_file("lib/uncloseai.ex", __DIR__)
|
||||
|
||||
# Demo program showing library usage
|
||||
alias UncloseAI.Client
|
||||
|
||||
IO.puts("=== UncloseAI Elixir Client (with Streaming) ===\n")
|
||||
|
||||
# Initialize client
|
||||
client = Client.new()
|
||||
|
||||
if Enum.empty?(client.models) do
|
||||
IO.puts("ERROR: No models discovered")
|
||||
System.halt(1)
|
||||
end
|
||||
|
||||
# Non-streaming chat example
|
||||
IO.puts("=== Non-Streaming Chat ===")
|
||||
IO.puts("Model: #{Enum.at(client.models, 0).id}")
|
||||
|
||||
messages = [%{role: "user", content: "Explain quantum computing in one sentence"}]
|
||||
|
||||
case Client.chat(client, messages) do
|
||||
{:ok, response} -> IO.puts("Response: #{response}\n")
|
||||
{:error, reason} -> IO.puts("Error: #{inspect(reason)}\n")
|
||||
end
|
||||
|
||||
# Streaming chat example
|
||||
model_idx = if length(client.models) >= 2, do: 1, else: 0
|
||||
IO.puts("=== Streaming Chat ===")
|
||||
IO.puts("Model: #{Enum.at(client.models, model_idx).id}")
|
||||
IO.write("Response: ")
|
||||
|
||||
messages = [%{role: "user", content: "Write a hello world program in Elixir"}]
|
||||
|
||||
client
|
||||
|> Client.chat_stream(messages, model_idx: model_idx)
|
||||
|> Enum.each(&IO.write/1)
|
||||
|
||||
IO.puts("\n")
|
||||
|
||||
# TTS example
|
||||
if !Enum.empty?(client.tts_endpoints) do
|
||||
IO.puts("=== TTS Speech Generation ===")
|
||||
IO.puts("Model: tts-1")
|
||||
|
||||
case Client.tts(client, "Hello from UncloseAI Elixir client!",
|
||||
output_file: "/tmp/speech.mp3") do
|
||||
{:ok, file} -> IO.puts("Audio saved to #{file}")
|
||||
{:error, reason} -> IO.puts("TTS failed: #{inspect(reason)}")
|
||||
end
|
||||
end
|
||||
|
||||
IO.puts("\n=== Examples Complete ===")
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
# 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"]
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
# UncloseAI Go Client
|
||||
# uncloseai. Go Client
|
||||
|
||||
A Go client library for interacting with vLLM, Ollama, and OpenAI-compatible APIs.
|
||||
|
||||
|
|
@ -118,7 +118,7 @@ Main client struct for interacting with AI APIs.
|
|||
|
||||
#### `func New(config *Config) (*Client, error)`
|
||||
|
||||
Create a new UncloseAI client.
|
||||
Create a new uncloseai. client.
|
||||
|
||||
**Parameters:**
|
||||
- `config` - Configuration options. If nil, uses defaults and auto-discovers from environment
|
||||
|
|
@ -271,7 +271,7 @@ Information about a discovered model.
|
|||
|
||||
#### `Config`
|
||||
|
||||
Configuration for the UncloseAI client.
|
||||
Configuration for the uncloseai. client.
|
||||
|
||||
**Fields:**
|
||||
- `Endpoints []string` - Model endpoints (nil = auto-discover)
|
||||
|
|
@ -384,7 +384,7 @@ response2, _ := client.Chat(ctx, messages, nil)
|
|||
```go
|
||||
import "os"
|
||||
|
||||
audio, err := client.TTS(ctx, "Hello from UncloseAI!", "alloy", "tts-1")
|
||||
audio, err := client.TTS(ctx, "Hello from uncloseai.!", "alloy", "tts-1")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +0,0 @@
|
|||
module uncloseai.com
|
||||
|
||||
go 1.23
|
||||
|
|
@ -6,13 +6,12 @@ RUN apk --no-cache add ca-certificates
|
|||
|
||||
WORKDIR /app
|
||||
|
||||
# Copy module files
|
||||
# Copy module files and source
|
||||
COPY go.mod .
|
||||
COPY uncloseai/ ./uncloseai/
|
||||
COPY examples/ ./examples/
|
||||
COPY uncloseai.go .
|
||||
|
||||
# Build the examples
|
||||
RUN go build -o basic examples/basic.go
|
||||
# Build the application
|
||||
RUN go build -o uncloseai uncloseai.go
|
||||
|
||||
# Use minimal alpine image for runtime
|
||||
FROM alpine:3.21
|
||||
|
|
@ -22,7 +21,6 @@ RUN apk --no-cache add ca-certificates
|
|||
|
||||
WORKDIR /app
|
||||
|
||||
COPY --from=builder /app/basic .
|
||||
COPY --from=builder /app/uncloseai .
|
||||
|
||||
# Default: run basic example
|
||||
CMD ["./basic"]
|
||||
CMD ["./uncloseai"]
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ FROM golang:1.22-alpine AS builder
|
|||
WORKDIR /app
|
||||
|
||||
# Copy go mod files
|
||||
COPY go.mod ./
|
||||
COPY go.mod go.sum ./
|
||||
RUN go mod download
|
||||
|
||||
# Copy source code
|
||||
|
|
|
|||
2
languages/go/openai/go.sum
Normal file
2
languages/go/openai/go.sum
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
github.com/openai/openai-go v0.1.0-alpha.39 h1:3SdE6BffOX9HPEQv8IL/fi3LYZ5TUpRYaqGQZbyk11A=
|
||||
github.com/openai/openai-go v0.1.0-alpha.39/go.mod h1:3SdE6BffOX9HPEQv8IL/fi3LYZ5TUpRYaqGQZbyk11A=
|
||||
|
|
@ -1,465 +0,0 @@
|
|||
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 ===")
|
||||
}
|
||||
17
languages/groovy/openai-client/Dockerfile
Normal file
17
languages/groovy/openai-client/Dockerfile
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
# Pin to specific Groovy version (checked 2025-10-15: groovy:4.0.25-jdk17 is latest stable)
|
||||
FROM groovy:4.0.25-jdk17
|
||||
|
||||
# Install Grape (Groovy dependency manager) dependencies
|
||||
# The dependencies will be downloaded at runtime via @Grab annotations
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Copy Groovy scripts
|
||||
COPY HermesNonStreaming.groovy .
|
||||
COPY HermesStreaming.groovy .
|
||||
COPY QwenNonStreaming.groovy .
|
||||
COPY QwenStreaming.groovy .
|
||||
COPY TTS.groovy .
|
||||
|
||||
# Default command shows available examples
|
||||
CMD ["sh", "-c", "echo 'Available examples:' && echo ' groovy HermesNonStreaming.groovy' && echo ' groovy HermesStreaming.groovy' && echo ' groovy QwenNonStreaming.groovy' && echo ' groovy QwenStreaming.groovy' && echo ' groovy TTS.groovy'"]
|
||||
48
languages/groovy/openai-client/HermesNonStreaming.groovy
Normal file
48
languages/groovy/openai-client/HermesNonStreaming.groovy
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
#!/usr/bin/env groovy
|
||||
|
||||
// Hermes AI Non-Streaming Example using OpenAI Java Client
|
||||
// Groovy leverages Java libraries seamlessly
|
||||
|
||||
@Grapes([
|
||||
@Grab(group='com.openai', module='openai-java', version='4.2.0')
|
||||
])
|
||||
|
||||
import com.openai.client.OpenAIClient
|
||||
import com.openai.client.okhttp.OpenAIOkHttpClient
|
||||
import com.openai.models.ChatCompletion
|
||||
import com.openai.models.ChatCompletionCreateParams
|
||||
import com.openai.models.ChatCompletionMessageParam
|
||||
import com.openai.models.ChatCompletionUserMessageParam
|
||||
|
||||
def baseUrl = "https://hermes.ai.unturf.com/v1"
|
||||
def model = "adamo1139/Hermes-3-Llama-3.1-8B-FP8-Dynamic"
|
||||
|
||||
try {
|
||||
println "Requesting from Hermes AI...\n"
|
||||
|
||||
// Create OpenAI client with custom base URL
|
||||
def client = OpenAIOkHttpClient.builder()
|
||||
.baseUrl(baseUrl)
|
||||
.apiKey("dummy-api-key")
|
||||
.build()
|
||||
|
||||
// Create chat completion request
|
||||
def params = ChatCompletionCreateParams.builder()
|
||||
.model(model)
|
||||
.addMessage(ChatCompletionUserMessageParam.builder()
|
||||
.content("Give a Python Fizzbuzz solution in one line of code?")
|
||||
.build())
|
||||
.temperature(0.5)
|
||||
.maxTokens(150)
|
||||
.build()
|
||||
|
||||
// Make request and get response
|
||||
def completion = client.chat().completions().create(params)
|
||||
def response = completion.choices()[0].message().content().get()
|
||||
|
||||
println "Response: ${response}"
|
||||
|
||||
} catch (Exception e) {
|
||||
println "Error: ${e.message}"
|
||||
e.printStackTrace()
|
||||
}
|
||||
55
languages/groovy/openai-client/HermesStreaming.groovy
Normal file
55
languages/groovy/openai-client/HermesStreaming.groovy
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
#!/usr/bin/env groovy
|
||||
|
||||
// Hermes AI Streaming Example using OpenAI Java Client
|
||||
// Demonstrates real-time streaming responses
|
||||
|
||||
@Grapes([
|
||||
@Grab(group='com.openai', module='openai-java', version='4.2.0')
|
||||
])
|
||||
|
||||
import com.openai.client.OpenAIClient
|
||||
import com.openai.client.okhttp.OpenAIOkHttpClient
|
||||
import com.openai.models.ChatCompletionChunk
|
||||
import com.openai.models.ChatCompletionCreateParams
|
||||
import com.openai.models.ChatCompletionUserMessageParam
|
||||
|
||||
def baseUrl = "https://hermes.ai.unturf.com/v1"
|
||||
def model = "adamo1139/Hermes-3-Llama-3.1-8B-FP8-Dynamic"
|
||||
|
||||
try {
|
||||
println "Streaming from Hermes AI...\n"
|
||||
|
||||
// Create OpenAI client with custom base URL
|
||||
def client = OpenAIOkHttpClient.builder()
|
||||
.baseUrl(baseUrl)
|
||||
.apiKey("dummy-api-key")
|
||||
.build()
|
||||
|
||||
// Create streaming chat completion request
|
||||
def params = ChatCompletionCreateParams.builder()
|
||||
.model(model)
|
||||
.addMessage(ChatCompletionUserMessageParam.builder()
|
||||
.content("Give a Python Fizzbuzz solution in one line of code?")
|
||||
.build())
|
||||
.temperature(0.5)
|
||||
.maxTokens(150)
|
||||
.stream(true)
|
||||
.build()
|
||||
|
||||
// Stream the response
|
||||
def stream = client.chat().completions().createStreaming(params)
|
||||
|
||||
stream.forEach { chunk ->
|
||||
def delta = chunk.choices()[0].delta()
|
||||
if (delta.content().isPresent()) {
|
||||
print delta.content().get()
|
||||
System.out.flush()
|
||||
}
|
||||
}
|
||||
|
||||
println "\n\nDone!"
|
||||
|
||||
} catch (Exception e) {
|
||||
println "\nError: ${e.message}"
|
||||
e.printStackTrace()
|
||||
}
|
||||
47
languages/groovy/openai-client/QwenNonStreaming.groovy
Normal file
47
languages/groovy/openai-client/QwenNonStreaming.groovy
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
#!/usr/bin/env groovy
|
||||
|
||||
// Qwen 3 Coder Non-Streaming Example using OpenAI Java Client
|
||||
|
||||
@Grapes([
|
||||
@Grab(group='com.openai', module='openai-java', version='4.2.0')
|
||||
])
|
||||
|
||||
import com.openai.client.OpenAIClient
|
||||
import com.openai.client.okhttp.OpenAIOkHttpClient
|
||||
import com.openai.models.ChatCompletion
|
||||
import com.openai.models.ChatCompletionCreateParams
|
||||
import com.openai.models.ChatCompletionMessageParam
|
||||
import com.openai.models.ChatCompletionUserMessageParam
|
||||
|
||||
def baseUrl = "https://qwen.ai.unturf.com/v1"
|
||||
def model = "hf.co/unsloth/Qwen3-Coder-30B-A3B-Instruct-GGUF:Q4_K_M"
|
||||
|
||||
try {
|
||||
println "Requesting from Qwen 3 Coder...\n"
|
||||
|
||||
// Create OpenAI client with custom base URL
|
||||
def client = OpenAIOkHttpClient.builder()
|
||||
.baseUrl(baseUrl)
|
||||
.apiKey("dummy-api-key")
|
||||
.build()
|
||||
|
||||
// Create chat completion request
|
||||
def params = ChatCompletionCreateParams.builder()
|
||||
.model(model)
|
||||
.addMessage(ChatCompletionUserMessageParam.builder()
|
||||
.content("Give a Python Fizzbuzz solution in one line of code?")
|
||||
.build())
|
||||
.temperature(0.5)
|
||||
.maxTokens(150)
|
||||
.build()
|
||||
|
||||
// Make request and get response
|
||||
def completion = client.chat().completions().create(params)
|
||||
def response = completion.choices()[0].message().content().get()
|
||||
|
||||
println "Response: ${response}"
|
||||
|
||||
} catch (Exception e) {
|
||||
println "Error: ${e.message}"
|
||||
e.printStackTrace()
|
||||
}
|
||||
54
languages/groovy/openai-client/QwenStreaming.groovy
Normal file
54
languages/groovy/openai-client/QwenStreaming.groovy
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
#!/usr/bin/env groovy
|
||||
|
||||
// Qwen 3 Coder Streaming Example using OpenAI Java Client
|
||||
|
||||
@Grapes([
|
||||
@Grab(group='com.openai', module='openai-java', version='4.2.0')
|
||||
])
|
||||
|
||||
import com.openai.client.OpenAIClient
|
||||
import com.openai.client.okhttp.OpenAIOkHttpClient
|
||||
import com.openai.models.ChatCompletionChunk
|
||||
import com.openai.models.ChatCompletionCreateParams
|
||||
import com.openai.models.ChatCompletionUserMessageParam
|
||||
|
||||
def baseUrl = "https://qwen.ai.unturf.com/v1"
|
||||
def model = "hf.co/unsloth/Qwen3-Coder-30B-A3B-Instruct-GGUF:Q4_K_M"
|
||||
|
||||
try {
|
||||
println "Streaming from Qwen 3 Coder...\n"
|
||||
|
||||
// Create OpenAI client with custom base URL
|
||||
def client = OpenAIOkHttpClient.builder()
|
||||
.baseUrl(baseUrl)
|
||||
.apiKey("dummy-api-key")
|
||||
.build()
|
||||
|
||||
// Create streaming chat completion request
|
||||
def params = ChatCompletionCreateParams.builder()
|
||||
.model(model)
|
||||
.addMessage(ChatCompletionUserMessageParam.builder()
|
||||
.content("Give a Python Fizzbuzz solution in one line of code?")
|
||||
.build())
|
||||
.temperature(0.5)
|
||||
.maxTokens(150)
|
||||
.stream(true)
|
||||
.build()
|
||||
|
||||
// Stream the response
|
||||
def stream = client.chat().completions().createStreaming(params)
|
||||
|
||||
stream.forEach { chunk ->
|
||||
def delta = chunk.choices()[0].delta()
|
||||
if (delta.content().isPresent()) {
|
||||
print delta.content().get()
|
||||
System.out.flush()
|
||||
}
|
||||
}
|
||||
|
||||
println "\n\nDone!"
|
||||
|
||||
} catch (Exception e) {
|
||||
println "\nError: ${e.message}"
|
||||
e.printStackTrace()
|
||||
}
|
||||
45
languages/groovy/openai-client/TTS.groovy
Normal file
45
languages/groovy/openai-client/TTS.groovy
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
#!/usr/bin/env groovy
|
||||
|
||||
// Text-to-Speech Example using OpenAI Java Client
|
||||
|
||||
@Grapes([
|
||||
@Grab(group='com.openai', module='openai-java', version='4.2.0')
|
||||
])
|
||||
|
||||
import com.openai.client.okhttp.OpenAIOkHttpClient
|
||||
import com.openai.models.SpeechCreateParams
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Paths
|
||||
|
||||
def baseUrl = "https://speech.ai.unturf.com/v1"
|
||||
def outputFile = "speech.mp3"
|
||||
|
||||
try {
|
||||
println "Generating speech from TTS...\n"
|
||||
|
||||
// Create OpenAI client with custom base URL
|
||||
def client = OpenAIOkHttpClient.builder()
|
||||
.baseUrl(baseUrl)
|
||||
.apiKey("YOLO")
|
||||
.build()
|
||||
|
||||
// Create TTS request
|
||||
def params = SpeechCreateParams.builder()
|
||||
.model(SpeechCreateParams.Model.TTS_1)
|
||||
.voice(SpeechCreateParams.Voice.ALLOY)
|
||||
.speed(0.9)
|
||||
.input("I think so therefore, Today is a wonderful day to grow something people love!")
|
||||
.build()
|
||||
|
||||
// Generate speech and get response as bytes
|
||||
def response = client.audio().speech().create(params)
|
||||
|
||||
// Write to file
|
||||
Files.write(Paths.get(outputFile), response.readAllBytes())
|
||||
|
||||
println "Speech saved to: ${outputFile}"
|
||||
|
||||
} catch (Exception e) {
|
||||
println "Error: ${e.message}"
|
||||
e.printStackTrace()
|
||||
}
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
FROM openjdk:17-jdk-slim
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY UncloseAI.java .
|
||||
|
||||
RUN javac UncloseAI.java
|
||||
|
||||
CMD ["java", "UncloseAI"]
|
||||
|
|
@ -1,366 +0,0 @@
|
|||
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 ===");
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
# UncloseAI Node.js Client
|
||||
# uncloseai. Node.js Client
|
||||
|
||||
A Node.js client library for interacting with vLLM, Ollama, and OpenAI-compatible APIs.
|
||||
|
||||
|
|
@ -26,10 +26,10 @@ node examples.js
|
|||
## Quick Start
|
||||
|
||||
```javascript
|
||||
const { UncloseAI } = require('./uncloseai_lib');
|
||||
const { uncloseai } = require('./uncloseai_lib');
|
||||
|
||||
// Initialize client (auto-discovers from environment variables)
|
||||
const client = new UncloseAI();
|
||||
const client = new uncloseai();
|
||||
|
||||
// Non-streaming chat
|
||||
const response = await client.chat({
|
||||
|
|
@ -64,7 +64,7 @@ export TTS_ENDPOINT_1="https://speech.ai.unturf.com/v1"
|
|||
### Programmatic Configuration
|
||||
|
||||
```javascript
|
||||
const client = new UncloseAI({
|
||||
const client = new uncloseai({
|
||||
endpoints: ['https://api.example.com/v1'],
|
||||
ttsEndpoints: ['https://tts.example.com/v1'],
|
||||
apiKey: 'your-api-key', // Optional
|
||||
|
|
@ -75,7 +75,7 @@ const client = new UncloseAI({
|
|||
|
||||
## API Reference
|
||||
|
||||
### UncloseAI
|
||||
### uncloseai. Client
|
||||
|
||||
Main client class for interacting with AI APIs.
|
||||
|
||||
|
|
@ -178,12 +178,12 @@ Generate speech from text.
|
|||
|
||||
**Throws:**
|
||||
- `ConnectionError`: If request fails
|
||||
- `UncloseAIError`: If no TTS endpoints available
|
||||
- `uncloseaiError`: If no TTS endpoints available
|
||||
|
||||
**Example:**
|
||||
```javascript
|
||||
const audioData = await client.tts({
|
||||
text: 'Hello from UncloseAI!',
|
||||
text: 'Hello from uncloseai.!',
|
||||
voice: 'alloy',
|
||||
model: 'tts-1'
|
||||
});
|
||||
|
|
@ -196,9 +196,9 @@ fs.writeFileSync('output.mp3', audioData);
|
|||
### Basic Chat
|
||||
|
||||
```javascript
|
||||
const { UncloseAI } = require('./uncloseai_lib');
|
||||
const { uncloseai } = require('./uncloseai_lib');
|
||||
|
||||
const client = new UncloseAI();
|
||||
const client = new uncloseai();
|
||||
|
||||
const response = await client.chat({
|
||||
model: 'auto',
|
||||
|
|
@ -252,7 +252,7 @@ const response2 = await client.chat({ model: 'auto', messages });
|
|||
const fs = require('fs');
|
||||
|
||||
const audioData = await client.tts({
|
||||
text: 'Hello from UncloseAI!',
|
||||
text: 'Hello from uncloseai.!',
|
||||
voice: 'alloy',
|
||||
model: 'tts-1'
|
||||
});
|
||||
|
|
@ -279,9 +279,9 @@ const response = await client.chat({
|
|||
### Error Handling
|
||||
|
||||
```javascript
|
||||
const { UncloseAI, UncloseAIError, ModelNotFoundError } = require('./uncloseai_lib');
|
||||
const { uncloseai, uncloseaiError, ModelNotFoundError } = require('./uncloseai_lib');
|
||||
|
||||
const client = new UncloseAI();
|
||||
const client = new uncloseai();
|
||||
|
||||
try {
|
||||
const response = await client.chat({
|
||||
|
|
@ -291,7 +291,7 @@ try {
|
|||
} catch (error) {
|
||||
if (error instanceof ModelNotFoundError) {
|
||||
console.log(`Model error: ${error.message}`);
|
||||
} else if (error instanceof UncloseAIError) {
|
||||
} else if (error instanceof uncloseaiError) {
|
||||
console.log(`API error: ${error.message}`);
|
||||
} else {
|
||||
throw error;
|
||||
|
|
@ -330,7 +330,7 @@ Tested with:
|
|||
|
||||
## Error Types
|
||||
|
||||
- `UncloseAIError` - Base error class for all library errors
|
||||
- `uncloseaiError` - Base error class for all library errors
|
||||
- `ConnectionError` - Network connection errors
|
||||
- `ModelNotFoundError` - Requested model not available
|
||||
- `StreamingError` - Errors during streaming requests
|
||||
|
|
|
|||
|
|
@ -1,17 +1,43 @@
|
|||
import OpenAI from 'openai';
|
||||
import fs from 'fs';
|
||||
|
||||
console.log('=== UncloseAI Node.js Client (Official OpenAI SDK) ===\n');
|
||||
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({
|
||||
// Discover endpoints from environment variables
|
||||
const modelEndpoint1 = process.env.MODEL_ENDPOINT_1;
|
||||
const modelEndpoint2 = process.env.MODEL_ENDPOINT_2;
|
||||
const ttsEndpoint1 = process.env.TTS_ENDPOINT_1;
|
||||
|
||||
if (!modelEndpoint1 || !modelEndpoint2 || !ttsEndpoint1) {
|
||||
console.error('ERROR: No models discovered. Set environment variables:');
|
||||
console.error(' MODEL_ENDPOINT_1, MODEL_ENDPOINT_2, TTS_ENDPOINT_1');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Discover models from endpoint 1
|
||||
console.log(`Discovering models from ${modelEndpoint1}...`);
|
||||
const client1 = new OpenAI({
|
||||
apiKey: 'dummy-key',
|
||||
baseURL: 'https://hermes.ai.unturf.com/v1'
|
||||
baseURL: modelEndpoint1
|
||||
});
|
||||
const models1 = await client1.models.list();
|
||||
const model1Id = models1.data[0].id;
|
||||
console.log(`Model 1: ${model1Id}\n`);
|
||||
|
||||
const hermesResponse = await hermesClient.chat.completions.create({
|
||||
model: 'adamo1139/Hermes-3-Llama-3.1-8B-FP8-Dynamic',
|
||||
// Discover models from endpoint 2
|
||||
console.log(`Discovering models from ${modelEndpoint2}...`);
|
||||
const client2 = new OpenAI({
|
||||
apiKey: 'dummy-key',
|
||||
baseURL: modelEndpoint2
|
||||
});
|
||||
const models2 = await client2.models.list();
|
||||
const model2Id = models2.data[0].id;
|
||||
console.log(`Model 2: ${model2Id}\n`);
|
||||
|
||||
// Non-streaming chat with Model 1
|
||||
console.log('=== Non-Streaming Chat (Model 1) ===');
|
||||
const response1 = await client1.chat.completions.create({
|
||||
model: model1Id,
|
||||
messages: [
|
||||
{ role: 'user', content: 'Give a Python Fizzbuzz solution in one line of code?' }
|
||||
],
|
||||
|
|
@ -19,12 +45,12 @@ const hermesResponse = await hermesClient.chat.completions.create({
|
|||
max_tokens: 150
|
||||
});
|
||||
|
||||
console.log(`Response: ${hermesResponse.choices[0].message.content}\n`);
|
||||
console.log(`Response: ${response1.choices[0].message.content}\n`);
|
||||
|
||||
// Streaming chat with Hermes
|
||||
console.log('=== Streaming Chat (Hermes) ===');
|
||||
const hermesStream = await hermesClient.chat.completions.create({
|
||||
model: 'adamo1139/Hermes-3-Llama-3.1-8B-FP8-Dynamic',
|
||||
// Streaming chat with Model 1
|
||||
console.log('=== Streaming Chat (Model 1) ===');
|
||||
const stream1 = await client1.chat.completions.create({
|
||||
model: model1Id,
|
||||
messages: [
|
||||
{ role: 'user', content: 'Explain quantum entanglement in one sentence.' }
|
||||
],
|
||||
|
|
@ -34,20 +60,15 @@ const hermesStream = await hermesClient.chat.completions.create({
|
|||
});
|
||||
|
||||
process.stdout.write('Response: ');
|
||||
for await (const chunk of hermesStream) {
|
||||
for await (const chunk of stream1) {
|
||||
process.stdout.write(chunk.choices[0]?.delta?.content || '');
|
||||
}
|
||||
console.log('\n');
|
||||
|
||||
// Non-streaming chat with Qwen
|
||||
console.log('=== Non-Streaming Chat (Qwen) ===');
|
||||
const qwenClient = new OpenAI({
|
||||
apiKey: 'dummy-key',
|
||||
baseURL: 'https://qwen.ai.unturf.com/v1'
|
||||
});
|
||||
|
||||
const qwenResponse = await qwenClient.chat.completions.create({
|
||||
model: 'hf.co/unsloth/Qwen3-Coder-30B-A3B-Instruct-GGUF:Q4_K_M',
|
||||
// Non-streaming chat with Model 2
|
||||
console.log('=== Non-Streaming Chat (Model 2) ===');
|
||||
const response2 = await client2.chat.completions.create({
|
||||
model: model2Id,
|
||||
messages: [
|
||||
{ role: 'user', content: 'Write a JavaScript function to check if a number is prime' }
|
||||
],
|
||||
|
|
@ -55,12 +76,12 @@ const qwenResponse = await qwenClient.chat.completions.create({
|
|||
max_tokens: 150
|
||||
});
|
||||
|
||||
console.log(`Response: ${qwenResponse.choices[0].message.content}\n`);
|
||||
console.log(`Response: ${response2.choices[0].message.content}\n`);
|
||||
|
||||
// Streaming chat with Qwen
|
||||
console.log('=== Streaming Chat (Qwen) ===');
|
||||
const qwenStream = await qwenClient.chat.completions.create({
|
||||
model: 'hf.co/unsloth/Qwen3-Coder-30B-A3B-Instruct-GGUF:Q4_K_M',
|
||||
// Streaming chat with Model 2
|
||||
console.log('=== Streaming Chat (Model 2) ===');
|
||||
const stream2 = await client2.chat.completions.create({
|
||||
model: model2Id,
|
||||
messages: [
|
||||
{ role: 'user', content: 'Give a Python Fizzbuzz solution in one line of code?' }
|
||||
],
|
||||
|
|
@ -70,7 +91,7 @@ const qwenStream = await qwenClient.chat.completions.create({
|
|||
});
|
||||
|
||||
process.stdout.write('Response: ');
|
||||
for await (const chunk of qwenStream) {
|
||||
for await (const chunk of stream2) {
|
||||
process.stdout.write(chunk.choices[0]?.delta?.content || '');
|
||||
}
|
||||
console.log('\n');
|
||||
|
|
@ -78,8 +99,8 @@ console.log('\n');
|
|||
// TTS example
|
||||
console.log('=== TTS Speech Generation ===');
|
||||
const ttsClient = new OpenAI({
|
||||
apiKey: 'YOLO',
|
||||
baseURL: 'https://speech.ai.unturf.com/v1'
|
||||
apiKey: 'dummy-key',
|
||||
baseURL: ttsEndpoint1
|
||||
});
|
||||
|
||||
const mp3 = await ttsClient.audio.speech.create({
|
||||
|
|
|
|||
16
languages/mojo/Dockerfile
Normal file
16
languages/mojo/Dockerfile
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
# Use Modular MAX container (checked 2025-10-15: latest stable Mojo runtime)
|
||||
# Note: This requires Modular license acceptance
|
||||
FROM modular/max:latest
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Install Lightbug HTTP client
|
||||
RUN magic add lightbug_http
|
||||
|
||||
# Copy Mojo source files
|
||||
COPY hermes_nonstreaming.mojo .
|
||||
COPY qwen_nonstreaming.mojo .
|
||||
COPY README.md .
|
||||
|
||||
# Default command shows available examples
|
||||
CMD ["sh", "-c", "cat README.md"]
|
||||
44
languages/mojo/README.md
Normal file
44
languages/mojo/README.md
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
# Mojo Language Examples
|
||||
|
||||
⚠️ **STREAMING NOT YET IMPLEMENTED**
|
||||
|
||||
The Mojo programming language is an AI-focused systems language from Modular.
|
||||
While Lightbug HTTP client exists, streaming support for Server-Sent Events (SSE)
|
||||
is not yet available in the Lightbug library as of 2025-10-15.
|
||||
|
||||
## Current Status
|
||||
|
||||
✅ **Non-streaming examples**: Implemented for Hermes and Qwen
|
||||
❌ **Streaming examples**: NOT IMPLEMENTED (waiting for Lightbug SSE support)
|
||||
❌ **TTS example**: NOT IMPLEMENTED (binary response handling not ready)
|
||||
|
||||
## Available Examples
|
||||
|
||||
```bash
|
||||
# Run Hermes non-streaming example
|
||||
magic run mojo hermes_nonstreaming.mojo
|
||||
|
||||
# Run Qwen non-streaming example
|
||||
magic run mojo qwen_nonstreaming.mojo
|
||||
```
|
||||
|
||||
## Why Mojo?
|
||||
|
||||
Mojo is a new language designed for AI/ML workloads with:
|
||||
- Performance comparable to C/C++
|
||||
- Python-like syntax
|
||||
- Zero-cost abstractions
|
||||
- Built for AI infrastructure
|
||||
|
||||
## Future Work
|
||||
|
||||
Once Lightbug HTTP adds streaming support, we will implement:
|
||||
- Hermes streaming example
|
||||
- Qwen streaming example
|
||||
- TTS speech generation example
|
||||
|
||||
## Links
|
||||
|
||||
- Mojo Language: https://www.modular.com/mojo
|
||||
- Lightbug HTTP: https://github.com/Lightbug-HQ/lightbug_http
|
||||
- Modular Docs: https://docs.modular.com/mojo/
|
||||
58
languages/mojo/hermes_nonstreaming.mojo
Normal file
58
languages/mojo/hermes_nonstreaming.mojo
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
from lightbug_http import *
|
||||
from lightbug_http.client import Client
|
||||
|
||||
fn main() raises:
|
||||
"""
|
||||
Hermes AI Non-Streaming Example in Mojo
|
||||
|
||||
NOTE: This is a basic implementation. Streaming is NOT yet supported
|
||||
by Lightbug HTTP as of 2025-10-15.
|
||||
"""
|
||||
|
||||
print("Requesting from Hermes AI...\n")
|
||||
|
||||
# Create JSON payload
|
||||
var payload = String("""
|
||||
{
|
||||
"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,
|
||||
"stream": false
|
||||
}
|
||||
""")
|
||||
|
||||
# Create HTTP client
|
||||
var client = Client()
|
||||
|
||||
# Parse URI
|
||||
var uri = URI.parse_raises("https://hermes.ai.unturf.com/v1/chat/completions")
|
||||
|
||||
# Create headers
|
||||
var headers = Header("Host", "hermes.ai.unturf.com")
|
||||
headers.append("Content-Type", "application/json")
|
||||
headers.append("Content-Length", String(len(payload)))
|
||||
|
||||
# Create request
|
||||
var request = HTTPRequest(uri, headers)
|
||||
request.body = payload.as_bytes()
|
||||
|
||||
try:
|
||||
# Make request
|
||||
var response = client.do(request^)
|
||||
|
||||
# Print response
|
||||
print("Status Code:", response.status_code)
|
||||
print("\nResponse Body:")
|
||||
print(to_string(response.body_raw))
|
||||
|
||||
print("\nNOTE: Streaming is not yet implemented in Lightbug HTTP.")
|
||||
print("This is a non-streaming example only.")
|
||||
|
||||
except e:
|
||||
print("Error:", e)
|
||||
58
languages/mojo/qwen_nonstreaming.mojo
Normal file
58
languages/mojo/qwen_nonstreaming.mojo
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
from lightbug_http import *
|
||||
from lightbug_http.client import Client
|
||||
|
||||
fn main() raises:
|
||||
"""
|
||||
Qwen 3 Coder Non-Streaming Example in Mojo
|
||||
|
||||
NOTE: This is a basic implementation. Streaming is NOT yet supported
|
||||
by Lightbug HTTP as of 2025-10-15.
|
||||
"""
|
||||
|
||||
print("Requesting from Qwen 3 Coder...\n")
|
||||
|
||||
# Create JSON payload
|
||||
var payload = String("""
|
||||
{
|
||||
"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": false
|
||||
}
|
||||
""")
|
||||
|
||||
# Create HTTP client
|
||||
var client = Client()
|
||||
|
||||
# Parse URI
|
||||
var uri = URI.parse_raises("https://qwen.ai.unturf.com/v1/chat/completions")
|
||||
|
||||
# Create headers
|
||||
var headers = Header("Host", "qwen.ai.unturf.com")
|
||||
headers.append("Content-Type", "application/json")
|
||||
headers.append("Content-Length", String(len(payload)))
|
||||
|
||||
# Create request
|
||||
var request = HTTPRequest(uri, headers)
|
||||
request.body = payload.as_bytes()
|
||||
|
||||
try:
|
||||
# Make request
|
||||
var response = client.do(request^)
|
||||
|
||||
# Print response
|
||||
print("Status Code:", response.status_code)
|
||||
print("\nResponse Body:")
|
||||
print(to_string(response.body_raw))
|
||||
|
||||
print("\nNOTE: Streaming is not yet implemented in Lightbug HTTP.")
|
||||
print("This is a non-streaming example only.")
|
||||
|
||||
except e:
|
||||
print("Error:", e)
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
UncloseAI - Async Python Client (aiohttp)
|
||||
uncloseai. - Async Python Client (aiohttp)
|
||||
A Python async client library for OpenAI-compatible APIs with streaming support
|
||||
Compatible with vLLM, Ollama, and OpenAI-compatible endpoints
|
||||
"""
|
||||
|
|
@ -12,7 +12,7 @@ import os
|
|||
from typing import List, Dict, Optional, AsyncIterator
|
||||
|
||||
|
||||
class UncloseAI:
|
||||
class uncloseai:
|
||||
"""Async client for OpenAI-compatible API endpoints with streaming support"""
|
||||
|
||||
def __init__(
|
||||
|
|
@ -23,7 +23,7 @@ class UncloseAI:
|
|||
timeout: float = 30.0
|
||||
):
|
||||
"""
|
||||
Initialize UncloseAI async client
|
||||
Initialize uncloseai. async client
|
||||
|
||||
Args:
|
||||
model_endpoints: List of model endpoint URLs (defaults to MODEL_ENDPOINT_* env vars)
|
||||
|
|
@ -273,10 +273,10 @@ class UncloseAI:
|
|||
|
||||
# Demo usage when run as script
|
||||
async def main():
|
||||
print("=== UncloseAI Python Async Client (aiohttp) ===\n")
|
||||
print("=== uncloseai. Python Async Client (aiohttp) ===\n")
|
||||
|
||||
# Initialize client (auto-discovers from environment)
|
||||
client = UncloseAI()
|
||||
client = uncloseai()
|
||||
|
||||
models = await client.list_models()
|
||||
if not models:
|
||||
|
|
@ -327,7 +327,7 @@ async def main():
|
|||
if client.tts_endpoints:
|
||||
print("=== TTS Speech Generation ===")
|
||||
audio_data = await client.tts(
|
||||
text="Hello from UncloseAI Python async client with aiohttp! This demonstrates text to speech with streaming support.",
|
||||
text="Hello from uncloseai. Python async client with aiohttp! This demonstrates text to speech with streaming support.",
|
||||
voice="alloy"
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
UncloseAI - Async Python Client (httpx)
|
||||
uncloseai. - Async Python Client (httpx)
|
||||
A Python async client for OpenAI-compatible APIs with streaming support
|
||||
"""
|
||||
|
||||
|
|
@ -11,7 +11,7 @@ import asyncio
|
|||
from typing import List, Dict, Optional, AsyncIterator
|
||||
|
||||
|
||||
class UncloseAI:
|
||||
class uncloseai:
|
||||
"""Async client for OpenAI-compatible API endpoints with streaming support"""
|
||||
|
||||
def __init__(
|
||||
|
|
@ -203,9 +203,9 @@ class UncloseAI:
|
|||
|
||||
|
||||
async def main():
|
||||
print("=== UncloseAI Python Async Client (httpx) ===\n")
|
||||
print("=== uncloseai. Python Async Client (httpx) ===\n")
|
||||
|
||||
client = UncloseAI()
|
||||
client = uncloseai()
|
||||
|
||||
models = await client.list_models()
|
||||
if not models:
|
||||
|
|
@ -256,7 +256,7 @@ async def main():
|
|||
if client.tts_endpoints:
|
||||
print("=== TTS Speech Generation ===")
|
||||
audio_data = await client.tts(
|
||||
text="Hello from UncloseAI Python async client! This demonstrates text to speech with streaming support.",
|
||||
text="Hello from uncloseai. Python async client! This demonstrates text to speech with streaming support.",
|
||||
voice="alloy"
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,13 +0,0 @@
|
|||
# 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"]
|
||||
|
|
@ -1 +0,0 @@
|
|||
openai==2.3.0
|
||||
|
|
@ -1,305 +0,0 @@
|
|||
#!/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 ===")
|
||||
|
|
@ -1 +1,2 @@
|
|||
openai==2.3.0
|
||||
requests==2.32.5
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import os
|
|||
from typing import List, Dict, Optional, Iterator, Union
|
||||
|
||||
|
||||
class UncloseAI:
|
||||
class uncloseai:
|
||||
"""Client for OpenAI-compatible API endpoints with streaming support"""
|
||||
|
||||
def __init__(
|
||||
|
|
@ -22,7 +22,7 @@ class UncloseAI:
|
|||
timeout: int = 30
|
||||
):
|
||||
"""
|
||||
Initialize UncloseAI client with automatic model discovery
|
||||
Initialize uncloseai. client with automatic model discovery
|
||||
|
||||
Args:
|
||||
model_endpoints: List of model endpoint URLs (defaults to MODEL_ENDPOINT_* env vars)
|
||||
|
|
@ -259,10 +259,10 @@ class UncloseAI:
|
|||
|
||||
# Demo usage when run as script
|
||||
if __name__ == "__main__":
|
||||
print("=== UncloseAI Python Client (with Streaming) ===\n")
|
||||
print("=== uncloseai. Python Client (with Streaming) ===\n")
|
||||
|
||||
# Initialize client (auto-discovers from environment)
|
||||
client = UncloseAI()
|
||||
client = uncloseai()
|
||||
|
||||
if not client.models:
|
||||
print("ERROR: No models discovered. Set environment variables:")
|
||||
|
|
@ -316,7 +316,7 @@ if __name__ == "__main__":
|
|||
if client.tts_endpoints:
|
||||
print("=== TTS Speech Generation ===")
|
||||
audio_data = client.tts(
|
||||
text="Hello from UncloseAI Python client! This demonstrates text to speech with streaming support.",
|
||||
text="Hello from uncloseai. Python client! This demonstrates text to speech with streaming support.",
|
||||
voice="alloy"
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +0,0 @@
|
|||
# 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"]
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
# UncloseAI Ruby Client
|
||||
# uncloseai. Ruby Client
|
||||
|
||||
A Ruby client library for interacting with vLLM, Ollama, and OpenAI-compatible APIs.
|
||||
|
||||
|
|
@ -27,7 +27,7 @@ require_relative 'uncloseai_lib'
|
|||
require_relative 'uncloseai_lib'
|
||||
|
||||
# Initialize client (auto-discovers from environment variables)
|
||||
client = UncloseAI::Client.new
|
||||
client = uncloseai::Client.new
|
||||
|
||||
# Non-streaming chat
|
||||
response = client.chat(
|
||||
|
|
@ -57,7 +57,7 @@ export TTS_ENDPOINT_1="https://speech.ai.unturf.com/v1"
|
|||
### Programmatic Configuration
|
||||
|
||||
```ruby
|
||||
client = UncloseAI::Client.new(
|
||||
client = uncloseai::Client.new(
|
||||
endpoints: ['https://api.example.com/v1'],
|
||||
tts_endpoints: ['https://tts.example.com/v1'],
|
||||
api_key: 'your-api-key',
|
||||
|
|
@ -68,7 +68,7 @@ client = UncloseAI::Client.new(
|
|||
|
||||
## API Reference
|
||||
|
||||
### UncloseAI::Client
|
||||
### uncloseai::Client
|
||||
|
||||
#### `new(endpoints: nil, tts_endpoints: nil, api_key: nil, timeout: 30, debug: false)`
|
||||
|
||||
|
|
@ -95,7 +95,7 @@ Generate speech from text. Returns binary MP3 data.
|
|||
### Basic Chat
|
||||
|
||||
```ruby
|
||||
client = UncloseAI::Client.new
|
||||
client = uncloseai::Client.new
|
||||
|
||||
response = client.chat(
|
||||
[
|
||||
|
|
@ -122,7 +122,7 @@ end
|
|||
### Text-to-Speech
|
||||
|
||||
```ruby
|
||||
audio = client.tts('Hello from UncloseAI!', voice: 'alloy')
|
||||
audio = client.tts('Hello from uncloseai.!', voice: 'alloy')
|
||||
File.binwrite('speech.mp3', audio)
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -6,7 +6,8 @@ require 'uri'
|
|||
require 'json'
|
||||
|
||||
# uncloseai. - Ruby client for OpenAI-compatible APIs with streaming support
|
||||
class uncloseai
|
||||
# Ruby requires class names to be constants, so we use a class constant name
|
||||
class Uncloseai
|
||||
attr_reader :models, :tts_endpoints
|
||||
|
||||
def initialize(endpoints: nil, tts_endpoints: nil, api_key: nil, timeout: 30, debug: false)
|
||||
|
|
@ -175,7 +176,7 @@ end
|
|||
if __FILE__ == $0
|
||||
puts "=== uncloseai. Ruby Client (with Streaming) ===\n"
|
||||
|
||||
client = uncloseai.new(debug: true)
|
||||
client = Uncloseai.new(debug: true)
|
||||
|
||||
if client.models.empty?
|
||||
puts "ERROR: No models discovered. Set environment variables:"
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
source 'https://rubygems.org'
|
||||
|
||||
gem 'openai', '~> 0.30.0'
|
||||
gem 'openai', '~> 0.31.0'
|
||||
|
|
|
|||
|
|
@ -1,73 +1,101 @@
|
|||
#!/usr/bin/env ruby
|
||||
# UncloseAI - Ruby Client using Official OpenAI SDK
|
||||
# uncloseai. - Ruby Client using Official OpenAI SDK
|
||||
# A Ruby client for OpenAI-compatible APIs with streaming support
|
||||
|
||||
require 'openai'
|
||||
require 'net/http'
|
||||
require 'json'
|
||||
|
||||
puts "=== UncloseAI Ruby Client (Official OpenAI SDK) ===\n\n"
|
||||
puts "=== uncloseai. Ruby Client (Official OpenAI SDK) ===\n\n"
|
||||
|
||||
# Non-streaming chat with Hermes
|
||||
puts "=== Non-Streaming Chat (Hermes) ==="
|
||||
hermes_client = OpenAI::Client.new(
|
||||
access_token: "dummy-key",
|
||||
uri_base: "https://hermes.ai.unturf.com/v1"
|
||||
# Discover endpoints from environment variables
|
||||
model_endpoint_1 = ENV['MODEL_ENDPOINT_1']
|
||||
model_endpoint_2 = ENV['MODEL_ENDPOINT_2']
|
||||
tts_endpoint_1 = ENV['TTS_ENDPOINT_1']
|
||||
|
||||
if !model_endpoint_1 || !model_endpoint_2 || !tts_endpoint_1
|
||||
puts "ERROR: No models discovered. Set environment variables:"
|
||||
puts " MODEL_ENDPOINT_1, MODEL_ENDPOINT_2, TTS_ENDPOINT_1"
|
||||
exit 1
|
||||
end
|
||||
|
||||
# Discover models from endpoint 1
|
||||
puts "Discovering models from #{model_endpoint_1}..."
|
||||
uri = URI("#{model_endpoint_1}/models")
|
||||
response = Net::HTTP.get_response(uri)
|
||||
models_1 = JSON.parse(response.body)['data']
|
||||
model_1_id = models_1.first['id']
|
||||
puts "Model 1: #{model_1_id}\n\n"
|
||||
|
||||
# Discover models from endpoint 2
|
||||
puts "Discovering models from #{model_endpoint_2}..."
|
||||
uri = URI("#{model_endpoint_2}/models")
|
||||
response = Net::HTTP.get_response(uri)
|
||||
models_2 = JSON.parse(response.body)['data']
|
||||
model_2_id = models_2.first['id']
|
||||
puts "Model 2: #{model_2_id}\n\n"
|
||||
|
||||
# Non-streaming chat with Model 1
|
||||
puts "=== Non-Streaming Chat (Model 1) ==="
|
||||
client_1 = OpenAI::Client.new(
|
||||
api_key: "dummy-key",
|
||||
base_url: model_endpoint_1
|
||||
)
|
||||
|
||||
response = hermes_client.chat(
|
||||
parameters: {
|
||||
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
|
||||
}
|
||||
response = client_1.chat.completions.create(
|
||||
model: model_1_id,
|
||||
messages: [
|
||||
{ role: "user", content: "Give a Python Fizzbuzz solution in one line of code?" }
|
||||
],
|
||||
temperature: 0.5,
|
||||
max_tokens: 150
|
||||
)
|
||||
|
||||
puts "Response: #{response.dig('choices', 0, 'message', 'content')}\n\n"
|
||||
puts "Response: #{response.choices[0].message.content}\n\n"
|
||||
|
||||
# Streaming chat with Qwen
|
||||
puts "=== Streaming Chat (Qwen) ==="
|
||||
qwen_client = OpenAI::Client.new(
|
||||
access_token: "dummy-key",
|
||||
uri_base: "https://qwen.ai.unturf.com/v1"
|
||||
# Streaming chat with Model 2
|
||||
puts "=== Streaming Chat (Model 2) ==="
|
||||
client_2 = OpenAI::Client.new(
|
||||
api_key: "dummy-key",
|
||||
base_url: model_endpoint_2
|
||||
)
|
||||
|
||||
print "Response: "
|
||||
qwen_client.chat(
|
||||
parameters: {
|
||||
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: proc do |chunk, _bytesize|
|
||||
content = chunk.dig('choices', 0, 'delta', 'content')
|
||||
print content if content
|
||||
end
|
||||
}
|
||||
stream = client_2.chat.completions.stream_raw(
|
||||
model: model_2_id,
|
||||
messages: [
|
||||
{ role: "user", content: "Give a Python Fizzbuzz solution in one line of code?" }
|
||||
],
|
||||
temperature: 0.5,
|
||||
max_tokens: 150
|
||||
)
|
||||
stream.each do |chunk|
|
||||
content = chunk.choices&.first&.delta&.content
|
||||
print content if content
|
||||
end
|
||||
puts "\n\n"
|
||||
|
||||
# TTS example
|
||||
puts "=== TTS Speech Generation ==="
|
||||
tts_client = OpenAI::Client.new(
|
||||
access_token: "YOLO",
|
||||
uri_base: "https://speech.ai.unturf.com/v1"
|
||||
api_key: "dummy-key",
|
||||
base_url: tts_endpoint_1
|
||||
)
|
||||
|
||||
response = tts_client.audio.speech(
|
||||
parameters: {
|
||||
begin
|
||||
response = tts_client.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
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
File.binwrite("speech.mp3", response)
|
||||
file_size = File.size("speech.mp3")
|
||||
puts "[OK] Speech file created: speech.mp3 (#{file_size} bytes)\n\n"
|
||||
# Response is a StringIO object, need to read its content
|
||||
audio_content = response.read
|
||||
File.binwrite("speech.mp3", audio_content)
|
||||
puts "[OK] Speech file created: speech.mp3 (#{audio_content.bytesize} bytes)\n\n"
|
||||
rescue => e
|
||||
puts "[ERROR] TTS failed: #{e.class} - #{e.message}\n\n"
|
||||
end
|
||||
|
||||
puts "=== Examples Complete ==="
|
||||
|
|
|
|||
|
|
@ -1,225 +0,0 @@
|
|||
#!/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
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
# UncloseAI Rust Client
|
||||
# uncloseai. Rust Client
|
||||
|
||||
A Rust client library for interacting with vLLM, Ollama, and OpenAI-compatible APIs.
|
||||
|
||||
|
|
@ -33,12 +33,12 @@ uncloseai = { path = "../path/to/uncloseai" }
|
|||
## Quick Start
|
||||
|
||||
```rust
|
||||
use uncloseai::{UncloseAI, ChatMessage};
|
||||
use uncloseai::{uncloseai, ChatMessage};
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// Initialize client (auto-discovers from environment variables)
|
||||
let client = UncloseAI::new(None).await?;
|
||||
let client = uncloseai::new(None).await?;
|
||||
|
||||
// Non-streaming chat
|
||||
let response = client.chat(
|
||||
|
|
@ -83,9 +83,9 @@ export TTS_ENDPOINT_1="https://speech.ai.unturf.com/v1"
|
|||
### Programmatic Configuration
|
||||
|
||||
```rust
|
||||
use uncloseai::{UncloseAI, UncloseAIConfig};
|
||||
use uncloseai::{uncloseai, uncloseaiConfig};
|
||||
|
||||
let config = UncloseAIConfig {
|
||||
let config = uncloseaiConfig {
|
||||
endpoints: Some(vec!["https://api.example.com/v1".to_string()]),
|
||||
tts_endpoints: Some(vec!["https://tts.example.com/v1".to_string()]),
|
||||
api_key: Some("your-api-key".to_string()),
|
||||
|
|
@ -93,16 +93,16 @@ let config = UncloseAIConfig {
|
|||
debug: true,
|
||||
};
|
||||
|
||||
let client = UncloseAI::new(Some(config)).await?;
|
||||
let client = uncloseai::new(Some(config)).await?;
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### UncloseAI
|
||||
### uncloseai
|
||||
|
||||
Main client struct for interacting with AI APIs.
|
||||
|
||||
#### `async fn new(config: Option<UncloseAIConfig>) -> Result<UncloseAI, UncloseAIError>`
|
||||
#### `async fn new(config: Option<uncloseaiConfig>) -> Result<uncloseai, uncloseaiError>`
|
||||
|
||||
Initialize the client.
|
||||
|
||||
|
|
@ -110,7 +110,7 @@ Initialize the client.
|
|||
- `config` - Optional configuration. If None, uses defaults and auto-discovers from environment
|
||||
|
||||
**Returns:**
|
||||
- `Result<UncloseAI, UncloseAIError>` - Initialized client or error
|
||||
- `Result<uncloseai, uncloseaiError>` - Initialized client or error
|
||||
|
||||
**Example:**
|
||||
```rust
|
||||
|
|
@ -118,11 +118,11 @@ Initialize the client.
|
|||
let client = UncloseAI::new(None).await?;
|
||||
|
||||
// Explicit configuration
|
||||
let config = UncloseAIConfig {
|
||||
let config = uncloseaiConfig {
|
||||
endpoints: Some(vec!["https://api.example.com/v1".to_string()]),
|
||||
..Default::default()
|
||||
};
|
||||
let client = UncloseAI::new(Some(config)).await?;
|
||||
let client = uncloseai::new(Some(config)).await?;
|
||||
```
|
||||
|
||||
#### `fn list_models(&self) -> &[ModelInfo]`
|
||||
|
|
@ -140,7 +140,7 @@ for model in models {
|
|||
}
|
||||
```
|
||||
|
||||
#### `async fn chat(&self, model: &str, messages: Vec<ChatMessage>, options: Option<ChatOptions>) -> Result<ChatResponse, UncloseAIError>`
|
||||
#### `async fn chat(&self, model: &str, messages: Vec<ChatMessage>, options: Option<ChatOptions>) -> Result<ChatResponse, uncloseaiError>`
|
||||
|
||||
Send a non-streaming chat completion request.
|
||||
|
||||
|
|
@ -150,7 +150,7 @@ Send a non-streaming chat completion request.
|
|||
- `options` - Optional `ChatOptions` for max_tokens, temperature, etc.
|
||||
|
||||
**Returns:**
|
||||
- `Result<ChatResponse, UncloseAIError>` - Chat completion response or error
|
||||
- `Result<ChatResponse, uncloseaiError>` - Chat completion response or error
|
||||
|
||||
**Example:**
|
||||
```rust
|
||||
|
|
@ -170,7 +170,7 @@ let response = client.chat(
|
|||
println!("{}", response.choices[0].message.content);
|
||||
```
|
||||
|
||||
#### `async fn chat_stream(&self, model: &str, messages: Vec<ChatMessage>, options: Option<ChatOptions>) -> Result<impl Stream<Item = Result<ChatChunk, UncloseAIError>>, UncloseAIError>`
|
||||
#### `async fn chat_stream(&self, model: &str, messages: Vec<ChatMessage>, options: Option<ChatOptions>) -> Result<impl Stream<Item = Result<ChatChunk, uncloseaiError>>, uncloseaiError>`
|
||||
|
||||
Send a streaming chat completion request.
|
||||
|
||||
|
|
@ -178,7 +178,7 @@ Send a streaming chat completion request.
|
|||
- Same as `chat()`
|
||||
|
||||
**Returns:**
|
||||
- `Result<Stream<...>, UncloseAIError>` - Stream of chat chunks or error
|
||||
- `Result<Stream<...>, uncloseaiError>` - Stream of chat chunks or error
|
||||
|
||||
**Example:**
|
||||
```rust
|
||||
|
|
@ -199,7 +199,7 @@ while let Some(chunk) = stream.next().await {
|
|||
}
|
||||
```
|
||||
|
||||
#### `async fn tts(&self, text: &str, voice: &str, model: &str) -> Result<Vec<u8>, UncloseAIError>`
|
||||
#### `async fn tts(&self, text: &str, voice: &str, model: &str) -> Result<Vec<u8>, uncloseaiError>`
|
||||
|
||||
Generate speech from text.
|
||||
|
||||
|
|
@ -209,7 +209,7 @@ Generate speech from text.
|
|||
- `model` - TTS model (tts-1 or tts-1-hd)
|
||||
|
||||
**Returns:**
|
||||
- `Result<Vec<u8>, UncloseAIError>` - Audio data (MP3 format) or error
|
||||
- `Result<Vec<u8>, uncloseaiError>` - Audio data (MP3 format) or error
|
||||
|
||||
**Example:**
|
||||
```rust
|
||||
|
|
@ -254,7 +254,7 @@ Information about a discovered model.
|
|||
- `endpoint: String` - Endpoint URL
|
||||
- `max_tokens: u32` - Maximum context length
|
||||
|
||||
#### `UncloseAIError`
|
||||
#### `uncloseaiError`
|
||||
|
||||
Error types for the library.
|
||||
|
||||
|
|
@ -269,11 +269,11 @@ Error types for the library.
|
|||
### Basic Chat
|
||||
|
||||
```rust
|
||||
use uncloseai::{UncloseAI, ChatMessage, ChatOptions};
|
||||
use uncloseai::{uncloseai, ChatMessage, ChatOptions};
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let client = UncloseAI::new(None).await?;
|
||||
let client = uncloseai::new(None).await?;
|
||||
|
||||
let response = client.chat(
|
||||
"auto",
|
||||
|
|
@ -295,12 +295,12 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||
### Streaming Chat
|
||||
|
||||
```rust
|
||||
use uncloseai::{UncloseAI, ChatMessage};
|
||||
use uncloseai::{uncloseai, ChatMessage};
|
||||
use futures_util::StreamExt;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let client = UncloseAI::new(None).await?;
|
||||
let client = uncloseai::new(None).await?;
|
||||
|
||||
let mut stream = client.chat_stream(
|
||||
"auto",
|
||||
|
|
@ -346,7 +346,7 @@ let response2 = client.chat("auto", messages, None).await?;
|
|||
use std::fs::File;
|
||||
use std::io::Write;
|
||||
|
||||
let audio = client.tts("Hello from UncloseAI!", "alloy", "tts-1").await?;
|
||||
let audio = client.tts("Hello from uncloseai.!", "alloy", "tts-1").await?;
|
||||
let mut file = File::create("output.mp3")?;
|
||||
file.write_all(&audio)?;
|
||||
```
|
||||
|
|
@ -371,12 +371,12 @@ let response = client.chat(
|
|||
### Error Handling
|
||||
|
||||
```rust
|
||||
use uncloseai::{UncloseAI, UncloseAIError, ChatMessage};
|
||||
use uncloseai::{uncloseai, uncloseaiError, ChatMessage};
|
||||
|
||||
match client.chat("non-existent-model", vec![ChatMessage::user("Hello")], None).await {
|
||||
Ok(response) => println!("{}", response.choices[0].message.content),
|
||||
Err(UncloseAIError::ModelNotFoundError(msg)) => println!("Model error: {}", msg),
|
||||
Err(UncloseAIError::ConnectionError(msg)) => println!("Connection error: {}", msg),
|
||||
Err(uncloseaiError::ModelNotFoundError(msg)) => println!("Model error: {}", msg),
|
||||
Err(uncloseaiError::ConnectionError(msg)) => println!("Connection error: {}", msg),
|
||||
Err(e) => println!("Other error: {}", e),
|
||||
}
|
||||
```
|
||||
|
|
|
|||
26
languages/scheme/Dockerfile
Normal file
26
languages/scheme/Dockerfile
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
# Build GNU Guile 3.0.10 (checked 2025-10-15: latest stable)
|
||||
FROM debian:bookworm-slim
|
||||
|
||||
# Install build dependencies and runtime libraries
|
||||
RUN apt-get update && \
|
||||
apt-get install -y \
|
||||
guile-3.0 \
|
||||
guile-3.0-dev \
|
||||
curl \
|
||||
ca-certificates && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Copy Scheme scripts
|
||||
COPY hermes-nonstreaming.scm .
|
||||
COPY hermes-streaming.scm .
|
||||
COPY qwen-nonstreaming.scm .
|
||||
COPY qwen-streaming.scm .
|
||||
COPY tts.scm .
|
||||
|
||||
# Make scripts executable
|
||||
RUN chmod +x *.scm
|
||||
|
||||
# Default command shows available examples
|
||||
CMD ["sh", "-c", "echo 'Available examples:' && echo ' guile hermes-nonstreaming.scm' && echo ' guile hermes-streaming.scm' && echo ' guile qwen-nonstreaming.scm' && echo ' guile qwen-streaming.scm' && echo ' guile tts.scm'"]
|
||||
43
languages/scheme/hermes-nonstreaming.scm
Normal file
43
languages/scheme/hermes-nonstreaming.scm
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
#!/usr/bin/env guile
|
||||
!#
|
||||
|
||||
;;; Hermes AI Non-Streaming Example in GNU Guile
|
||||
;;; Uses (web client) and (json) modules
|
||||
|
||||
(use-modules (web client)
|
||||
(web response)
|
||||
(ice-9 textual-ports)
|
||||
(json))
|
||||
|
||||
(define base-url "https://hermes.ai.unturf.com/v1/chat/completions")
|
||||
(define model "adamo1139/Hermes-3-Llama-3.1-8B-FP8-Dynamic")
|
||||
|
||||
;; Create JSON payload
|
||||
(define payload
|
||||
(scm->json-string
|
||||
`((model . ,model)
|
||||
(messages . #(((role . "user")
|
||||
(content . "Give a Python Fizzbuzz solution in one line of code?"))))
|
||||
(temperature . 0.5)
|
||||
(max_tokens . 150)
|
||||
(stream . #f))))
|
||||
|
||||
(display "Requesting from Hermes AI...\n\n")
|
||||
|
||||
(catch #t
|
||||
(lambda ()
|
||||
;; Make POST request
|
||||
(call-with-values
|
||||
(lambda ()
|
||||
(http-post base-url
|
||||
#:body payload
|
||||
#:headers '((Content-Type . "application/json"))))
|
||||
(lambda (response body)
|
||||
;; Parse JSON response
|
||||
(let* ((json-response (json-string->scm (utf8->string body)))
|
||||
(choices (assoc-ref json-response "choices"))
|
||||
(message (assoc-ref (vector-ref choices 0) "message"))
|
||||
(content (assoc-ref message "content")))
|
||||
(format #t "Response: ~a\n" content)))))
|
||||
(lambda (key . args)
|
||||
(format #t "Error: ~a ~a\n" key args)))
|
||||
59
languages/scheme/hermes-streaming.scm
Normal file
59
languages/scheme/hermes-streaming.scm
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
#!/usr/bin/env guile
|
||||
!#
|
||||
|
||||
;;; Hermes AI Streaming Example in GNU Guile
|
||||
;;; Uses (web client) for streaming SSE responses
|
||||
|
||||
(use-modules (web client)
|
||||
(web response)
|
||||
(ice-9 textual-ports)
|
||||
(ice-9 rdelim)
|
||||
(json)
|
||||
(srfi srfi-1))
|
||||
|
||||
(define base-url "https://hermes.ai.unturf.com/v1/chat/completions")
|
||||
(define model "adamo1139/Hermes-3-Llama-3.1-8B-FP8-Dynamic")
|
||||
|
||||
;; Create JSON payload for streaming
|
||||
(define payload
|
||||
(scm->json-string
|
||||
`((model . ,model)
|
||||
(messages . #(((role . "user")
|
||||
(content . "Give a Python Fizzbuzz solution in one line of code?"))))
|
||||
(temperature . 0.5)
|
||||
(max_tokens . 150)
|
||||
(stream . #t))))
|
||||
|
||||
(display "Streaming from Hermes AI...\n\n")
|
||||
|
||||
(catch #t
|
||||
(lambda ()
|
||||
;; Make streaming POST request
|
||||
(call-with-values
|
||||
(lambda ()
|
||||
(http-post base-url
|
||||
#:body payload
|
||||
#:headers '((Content-Type . "application/json"))
|
||||
#:streaming? #t))
|
||||
(lambda (response port)
|
||||
;; Read and process SSE stream line by line
|
||||
(let loop ((line (read-line port)))
|
||||
(unless (eof-object? line)
|
||||
(when (string-prefix? "data: " line)
|
||||
(let ((json-data (substring line 6)))
|
||||
(unless (string=? json-data "[DONE]")
|
||||
(catch #t
|
||||
(lambda ()
|
||||
(let* ((parsed (json-string->scm json-data))
|
||||
(choices (assoc-ref parsed "choices")))
|
||||
(when (and choices (> (vector-length choices) 0))
|
||||
(let* ((delta (assoc-ref (vector-ref choices 0) "delta"))
|
||||
(content (assoc-ref delta "content")))
|
||||
(when content
|
||||
(display content)
|
||||
(force-output))))))
|
||||
(lambda (key . args) #f)))))
|
||||
(loop (read-line port)))))
|
||||
(display "\n\nDone!\n"))))
|
||||
(lambda (key . args)
|
||||
(format #t "\nError: ~a ~a\n" key args)))
|
||||
39
languages/scheme/qwen-nonstreaming.scm
Normal file
39
languages/scheme/qwen-nonstreaming.scm
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
#!/usr/bin/env guile
|
||||
!#
|
||||
|
||||
;;; Qwen 3 Coder Non-Streaming Example in GNU Guile
|
||||
|
||||
(use-modules (web client)
|
||||
(web response)
|
||||
(ice-9 textual-ports)
|
||||
(json))
|
||||
|
||||
(define base-url "https://qwen.ai.unturf.com/v1/chat/completions")
|
||||
(define model "hf.co/unsloth/Qwen3-Coder-30B-A3B-Instruct-GGUF:Q4_K_M")
|
||||
|
||||
(define payload
|
||||
(scm->json-string
|
||||
`((model . ,model)
|
||||
(messages . #(((role . "user")
|
||||
(content . "Give a Python Fizzbuzz solution in one line of code?"))))
|
||||
(temperature . 0.5)
|
||||
(max_tokens . 150)
|
||||
(stream . #f))))
|
||||
|
||||
(display "Requesting from Qwen 3 Coder...\n\n")
|
||||
|
||||
(catch #t
|
||||
(lambda ()
|
||||
(call-with-values
|
||||
(lambda ()
|
||||
(http-post base-url
|
||||
#:body payload
|
||||
#:headers '((Content-Type . "application/json"))))
|
||||
(lambda (response body)
|
||||
(let* ((json-response (json-string->scm (utf8->string body)))
|
||||
(choices (assoc-ref json-response "choices"))
|
||||
(message (assoc-ref (vector-ref choices 0) "message"))
|
||||
(content (assoc-ref message "content")))
|
||||
(format #t "Response: ~a\n" content)))))
|
||||
(lambda (key . args)
|
||||
(format #t "Error: ~a ~a\n" key args)))
|
||||
55
languages/scheme/qwen-streaming.scm
Normal file
55
languages/scheme/qwen-streaming.scm
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
#!/usr/bin/env guile
|
||||
!#
|
||||
|
||||
;;; Qwen 3 Coder Streaming Example in GNU Guile
|
||||
|
||||
(use-modules (web client)
|
||||
(web response)
|
||||
(ice-9 textual-ports)
|
||||
(ice-9 rdelim)
|
||||
(json)
|
||||
(srfi srfi-1))
|
||||
|
||||
(define base-url "https://qwen.ai.unturf.com/v1/chat/completions")
|
||||
(define model "hf.co/unsloth/Qwen3-Coder-30B-A3B-Instruct-GGUF:Q4_K_M")
|
||||
|
||||
(define payload
|
||||
(scm->json-string
|
||||
`((model . ,model)
|
||||
(messages . #(((role . "user")
|
||||
(content . "Give a Python Fizzbuzz solution in one line of code?"))))
|
||||
(temperature . 0.5)
|
||||
(max_tokens . 150)
|
||||
(stream . #t))))
|
||||
|
||||
(display "Streaming from Qwen 3 Coder...\n\n")
|
||||
|
||||
(catch #t
|
||||
(lambda ()
|
||||
(call-with-values
|
||||
(lambda ()
|
||||
(http-post base-url
|
||||
#:body payload
|
||||
#:headers '((Content-Type . "application/json"))
|
||||
#:streaming? #t))
|
||||
(lambda (response port)
|
||||
(let loop ((line (read-line port)))
|
||||
(unless (eof-object? line)
|
||||
(when (string-prefix? "data: " line)
|
||||
(let ((json-data (substring line 6)))
|
||||
(unless (string=? json-data "[DONE]")
|
||||
(catch #t
|
||||
(lambda ()
|
||||
(let* ((parsed (json-string->scm json-data))
|
||||
(choices (assoc-ref parsed "choices")))
|
||||
(when (and choices (> (vector-length choices) 0))
|
||||
(let* ((delta (assoc-ref (vector-ref choices 0) "delta"))
|
||||
(content (assoc-ref delta "content")))
|
||||
(when content
|
||||
(display content)
|
||||
(force-output))))))
|
||||
(lambda (key . args) #f)))))
|
||||
(loop (read-line port)))))
|
||||
(display "\n\nDone!\n"))))
|
||||
(lambda (key . args)
|
||||
(format #t "\nError: ~a ~a\n" key args)))
|
||||
37
languages/scheme/tts.scm
Normal file
37
languages/scheme/tts.scm
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
#!/usr/bin/env guile
|
||||
!#
|
||||
|
||||
;;; Text-to-Speech Example in GNU Guile
|
||||
|
||||
(use-modules (web client)
|
||||
(web response)
|
||||
(ice-9 binary-ports)
|
||||
(json))
|
||||
|
||||
(define base-url "https://speech.ai.unturf.com/v1/audio/speech")
|
||||
(define output-file "speech.mp3")
|
||||
|
||||
(define payload
|
||||
(scm->json-string
|
||||
'((model . "tts-1")
|
||||
(voice . "alloy")
|
||||
(speed . 0.9)
|
||||
(input . "I think so therefore, Today is a wonderful day to grow something people love!"))))
|
||||
|
||||
(display "Generating speech from TTS...\n\n")
|
||||
|
||||
(catch #t
|
||||
(lambda ()
|
||||
(call-with-values
|
||||
(lambda ()
|
||||
(http-post base-url
|
||||
#:body payload
|
||||
#:headers '((Content-Type . "application/json"))))
|
||||
(lambda (response body)
|
||||
;; Write binary response to file
|
||||
(call-with-output-file output-file
|
||||
(lambda (port)
|
||||
(put-bytevector port body)))
|
||||
(format #t "Speech saved to: ~a\n" output-file))))
|
||||
(lambda (key . args)
|
||||
(format #t "Error: ~a ~a\n" key args)))
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
/* UncloseAI Modal - Built-in Styles (for blog versions without PicoCSS) */
|
||||
/* uncloseai. Modal - Built-in Styles (for blog versions without PicoCSS) */
|
||||
|
||||
/* Main modal dialog */
|
||||
dialog#uncloseai-embedded-modal {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
/* UncloseAI Modal - PicoCSS Override Styles */
|
||||
/* uncloseai. Modal - PicoCSS Override Styles */
|
||||
|
||||
/* Remove PicoCSS backdrop for main modal */
|
||||
dialog#uncloseai-embedded-modal::backdrop {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue