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 8f898da90..175ebf014 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 e763fca53..10b2fb515 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 7543ca640..ff868d6bd 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 79dc96acc..f4b58619b 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 b8fd19dac..5002fe765 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 abbdb4de3..0191482cf 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 602ea165a..b39b6a8c7 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 000000000..11acb8776 --- /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 6a14fc64b..87cf46bf4 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 6cde18fcb..e3f1dffba 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 22fadbab1..1e32df753 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 bfe7807b0..a1c00e5aa 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',