uncloseai.com/public/languages/bash/uncloseai.sh

284 lines
8.7 KiB
Bash
Executable file

#!/bin/bash
# UncloseAI Bash 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_json> - Non-streaming chat completion
# uncloseai_chat_stream <messages_json> - Streaming chat completion
# uncloseai_tts <text> <voice> - Text-to-speech generation
# Global client state
declare -a UNCLOSEAI_MODEL_IDS
declare -a UNCLOSEAI_MODEL_ENDPOINTS
declare -a UNCLOSEAI_MODEL_MAX_TOKENS
declare -a UNCLOSEAI_TTS_ENDPOINTS
# Initialize client and discover models
uncloseai_init() {
UNCLOSEAI_MODEL_IDS=()
UNCLOSEAI_MODEL_ENDPOINTS=()
UNCLOSEAI_MODEL_MAX_TOKENS=()
UNCLOSEAI_TTS_ENDPOINTS=()
# Discover chat/code models from MODEL_ENDPOINT_N
for i in {1..9999}; do
local var_name="MODEL_ENDPOINT_$i"
local endpoint="${!var_name}"
if [ -z "$endpoint" ]; then
break
fi
local response=$(curl -s "${endpoint}/models")
# Extract model IDs from JSON, filtering out modelperm entries
local model_ids=$(echo "$response" | grep -o '"id":"[^"]*"' | sed 's/"id":"//g' | sed 's/"//g' | grep -v "^modelperm-")
# Add each discovered model to arrays
while IFS= read -r model_id; do
if [ -n "$model_id" ]; then
# Try to extract max_model_len from vLLM response
local max_tokens=$(echo "$response" | grep -o '"max_model_len":[0-9]*' | head -1 | sed 's/"max_model_len"://g')
if [ -z "$max_tokens" ]; then
max_tokens=8192 # Default for Ollama
fi
UNCLOSEAI_MODEL_IDS+=("$model_id")
UNCLOSEAI_MODEL_ENDPOINTS+=("$endpoint")
UNCLOSEAI_MODEL_MAX_TOKENS+=("$max_tokens")
fi
done <<< "$model_ids"
done
# Discover TTS endpoints from TTS_ENDPOINT_N
for i in {1..9999}; do
local var_name="TTS_ENDPOINT_$i"
local endpoint="${!var_name}"
if [ -z "$endpoint" ]; then
break
fi
UNCLOSEAI_TTS_ENDPOINTS+=("$endpoint")
done
return ${#UNCLOSEAI_MODEL_IDS[@]}
}
# List all discovered models
uncloseai_list_models() {
for i in "${!UNCLOSEAI_MODEL_IDS[@]}"; do
echo " - ${UNCLOSEAI_MODEL_IDS[$i]} (max_tokens: ${UNCLOSEAI_MODEL_MAX_TOKENS[$i]})"
done
}
# Get model index by ID or return 0 for first model
uncloseai_get_model_idx() {
local model_id="$1"
if [ -z "$model_id" ]; then
echo "0"
return
fi
for i in "${!UNCLOSEAI_MODEL_IDS[@]}"; do
if [ "${UNCLOSEAI_MODEL_IDS[$i]}" == "$model_id" ]; then
echo "$i"
return
fi
done
echo "-1" # Not found
}
# Non-streaming chat completion
# Usage: uncloseai_chat '<messages_json>' [model_id] [max_tokens] [temperature]
uncloseai_chat() {
local messages_json="$1"
local model_id="${2:-}"
local max_tokens="${3:-100}"
local temperature="${4:-0.7}"
local model_idx=$(uncloseai_get_model_idx "$model_id")
if [ "$model_idx" == "-1" ]; then
echo "ERROR: Model not found"
return 1
fi
local endpoint="${UNCLOSEAI_MODEL_ENDPOINTS[$model_idx]}"
local model="${UNCLOSEAI_MODEL_IDS[$model_idx]}"
local response=$(curl -s -X POST "${endpoint}/chat/completions" \
-H "Content-Type: application/json" \
-d "{
\"model\": \"${model}\",
\"messages\": ${messages_json},
\"max_tokens\": ${max_tokens},
\"temperature\": ${temperature},
\"stream\": false
}")
# Extract content using jq if available, otherwise use grep
if command -v jq &> /dev/null; then
echo "$response" | jq -r '.choices[0].message.content'
else
echo "$response" | grep -o '"content":"[^"]*"' | head -1 | sed 's/"content":"//g' | sed 's/"$//g'
fi
}
# Streaming chat completion with SSE parsing
# Usage: uncloseai_chat_stream '<messages_json>' [model_id] [max_tokens] [temperature]
uncloseai_chat_stream() {
local messages_json="$1"
local model_id="${2:-}"
local max_tokens="${3:-500}"
local temperature="${4:-0.7}"
local model_idx=$(uncloseai_get_model_idx "$model_id")
if [ "$model_idx" == "-1" ]; then
echo "ERROR: Model not found"
return 1
fi
local endpoint="${UNCLOSEAI_MODEL_ENDPOINTS[$model_idx]}"
local model="${UNCLOSEAI_MODEL_IDS[$model_idx]}"
# Use curl with --no-buffer for line-by-line streaming
curl -s --no-buffer -X POST "${endpoint}/chat/completions" \
-H "Content-Type: application/json" \
-d "{
\"model\": \"${model}\",
\"messages\": ${messages_json},
\"max_tokens\": ${max_tokens},
\"temperature\": ${temperature},
\"stream\": true
}" | while IFS= read -r line; do
# SSE format: "data: {...}"
if [[ "$line" =~ ^data:\ (.+)$ ]]; then
local data="${BASH_REMATCH[1]}"
# Check for stream termination
if [ "$data" == "[DONE]" ]; then
break
fi
# Extract delta content from streaming chunk
# Use jq if available, otherwise grep
if command -v jq &> /dev/null; then
local content=$(echo "$data" | jq -r '.choices[0].delta.content // empty' 2>/dev/null)
else
local content=$(echo "$data" | grep -o '"content":"[^"\\]*\\*[^"]*"' | sed 's/"content":"//g' | sed 's/"$//g' | sed 's/\\n/\n/g' | sed 's/\\"/"/g')
fi
if [ -n "$content" ] && [ "$content" != "null" ]; then
printf "%s" "$content"
fi
fi
done
}
# Text-to-speech generation
# Usage: uncloseai_tts <text> [voice] [model] [output_file]
uncloseai_tts() {
local text="$1"
local voice="${2:-alloy}"
local model="${3:-tts-1}"
local output_file="${4:-/tmp/speech.mp3}"
if [ ${#UNCLOSEAI_TTS_ENDPOINTS[@]} -eq 0 ]; then
echo "ERROR: No TTS endpoints available"
return 1
fi
local endpoint="${UNCLOSEAI_TTS_ENDPOINTS[0]}"
curl -s -X POST "${endpoint}/audio/speech" \
-H "Content-Type: application/json" \
-d "{
\"model\": \"${model}\",
\"voice\": \"${voice}\",
\"input\": \"${text}\"
}" \
--output "$output_file"
if [ -f "$output_file" ]; then
local file_size=$(stat -f%z "$output_file" 2>/dev/null || stat -c%s "$output_file" 2>/dev/null)
echo "$file_size"
return 0
else
return 1
fi
}
# Demo usage when run as script
if [ "${BASH_SOURCE[0]}" == "${0}" ]; then
echo "=== UncloseAI Bash Client (with Streaming) ==="
echo ""
# Initialize client with model discovery
uncloseai_init
model_count=${#UNCLOSEAI_MODEL_IDS[@]}
if [ "$model_count" -eq 0 ]; then
echo "ERROR: No models discovered. Set environment variables:"
echo " MODEL_ENDPOINT_1, MODEL_ENDPOINT_2, etc."
exit 1
fi
echo "Discovered $model_count model(s)"
uncloseai_list_models
echo ""
# Non-streaming chat example
echo "=== Non-Streaming Chat ==="
messages='[{"role":"system","content":"You are a helpful AI assistant."},{"role":"user","content":"Explain quantum computing in one sentence."}]'
response=$(uncloseai_chat "$messages")
echo "Model: ${UNCLOSEAI_MODEL_IDS[0]}"
echo "Response: $response"
echo ""
# Streaming chat example
echo "=== Streaming Chat ==="
# Use second model if available, otherwise first
if [ "$model_count" -ge 2 ]; then
model_id="${UNCLOSEAI_MODEL_IDS[1]}"
else
model_id="${UNCLOSEAI_MODEL_IDS[0]}"
fi
echo "Model: $model_id"
echo "Response: "
messages='[{"role":"system","content":"You are a coding assistant."},{"role":"user","content":"Write a bash function to check if a port is open"}]'
uncloseai_chat_stream "$messages" "$model_id" 200
echo ""
echo ""
# TTS example
if [ ${#UNCLOSEAI_TTS_ENDPOINTS[@]} -gt 0 ]; then
echo "=== TTS Speech Generation ==="
text="Hello from UncloseAI Bash 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 [ $? -eq 0 ]; then
echo "[OK] Speech file created: $output_file ($file_size bytes)"
echo ""
else
echo "[ERROR] TTS generation failed"
echo ""
fi
fi
echo "=== Examples Complete ==="
fi