Consolidate Scheme and Common Lisp into single uncloseai files with environment-based discovery

This commit is contained in:
Russell Ballestrini 2025-10-24 14:02:10 -04:00
parent c67d1b13da
commit dad7943c54
14 changed files with 574 additions and 472 deletions

View file

@ -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"]

View file

@ -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)))

View file

@ -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)))

View file

@ -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)))

View file

@ -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)))

View file

@ -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)))

View file

@ -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)

View file

@ -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"]

View file

@ -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)))

View file

@ -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)))

View file

@ -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)))

View file

@ -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)))

View file

@ -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)))

View file

@ -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")))))