modified: CLAUDE.md modified: Makefile new file: languages/awk/Dockerfile new file: languages/awk/uncloseai.awk modified: languages/bash/Dockerfile deleted: languages/bash/examples.sh new file: languages/bash/uncloseai.sh new file: languages/c/curl/Dockerfile new file: languages/c/curl/Makefile new file: languages/c/curl/index.html new file: languages/c/curl/uncloseai.c new file: languages/c/libh2o/Dockerfile new file: languages/c/libh2o/Makefile new file: languages/c/libh2o/uncloseai.c new file: languages/c/nghttp2/Dockerfile new file: languages/c/nghttp2/Makefile new file: languages/c/nghttp2/uncloseai.c new file: languages/clojure/Dockerfile new file: languages/clojure/deps.edn new file: languages/clojure/uncloseai.clj new file: languages/cobol/Dockerfile new file: languages/cobol/discover.sh new file: languages/cobol/hermes.sh new file: languages/cobol/qwen.sh new file: languages/cobol/tts.sh new file: languages/cobol/uncloseai.cob new file: languages/cpp/boost-beast/Dockerfile new file: languages/cpp/boost-beast/Makefile new file: languages/cpp/boost-beast/uncloseai.cpp new file: languages/cpp/cpp-httplib/Dockerfile new file: languages/cpp/cpp-httplib/Makefile new file: languages/cpp/cpp-httplib/uncloseai.cpp new file: languages/cpp/libcurl/Dockerfile new file: languages/cpp/libcurl/Makefile new file: languages/cpp/libcurl/uncloseai.cpp new file: languages/crystal/Dockerfile new file: languages/crystal/uncloseai.cr new file: languages/csharp/Dockerfile new file: languages/csharp/Uncloseai.cs new file: languages/csharp/csharp.csproj new file: languages/dart/Dockerfile new file: languages/dart/bin/uncloseai.dart new file: languages/dart/pubspec.yaml new file: languages/deno/Dockerfile new file: languages/deno/uncloseai.ts new file: languages/elixir/Dockerfile new file: languages/elixir/lib/uncloseai.ex new file: languages/elixir/mix.exs new file: languages/elixir/run.exs new file: languages/erlang/Dockerfile new file: languages/erlang/rebar.config new file: languages/erlang/src/uncloseai.app.src new file: languages/erlang/src/uncloseai.erl new file: languages/fortran/Dockerfile new file: languages/fortran/uncloseai.f90 new file: languages/fsharp/Dockerfile new file: languages/fsharp/Uncloseai.fs new file: languages/fsharp/fsharp.fsproj new file: languages/go/Dockerfile new file: languages/go/README.md new file: languages/go/examples/basic.go new file: languages/go/go.mod new file: languages/go/uncloseai.go new file: languages/go/uncloseai/uncloseai.go new file: languages/haskell/Dockerfile new file: languages/haskell/UncloseAI.hs new file: languages/haskell/uncloseai.cabal new file: languages/java/Dockerfile new file: languages/java/UncloseAI.java new file: languages/javascript/bun/Dockerfile new file: languages/javascript/bun/uncloseai.ts new file: languages/javascript/nodejs/Dockerfile new file: languages/javascript/nodejs/README.md new file: languages/javascript/nodejs/package.json new file: languages/javascript/nodejs/uncloseai.js new file: languages/javascript/typescript/Dockerfile new file: languages/javascript/typescript/package.json new file: languages/javascript/typescript/tsconfig.json new file: languages/javascript/typescript/uncloseai.ts new file: languages/javascript/vanilla/Dockerfile new file: languages/javascript/vanilla/uncloseai.html new file: languages/julia/Dockerfile new file: languages/julia/Project.toml new file: languages/julia/src/uncloseai.jl new file: languages/kotlin/Dockerfile new file: languages/kotlin/build.gradle.kts new file: languages/kotlin/src/main/kotlin/UncloseAI.kt new file: languages/kotlin/uncloseai.kt new file: languages/lua/Dockerfile new file: languages/lua/uncloseai.lua new file: languages/nim/Dockerfile new file: languages/nim/uncloseai.nim new file: languages/ocaml/Dockerfile new file: languages/ocaml/dune new file: languages/ocaml/dune-project new file: languages/ocaml/uncloseai.ml new file: languages/odin/Dockerfile new file: languages/odin/uncloseai.odin new file: languages/perl/Dockerfile new file: languages/perl/uncloseai.pl new file: languages/php/Dockerfile new file: languages/php/uncloseai.php new file: languages/powershell/Dockerfile new file: languages/powershell/uncloseai.ps1 new file: languages/prolog/Dockerfile new file: languages/prolog/uncloseai.pl new file: languages/python/aiohttp/Dockerfile new file: languages/python/aiohttp/requirements.txt new file: languages/python/aiohttp/uncloseai.py modified: languages/python/httpx-async/Dockerfile deleted: languages/python/httpx-async/examples.py new file: languages/python/httpx-async/uncloseai.py modified: languages/python/openai-client/Dockerfile deleted: languages/python/openai-client/examples.py new file: languages/python/openai-client/uncloseai.py modified: languages/python/requests/Dockerfile deleted: languages/python/requests/examples.py new file: languages/python/requests/uncloseai.py new file: languages/r/Dockerfile new file: languages/r/uncloseai.R new file: languages/ruby/Dockerfile new file: languages/ruby/README.md new file: languages/ruby/uncloseai.rb new file: languages/rust/Cargo.toml new file: languages/rust/Dockerfile new file: languages/rust/README.md new file: languages/rust/examples/basic.rs new file: languages/rust/src/lib.rs new file: languages/rust/src/uncloseai.rs new file: languages/scala/Dockerfile new file: languages/scala/build.sbt new file: languages/scala/project/build.properties new file: languages/scala/project/plugins.sbt new file: languages/scala/src/main/scala/UncloseAI.scala new file: languages/tcl/Dockerfile new file: languages/tcl/uncloseai.tcl new file: languages/v/Dockerfile new file: languages/v/uncloseai.v new file: languages/v/v.mod new file: languages/vbnet/Dockerfile new file: languages/vbnet/UncloseAI.vb new file: languages/vbnet/UncloseAI.vbproj modified: languages/zig/Dockerfile modified: languages/zig/build.zig deleted: languages/zig/src/main.zig new file: languages/zig/src/uncloseai.zig
370 lines
11 KiB
Haskell
370 lines
11 KiB
Haskell
{-# LANGUAGE OverloadedStrings #-}
|
|
{-# LANGUAGE DeriveGeneric #-}
|
|
{-# LANGUAGE ScopedTypeVariables #-}
|
|
|
|
-- UncloseAI Haskell Library
|
|
-- OpenAI-compatible API client with streaming support
|
|
-- Compatible with vLLM, Ollama, and OpenAI-compatible endpoints
|
|
|
|
import Network.HTTP.Simple
|
|
import Network.HTTP.Client (responseBody)
|
|
import Network.HTTP.Client.Conduit (streamResponseBody)
|
|
import Data.Aeson
|
|
import Data.Text (Text)
|
|
import qualified Data.Text as T
|
|
import qualified Data.Text.IO as TIO
|
|
import qualified Data.Text.Encoding as TE
|
|
import qualified Data.ByteString.Lazy as BL
|
|
import qualified Data.ByteString as BS
|
|
import GHC.Generics
|
|
import Control.Exception
|
|
import Control.Monad (unless)
|
|
import Control.Monad.IO.Class (liftIO)
|
|
import System.IO
|
|
import System.Environment (lookupEnv)
|
|
import Data.Maybe (fromMaybe, isJust)
|
|
import Data.Conduit
|
|
import qualified Data.Conduit.List as CL
|
|
import qualified Data.Conduit.Combinators as CC
|
|
|
|
-- Model info type
|
|
data ModelInfo = ModelInfo
|
|
{ modelId :: Text
|
|
, modelEndpoint :: String
|
|
, modelMaxTokens :: Int
|
|
} deriving (Show)
|
|
|
|
-- UncloseAI Client type
|
|
data UncloseAIClient = UncloseAIClient
|
|
{ clientModels :: [ModelInfo]
|
|
, clientTtsEndpoints :: [String]
|
|
, clientTimeout :: Int
|
|
} deriving (Show)
|
|
|
|
-- Message types
|
|
data ChatMessage = ChatMessage
|
|
{ role :: Text
|
|
, content :: Text
|
|
} deriving (Generic, Show)
|
|
|
|
instance ToJSON ChatMessage
|
|
|
|
data ChatRequest = ChatRequest
|
|
{ model :: Text
|
|
, messages :: [ChatMessage]
|
|
, max_tokens :: Int
|
|
, stream :: Maybe Bool
|
|
} deriving (Generic, Show)
|
|
|
|
instance ToJSON ChatRequest where
|
|
toJSON (ChatRequest m msgs mt s) = object $
|
|
[ "model" .= m
|
|
, "messages" .= msgs
|
|
, "max_tokens" .= mt
|
|
] ++ case s of
|
|
Just True -> ["stream" .= True]
|
|
_ -> []
|
|
|
|
data ChatResponse = ChatResponse
|
|
{ choices :: [Choice]
|
|
} deriving (Generic, Show)
|
|
|
|
data Choice = Choice
|
|
{ message :: ResponseMessage
|
|
} deriving (Generic, Show)
|
|
|
|
data ResponseMessage = ResponseMessage
|
|
{ respContent :: Text
|
|
} deriving (Generic, Show)
|
|
|
|
instance FromJSON ChatResponse
|
|
instance FromJSON Choice
|
|
instance FromJSON ResponseMessage where
|
|
parseJSON = withObject "ResponseMessage" $ \v ->
|
|
ResponseMessage <$> v .: "content"
|
|
|
|
data TTSRequest = TTSRequest
|
|
{ tts_model :: Text
|
|
, voice :: Text
|
|
, input :: Text
|
|
} deriving (Show)
|
|
|
|
instance ToJSON TTSRequest where
|
|
toJSON (TTSRequest m v i) = object
|
|
[ "model" .= m
|
|
, "voice" .= v
|
|
, "input" .= i
|
|
]
|
|
|
|
-- Models discovery types
|
|
data ModelData = ModelData
|
|
{ mdId :: Text
|
|
, mdMaxModelLen :: Maybe Int
|
|
} deriving (Generic, Show)
|
|
|
|
instance FromJSON ModelData where
|
|
parseJSON = withObject "ModelData" $ \v ->
|
|
ModelData
|
|
<$> v .: "id"
|
|
<*> v .:? "max_model_len"
|
|
|
|
data ModelsResponse = ModelsResponse
|
|
{ modelsData :: [ModelData]
|
|
} deriving (Generic, Show)
|
|
|
|
instance FromJSON ModelsResponse where
|
|
parseJSON = withObject "ModelsResponse" $ \v ->
|
|
ModelsResponse <$> v .: "data"
|
|
|
|
-- Initialize client with auto-discovery
|
|
initClient :: Int -> IO UncloseAIClient
|
|
initClient timeout = do
|
|
putStrLn "Initializing UncloseAI client..."
|
|
|
|
-- Discover chat/code models
|
|
models <- discoverModelsLoop 1 []
|
|
|
|
-- Discover TTS endpoints
|
|
ttsEndpoints <- discoverTtsLoop 1 []
|
|
|
|
putStrLn $ "Discovered " ++ show (length models) ++ " models, " ++
|
|
show (length ttsEndpoints) ++ " TTS endpoints\n"
|
|
|
|
return $ UncloseAIClient
|
|
{ clientModels = models
|
|
, clientTtsEndpoints = ttsEndpoints
|
|
, clientTimeout = timeout
|
|
}
|
|
|
|
-- Model discovery
|
|
discoverModelsFromEndpoint :: String -> IO [ModelInfo]
|
|
discoverModelsFromEndpoint ep = do
|
|
putStrLn $ "Endpoint: " ++ ep
|
|
|
|
result <- try $ do
|
|
request <- parseRequest $ "GET " ++ ep ++ "/models"
|
|
response <- httpLBS request
|
|
return $ getResponseBody response
|
|
|
|
case result of
|
|
Left (e :: SomeException) -> return []
|
|
Right body ->
|
|
case decode body :: Maybe ModelsResponse of
|
|
Nothing -> return []
|
|
Just modelsResp -> do
|
|
let modelsList = modelsData modelsResp
|
|
-- Filter out modelperm-* entries
|
|
filtered = filter (\md -> not $ T.isPrefixOf "modelperm-" (mdId md)) modelsList
|
|
mapM (\md -> do
|
|
let maxToks = fromMaybe 8192 (mdMaxModelLen md)
|
|
return $ ModelInfo (mdId md) ep maxToks
|
|
) filtered
|
|
|
|
discoverModelsLoop :: Int -> [ModelInfo] -> IO [ModelInfo]
|
|
discoverModelsLoop i acc | i > 9999 = return $ reverse acc
|
|
discoverModelsLoop i acc = do
|
|
maybeEndpoint <- lookupEnv $ "MODEL_ENDPOINT_" ++ show i
|
|
case maybeEndpoint of
|
|
Nothing -> return $ reverse acc
|
|
Just ep -> do
|
|
newModels <- discoverModelsFromEndpoint ep
|
|
discoverModelsLoop (i + 1) (reverse newModels ++ acc)
|
|
|
|
discoverTtsLoop :: Int -> [String] -> IO [String]
|
|
discoverTtsLoop i acc | i > 9999 = return $ reverse acc
|
|
discoverTtsLoop i acc = do
|
|
maybeEndpoint <- lookupEnv $ "TTS_ENDPOINT_" ++ show i
|
|
case maybeEndpoint of
|
|
Nothing -> return $ reverse acc
|
|
Just ep -> do
|
|
putStrLn $ "Discovering TTS from: " ++ ep
|
|
discoverTtsLoop (i + 1) (ep : acc)
|
|
|
|
-- Streaming response types
|
|
data StreamDelta = StreamDelta
|
|
{ deltaContent :: Maybe Text
|
|
} deriving (Generic, Show)
|
|
|
|
instance FromJSON StreamDelta where
|
|
parseJSON = withObject "StreamDelta" $ \v ->
|
|
StreamDelta <$> v .:? "content"
|
|
|
|
data StreamChoice = StreamChoice
|
|
{ delta :: StreamDelta
|
|
} deriving (Generic, Show)
|
|
|
|
instance FromJSON StreamChoice
|
|
|
|
data StreamChunk = StreamChunk
|
|
{ streamChoices :: [StreamChoice]
|
|
} deriving (Generic, Show)
|
|
|
|
instance FromJSON StreamChunk where
|
|
parseJSON = withObject "StreamChunk" $ \v ->
|
|
StreamChunk <$> v .: "choices"
|
|
|
|
-- Non-streaming chat completion
|
|
chat :: UncloseAIClient -> [ChatMessage] -> Maybe Int -> Maybe Int -> Maybe Double -> IO (Either String Text)
|
|
chat client msgs maybeModelIdx maybeMaxToks maybeTemp = do
|
|
let modelIdx = fromMaybe 0 maybeModelIdx
|
|
maxToks = fromMaybe 100 maybeMaxToks
|
|
temp = fromMaybe 0.7 maybeTemp
|
|
models = clientModels client
|
|
|
|
if modelIdx >= length models
|
|
then return $ Left "Invalid model index"
|
|
else do
|
|
let modelInfo = models !! modelIdx
|
|
let req = ChatRequest
|
|
{ model = modelId modelInfo
|
|
, messages = msgs
|
|
, max_tokens = maxToks
|
|
, stream = Nothing
|
|
}
|
|
|
|
result <- try $ do
|
|
request <- parseRequest $ "POST " ++ modelEndpoint modelInfo ++ "/chat/completions"
|
|
let request' = setRequestBodyJSON req request
|
|
response <- httpLBS request'
|
|
return $ getResponseBody response
|
|
|
|
case result of
|
|
Right body ->
|
|
case decode body :: Maybe ChatResponse of
|
|
Just resp ->
|
|
case choices resp of
|
|
(c:_) -> return $ Right $ respContent $ message c
|
|
[] -> return $ Left "No response choices"
|
|
Nothing -> return $ Left "Could not parse response"
|
|
Left (e :: SomeException) -> return $ Left $ show e
|
|
|
|
-- Streaming chat completion - yields content via IO action
|
|
chatStream :: UncloseAIClient -> [ChatMessage] -> Maybe Int -> Maybe Int -> Maybe Double -> IO (Either String ())
|
|
chatStream client msgs maybeModelIdx maybeMaxToks maybeTemp = do
|
|
let modelIdx = fromMaybe 0 maybeModelIdx
|
|
maxToks = fromMaybe 500 maybeMaxToks
|
|
temp = fromMaybe 0.7 maybeTemp
|
|
models = clientModels client
|
|
|
|
if modelIdx >= length models
|
|
then return $ Left "Invalid model index"
|
|
else do
|
|
let modelInfo = models !! modelIdx
|
|
let req = ChatRequest
|
|
{ model = modelId modelInfo
|
|
, messages = msgs
|
|
, max_tokens = maxToks
|
|
, stream = Just True
|
|
}
|
|
|
|
result <- try $ do
|
|
request <- parseRequest $ "POST " ++ modelEndpoint modelInfo ++ "/chat/completions"
|
|
let request' = setRequestBodyJSON req request
|
|
httpSink request' $ \response -> do
|
|
responseBody response
|
|
.| CC.linesUnboundedAscii
|
|
.| CL.mapM_ processSSELine
|
|
|
|
case result of
|
|
Right () -> return $ Right ()
|
|
Left (e :: SomeException) -> return $ Left $ show e
|
|
|
|
-- Text-to-speech generation
|
|
tts :: UncloseAIClient -> Text -> Maybe Text -> Maybe String -> IO (Either String String)
|
|
tts client text maybeVoice maybeOutputFile = do
|
|
let voice = fromMaybe "alloy" maybeVoice
|
|
outputFile = fromMaybe "/tmp/speech.mp3" maybeOutputFile
|
|
ttsEndpoints = clientTtsEndpoints client
|
|
|
|
if null ttsEndpoints
|
|
then return $ Left "No TTS endpoints available"
|
|
else do
|
|
let endpoint = head ttsEndpoints
|
|
let req = TTSRequest
|
|
{ tts_model = "tts-1"
|
|
, voice = voice
|
|
, input = text
|
|
}
|
|
|
|
result <- try $ do
|
|
request <- parseRequest $ "POST " ++ endpoint ++ "/audio/speech"
|
|
let request' = setRequestBodyJSON req request
|
|
response <- httpLBS request'
|
|
let body = getResponseBody response
|
|
BL.writeFile outputFile body
|
|
return outputFile
|
|
|
|
case result of
|
|
Right file -> return $ Right file
|
|
Left (e :: SomeException) -> return $ Left $ show e
|
|
|
|
-- Process SSE line
|
|
processSSELine :: BS.ByteString -> IO ()
|
|
processSSELine line
|
|
| BS.isPrefixOf "data: " line = do
|
|
let dataStr = BS.drop 6 line
|
|
unless (dataStr == "[DONE]") $ do
|
|
case decode (BL.fromStrict dataStr) :: Maybe StreamChunk of
|
|
Just chunk ->
|
|
case streamChoices chunk of
|
|
(c:_) ->
|
|
case deltaContent (delta c) of
|
|
Just content -> TIO.putStr content >> hFlush stdout
|
|
Nothing -> return ()
|
|
[] -> return ()
|
|
Nothing -> return ()
|
|
| otherwise = return ()
|
|
|
|
-- Demo program showing library usage
|
|
main :: IO ()
|
|
main = do
|
|
hSetBuffering stdout NoBuffering
|
|
putStrLn "=== UncloseAI Haskell Client (with Streaming) ===\n"
|
|
|
|
-- Initialize client
|
|
client <- initClient 30
|
|
|
|
if null (clientModels client)
|
|
then do
|
|
putStrLn "ERROR: No models discovered"
|
|
else do
|
|
let models = clientModels client
|
|
let firstModel = head models
|
|
|
|
-- Non-streaming chat example
|
|
putStrLn "=== Non-Streaming Chat ==="
|
|
putStrLn $ "Model: " ++ T.unpack (modelId firstModel)
|
|
|
|
let messages = [ChatMessage "user" "Explain quantum computing in one sentence"]
|
|
result <- chat client messages Nothing Nothing Nothing
|
|
case result of
|
|
Right response -> putStrLn $ "Response: " ++ T.unpack response ++ "\n"
|
|
Left err -> putStrLn $ "Error: " ++ err ++ "\n"
|
|
|
|
-- Streaming chat example
|
|
let modelIdx = if length models >= 2 then 1 else 0
|
|
let streamModel = models !! modelIdx
|
|
|
|
putStrLn "=== Streaming Chat ==="
|
|
putStrLn $ "Model: " ++ T.unpack (modelId streamModel)
|
|
putStr "Response: "
|
|
|
|
let streamMessages = [ChatMessage "user" "Write a hello world program in Haskell"]
|
|
streamResult <- chatStream client streamMessages (Just modelIdx) Nothing Nothing
|
|
case streamResult of
|
|
Right () -> putStrLn "\n"
|
|
Left err -> putStrLn $ "\nError: " ++ err ++ "\n"
|
|
|
|
-- TTS example
|
|
if not (null (clientTtsEndpoints client))
|
|
then do
|
|
putStrLn "=== TTS Speech Generation ==="
|
|
putStrLn "Model: tts-1"
|
|
|
|
ttsResult <- tts client "Hello from UncloseAI Haskell client!" Nothing (Just "/tmp/speech.mp3")
|
|
case ttsResult of
|
|
Right file -> putStrLn $ "Audio saved to " ++ file
|
|
Left err -> putStrLn $ "TTS failed: " ++ err
|
|
else return ()
|
|
|
|
putStrLn "\n=== Examples Complete ==="
|