From 6023eb27aca83b3b1b11d575cd81f143372debcc Mon Sep 17 00:00:00 2001 From: Jonah Hartmann Date: Fri, 6 Mar 2026 02:37:27 +0100 Subject: [PATCH] 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> --- conf/llm_factories.json | 8 ++++ rag/llm/chat_model.py | 14 ++++++ rag/llm/cv_model.py | 23 +++++++++ rag/llm/embedding_model.py | 14 ++++++ rag/llm/rerank_model.py | 44 +++++++++++++++++ rag/llm/sequence2txt_model.py | 45 +++++++++++++++++ rag/llm/tts_model.py | 48 +++++++++++++++++++ web/src/assets/svg/llm/ragcon.svg | 24 ++++++++++ web/src/components/svg-icon.tsx | 1 + web/src/constants/llm.ts | 2 + web/src/pages/user-setting/constants.tsx | 1 + .../modal/ollama-modal/index.tsx | 9 ++++ 12 files changed, 233 insertions(+) create mode 100644 web/src/assets/svg/llm/ragcon.svg diff --git a/conf/llm_factories.json b/conf/llm_factories.json index 8f898da902..175ebf0146 100644 --- a/conf/llm_factories.json +++ b/conf/llm_factories.json @@ -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": [] } ] } diff --git a/rag/llm/chat_model.py b/rag/llm/chat_model.py index e763fca53a..10b2fb5155 100644 --- a/rag/llm/chat_model.py +++ b/rag/llm/chat_model.py @@ -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) diff --git a/rag/llm/cv_model.py b/rag/llm/cv_model.py index 7543ca6408..ff868d6bdb 100644 --- a/rag/llm/cv_model.py +++ b/rag/llm/cv_model.py @@ -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) \ No newline at end of file diff --git a/rag/llm/embedding_model.py b/rag/llm/embedding_model.py index 79dc96accf..f4b58619ba 100644 --- a/rag/llm/embedding_model.py +++ b/rag/llm/embedding_model.py @@ -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) \ No newline at end of file diff --git a/rag/llm/rerank_model.py b/rag/llm/rerank_model.py index b8fd19dacd..5002fe7651 100644 --- a/rag/llm/rerank_model.py +++ b/rag/llm/rerank_model.py @@ -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 \ No newline at end of file diff --git a/rag/llm/sequence2txt_model.py b/rag/llm/sequence2txt_model.py index abbdb4de3f..0191482cf7 100644 --- a/rag/llm/sequence2txt_model.py +++ b/rag/llm/sequence2txt_model.py @@ -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) diff --git a/rag/llm/tts_model.py b/rag/llm/tts_model.py index 602ea165a1..b39b6a8c7b 100644 --- a/rag/llm/tts_model.py +++ b/rag/llm/tts_model.py @@ -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 diff --git a/web/src/assets/svg/llm/ragcon.svg b/web/src/assets/svg/llm/ragcon.svg new file mode 100644 index 0000000000..11acb87762 --- /dev/null +++ b/web/src/assets/svg/llm/ragcon.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + diff --git a/web/src/components/svg-icon.tsx b/web/src/components/svg-icon.tsx index 6a14fc64bb..87cf46bf4c 100644 --- a/web/src/components/svg-icon.tsx +++ b/web/src/components/svg-icon.tsx @@ -85,6 +85,7 @@ const svgIcons = [ LLMFactory.N1n, // LLMFactory.DeerAPI, LLMFactory.Avian, + LLMFactory.RAGcon, ]; export const LlmIcon = ({ diff --git a/web/src/constants/llm.ts b/web/src/constants/llm.ts index 6cde18fcb9..e3f1dffbaf 100644 --- a/web/src/constants/llm.ts +++ b/web/src/constants/llm.ts @@ -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 = { diff --git a/web/src/pages/user-setting/constants.tsx b/web/src/pages/user-setting/constants.tsx index 22fadbab11..1e32df7536 100644 --- a/web/src/pages/user-setting/constants.tsx +++ b/web/src/pages/user-setting/constants.tsx @@ -39,6 +39,7 @@ export const LocalLlmFactories = [ LLMFactory.GPUStack, LLMFactory.ModelScope, LLMFactory.VLLM, + LLMFactory.RAGcon, ]; export enum TenantRole { diff --git a/web/src/pages/user-setting/setting-model/modal/ollama-modal/index.tsx b/web/src/pages/user-setting/setting-model/modal/ollama-modal/index.tsx index bfe7807b00..a1c00e5aa0 100644 --- a/web/src/pages/user-setting/setting-model/modal/ollama-modal/index.tsx +++ b/web/src/pages/user-setting/setting-model/modal/ollama-modal/index.tsx @@ -27,6 +27,7 @@ const llmFactoryToUrlMap: Partial> = { [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',