296 lines
10 KiB
Fortran
296 lines
10 KiB
Fortran
! UncloseAI Fortran Library
|
|
! OpenAI-compatible API client with streaming support
|
|
! Compatible with vLLM, Ollama, and OpenAI-compatible endpoints
|
|
!
|
|
! Note: Fortran lacks native HTTP support, so this uses curl via shell commands
|
|
|
|
module uncloseai_lib
|
|
implicit none
|
|
private
|
|
public :: uncloseai_client, uncloseai_init, uncloseai_chat, uncloseai_chat_stream, uncloseai_tts
|
|
|
|
type :: model_info
|
|
character(len=512) :: id
|
|
character(len=512) :: endpoint
|
|
integer :: max_tokens
|
|
end type model_info
|
|
|
|
type :: uncloseai_client
|
|
type(model_info), dimension(:), allocatable :: models
|
|
character(len=512), dimension(:), allocatable :: tts_endpoints
|
|
integer :: model_count
|
|
integer :: tts_count
|
|
integer :: timeout
|
|
end type uncloseai_client
|
|
|
|
contains
|
|
|
|
! Initialize client with auto-discovery
|
|
subroutine uncloseai_init(client)
|
|
type(uncloseai_client), intent(out) :: client
|
|
integer :: i, ios
|
|
character(len=512) :: var_name, endpoint
|
|
character(len=1024) :: cmd
|
|
logical :: file_exists
|
|
|
|
write(*,*) 'Initializing UncloseAI client...'
|
|
|
|
client%model_count = 0
|
|
client%tts_count = 0
|
|
client%timeout = 30
|
|
|
|
allocate(client%models(100))
|
|
allocate(client%tts_endpoints(10))
|
|
|
|
! Discover chat/code models
|
|
do i = 1, 9999
|
|
write(var_name, '(A,I0)') 'MODEL_ENDPOINT_', i
|
|
call get_environment_variable(trim(var_name), endpoint)
|
|
if (len_trim(endpoint) == 0) exit
|
|
|
|
write(*,'(A,I0,A,A)') 'Endpoint ', i, ': ', trim(endpoint)
|
|
|
|
! Use curl to get models, filter modelperm-*, save model ID
|
|
write(cmd, '(A,A,A)') &
|
|
'curl -s "', trim(endpoint), '/models" | ' // &
|
|
'jq -r ''.data[]|select(.id|test("^modelperm-")|not)|.id'' | ' // &
|
|
'head -1 > /tmp/fortran_model.txt 2>/dev/null || echo "" > /tmp/fortran_model.txt'
|
|
call execute_command_line(trim(cmd), exitstat=ios)
|
|
|
|
! Read discovered model
|
|
inquire(file='/tmp/fortran_model.txt', exist=file_exists)
|
|
if (file_exists) then
|
|
open(unit=10, file='/tmp/fortran_model.txt', status='old', action='read', iostat=ios)
|
|
if (ios == 0) then
|
|
read(10, '(A)', iostat=ios) client%models(client%model_count + 1)%id
|
|
close(10)
|
|
if (ios == 0 .and. len_trim(client%models(client%model_count + 1)%id) > 0) then
|
|
client%models(client%model_count + 1)%endpoint = trim(endpoint)
|
|
client%models(client%model_count + 1)%max_tokens = 8192
|
|
client%model_count = client%model_count + 1
|
|
end if
|
|
else
|
|
close(10)
|
|
end if
|
|
end if
|
|
end do
|
|
|
|
! Discover TTS endpoints
|
|
do i = 1, 9999
|
|
write(var_name, '(A,I0)') 'TTS_ENDPOINT_', i
|
|
call get_environment_variable(trim(var_name), endpoint)
|
|
if (len_trim(endpoint) == 0) exit
|
|
|
|
client%tts_endpoints(client%tts_count + 1) = trim(endpoint)
|
|
client%tts_count = client%tts_count + 1
|
|
end do
|
|
|
|
write(*,'(A,I0,A,I0,A)') 'Discovered ', client%model_count, &
|
|
' models, ', client%tts_count, ' TTS endpoints'
|
|
write(*,*)
|
|
|
|
end subroutine uncloseai_init
|
|
|
|
! Non-streaming chat completion
|
|
subroutine uncloseai_chat(client, messages, model_idx, response, status)
|
|
type(uncloseai_client), intent(in) :: client
|
|
character(len=*), intent(in) :: messages
|
|
integer, intent(in), optional :: model_idx
|
|
character(len=4096), intent(out) :: response
|
|
integer, intent(out) :: status
|
|
integer :: idx, ios
|
|
character(len=4096) :: cmd
|
|
logical :: file_exists
|
|
|
|
idx = 0
|
|
if (present(model_idx)) idx = model_idx
|
|
|
|
if (idx >= client%model_count) then
|
|
response = 'ERROR: Invalid model index'
|
|
status = -1
|
|
return
|
|
end if
|
|
|
|
! Build curl command for non-streaming
|
|
write(cmd, '(A,A,A,A,A)') &
|
|
'curl -s -X POST "', trim(client%models(idx + 1)%endpoint), '/chat/completions" ', &
|
|
'-H "Content-Type: application/json" ', &
|
|
'-d ''{"model":"', trim(client%models(idx + 1)%id), &
|
|
'","messages":', trim(messages), &
|
|
',"stream":false,"max_tokens":100}'' | ' // &
|
|
'jq -r ''.choices[0].message.content'' > /tmp/fortran_response.txt 2>/dev/null'
|
|
|
|
call execute_command_line(trim(cmd), exitstat=ios)
|
|
|
|
if (ios == 0) then
|
|
inquire(file='/tmp/fortran_response.txt', exist=file_exists)
|
|
if (file_exists) then
|
|
open(unit=10, file='/tmp/fortran_response.txt', status='old', action='read', iostat=ios)
|
|
if (ios == 0) then
|
|
read(10, '(A)', iostat=ios) response
|
|
close(10)
|
|
status = 0
|
|
else
|
|
close(10)
|
|
response = 'ERROR: Failed to read response'
|
|
status = -1
|
|
end if
|
|
else
|
|
response = 'ERROR: No response file'
|
|
status = -1
|
|
end if
|
|
else
|
|
response = 'ERROR: HTTP request failed'
|
|
status = -1
|
|
end if
|
|
|
|
call execute_command_line('rm -f /tmp/fortran_response.txt', exitstat=ios)
|
|
|
|
end subroutine uncloseai_chat
|
|
|
|
! Streaming chat completion
|
|
subroutine uncloseai_chat_stream(client, messages, model_idx, status)
|
|
type(uncloseai_client), intent(in) :: client
|
|
character(len=*), intent(in) :: messages
|
|
integer, intent(in), optional :: model_idx
|
|
integer, intent(out) :: status
|
|
integer :: idx, ios
|
|
character(len=4096) :: cmd
|
|
|
|
idx = 0
|
|
if (present(model_idx)) idx = model_idx
|
|
|
|
if (idx >= client%model_count) then
|
|
write(*,*) 'ERROR: Invalid model index'
|
|
status = -1
|
|
return
|
|
end if
|
|
|
|
! Build curl command for streaming with SSE parsing
|
|
write(cmd, '(A,A,A,A,A)') &
|
|
'curl -s --no-buffer -X POST "', trim(client%models(idx + 1)%endpoint), &
|
|
'/chat/completions" ', &
|
|
'-H "Content-Type: application/json" ', &
|
|
'-d ''{"model":"', trim(client%models(idx + 1)%id), &
|
|
'","messages":', trim(messages), &
|
|
',"stream":true,"max_tokens":500}'' | ' // &
|
|
'while IFS= read -r line; do ' // &
|
|
'[[ "$line" =~ ^data:\ (.+)$ ]] || continue; ' // &
|
|
'data="${BASH_REMATCH[1]}"; ' // &
|
|
'[ "$data" = "[DONE]" ] && break; ' // &
|
|
'printf "%s" "$(echo "$data"|jq -r ''.choices[0].delta.content//empty'')"; ' // &
|
|
'done'
|
|
|
|
call execute_command_line(trim(cmd), exitstat=ios)
|
|
|
|
status = ios
|
|
|
|
end subroutine uncloseai_chat_stream
|
|
|
|
! Text-to-speech generation
|
|
subroutine uncloseai_tts(client, text, voice, output_file, status)
|
|
type(uncloseai_client), intent(in) :: client
|
|
character(len=*), intent(in) :: text
|
|
character(len=*), intent(in), optional :: voice
|
|
character(len=*), intent(in), optional :: output_file
|
|
integer, intent(out) :: status
|
|
character(len=256) :: voice_str, file_str
|
|
character(len=2048) :: cmd
|
|
integer :: ios
|
|
|
|
voice_str = 'alloy'
|
|
if (present(voice)) voice_str = trim(voice)
|
|
|
|
file_str = '/tmp/speech.mp3'
|
|
if (present(output_file)) file_str = trim(output_file)
|
|
|
|
if (client%tts_count == 0) then
|
|
status = -1
|
|
return
|
|
end if
|
|
|
|
! Build curl command for TTS
|
|
write(cmd, '(A,A,A,A,A,A,A)') &
|
|
'curl -s -X POST "', trim(client%tts_endpoints(1)), '/audio/speech" ', &
|
|
'-H "Content-Type: application/json" ', &
|
|
'-d ''{"model":"tts-1","voice":"', trim(voice_str), &
|
|
'","input":"', trim(text), '"}'' ', &
|
|
'-o ', trim(file_str)
|
|
|
|
call execute_command_line(trim(cmd), exitstat=ios)
|
|
|
|
status = ios
|
|
|
|
end subroutine uncloseai_tts
|
|
|
|
end module uncloseai_lib
|
|
|
|
! Demo program showing library usage
|
|
program main
|
|
use uncloseai_lib
|
|
implicit none
|
|
type(uncloseai_client) :: client
|
|
character(len=4096) :: response
|
|
integer :: status, model_idx
|
|
logical :: file_exists
|
|
|
|
write(*,*) '=== UncloseAI Fortran Client (with Streaming) ==='
|
|
write(*,*)
|
|
|
|
! Initialize client
|
|
call uncloseai_init(client)
|
|
|
|
if (client%model_count == 0) then
|
|
write(*,*) 'ERROR: No models discovered'
|
|
stop 1
|
|
end if
|
|
|
|
! Non-streaming chat example
|
|
write(*,*) '=== Non-Streaming Chat ==='
|
|
write(*,'(A,A)') 'Model: ', trim(client%models(1)%id)
|
|
|
|
call uncloseai_chat(client, &
|
|
'[{"role":"user","content":"Explain quantum computing in one sentence"}]', &
|
|
0, response, status)
|
|
|
|
if (status == 0) then
|
|
write(*,'(A,A)') 'Response: ', trim(response)
|
|
else
|
|
write(*,*) trim(response)
|
|
end if
|
|
write(*,*)
|
|
|
|
! Streaming chat example
|
|
model_idx = 0
|
|
if (client%model_count >= 2) model_idx = 1
|
|
|
|
write(*,*) '=== Streaming Chat ==='
|
|
write(*,'(A,A)') 'Model: ', trim(client%models(model_idx + 1)%id)
|
|
write(*,'(A)', advance='no') 'Response: '
|
|
|
|
call uncloseai_chat_stream(client, &
|
|
'[{"role":"user","content":"Write a hello world program in Fortran"}]', &
|
|
model_idx, status)
|
|
write(*,*)
|
|
write(*,*)
|
|
|
|
! TTS example
|
|
if (client%tts_count > 0) then
|
|
write(*,*) '=== TTS Speech Generation ==='
|
|
write(*,*) 'Model: tts-1'
|
|
|
|
call uncloseai_tts(client, &
|
|
'Hello from UncloseAI Fortran client!', &
|
|
'alloy', '/tmp/speech.mp3', status)
|
|
|
|
if (status == 0) then
|
|
write(*,*) 'Audio saved to /tmp/speech.mp3'
|
|
else
|
|
write(*,*) 'TTS failed'
|
|
end if
|
|
end if
|
|
|
|
write(*,*)
|
|
write(*,*) '=== Examples Complete ==='
|
|
|
|
end program main
|