2026-07-15 14:59:23 +02:00
//
// Copyright 2026 The InfiniFlow Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
package models
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"mime/multipart"
"net/http"
"os"
"path/filepath"
2026-07-18 21:02:07 +08:00
"ragflow/internal/common"
2026-07-15 14:59:23 +02:00
"strings"
)
// RAGconModel implements ModelDriver for RAGcon (https://connect.ragcon.com),
// a LiteLLM-proxy gateway exposing OpenAI-compatible endpoints for chat,
// embedding, rerank, ASR and TTS. Model names are user-supplied (no fixed
// catalog), matching the Ollama/Xinference-style providers.
type RAGconModel struct {
baseModel BaseModel
}
// NewRAGconModel creates a new RAGcon model instance.
func NewRAGconModel ( baseURL map [ string ] string , urlSuffix URLSuffix ) * RAGconModel {
return & RAGconModel {
baseModel : BaseModel {
BaseURL : baseURL ,
URLSuffix : urlSuffix ,
2026-07-31 19:15:38 +08:00
httpClient : NewDriverHTTPClient ( false ) ,
2026-07-15 14:59:23 +02:00
} ,
}
}
func ( r * RAGconModel ) NewInstance ( baseURL map [ string ] string ) ModelDriver {
return NewRAGconModel ( baseURL , r . baseModel . URLSuffix )
}
func ( r * RAGconModel ) Name ( ) string {
return "ragcon"
}
func ( r * RAGconModel ) chatURL ( apiConfig * APIConfig ) ( string , error ) {
baseURL , err := r . baseModel . GetBaseURL ( apiConfig )
if err != nil {
return "" , err
}
baseURL = strings . TrimSuffix ( baseURL , "/" )
return fmt . Sprintf ( "%s/%s" , baseURL , r . baseModel . URLSuffix . Chat ) , nil
}
2026-07-22 22:30:57 +08:00
func ( r * RAGconModel ) ChatWithMessages ( ctx context . Context , modelName string , messages [ ] Message , apiConfig * APIConfig , chatModelConfig * ChatConfig , modelUsage * common . ModelUsage ) ( * ChatResponse , error ) {
2026-07-15 14:59:23 +02:00
if err := r . baseModel . APIConfigCheck ( apiConfig ) ; err != nil {
return nil , err
}
if strings . TrimSpace ( modelName ) == "" {
return nil , fmt . Errorf ( "model name is required" )
}
if len ( messages ) == 0 {
return nil , fmt . Errorf ( "messages is empty" )
}
url , err := r . chatURL ( apiConfig )
if err != nil {
return nil , err
}
2026-07-23 19:12:58 +08:00
reqBody := buildRequestBody ( chatModelConfig , modelName , messages , false )
if strings . Contains ( strings . ToLower ( modelName ) , "qwen3" ) && ( chatModelConfig == nil || chatModelConfig . Thinking == nil ) {
reqBody [ "enable_thinking" ] = false
}
2026-07-15 14:59:23 +02:00
feat(go-models): migrate batch 4 model drivers to unified handlers (#17699)
## Summary
Relate to #17284
Migrate 10 OpenAI-compatible drivers (`orcarouter`, `perplexity`,
`ppio`, `qiniu`, `ragcon`, `stepfun`, `togetherai`, `tokenhub`,
`tokenpony`, `upstage`) to use the unified response handlers
(`HandleNonStreamingResponse` / `HandleStreamingResponse`), following
the same pattern established by `deepseek` in #17634.
- Cut ~150 lines per driver (1436 lines removed, 62 added across 10
files).
- No functional changes — pure deduplication of HTTP plumbing.
- Each driver now routes through `baseModel.doRequest()` and
`HandleNonStreamingResponse()`.
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-03 17:48:45 +08:00
body , err := r . baseModel . doRequest ( ctx , url , apiConfig , reqBody , nonStreamCallTimeout )
2026-07-15 14:59:23 +02:00
if err != nil {
feat(go-models): migrate batch 4 model drivers to unified handlers (#17699)
## Summary
Relate to #17284
Migrate 10 OpenAI-compatible drivers (`orcarouter`, `perplexity`,
`ppio`, `qiniu`, `ragcon`, `stepfun`, `togetherai`, `tokenhub`,
`tokenpony`, `upstage`) to use the unified response handlers
(`HandleNonStreamingResponse` / `HandleStreamingResponse`), following
the same pattern established by `deepseek` in #17634.
- Cut ~150 lines per driver (1436 lines removed, 62 added across 10
files).
- No functional changes — pure deduplication of HTTP plumbing.
- Each driver now routes through `baseModel.doRequest()` and
`HandleNonStreamingResponse()`.
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-03 17:48:45 +08:00
return nil , err
2026-07-15 14:59:23 +02:00
}
feat(go-models): migrate batch 4 model drivers to unified handlers (#17699)
## Summary
Relate to #17284
Migrate 10 OpenAI-compatible drivers (`orcarouter`, `perplexity`,
`ppio`, `qiniu`, `ragcon`, `stepfun`, `togetherai`, `tokenhub`,
`tokenpony`, `upstage`) to use the unified response handlers
(`HandleNonStreamingResponse` / `HandleStreamingResponse`), following
the same pattern established by `deepseek` in #17634.
- Cut ~150 lines per driver (1436 lines removed, 62 added across 10
files).
- No functional changes — pure deduplication of HTTP plumbing.
- Each driver now routes through `baseModel.doRequest()` and
`HandleNonStreamingResponse()`.
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-03 17:48:45 +08:00
return HandleNonStreamingResponse ( body , modelUsage , chatModelConfig , OpenAIParserConfig )
2026-07-15 14:59:23 +02:00
}
2026-07-22 22:30:57 +08:00
func ( r * RAGconModel ) ChatStreamlyWithSender ( ctx context . Context , modelName string , messages [ ] Message , apiConfig * APIConfig , chatModelConfig * ChatConfig , modelUsage * common . ModelUsage , sender func ( * string , * string ) error ) error {
2026-07-15 14:59:23 +02:00
if err := r . baseModel . APIConfigCheck ( apiConfig ) ; err != nil {
return err
}
if sender == nil {
return fmt . Errorf ( "sender is required" )
}
if strings . TrimSpace ( modelName ) == "" {
return fmt . Errorf ( "model name is required" )
}
if len ( messages ) == 0 {
return fmt . Errorf ( "messages is empty" )
}
if chatModelConfig != nil && chatModelConfig . Stream != nil && ! * chatModelConfig . Stream {
return fmt . Errorf ( "stream must be true in ChatStreamlyWithSender" )
}
url , err := r . chatURL ( apiConfig )
if err != nil {
return err
}
2026-07-23 19:12:58 +08:00
reqBody := buildRequestBody ( chatModelConfig , modelName , messages , true )
reqBody [ "stream_options" ] = map [ string ] interface { } { "include_usage" : true }
if strings . Contains ( strings . ToLower ( modelName ) , "qwen3" ) && ( chatModelConfig == nil || chatModelConfig . Thinking == nil ) {
reqBody [ "enable_thinking" ] = false
}
2026-07-15 14:59:23 +02:00
feat(go-models): migrate batch 4 model drivers to unified handlers (#17699)
## Summary
Relate to #17284
Migrate 10 OpenAI-compatible drivers (`orcarouter`, `perplexity`,
`ppio`, `qiniu`, `ragcon`, `stepfun`, `togetherai`, `tokenhub`,
`tokenpony`, `upstage`) to use the unified response handlers
(`HandleNonStreamingResponse` / `HandleStreamingResponse`), following
the same pattern established by `deepseek` in #17634.
- Cut ~150 lines per driver (1436 lines removed, 62 added across 10
files).
- No functional changes — pure deduplication of HTTP plumbing.
- Each driver now routes through `baseModel.doRequest()` and
`HandleNonStreamingResponse()`.
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-03 17:48:45 +08:00
return r . baseModel . doStreamRequest ( ctx , url , apiConfig , reqBody , streamCallTimeout , func ( body io . ReadCloser ) error {
return HandleStreamingResponse ( body , modelUsage , chatModelConfig , OpenAIParserConfig , sender )
} )
2026-07-15 14:59:23 +02:00
}
type ragconEmbeddingResponse struct {
Data [ ] struct {
Embedding [ ] float64 ` json:"embedding" `
Index int ` json:"index" `
} ` json:"data" `
}
2026-07-22 22:30:57 +08:00
func ( r * RAGconModel ) Embed ( ctx context . Context , modelName * string , texts [ ] string , apiConfig * APIConfig , embeddingConfig * EmbeddingConfig , modelUsage * common . ModelUsage ) ( [ ] EmbeddingData , error ) {
2026-07-15 14:59:23 +02:00
if err := r . baseModel . APIConfigCheck ( apiConfig ) ; err != nil {
return nil , err
}
if modelName == nil || strings . TrimSpace ( * modelName ) == "" {
return nil , fmt . Errorf ( "model name is required" )
}
if len ( texts ) == 0 {
return [ ] EmbeddingData { } , nil
}
baseURL , err := r . baseModel . GetBaseURL ( apiConfig )
if err != nil {
return nil , err
}
baseURL = strings . TrimSuffix ( baseURL , "/" )
url := fmt . Sprintf ( "%s/%s" , baseURL , r . baseModel . URLSuffix . Embedding )
reqBody := map [ string ] interface { } {
"model" : * modelName ,
"input" : texts ,
}
if embeddingConfig != nil && embeddingConfig . Dimension > 0 {
reqBody [ "dimensions" ] = embeddingConfig . Dimension
}
jsonData , err := json . Marshal ( reqBody )
if err != nil {
return nil , fmt . Errorf ( "failed to marshal request: %w" , err )
}
2026-07-22 22:30:57 +08:00
ctx , cancel := context . WithTimeout ( ctx , nonStreamCallTimeout )
2026-07-15 14:59:23 +02:00
defer cancel ( )
req , err := http . NewRequestWithContext ( ctx , http . MethodPost , url , bytes . NewBuffer ( jsonData ) )
if err != nil {
return nil , fmt . Errorf ( "failed to create request: %w" , err )
}
req . Header . Set ( "Content-Type" , "application/json" )
req . Header . Set ( "Authorization" , fmt . Sprintf ( "Bearer %s" , * apiConfig . ApiKey ) )
resp , err := r . baseModel . httpClient . Do ( req )
if err != nil {
return nil , fmt . Errorf ( "failed to send request: %w" , err )
}
defer resp . Body . Close ( )
body , err := io . ReadAll ( resp . Body )
if err != nil {
return nil , fmt . Errorf ( "failed to read response: %w" , err )
}
if resp . StatusCode != http . StatusOK {
return nil , fmt . Errorf ( "RAGcon embeddings API error: %s, body: %s" , resp . Status , string ( body ) )
}
var parsed ragconEmbeddingResponse
if err = json . Unmarshal ( body , & parsed ) ; err != nil {
return nil , fmt . Errorf ( "failed to parse response: %w" , err )
}
embeddings := make ( [ ] EmbeddingData , 0 , len ( parsed . Data ) )
for _ , d := range parsed . Data {
embeddings = append ( embeddings , EmbeddingData { Embedding : d . Embedding , Index : d . Index } )
}
return embeddings , nil
}
// Rerank POSTs to RAGcon's /rerank endpoint (LiteLLM proxy passthrough).
2026-07-22 22:30:57 +08:00
func ( r * RAGconModel ) Rerank ( ctx context . Context , modelName * string , query string , documents [ ] string , apiConfig * APIConfig , rerankConfig * RerankConfig , modelUsage * common . ModelUsage ) ( * RerankResponse , error ) {
2026-07-15 14:59:23 +02:00
if err := r . baseModel . APIConfigCheck ( apiConfig ) ; err != nil {
return nil , err
}
if modelName == nil || strings . TrimSpace ( * modelName ) == "" {
return nil , fmt . Errorf ( "model name is required" )
}
if len ( documents ) == 0 {
return & RerankResponse { } , nil
}
baseURL , err := r . baseModel . GetBaseURL ( apiConfig )
if err != nil {
return nil , err
}
baseURL = strings . TrimSuffix ( baseURL , "/" )
url := fmt . Sprintf ( "%s/%s" , baseURL , r . baseModel . URLSuffix . Rerank )
topN := len ( documents )
if rerankConfig != nil && rerankConfig . TopN != 0 {
topN = rerankConfig . TopN
}
reqBody := map [ string ] interface { } {
"model" : * modelName ,
"query" : query ,
"documents" : documents ,
"top_n" : topN ,
}
jsonData , err := json . Marshal ( reqBody )
if err != nil {
return nil , fmt . Errorf ( "failed to marshal request: %w" , err )
}
2026-07-22 22:30:57 +08:00
ctx , cancel := context . WithTimeout ( ctx , nonStreamCallTimeout )
2026-07-15 14:59:23 +02:00
defer cancel ( )
req , err := http . NewRequestWithContext ( ctx , http . MethodPost , url , bytes . NewBuffer ( jsonData ) )
if err != nil {
return nil , fmt . Errorf ( "failed to create request: %w" , err )
}
req . Header . Set ( "Content-Type" , "application/json" )
req . Header . Set ( "Authorization" , fmt . Sprintf ( "Bearer %s" , * apiConfig . ApiKey ) )
resp , err := r . baseModel . httpClient . Do ( req )
if err != nil {
return nil , fmt . Errorf ( "failed to send request: %w" , err )
}
defer resp . Body . Close ( )
body , err := io . ReadAll ( resp . Body )
if err != nil {
return nil , fmt . Errorf ( "failed to read response: %w" , err )
}
if resp . StatusCode != http . StatusOK {
return nil , fmt . Errorf ( "RAGcon rerank API error: status %d, body: %s" , resp . StatusCode , string ( body ) )
}
var rerankResp struct {
Results [ ] struct {
Index int ` json:"index" `
RelevanceScore float64 ` json:"relevance_score" `
} ` json:"results" `
}
if err = json . Unmarshal ( body , & rerankResp ) ; err != nil {
return nil , fmt . Errorf ( "failed to decode response: %w" , err )
}
var rerankResponse RerankResponse
for _ , result := range rerankResp . Results {
rerankResponse . Data = append ( rerankResponse . Data , RerankResult {
Index : result . Index ,
RelevanceScore : result . RelevanceScore ,
} )
}
return & rerankResponse , nil
}
// ListModels returns the model ids visible to the configured API key.
2026-07-22 22:30:57 +08:00
func ( r * RAGconModel ) ListModels ( ctx context . Context , apiConfig * APIConfig ) ( [ ] ListModelResponse , error ) {
2026-07-15 14:59:23 +02:00
if err := r . baseModel . APIConfigCheck ( apiConfig ) ; err != nil {
return nil , err
}
baseURL , err := r . baseModel . GetBaseURL ( apiConfig )
if err != nil {
return nil , err
}
baseURL = strings . TrimSuffix ( baseURL , "/" )
url := fmt . Sprintf ( "%s/%s" , baseURL , r . baseModel . URLSuffix . Models )
2026-07-22 22:30:57 +08:00
ctx , cancel := context . WithTimeout ( ctx , nonStreamCallTimeout )
2026-07-15 14:59:23 +02:00
defer cancel ( )
req , err := http . NewRequestWithContext ( ctx , http . MethodGet , url , nil )
if err != nil {
return nil , fmt . Errorf ( "failed to create request: %w" , err )
}
req . Header . Set ( "Content-Type" , "application/json" )
req . Header . Set ( "Authorization" , fmt . Sprintf ( "Bearer %s" , * apiConfig . ApiKey ) )
resp , err := r . baseModel . httpClient . Do ( req )
if err != nil {
return nil , fmt . Errorf ( "failed to send request: %w" , err )
}
defer resp . Body . Close ( )
body , err := io . ReadAll ( resp . Body )
if err != nil {
return nil , fmt . Errorf ( "failed to read response: %w" , err )
}
if resp . StatusCode != http . StatusOK {
return nil , fmt . Errorf ( "API request failed with status %d: %s" , resp . StatusCode , string ( body ) )
}
var modelList ModelList
if err = json . Unmarshal ( body , & modelList ) ; err != nil {
return nil , fmt . Errorf ( "failed to parse response: %w" , err )
}
return ParseListModel ( modelList ) , nil
}
2026-07-22 22:30:57 +08:00
func ( r * RAGconModel ) CheckConnection ( ctx context . Context , apiConfig * APIConfig ) error {
_ , err := r . ListModels ( ctx , apiConfig )
2026-07-15 14:59:23 +02:00
return err
}
// Balance is not exposed by RAGcon's LiteLLM proxy.
2026-07-22 22:30:57 +08:00
func ( r * RAGconModel ) Balance ( ctx context . Context , apiConfig * APIConfig ) ( map [ string ] interface { } , error ) {
2026-07-15 14:59:23 +02:00
return nil , fmt . Errorf ( "%s, no such method" , r . Name ( ) )
}
func ( r * RAGconModel ) newASRRequest ( ctx context . Context , modelName * string , file * string , apiConfig * APIConfig , asrConfig * ASRConfig , stream bool ) ( * http . Request , string , error ) {
if err := r . baseModel . APIConfigCheck ( apiConfig ) ; err != nil {
return nil , "" , err
}
if modelName == nil || strings . TrimSpace ( * modelName ) == "" {
return nil , "" , fmt . Errorf ( "model name is required" )
}
if file == nil || * file == "" {
return nil , "" , fmt . Errorf ( "file is missing" )
}
baseURL , err := r . baseModel . GetBaseURL ( apiConfig )
if err != nil {
return nil , "" , err
}
baseURL = strings . TrimSuffix ( baseURL , "/" )
url := fmt . Sprintf ( "%s/%s" , baseURL , strings . TrimPrefix ( r . baseModel . URLSuffix . ASR , "/" ) )
var body bytes . Buffer
writer := multipart . NewWriter ( & body )
// codeql[go/path-injection] False positive: *file is the audio file path the caller passes in to upload. The caller (an operator-supplied pipeline) explicitly chose this path, and the OS access check enforces permissions anyway.
audioFile , err := os . Open ( * file )
if err != nil {
return nil , "" , fmt . Errorf ( "failed to open audio file: %w" , err )
}
defer audioFile . Close ( )
part , err := writer . CreateFormFile ( "file" , filepath . Base ( * file ) )
if err != nil {
return nil , "" , fmt . Errorf ( "failed to create multipart file: %w" , err )
}
if _ , err = io . Copy ( part , audioFile ) ; err != nil {
return nil , "" , fmt . Errorf ( "failed to copy audio data: %w" , err )
}
if err = writer . WriteField ( "model" , * modelName ) ; err != nil {
return nil , "" , fmt . Errorf ( "failed to write model field: %w" , err )
}
responseFormat := ""
if asrConfig != nil && asrConfig . Params != nil {
if value , ok := asrConfig . Params [ "response_format" ] . ( string ) ; ok {
responseFormat = value
}
for key , value := range asrConfig . Params {
if stream && key == "stream" {
continue
}
if err = writeOpenAIMultipartField ( writer , key , value ) ; err != nil {
return nil , "" , err
}
}
}
if stream {
if err = writer . WriteField ( "stream" , "true" ) ; err != nil {
return nil , "" , fmt . Errorf ( "failed to write stream field: %w" , err )
}
}
if err = writer . Close ( ) ; err != nil {
return nil , "" , fmt . Errorf ( "failed to close multipart writer: %w" , err )
}
req , err := http . NewRequestWithContext ( ctx , http . MethodPost , url , & body )
if err != nil {
return nil , "" , fmt . Errorf ( "failed to create request: %w" , err )
}
req . Header . Set ( "Authorization" , fmt . Sprintf ( "Bearer %s" , * apiConfig . ApiKey ) )
req . Header . Set ( "Content-Type" , writer . FormDataContentType ( ) )
return req , responseFormat , nil
}
2026-07-22 22:30:57 +08:00
func ( r * RAGconModel ) TranscribeAudio ( ctx context . Context , modelName * string , file * string , apiConfig * APIConfig , asrConfig * ASRConfig , modelUsage * common . ModelUsage ) ( * ASRResponse , error ) {
ctx , cancel := context . WithTimeout ( ctx , nonStreamCallTimeout )
2026-07-15 14:59:23 +02:00
defer cancel ( )
req , responseFormat , err := r . newASRRequest ( ctx , modelName , file , apiConfig , asrConfig , false )
if err != nil {
return nil , err
}
req . Header . Set ( "Accept" , "application/json" )
resp , err := r . baseModel . httpClient . Do ( req )
if err != nil {
return nil , fmt . Errorf ( "failed to send request: %w" , err )
}
defer resp . Body . Close ( )
respBody , err := io . ReadAll ( resp . Body )
if err != nil {
return nil , fmt . Errorf ( "failed to read response body: %w" , err )
}
if resp . StatusCode != http . StatusOK {
return nil , fmt . Errorf ( "RAGcon ASR API error: %s, body: %s" , resp . Status , string ( respBody ) )
}
return decodeOpenAIASRResponse ( respBody , responseFormat )
}
2026-07-22 22:30:57 +08:00
func ( r * RAGconModel ) TranscribeAudioWithSender ( ctx context . Context , modelName * string , file * string , apiConfig * APIConfig , asrConfig * ASRConfig , modelUsage * common . ModelUsage , sender func ( * string , * string ) error ) error {
2026-07-15 14:59:23 +02:00
if sender == nil {
return fmt . Errorf ( "sender is required" )
}
2026-07-22 22:30:57 +08:00
req , responseFormat , err := r . newASRRequest ( ctx , modelName , file , apiConfig , asrConfig , true )
2026-07-15 14:59:23 +02:00
if err != nil {
return err
}
req . Header . Set ( "Accept" , "text/event-stream" )
resp , err := r . baseModel . httpClient . Do ( req )
if err != nil {
return fmt . Errorf ( "failed to send request: %w" , err )
}
defer resp . Body . Close ( )
if resp . StatusCode != http . StatusOK {
respBody , _ := io . ReadAll ( resp . Body )
return fmt . Errorf ( "RAGcon ASR stream API error: %s, body: %s" , resp . Status , string ( respBody ) )
}
if ! strings . Contains ( resp . Header . Get ( "Content-Type" ) , "text/event-stream" ) {
respBody , err := io . ReadAll ( resp . Body )
if err != nil {
return fmt . Errorf ( "failed to read response body: %w" , err )
}
response , err := decodeOpenAIASRResponse ( respBody , responseFormat )
if err != nil {
return err
}
if response != nil && response . Text != "" {
if err = sender ( & response . Text , nil ) ; err != nil {
return err
}
}
done := "[DONE]"
return sender ( & done , nil )
}
sentDelta := false
if _ , err = ParseSSEStream [ struct {
Type string ` json:"type" `
Delta string ` json:"delta" `
Text string ` json:"text" `
} ] ( resp . Body , func ( event struct {
Type string ` json:"type" `
Delta string ` json:"delta" `
Text string ` json:"text" `
} ) error {
switch {
case event . Delta != "" :
if err = sender ( & event . Delta , nil ) ; err != nil {
return err
}
sentDelta = true
case event . Type == "transcript.text.segment" && event . Text != "" :
if err = sender ( & event . Text , nil ) ; err != nil {
return err
}
sentDelta = true
case event . Type == "transcript.text.done" && ! sentDelta && event . Text != "" :
if err = sender ( & event . Text , nil ) ; err != nil {
return err
}
}
return nil
} ) ; err != nil {
return fmt . Errorf ( "error reading RAGcon ASR stream: %w" , err )
}
done := "[DONE]"
return sender ( & done , nil )
}
func ( r * RAGconModel ) newTTSRequest ( ctx context . Context , modelName * string , audioContent * string , apiConfig * APIConfig , ttsConfig * TTSConfig , stream bool ) ( * http . Request , string , error ) {
if err := r . baseModel . APIConfigCheck ( apiConfig ) ; err != nil {
return nil , "" , err
}
if modelName == nil || strings . TrimSpace ( * modelName ) == "" {
return nil , "" , fmt . Errorf ( "model name is required" )
}
if audioContent == nil || * audioContent == "" {
return nil , "" , fmt . Errorf ( "audio content is empty" )
}
baseURL , err := r . baseModel . GetBaseURL ( apiConfig )
if err != nil {
return nil , "" , err
}
baseURL = strings . TrimSuffix ( baseURL , "/" )
url := fmt . Sprintf ( "%s/%s" , baseURL , strings . TrimPrefix ( r . baseModel . URLSuffix . TTS , "/" ) )
reqBody := map [ string ] interface { } {
"model" : * modelName ,
"input" : * audioContent ,
}
if ttsConfig != nil && ttsConfig . Params != nil {
for key , value := range ttsConfig . Params {
reqBody [ key ] = value
}
}
if ttsConfig != nil && ttsConfig . Format != "" {
reqBody [ "response_format" ] = ttsConfig . Format
}
if stream {
if _ , ok := reqBody [ "stream_format" ] ; ! ok {
reqBody [ "stream_format" ] = "audio"
}
}
voice , ok := reqBody [ "voice" ]
if ! ok || voice == nil {
return nil , "" , fmt . Errorf ( "voice is required" )
}
voiceString , ok := voice . ( string )
if ! ok || strings . TrimSpace ( voiceString ) == "" {
return nil , "" , fmt . Errorf ( "voice is required" )
}
streamFormat , _ := reqBody [ "stream_format" ] . ( string )
jsonData , err := json . Marshal ( reqBody )
if err != nil {
return nil , "" , fmt . Errorf ( "failed to marshal request: %w" , err )
}
req , err := http . NewRequestWithContext ( ctx , http . MethodPost , url , bytes . NewBuffer ( jsonData ) )
if err != nil {
return nil , "" , fmt . Errorf ( "failed to create request: %w" , err )
}
req . Header . Set ( "Content-Type" , "application/json" )
req . Header . Set ( "Authorization" , fmt . Sprintf ( "Bearer %s" , * apiConfig . ApiKey ) )
return req , streamFormat , nil
}
2026-07-22 22:30:57 +08:00
func ( r * RAGconModel ) AudioSpeech ( ctx context . Context , modelName * string , audioContent * string , apiConfig * APIConfig , ttsConfig * TTSConfig , modelUsage * common . ModelUsage ) ( * TTSResponse , error ) {
ctx , cancel := context . WithTimeout ( ctx , nonStreamCallTimeout )
2026-07-15 14:59:23 +02:00
defer cancel ( )
req , _ , err := r . newTTSRequest ( ctx , modelName , audioContent , apiConfig , ttsConfig , false )
if err != nil {
return nil , err
}
resp , err := r . baseModel . httpClient . Do ( req )
if err != nil {
return nil , fmt . Errorf ( "failed to send request: %w" , err )
}
defer resp . Body . Close ( )
body , err := io . ReadAll ( resp . Body )
if err != nil {
return nil , fmt . Errorf ( "failed to read response body: %w" , err )
}
if resp . StatusCode != http . StatusOK {
return nil , fmt . Errorf ( "RAGcon TTS API error: %s, body: %s" , resp . Status , string ( body ) )
}
return & TTSResponse { Audio : body } , nil
}
2026-07-22 22:30:57 +08:00
func ( r * RAGconModel ) AudioSpeechWithSender ( ctx context . Context , modelName * string , audioContent * string , apiConfig * APIConfig , ttsConfig * TTSConfig , modelUsage * common . ModelUsage , sender func ( * string , * string ) error ) error {
2026-07-15 14:59:23 +02:00
if sender == nil {
return fmt . Errorf ( "sender is required" )
}
2026-07-22 22:30:57 +08:00
req , streamFormat , err := r . newTTSRequest ( ctx , modelName , audioContent , apiConfig , ttsConfig , true )
2026-07-15 14:59:23 +02:00
if err != nil {
return err
}
if streamFormat == "sse" {
req . Header . Set ( "Accept" , "text/event-stream" )
}
resp , err := r . baseModel . httpClient . Do ( req )
if err != nil {
return fmt . Errorf ( "failed to send request: %w" , err )
}
defer resp . Body . Close ( )
if resp . StatusCode != http . StatusOK {
body , _ := io . ReadAll ( resp . Body )
return fmt . Errorf ( "RAGcon TTS stream API error: %s, body: %s" , resp . Status , string ( body ) )
}
if streamFormat == "sse" || strings . Contains ( resp . Header . Get ( "Content-Type" ) , "text/event-stream" ) {
return readOpenAITTSSSEStream ( resp . Body , sender )
}
return readOpenAITTSRawStream ( resp . Body , sender )
}
// OCRFile is not offered by RAGcon.
2026-07-22 22:30:57 +08:00
func ( r * RAGconModel ) OCRFile ( ctx context . Context , modelName * string , content [ ] byte , url * string , apiConfig * APIConfig , ocrConfig * OCRConfig , modelUsage * common . ModelUsage ) ( * OCRFileResponse , error ) {
2026-07-15 14:59:23 +02:00
return nil , fmt . Errorf ( "%s, no such method" , r . Name ( ) )
}
// ParseFile is not offered by RAGcon.
2026-07-22 22:30:57 +08:00
func ( r * RAGconModel ) ParseFile ( ctx context . Context , modelName * string , content [ ] byte , url * string , apiConfig * APIConfig , parseFileConfig * ParseFileConfig , modelUsage * common . ModelUsage ) ( * ParseFileResponse , error ) {
2026-07-15 14:59:23 +02:00
return nil , fmt . Errorf ( "%s, no such method" , r . Name ( ) )
}
2026-07-22 22:30:57 +08:00
func ( r * RAGconModel ) ListTasks ( ctx context . Context , apiConfig * APIConfig ) ( [ ] ListTaskStatus , error ) {
2026-07-15 14:59:23 +02:00
return nil , fmt . Errorf ( "%s, no such method" , r . Name ( ) )
}
2026-07-22 22:30:57 +08:00
func ( r * RAGconModel ) ShowTask ( ctx context . Context , taskID string , apiConfig * APIConfig ) ( * TaskResponse , error ) {
2026-07-15 14:59:23 +02:00
return nil , fmt . Errorf ( "%s, no such method" , r . Name ( ) )
}