feat: add Ragcon provider (#13425)

### What problem does this PR solve?

This PR aims to extend the list of possible providers. Adds new Provider
"RAGcon" within the Ollama Modal. It provides all model types except OCR
via Openai-compatible endpoints.

### Type of change

- [x] New Feature (non-breaking change which adds functionality)

---------

Co-authored-by: Jakob <16180662+hauberj@users.noreply.github.com>
This commit is contained in:
Jonah Hartmann
2026-03-06 02:37:27 +01:00
committed by GitHub
parent c35b210c3a
commit 6023eb27ac
12 changed files with 233 additions and 0 deletions

View File

@@ -6267,6 +6267,14 @@
"is_tools": true
}
]
},
{
"name": "RAGcon",
"logo": "",
"tags": "LLM,TEXT EMBEDDING,TTS,TEXT RE-RANK,SPEECH2TEXT,IMAGE2TEXT",
"status": "1",
"rank": "100",
"llm": []
}
]
}

View File

@@ -1658,3 +1658,17 @@ class LiteLLMBase(ABC):
completion_args["extra_headers"] = extra_headers
return completion_args
class RAGconChat(Base):
"""
RAGcon Chat Provider - routes through LiteLLM proxy
All model types are handled through a unified LiteLLM endpoint.
Default Base URL: https://connect.ragcon.com/v1
"""
_FACTORY_NAME = "RAGcon"
def __init__(self, key, model_name, base_url=None, **kwargs):
if not base_url:
base_url = "https://connect.ragcon.com/v1"
super().__init__(key, model_name, base_url, **kwargs)

View File

@@ -1252,3 +1252,26 @@ class MoonshotCV(GptV4):
if not base_url:
base_url = "https://api.moonshot.cn/v1"
super().__init__(key, model_name, lang=lang, base_url=base_url, **kwargs)
class RAGconCV(GptV4):
"""
RAGcon CV Provider - routes through LiteLLM proxy
Supports vision models through LiteLLM.
Default Base URL: https://connect.ragcon.ai/v1
"""
_FACTORY_NAME = "RAGcon"
def __init__(self, key, model_name, lang="Chinese", base_url="", **kwargs):
if not base_url:
base_url = "https://connect.ragcon.com/v1"
# Initialize client
self.client = OpenAI(api_key=key, base_url=base_url)
self.async_client = AsyncOpenAI(api_key=key, base_url=base_url)
self.model_name = model_name
self.lang = lang
Base.__init__(self, **kwargs)

View File

@@ -1080,3 +1080,17 @@ class JiekouAIEmbed(OpenAIEmbed):
if not base_url:
base_url = "https://api.jiekou.ai/openai/v1/embeddings"
super().__init__(key, model_name, base_url)
class RAGconEmbed(OpenAIEmbed):
"""
RAGcon Embedding Provider - routes through LiteLLM proxy
Default Base URL: https://connect.ragcon.ai/v1
"""
_FACTORY_NAME = "RAGcon"
def __init__(self, key, model_name="text-embedding-3-small", base_url=None):
if not base_url:
base_url = "https://connect.ragcon.com/v1"
super().__init__(key, model_name, base_url)

View File

@@ -506,3 +506,47 @@ class JiekouAIRerank(JinaRerank):
if not base_url:
base_url = "https://api.jiekou.ai/openai/v1/rerank"
super().__init__(key, model_name, base_url)
class RAGconRerank(Base):
"""
RAGcon Rerank Provider - routes through LiteLLM proxy
Assumes LiteLLM proxy supports /rerank endpoint.
Default Base URL: https://connect.ragcon.ai/v1
"""
_FACTORY_NAME = "RAGcon"
def __init__(self, key, model_name, base_url=None, **kwargs):
if not base_url:
base_url = "https://connect.ragcon.com/v1"
self._api_key = key
self._base_url = base_url
self.headers = {"Content-Type": "application/json", "Authorization": f"Bearer {key}"}
self.model_name = model_name
def similarity(self, query: str, texts: list):
# noway to config Ragflow , use fix setting
texts = [truncate(t, 500) for t in texts]
data = {
"model": self.model_name,
"query": query,
"documents": texts,
"top_n": len(texts),
}
token_count = 0
for t in texts:
token_count += num_tokens_from_string(t)
res = requests.post(self._base_url + "/rerank", headers=self.headers, json=data).json()
rank = np.zeros(len(texts), dtype=float)
try:
for d in res["results"]:
rank[d["index"]] = d["relevance_score"]
except Exception as _e:
log_exception(_e, res)
rank = Base._normalize_rank(rank)
return rank, token_count

View File

@@ -376,3 +376,48 @@ class ZhipuSeq2txt(Base):
return f"**ERROR**: code: {error['code']}, message: {error['message']}", 0
except Exception as e:
return "**ERROR**: " + str(e), 0
class RAGconSeq2txt(Base):
"""
RAGcon Sequence2Text Provider - routes through LiteLLM proxy
Speech-to-text models routed through LiteLLM.
Default Base URL: https://connect.ragcon.com/v1
"""
_FACTORY_NAME = "RAGcon"
def __init__(self, key, model_name, base_url=None, lang="English", **kwargs):
# Use provided base_url or fallback to default
if not base_url:
base_url = "https://connect.ragcon.com/v1"
self.base_url = base_url
self.model_name = model_name
self.key = key
self.lang = lang
self.client = OpenAI(api_key=key, base_url=self.base_url)
def transcription(self, audio_path, **kwargs):
"""
Transcribe audio file using RAGcon's OpenAI-compatible API.
Uses Whisper's automatic language detection for German and English audio.
Args:
audio_path: Path to the audio file
**kwargs: Additional parameters (currently unused but maintained for compatibility)
Returns:
tuple: (transcribed_text, token_count)
"""
with open(audio_path, "rb") as audio_file:
# Call RAGcon API - Whisper will auto-detect language
transcription = self.client.audio.transcriptions.create(
model=self.model_name,
file=audio_file
)
# Return text and token count
text = transcription.text.strip()
return text, num_tokens_from_string(text)

View File

@@ -482,3 +482,51 @@ class StepFunTTS(OpenAITTS):
yield chunk
yield num_tokens_from_string(text)
class RAGconTTS(Base):
"""
RAGcon TTS Provider - routes through LiteLLM proxy
Text-to-speech models routed through LiteLLM.
Default Base URL: https://connect.ragcon.ai/v1
"""
_FACTORY_NAME = "RAGcon"
def __init__(self, key, model_name, base_url=None, **kwargs):
if not base_url:
base_url = "https://connect.ragcon.com/v1"
self.base_url = base_url
self.api_key = key
self.model_name = model_name
self.headers = {
"accept": "application/json",
"Content-Type": "application/json",
"Authorization": f"Bearer {self.api_key}"
}
def tts(self, text, voice="English Female", stream=True):
"""
Uses LiteLLM's /v1/audio/speech endpoint
"""
payload = {
"model": self.model_name,
"input": text,
"voice": voice
}
response = requests.post(
f"{self.base_url}/audio/speech",
headers=self.headers,
json=payload,
stream=stream
)
if response.status_code != 200:
raise Exception(f"**Error**: {response.status_code}, {response.text}")
for chunk in response.iter_content(chunk_size=1024):
if chunk:
yield chunk

View File

@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" width="75" height="75" version="1.1" viewBox="0 0 75 75">
<defs>
<style>
.cls-1 {
fill: #63c3d1;
}
.cls-2 {
fill: #194870;
}
</style>
</defs>
<!-- Generator: Adobe Illustrator 28.7.7, SVG Export Plug-In . SVG Version: 1.2.0 Build 194) -->
<g>
<g id="Ebene_1">
<g>
<path class="cls-2" d="M8.889.148C4.043.148.115,4.078.115,8.923s3.928,8.774,8.774,8.774,8.774-3.929,8.774-8.774S13.735.148,8.889.148ZM8.889,13.179c-2.351,0-4.256-1.905-4.256-4.256s1.905-4.256,4.256-4.256,4.256,1.905,4.256,4.256-1.905,4.256-4.256,4.256Z"/>
<path class="cls-2" d="M25.866,33.473c-4.846,0-8.774,3.93-8.774,8.775s3.928,8.774,8.774,8.774,8.774-3.929,8.774-8.774-3.928-8.775-8.774-8.775ZM25.866,46.503c-2.351,0-4.256-1.905-4.256-4.256s1.905-4.256,4.256-4.256,4.256,1.905,4.256,4.256-1.905,4.256-4.256,4.256Z"/>
</g>
<path class="cls-1" d="M73.319,63.835l-10.95-21.456c4.362-4.335,7.063-10.261,7.063-16.796,0-13.242-11.058-24.015-24.651-24.015l-31.11.003c2.402,1.566,3.992,4.271,3.992,7.352s-1.591,5.786-3.994,7.351l31.112-.003c5.485,0,9.947,4.177,9.947,9.312s-4.462,9.311-9.948,9.311l-13.302.002c-.258,0-.513.014-.764.04,2.365,1.572,3.926,4.259,3.926,7.311s-1.561,5.74-3.927,7.311c.253.026.509.04.768.04h13.301c1.555,0,3.07-.159,4.546-.428l10.895,21.347c1.3,2.547,3.879,4.012,6.554,4.012,1.124,0,2.265-.259,3.336-.806,3.616-1.845,5.051-6.272,3.206-9.889Z"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -85,6 +85,7 @@ const svgIcons = [
LLMFactory.N1n,
// LLMFactory.DeerAPI,
LLMFactory.Avian,
LLMFactory.RAGcon,
];
export const LlmIcon = ({

View File

@@ -64,6 +64,7 @@ export enum LLMFactory {
PaddleOCR = 'PaddleOCR',
N1n = 'n1n',
Avian = 'Avian',
RAGcon = 'RAGcon',
}
// Please lowercase the file name
@@ -133,6 +134,7 @@ export const IconMap = {
[LLMFactory.PaddleOCR]: 'paddleocr',
[LLMFactory.N1n]: 'n1n',
[LLMFactory.Avian]: 'avian',
[LLMFactory.RAGcon]: 'ragcon',
};
export const APIMapUrl = {

View File

@@ -39,6 +39,7 @@ export const LocalLlmFactories = [
LLMFactory.GPUStack,
LLMFactory.ModelScope,
LLMFactory.VLLM,
LLMFactory.RAGcon,
];
export enum TenantRole {

View File

@@ -27,6 +27,7 @@ const llmFactoryToUrlMap: Partial<Record<LLMFactory, string>> = {
[LLMFactory.LMStudio]: 'https://lmstudio.ai/docs/basics',
[LLMFactory.OpenAiAPICompatible]:
'https://platform.openai.com/docs/models/gpt-4',
[LLMFactory.RAGcon]: 'https://www.ragcon.ai/erste-schritte-mit-ragflow/',
[LLMFactory.TogetherAI]: 'https://docs.together.ai/docs/deployment-options',
[LLMFactory.Replicate]: 'https://replicate.com/docs/topics/deployments',
[LLMFactory.OpenRouter]: 'https://openrouter.ai/docs',
@@ -81,6 +82,14 @@ const OllamaModal = ({
'speech2text',
'tts',
]),
[LLMFactory.RAGcon]: buildModelTypeOptions([
'chat',
'embedding',
'rerank',
'image2text',
'speech2text',
'tts',
]),
[LLMFactory.ModelScope]: buildModelTypeOptions(['chat']),
[LLMFactory.GPUStack]: buildModelTypeOptions([
'chat',