diff --git a/public/languages/commonlisp/Dockerfile b/public/languages/commonlisp/Dockerfile index 21b7b35..57c63fb 100644 --- a/public/languages/commonlisp/Dockerfile +++ b/public/languages/commonlisp/Dockerfile @@ -15,11 +15,7 @@ RUN sbcl --eval '(ql:quickload :dexador)' \ WORKDIR /app # Copy application files -COPY hermes-nonstreaming.lisp . -COPY hermes-streaming.lisp . -COPY qwen-nonstreaming.lisp . -COPY qwen-streaming.lisp . -COPY tts.lisp . +COPY uncloseai.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"] +# Default command runs the demo +CMD ["sbcl", "--script", "uncloseai.lisp"] diff --git a/public/languages/commonlisp/hermes-nonstreaming.lisp b/public/languages/commonlisp/hermes-nonstreaming.lisp deleted file mode 100644 index 61c6aca..0000000 --- a/public/languages/commonlisp/hermes-nonstreaming.lisp +++ /dev/null @@ -1,37 +0,0 @@ -#!/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))) diff --git a/public/languages/commonlisp/hermes-streaming.lisp b/public/languages/commonlisp/hermes-streaming.lisp deleted file mode 100644 index 74ab9b8..0000000 --- a/public/languages/commonlisp/hermes-streaming.lisp +++ /dev/null @@ -1,55 +0,0 @@ -#!/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))) diff --git a/public/languages/commonlisp/qwen-nonstreaming.lisp b/public/languages/commonlisp/qwen-nonstreaming.lisp deleted file mode 100644 index 36be786..0000000 --- a/public/languages/commonlisp/qwen-nonstreaming.lisp +++ /dev/null @@ -1,37 +0,0 @@ -#!/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))) diff --git a/public/languages/commonlisp/qwen-streaming.lisp b/public/languages/commonlisp/qwen-streaming.lisp deleted file mode 100644 index 2338de5..0000000 --- a/public/languages/commonlisp/qwen-streaming.lisp +++ /dev/null @@ -1,55 +0,0 @@ -#!/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))) diff --git a/public/languages/commonlisp/tts.lisp b/public/languages/commonlisp/tts.lisp deleted file mode 100644 index 4f89869..0000000 --- a/public/languages/commonlisp/tts.lisp +++ /dev/null @@ -1,38 +0,0 @@ -#!/usr/bin/env sbcl --script - -;;; Text-to-Speech Example -;;; Uses Dexador to download audio file from TTS endpoint - -(load "~/quicklisp/setup.lisp") -(ql:quickload '(:dexador :jonathan) :silent t) - -(defparameter *base-url* "https://speech.ai.unturf.com/v1/audio/speech") - -(defun generate-speech (text output-file) - "Generate speech from text and save to file" - (let ((payload (jonathan:to-json - (list :|model| "tts-1" - :|voice| "alloy" - :|speed| 0.9 - :|input| text)))) - (with-open-file (out output-file - :direction :output - :if-exists :supersede - :if-does-not-exist :create - :element-type '(unsigned-byte 8)) - (let ((response (dex:post *base-url* - :headers '(("Content-Type" . "application/json")) - :content payload - :force-binary t))) - (write-sequence response out))) - (format t "Speech saved to: ~A~%" output-file))) - -;; Main execution -(handler-case - (progn - (format t "Generating speech from TTS...~%~%") - (generate-speech - "I think so therefore, Today is a wonderful day to grow something people love!" - "speech.mp3")) - (error (e) - (format t "Error: ~A~%" e))) diff --git a/public/languages/commonlisp/uncloseai.lisp b/public/languages/commonlisp/uncloseai.lisp new file mode 100644 index 0000000..1886192 --- /dev/null +++ b/public/languages/commonlisp/uncloseai.lisp @@ -0,0 +1,302 @@ +#!/usr/bin/env sbcl --script + +;;; uncloseai - Common Lisp client library for OpenAI-compatible APIs +;;; Supports streaming and non-streaming chat, model discovery, and TTS +;;; Compatible with vLLM, Ollama, and OpenAI-compatible endpoints +;;; +;;; Uses Dexador HTTP client and Jonathan JSON library + +(load "~/quicklisp/setup.lisp") +(ql:quickload '(:dexador :jonathan) :silent t) + +;;; Data structures + +(defclass uncloseai () + ((models :initform nil :accessor models + :documentation "List of discovered models with metadata") + (tts-endpoints :initform nil :accessor tts-endpoints + :documentation "List of TTS endpoint URLs") + (api-key :initform nil :initarg :api-key :accessor api-key + :documentation "Optional API key for authentication") + (timeout :initform 30 :initarg :timeout :accessor timeout + :documentation "Request timeout in seconds"))) + +(defun make-model-info (id endpoint max-tokens) + "Create a model info plist" + (list :id id :endpoint endpoint :max-tokens max-tokens)) + +;;; Environment discovery + +(defun discover-env-endpoints (prefix) + "Discover endpoints from environment variables like PREFIX_1, PREFIX_2, ..." + (loop for i from 1 to 9999 + for var-name = (format nil "~A_~D" prefix i) + for endpoint = (uiop:getenv var-name) + while endpoint + collect endpoint)) + +;;; Model discovery + +(defun discover-models-from-endpoint (client endpoint) + "Discover available models from an endpoint" + (handler-case + (let* ((url (concatenate 'string endpoint "/models")) + (headers (when (api-key client) + (list (cons "Authorization" + (format nil "Bearer ~A" (api-key client)))))) + (response (dex:get url :headers headers)) + (parsed (jonathan:parse response)) + (data (getf parsed :|data|))) + (loop for model across data + for model-id = (getf model :|id|) + for max-tokens = (or (getf model :|max_model_len|) 8192) + do (push (make-model-info model-id endpoint max-tokens) + (models client)))) + (error (e) + (format t "Warning: Failed to discover models from ~A: ~A~%" endpoint e)))) + +(defun initialize-client (client model-endpoints tts-endpoints) + "Initialize client with endpoint discovery and model detection" + ;; Discover endpoints from environment if not provided + (let ((model-eps (or model-endpoints (discover-env-endpoints "MODEL_ENDPOINT"))) + (tts-eps (or tts-endpoints (discover-env-endpoints "TTS_ENDPOINT")))) + + ;; Discover models from each endpoint + (dolist (endpoint model-eps) + (discover-models-from-endpoint client endpoint)) + + ;; Reverse models list (they were pushed in reverse order) + (setf (models client) (nreverse (models client))) + + ;; Store TTS endpoints + (setf (tts-endpoints client) tts-eps)) + + client) + +(defun make-uncloseai (&key model-endpoints tts-endpoints api-key (timeout 30)) + "Create a new uncloseai client with auto-discovery from environment variables" + (let ((client (make-instance 'uncloseai + :api-key api-key + :timeout timeout))) + (initialize-client client model-endpoints tts-endpoints))) + +;;; Helper functions + +(defun get-model-info (client model-id) + "Get model info by ID or return first available model" + (when (null (models client)) + (error "No models available. Check endpoint configuration.")) + + (if (null model-id) + (first (models client)) + (or (find model-id (models client) :key (lambda (m) (getf m :id)) :test #'string=) + (error "Model '~A' not found in discovered models" model-id)))) + +(defun make-headers (client &optional (content-type t)) + "Create HTTP headers with optional authorization" + (let ((headers nil)) + (when content-type + (push (cons "Content-Type" "application/json") headers)) + (when (api-key client) + (push (cons "Authorization" + (format nil "Bearer ~A" (api-key client))) + headers)) + headers)) + +;;; Non-streaming chat completion + +(defun chat (client messages &key model (max-tokens 100) (temperature 0.7)) + "Non-streaming chat completion + + Args: + messages - List of message plists with :role and :content + model - Model ID (defaults to first available model) + max-tokens - Maximum tokens in response + temperature - Sampling temperature + + Returns: + Response plist with :choices containing the completion" + (let* ((model-info (get-model-info client model)) + (endpoint (getf model-info :endpoint)) + (model-id (getf model-info :id)) + (url (concatenate 'string endpoint "/chat/completions")) + (payload (jonathan:to-json + (list :|model| model-id + :|messages| (coerce messages 'vector) + :|max_tokens| max-tokens + :|temperature| temperature + :|stream| :false))) + (headers (make-headers client)) + (response (dex:post url + :headers headers + :content payload))) + (jonathan:parse response))) + +;;; Streaming chat completion + +(defun process-sse-line (line) + "Process a single Server-Sent Event line, return parsed content or nil" + (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|)))) + content) + (error (e) nil)))))) + +(defun chat-stream (client messages callback &key model (max-tokens 500) (temperature 0.7)) + "Streaming chat completion using Server-Sent Events + + Args: + messages - List of message plists with :role and :content + callback - Function to call for each content chunk (receives string) + model - Model ID (defaults to first available model) + max-tokens - Maximum tokens in response + temperature - Sampling temperature" + (let* ((model-info (get-model-info client model)) + (endpoint (getf model-info :endpoint)) + (model-id (getf model-info :id)) + (url (concatenate 'string endpoint "/chat/completions")) + (payload (jonathan:to-json + (list :|model| model-id + :|messages| (coerce messages 'vector) + :|max_tokens| max-tokens + :|temperature| temperature + :|stream| t))) + (headers (make-headers client))) + (dex:request url + :method :post + :headers headers + :content payload + :want-stream t + :stream-callback + (lambda (stream) + (loop for line = (read-line stream nil nil) + while line + do (let ((content (process-sse-line line))) + (when content + (funcall callback content)))))))) + +;;; Text-to-Speech + +(defun tts (client text &key (voice "alloy") (model "tts-1") (response-format "mp3")) + "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) + response-format - Audio format (mp3, opus, aac, flac) + + Returns: + Audio data as byte array" + (when (null (tts-endpoints client)) + (error "No TTS endpoints available")) + + (let* ((endpoint (first (tts-endpoints client))) + (url (concatenate 'string endpoint "/audio/speech")) + (payload (jonathan:to-json + (list :|model| model + :|voice| voice + :|input| text + :|response_format| response-format))) + (headers (make-headers client)) + (response (dex:post url + :headers headers + :content payload + :force-binary t))) + response)) + +;;; Demo usage + +(defun demo-nonstreaming (client) + "Demonstrate non-streaming chat" + (format t "=== Non-Streaming Chat ===~%") + (let* ((response (chat client + (list (list :|role| "system" + :|content| "You are a helpful AI assistant.") + (list :|role| "user" + :|content| "Explain quantum computing in one sentence.")) + :max-tokens 100 + :temperature 0.7)) + (model-used (getf response :|model|)) + (choice (aref (getf response :|choices|) 0)) + (message (getf choice :|message|)) + (content (getf message :|content|))) + (format t "Model: ~A~%" model-used) + (format t "Response: ~A~%~%" content))) + +(defun demo-streaming (client) + "Demonstrate streaming chat" + (format t "=== Streaming Chat ===~%") + (let ((model-id (if (> (length (models client)) 1) + (getf (second (models client)) :id) + nil))) + (format t "Model: ~A~%" (or model-id (getf (first (models client)) :id))) + (format t "Response: ") + (force-output) + + (chat-stream client + (list (list :|role| "system" + :|content| "You are a coding assistant.") + (list :|role| "user" + :|content| "Write a Common Lisp function to check if a number is prime")) + (lambda (content) + (format t "~A" content) + (force-output)) + :model model-id + :max-tokens 200 + :temperature 0.7) + + (format t "~%~%"))) + +(defun demo-tts (client) + "Demonstrate text-to-speech" + (when (tts-endpoints client) + (format t "=== TTS Speech Generation ===~%") + (let ((audio-data (tts client + "Hello from uncloseai Common Lisp client! This demonstrates text to speech with streaming support." + :voice "alloy"))) + (with-open-file (out "speech.mp3" + :direction :output + :if-exists :supersede + :if-does-not-exist :create + :element-type '(unsigned-byte 8)) + (write-sequence audio-data out)) + (format t "[OK] Speech file created: speech.mp3 (~D bytes)~%~%" (length audio-data))))) + +(defun main () + "Main demo function" + (format t "=== uncloseai Common Lisp Client (with Streaming) ===~%~%") + + ;; Initialize client (auto-discovers from environment) + (let ((client (make-uncloseai))) + + (when (null (models client)) + (format t "ERROR: No models discovered. Set environment variables:~%") + (format t " MODEL_ENDPOINT_1, MODEL_ENDPOINT_2, etc.~%") + (uiop:quit 1)) + + (format t "Discovered ~D model(s)~%" (length (models client))) + (dolist (model (models client)) + (format t " - ~A (max_tokens: ~D)~%" + (getf model :id) + (getf model :max-tokens))) + (format t "~%") + + ;; Run demos + (handler-case + (progn + (demo-nonstreaming client) + (demo-streaming client) + (demo-tts client) + (format t "=== Examples Complete ===~%")) + (error (e) + (format t "~%Error: ~A~%" e))))) + +;; Main execution +(main) diff --git a/public/languages/scheme/Dockerfile b/public/languages/scheme/Dockerfile index a4936ba..cd911c7 100644 --- a/public/languages/scheme/Dockerfile +++ b/public/languages/scheme/Dockerfile @@ -12,15 +12,11 @@ RUN apt-get update && \ WORKDIR /app -# Copy Scheme scripts -COPY hermes-nonstreaming.scm . -COPY hermes-streaming.scm . -COPY qwen-nonstreaming.scm . -COPY qwen-streaming.scm . -COPY tts.scm . +# Copy Scheme script +COPY uncloseai.scm . -# Make scripts executable -RUN chmod +x *.scm +# Make script executable +RUN chmod +x uncloseai.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'"] +# Default command runs the demo +CMD ["guile", "uncloseai.scm"] diff --git a/public/languages/scheme/hermes-nonstreaming.scm b/public/languages/scheme/hermes-nonstreaming.scm deleted file mode 100644 index e605efe..0000000 --- a/public/languages/scheme/hermes-nonstreaming.scm +++ /dev/null @@ -1,43 +0,0 @@ -#!/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))) diff --git a/public/languages/scheme/hermes-streaming.scm b/public/languages/scheme/hermes-streaming.scm deleted file mode 100644 index f3fa99e..0000000 --- a/public/languages/scheme/hermes-streaming.scm +++ /dev/null @@ -1,59 +0,0 @@ -#!/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))) diff --git a/public/languages/scheme/qwen-nonstreaming.scm b/public/languages/scheme/qwen-nonstreaming.scm deleted file mode 100644 index 79a4907..0000000 --- a/public/languages/scheme/qwen-nonstreaming.scm +++ /dev/null @@ -1,39 +0,0 @@ -#!/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))) diff --git a/public/languages/scheme/qwen-streaming.scm b/public/languages/scheme/qwen-streaming.scm deleted file mode 100644 index c54ab47..0000000 --- a/public/languages/scheme/qwen-streaming.scm +++ /dev/null @@ -1,55 +0,0 @@ -#!/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))) diff --git a/public/languages/scheme/tts.scm b/public/languages/scheme/tts.scm deleted file mode 100644 index c870635..0000000 --- a/public/languages/scheme/tts.scm +++ /dev/null @@ -1,37 +0,0 @@ -#!/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))) diff --git a/public/languages/scheme/uncloseai.scm b/public/languages/scheme/uncloseai.scm new file mode 100644 index 0000000..8aa0c12 --- /dev/null +++ b/public/languages/scheme/uncloseai.scm @@ -0,0 +1,263 @@ +#!/usr/bin/env guile +!# + +;;; UncloseAI GNU Guile Library - OpenAI-compatible API client with streaming support +;;; Compatible with vLLM, Ollama, and OpenAI-compatible endpoints +;;; +;;; Library Functions: +;;; (uncloseai-init) - Initialize client with model discovery +;;; (uncloseai-list-models) - List discovered models +;;; (uncloseai-chat messages model-id) - Non-streaming chat completion +;;; (uncloseai-chat-stream messages model-id) - Streaming chat completion +;;; (uncloseai-tts text voice model) - Text-to-speech generation + +(use-modules (web client) + (web response) + (ice-9 textual-ports) + (ice-9 rdelim) + (ice-9 binary-ports) + (json) + (srfi srfi-1)) + +;;; Global client state +(define *model-ids* '()) +(define *model-endpoints* '()) +(define *model-max-tokens* '()) +(define *tts-endpoints* '()) + +;;; Initialize client and discover models from environment variables +(define (uncloseai-init) + (set! *model-ids* '()) + (set! *model-endpoints* '()) + (set! *model-max-tokens* '()) + (set! *tts-endpoints* '()) + + ;; Discover chat/code models from MODEL_ENDPOINT_N (N = 1 to 9999) + (let loop ((i 1)) + (when (<= i 9999) + (let ((endpoint (getenv (string-append "MODEL_ENDPOINT_" (number->string i))))) + (if endpoint + (begin + ;; Fetch models from endpoint + (catch #t + (lambda () + (call-with-values + (lambda () + (http-get (string-append endpoint "/models"))) + (lambda (response body) + (when (= (response-code response) 200) + (let* ((json-response (json-string->scm (utf8->string body))) + (models-data (assoc-ref json-response "data"))) + (when models-data + ;; Process each model in the response + (vector-for-each + (lambda (model) + (let ((model-id (assoc-ref model "id"))) + ;; Skip modelperm entries + (when (and model-id + (not (string-prefix? "modelperm-" model-id))) + ;; Extract max_model_len if available (vLLM) + (let ((max-tokens (or (assoc-ref model "max_model_len") 8192))) + (set! *model-ids* (append *model-ids* (list model-id))) + (set! *model-endpoints* (append *model-endpoints* (list endpoint))) + (set! *model-max-tokens* (append *model-max-tokens* (list max-tokens))))))) + models-data))))))) + (lambda (key . args) + ;; Silently skip endpoints that fail + #f)) + (loop (+ i 1))) + #f)))) + + ;; Discover TTS endpoints from TTS_ENDPOINT_N (N = 1 to 9999) + (let loop ((i 1)) + (when (<= i 9999) + (let ((endpoint (getenv (string-append "TTS_ENDPOINT_" (number->string i))))) + (if endpoint + (begin + (set! *tts-endpoints* (append *tts-endpoints* (list endpoint))) + (loop (+ i 1))) + #f)))) + + (length *model-ids*)) + +;;; List all discovered models +(define (uncloseai-list-models) + (for-each + (lambda (model-id max-tokens) + (format #t " - ~a (max_tokens: ~a)\n" model-id max-tokens)) + *model-ids* + *model-max-tokens*)) + +;;; Get model index by ID or return 0 for first model +(define (uncloseai-get-model-idx model-id) + (if (or (not model-id) (string=? model-id "")) + 0 ;; Return first model index + (let loop ((i 0)) + (cond + ((>= i (length *model-ids*)) -1) ;; Not found + ((string=? (list-ref *model-ids* i) model-id) i) + (else (loop (+ i 1))))))) + +;;; Non-streaming chat completion +;;; Usage: (uncloseai-chat messages model-id max-tokens temperature) +;;; messages: vector of message objects like #(((role . "user") (content . "..."))) +;;; Returns: content string +(define* (uncloseai-chat messages #:optional (model-id "") (max-tokens 100) (temperature 0.7)) + (let ((model-idx (uncloseai-get-model-idx model-id))) + (if (= model-idx -1) + "ERROR: Model not found" + (let ((endpoint (list-ref *model-endpoints* model-idx)) + (model (list-ref *model-ids* model-idx))) + (catch #t + (lambda () + (let ((payload (scm->json-string + `((model . ,model) + (messages . ,messages) + (max_tokens . ,max-tokens) + (temperature . ,temperature) + (stream . #f))))) + (call-with-values + (lambda () + (http-post (string-append endpoint "/chat/completions") + #:body payload + #:headers '((Content-Type . "application/json")))) + (lambda (response body) + (if (= (response-code response) 200) + (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"))) + content) + "ERROR: Request failed"))))) + (lambda (key . args) + (format #f "ERROR: ~a" key))))))) + +;;; Streaming chat completion with SSE parsing +;;; Usage: (uncloseai-chat-stream messages model-id max-tokens temperature) +;;; messages: vector of message objects +;;; Prints content chunks as they arrive +(define* (uncloseai-chat-stream messages #:optional (model-id "") (max-tokens 500) (temperature 0.7)) + (let ((model-idx (uncloseai-get-model-idx model-id))) + (if (= model-idx -1) + (display "ERROR: Model not found\n") + (let ((endpoint (list-ref *model-endpoints* model-idx)) + (model (list-ref *model-ids* model-idx))) + (catch #t + (lambda () + (let ((payload (scm->json-string + `((model . ,model) + (messages . ,messages) + (max_tokens . ,max-tokens) + (temperature . ,temperature) + (stream . #t))))) + (call-with-values + (lambda () + (http-post (string-append endpoint "/chat/completions") + #: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)))))))) + (lambda (key . args) + (format #t "\nERROR: ~a\n" key))))))) + +;;; Text-to-speech generation +;;; Usage: (uncloseai-tts text voice model output-file) +;;; Returns: file size in bytes (0 on error) +(define* (uncloseai-tts text #:optional (voice "alloy") (model "tts-1") (output-file "/tmp/speech.mp3")) + (if (null? *tts-endpoints*) + (begin + (display "ERROR: No TTS endpoints available\n") + 0) + (let ((endpoint (car *tts-endpoints*))) + (catch #t + (lambda () + (let ((payload (scm->json-string + `((model . ,model) + (voice . ,voice) + (input . ,text))))) + (call-with-values + (lambda () + (http-post (string-append endpoint "/audio/speech") + #:body payload + #:headers '((Content-Type . "application/json")))) + (lambda (response body) + (if (= (response-code response) 200) + (begin + ;; Write binary response to file + (call-with-output-file output-file + (lambda (port) + (put-bytevector port body))) + (stat:size (stat output-file))) + 0))))) + (lambda (key . args) + 0))))) + +;;; Demo usage when run as script +(when (batch-mode?) + (display "=== UncloseAI GNU Guile Client (with Streaming) ===\n\n") + + ;; Initialize client with model discovery + (let ((model-count (uncloseai-init))) + (if (= model-count 0) + (begin + (display "ERROR: No models discovered. Set environment variables:\n") + (display " MODEL_ENDPOINT_1, MODEL_ENDPOINT_2, etc.\n") + (exit 1)) + (begin + (format #t "Discovered ~a model(s)\n" model-count) + (uncloseai-list-models) + (display "\n") + + ;; Non-streaming chat example + (display "=== Non-Streaming Chat ===\n") + (let* ((messages #(((role . "system") + (content . "You are a helpful AI assistant.")) + ((role . "user") + (content . "Explain quantum computing in one sentence.")))) + (response (uncloseai-chat messages))) + (format #t "Model: ~a\n" (car *model-ids*)) + (format #t "Response: ~a\n\n" response)) + + ;; Streaming chat example + (display "=== Streaming Chat ===\n") + (let* ((model-id (if (>= model-count 2) + (list-ref *model-ids* 1) + (car *model-ids*))) + (messages #(((role . "system") + (content . "You are a coding assistant.")) + ((role . "user") + (content . "Write a Scheme function to calculate factorial."))))) + (format #t "Model: ~a\n" model-id) + (display "Response: ") + (uncloseai-chat-stream messages model-id 200) + (display "\n\n")) + + ;; TTS example + (when (not (null? *tts-endpoints*)) + (display "=== TTS Speech Generation ===\n") + (let* ((text "Hello from UncloseAI GNU Guile client! This demonstrates text to speech with streaming support.") + (output-file "/tmp/speech.mp3") + (file-size (uncloseai-tts text "alloy" "tts-1" output-file))) + (if (> file-size 0) + (format #t "[OK] Speech file created: ~a (~a bytes)\n\n" output-file file-size) + (display "[ERROR] TTS generation failed\n\n")))) + + (display "=== Examples Complete ===\n")))))