uncloseai.com/languages/fsharp/Uncloseai.fs
Russell Ballestrini 9e3eccb1b9 modified: .gitignore
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
2025-10-14 10:24:16 -04:00

275 lines
10 KiB
FSharp

// UncloseAI F# Library
// OpenAI-compatible API client with streaming support
// Compatible with vLLM, Ollama, and OpenAI-compatible endpoints
open System
open System.Net.Http
open System.Text
open System.Text.Json
open System.IO
open System.Collections.Generic
open System.Threading.Tasks
type ModelInfo = {
Id: string
Endpoint: string
MaxTokens: int
}
type Message = {
Role: string
Content: string
}
/// UncloseAI Client class
type UncloseAIClient(timeout: int) =
let httpClient = new HttpClient(Timeout = TimeSpan.FromSeconds(float timeout))
let models = List<ModelInfo>()
let ttsEndpoints = List<string>()
/// Initialize client with auto-discovery
member this.Init() =
task {
printfn "Initializing UncloseAI client..."
// Discover chat/code models
let mutable i = 1
let mutable continueLoop = true
while continueLoop && i <= 9999 do
let endpoint = Environment.GetEnvironmentVariable($"MODEL_ENDPOINT_{i}")
if String.IsNullOrEmpty(endpoint) then
continueLoop <- false
else
printfn $"Endpoint {i}: {endpoint}"
do! this.DiscoverModelsFromEndpoint(endpoint)
i <- i + 1
// Discover TTS endpoints
i <- 1
continueLoop <- true
while continueLoop && i <= 9999 do
let endpoint = Environment.GetEnvironmentVariable($"TTS_ENDPOINT_{i}")
if String.IsNullOrEmpty(endpoint) then
continueLoop <- false
else
ttsEndpoints.Add(endpoint)
i <- i + 1
printfn $"Discovered {models.Count} models, {ttsEndpoints.Count} TTS endpoints\n"
}
member private this.DiscoverModelsFromEndpoint(endpoint: string) =
task {
try
let! response = httpClient.GetAsync($"{endpoint}/models")
let! body = response.Content.ReadAsStringAsync()
use doc = JsonDocument.Parse(body)
let root = doc.RootElement
match root.TryGetProperty("data") with
| (true, data) ->
for model in data.EnumerateArray() do
let modelId = model.GetProperty("id").GetString()
// Skip modelperm-* entries
if not (modelId.StartsWith("modelperm-")) then
let maxTokens =
match model.TryGetProperty("max_model_len") with
| (true, prop) -> prop.GetInt32()
| (false, _) -> 8192
models.Add({ Id = modelId; Endpoint = endpoint; MaxTokens = maxTokens })
| (false, _) -> ()
with ex ->
() // Silently skip failed endpoints
}
member this.Models = models :> IReadOnlyList<ModelInfo>
member this.TtsEndpoints = ttsEndpoints :> IReadOnlyList<string>
/// Non-streaming chat completion
member this.Chat(messages: Message[], ?modelIdx: int, ?maxTokens: int, ?temperature: float) =
task {
let idx = defaultArg modelIdx 0
let tokens = defaultArg maxTokens 100
let temp = defaultArg temperature 0.7
if idx >= models.Count then
return Error "Invalid model index"
else
let model = models.[idx]
let url = $"{model.Endpoint}/chat/completions"
let payload = {|
model = model.Id
messages = messages |> Array.map (fun m -> {| role = m.Role; content = m.Content |})
stream = false
max_tokens = tokens
temperature = temp
|}
let json = JsonSerializer.Serialize(payload)
let content = new StringContent(json, Encoding.UTF8, "application/json")
try
let! response = httpClient.PostAsync(url, content)
let! body = response.Content.ReadAsStringAsync()
use doc = JsonDocument.Parse(body)
let root = doc.RootElement
let responseContent = root.GetProperty("choices").[0].GetProperty("message").GetProperty("content").GetString()
return Ok responseContent
with ex ->
return Error ex.Message
}
/// Streaming chat completion - yields content chunks
member this.ChatStream(messages: Message[], ?modelIdx: int, ?maxTokens: int, ?temperature: float) =
seq {
let idx = defaultArg modelIdx 0
let tokens = defaultArg maxTokens 500
let temp = defaultArg temperature 0.7
if idx >= models.Count then
yield Error "Invalid model index"
else
let model = models.[idx]
let url = $"{model.Endpoint}/chat/completions"
let payload = {|
model = model.Id
messages = messages |> Array.map (fun m -> {| role = m.Role; content = m.Content |})
stream = true
max_tokens = tokens
temperature = temp
|}
let json = JsonSerializer.Serialize(payload)
let content = new StringContent(json, Encoding.UTF8, "application/json")
try
use request = new HttpRequestMessage(HttpMethod.Post, url, Content = content)
let response = httpClient.Send(request, HttpCompletionOption.ResponseHeadersRead)
use stream = response.Content.ReadAsStream()
use reader = new StreamReader(stream)
let mutable line = reader.ReadLine()
while not (isNull line) do
if line.StartsWith("data: ") then
let data = line.Substring(6)
if data = "[DONE]" then
line <- null
else
try
use doc = JsonDocument.Parse(data)
let root = doc.RootElement
match root.TryGetProperty("choices") with
| (true, choices) ->
let choice = choices.[0]
match choice.TryGetProperty("delta") with
| (true, delta) ->
match delta.TryGetProperty("content") with
| (true, contentProp) ->
let content = contentProp.GetString()
if not (String.IsNullOrEmpty(content)) then
yield Ok content
| (false, _) -> ()
| (false, _) -> ()
| (false, _) -> ()
with _ ->
() // Skip malformed JSON
line <- reader.ReadLine()
else
line <- reader.ReadLine()
with ex ->
yield Error ex.Message
}
/// Text-to-speech generation
member this.Tts(text: string, ?voice: string, ?outputFile: string) =
task {
let voiceStr = defaultArg voice "alloy"
let fileStr = defaultArg outputFile "/tmp/speech.mp3"
if ttsEndpoints.Count = 0 then
return Error "No TTS endpoints available"
else
let endpoint = ttsEndpoints.[0]
let url = $"{endpoint}/audio/speech"
let payload = {|
model = "tts-1"
voice = voiceStr
input = text
|}
let json = JsonSerializer.Serialize(payload)
let content = new StringContent(json, Encoding.UTF8, "application/json")
try
let! response = httpClient.PostAsync(url, content)
let! audioData = response.Content.ReadAsByteArrayAsync()
File.WriteAllBytes(fileStr, audioData)
return Ok fileStr
with ex ->
return Error ex.Message
}
interface IDisposable with
member this.Dispose() =
httpClient.Dispose()
/// Demo program showing library usage
[<EntryPoint>]
let main argv =
printfn "=== UncloseAI F# Client (with Streaming) ===\n"
use client = new UncloseAIClient(30)
task {
// Initialize client
do! client.Init()
if client.Models.Count = 0 then
printfn "ERROR: No models discovered"
Environment.Exit(1)
// Non-streaming chat example
printfn "=== Non-Streaming Chat ==="
printfn $"Model: {client.Models.[0].Id}"
let! result = client.Chat([| { Role = "user"; Content = "Explain quantum computing in one sentence" } |])
match result with
| Ok response -> printfn $"Response: {response}\n"
| Error err -> printfn $"Error: {err}\n"
// Streaming chat example
let modelIdx = if client.Models.Count >= 2 then 1 else 0
printfn "=== Streaming Chat ==="
printfn $"Model: {client.Models.[modelIdx].Id}"
printf "Response: "
let messages = [| { Role = "user"; Content = "Write a hello world program in F#" } |]
for chunk in client.ChatStream(messages, modelIdx) do
match chunk with
| Ok content -> printf $"{content}"
| Error _ -> ()
printfn "\n"
// TTS example
if client.TtsEndpoints.Count > 0 then
printfn "=== TTS Speech Generation ==="
printfn "Model: tts-1"
let! ttsResult = client.Tts("Hello from UncloseAI F# client!", "alloy", "/tmp/speech.mp3")
match ttsResult with
| Ok file -> printfn $"Audio saved to {file}"
| Error err -> printfn $"TTS failed: {err}"
printfn "\n=== Examples Complete ==="
} |> Async.AwaitTask |> Async.RunSynchronously
0