Feat: optional url v1 (#16774)

This commit is contained in:
Lynn
2026-07-09 15:53:06 +08:00
committed by GitHub
parent a9420a7832
commit 0ba1d37a10
7 changed files with 142 additions and 64 deletions

View File

@@ -2008,7 +2008,8 @@
"max_tokens": 512,
"model_type": "embedding"
}
]
],
"url": "https://api.baichuan-ai.com/v1"
},
{
"name": "Jina",
@@ -2641,7 +2642,8 @@
"model_type": "speech2text",
"is_tools": false
}
]
],
"url": "https://api.stepfun.com/v1"
},
{
"name": "NVIDIA",
@@ -3586,7 +3588,8 @@
"max_tokens": 4000,
"model_type": "embedding"
}
]
],
"url": "https://api.upstage.ai/v1/solar"
},
{
"name": "NovitaAI",
@@ -3713,7 +3716,8 @@
"max_tokens": 8192,
"model_type": "embedding"
}
]
],
"url": "https://api.novita.ai/v3/openai"
},
{
"name": "SILICONFLOW",
@@ -4840,7 +4844,8 @@
"max_tokens": 32768,
"model_type": "chat"
}
]
],
"url": "https://api.ppinfra.com/v3/openai"
},
{
"name": "Replicate",
@@ -5003,7 +5008,8 @@
"thinking"
]
}
]
],
"url": "https://spark-api-open.xf-yun.com/v1"
},
{
"name": "BaiduYiyan",
@@ -6199,7 +6205,8 @@
"model_type": "rerank",
"is_tools": false
}
]
],
"url": "https://api.302.ai/v1"
},
{
"name": "CometAPI",
@@ -6493,7 +6500,8 @@
"model_type": "tts",
"is_tools": false
}
]
],
"url": "https://api.cometapi.com/v1"
},
{
"name": "LongCat",
@@ -6810,7 +6818,8 @@
"model_type": "tts",
"is_tools": false
}
]
],
"url": "https://api.deerapi.com/v1"
},
{
"name": "Jiekou.AI",
@@ -7464,7 +7473,8 @@
"max_tokens": 32768,
"model_type": "rerank"
}
]
],
"url": "https://api.jiekou.ai/openai"
},
{
"name": "MinerU",
@@ -7996,14 +8006,16 @@
"model_type": "chat",
"is_tools": true
}
]
],
"url": "https://api.avian.io/v1"
},
{
"name": "RAGcon",
"logo": "",
"tags": "LLM,TEXT EMBEDDING,TTS,TEXT RE-RANK,SPEECH2TEXT,IMAGE2TEXT",
"status": "1",
"llm": []
"llm": [],
"url": "https://connect.ragcon.com/v1"
},
{
"name": "Xiaomi",

View File

@@ -38,6 +38,7 @@ from rag.llm import FACTORY_DEFAULT_BASE_URL, LITELLM_PROVIDER_PREFIX, Supported
from rag.llm.key_utils import _normalize_replicate_key
from rag.llm.tool_decorator import FunctionToolSession, is_tool
from rag.nlp import is_chinese, is_english
from rag.utils.url_utils import ensure_v1
class LLMErrorCode(StrEnum):
@@ -218,8 +219,9 @@ def _move_litellm_provider_body_fields(provider: SupportedLiteLLMProvider | str
class Base(ABC):
def __init__(self, key, model_name, base_url, **kwargs):
timeout = int(os.environ.get("LLM_TIMEOUT_SECONDS", 600))
self.client = OpenAI(api_key=key, base_url=base_url, timeout=timeout)
self.async_client = AsyncOpenAI(api_key=key, base_url=base_url, timeout=timeout)
self.base_url = ensure_v1(base_url)
self.client = OpenAI(api_key=key, base_url=self.base_url, timeout=timeout)
self.async_client = AsyncOpenAI(api_key=key, base_url=self.base_url, timeout=timeout)
self.model_name = model_name
# Configure retry parameters
self.max_retries = kwargs.get("max_retries", int(os.environ.get("LLM_MAX_RETRIES", 5)))
@@ -783,7 +785,6 @@ class XinferenceChat(Base):
def __init__(self, key=None, model_name="", base_url="", **kwargs):
if not base_url:
raise ValueError("Local llm url cannot be None")
base_url = urljoin(base_url, "v1")
super().__init__(key, model_name, base_url, **kwargs)
@@ -793,7 +794,6 @@ class HuggingFaceChat(Base):
def __init__(self, key=None, model_name="", base_url="", **kwargs):
if not base_url:
raise ValueError("Local llm url cannot be None")
base_url = urljoin(base_url, "v1")
super().__init__(key, model_name.split("___")[0], base_url, **kwargs)
@@ -803,7 +803,6 @@ class ModelScopeChat(Base):
def __init__(self, key=None, model_name="", base_url="", **kwargs):
if not base_url:
raise ValueError("Local llm url cannot be None")
base_url = urljoin(base_url, "v1")
super().__init__(key, model_name.split("___")[0], base_url, **kwargs)
@@ -894,8 +893,7 @@ class LocalAIChat(Base):
if not base_url:
raise ValueError("Local llm url cannot be None")
base_url = urljoin(base_url, "v1")
self.client = OpenAI(api_key="empty", base_url=base_url)
self.client = OpenAI(api_key="empty", base_url=self.base_url)
self.model_name = model_name.split("___")[0]
@@ -1032,9 +1030,8 @@ class LmStudioChat(Base):
def __init__(self, key, model_name, base_url, **kwargs):
if not base_url:
raise ValueError("Local llm url cannot be None")
base_url = urljoin(base_url, "v1")
super().__init__(key, model_name, base_url, **kwargs)
self.client = OpenAI(api_key="lm-studio", base_url=base_url)
self.client = OpenAI(api_key="lm-studio", base_url=self.base_url)
self.model_name = model_name

View File

@@ -34,6 +34,7 @@ from openai.lib.azure import AzureOpenAI, AsyncAzureOpenAI
from common.token_utils import num_tokens_from_string, total_token_count_from_response
from rag.nlp import is_english
from rag.prompts.generator import vision_llm_describe_prompt
from rag.utils.url_utils import ensure_v1
from common.misc_utils import thread_pool_exec
@@ -267,9 +268,10 @@ class GptV4(Base):
def __init__(self, key, model_name="gpt-4-vision-preview", lang="Chinese", base_url="https://api.openai.com/v1", **kwargs):
if not base_url:
base_url = "https://api.openai.com/v1"
self.base_url = ensure_v1(base_url)
self.api_key = key
self.client = OpenAI(api_key=key, base_url=base_url)
self.async_client = AsyncOpenAI(api_key=key, base_url=base_url)
self.client = OpenAI(api_key=key, base_url=self.base_url)
self.async_client = AsyncOpenAI(api_key=key, base_url=self.base_url)
self.model_name = model_name
self.lang = lang
super().__init__(**kwargs)
@@ -309,8 +311,9 @@ class AzureGptV4(GptV4):
def __init__(self, key, model_name, lang="Chinese", **kwargs):
api_key, api_version = _resolve_azure_credentials(key)
self.client = AzureOpenAI(api_key=api_key, azure_endpoint=kwargs["base_url"], api_version=api_version)
self.async_client = AsyncAzureOpenAI(api_key=api_key, azure_endpoint=kwargs["base_url"], api_version=api_version)
self.base_url = ensure_v1(kwargs["base_url"])
self.client = AzureOpenAI(api_key=api_key, azure_endpoint=self.base_url, api_version=api_version)
self.async_client = AsyncAzureOpenAI(api_key=api_key, azure_endpoint=self.base_url, api_version=api_version)
self.model_name = model_name
self.lang = lang
Base.__init__(self, **kwargs)
@@ -551,8 +554,9 @@ class StepFunCV(GptV4):
def __init__(self, key, model_name="step-1v-8k", lang="Chinese", base_url="https://api.stepfun.com/v1", **kwargs):
if not base_url:
base_url = "https://api.stepfun.com/v1"
self.client = OpenAI(api_key=key, base_url=base_url)
self.async_client = AsyncOpenAI(api_key=key, base_url=base_url)
self.base_url = ensure_v1(base_url)
self.client = OpenAI(api_key=key, base_url=self.base_url)
self.async_client = AsyncOpenAI(api_key=key, base_url=self.base_url)
self.model_name = model_name
self.lang = lang
Base.__init__(self, **kwargs)
@@ -572,9 +576,9 @@ class VolcEngineCV(GptV4):
except JSONDecodeError:
api_key = key
llm_name = model_name
self.client = OpenAI(api_key=api_key, base_url=base_url)
self.async_client = AsyncOpenAI(api_key=api_key, base_url=base_url)
self.base_url = ensure_v1(base_url)
self.client = OpenAI(api_key=api_key, base_url=self.base_url)
self.async_client = AsyncOpenAI(api_key=api_key, base_url=self.base_url)
self.model_name = llm_name
self.lang = lang
Base.__init__(self, **kwargs)
@@ -586,9 +590,9 @@ class LmStudioCV(GptV4):
def __init__(self, key, model_name, lang="Chinese", base_url="", **kwargs):
if not base_url:
raise ValueError("Local llm url cannot be None")
base_url = urljoin(base_url, "v1")
self.client = OpenAI(api_key="lm-studio", base_url=base_url)
self.async_client = AsyncOpenAI(api_key="lm-studio", base_url=base_url)
self.base_url = ensure_v1(base_url)
self.client = OpenAI(api_key="lm-studio", base_url=self.base_url)
self.async_client = AsyncOpenAI(api_key="lm-studio", base_url=self.base_url)
self.model_name = model_name
self.lang = lang
Base.__init__(self, **kwargs)
@@ -600,9 +604,9 @@ class OpenAI_APICV(GptV4):
def __init__(self, key, model_name, lang="Chinese", base_url="", **kwargs):
if not base_url:
raise ValueError("url cannot be None")
base_url = urljoin(base_url, "v1")
self.client = OpenAI(api_key=key, base_url=base_url)
self.async_client = AsyncOpenAI(api_key=key, base_url=base_url)
self.base_url = ensure_v1(base_url)
self.client = OpenAI(api_key=key, base_url=self.base_url)
self.async_client = AsyncOpenAI(api_key=key, base_url=self.base_url)
self.model_name = model_name.split("___")[0]
self.lang = lang
Base.__init__(self, **kwargs)
@@ -647,8 +651,9 @@ class OpenRouterCV(GptV4):
except JSONDecodeError:
api_key = key
provider_order = ""
self.client = OpenAI(api_key=api_key, base_url=base_url)
self.async_client = AsyncOpenAI(api_key=api_key, base_url=base_url)
self.base_url = ensure_v1(base_url)
self.client = OpenAI(api_key=api_key, base_url=self.base_url)
self.async_client = AsyncOpenAI(api_key=api_key, base_url=self.base_url)
self.model_name = model_name
self.lang = lang
Base.__init__(self, **kwargs)
@@ -677,7 +682,7 @@ class LocalAICV(GptV4):
def __init__(self, key, model_name, base_url, lang="Chinese", **kwargs):
if not base_url:
raise ValueError("Local cv model url cannot be None")
base_url = urljoin(base_url, "v1")
base_url = ensure_v1(base_url)
self.client = OpenAI(api_key="empty", base_url=base_url)
self.async_client = AsyncOpenAI(api_key="empty", base_url=base_url)
self.model_name = model_name.split("___")[0]
@@ -689,7 +694,7 @@ class XinferenceCV(GptV4):
_FACTORY_NAME = "Xinference"
def __init__(self, key, model_name="", lang="Chinese", base_url="", **kwargs):
base_url = urljoin(base_url, "v1")
base_url = ensure_v1(base_url)
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
@@ -703,7 +708,7 @@ class GPUStackCV(GptV4):
def __init__(self, key, model_name, lang="Chinese", base_url="", **kwargs):
if not base_url:
raise ValueError("Local llm url cannot be None")
base_url = urljoin(base_url, "v1")
base_url = ensure_v1(base_url)
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
@@ -727,7 +732,8 @@ class OllamaCV(Base):
def __init__(self, key, model_name, lang="Chinese", **kwargs):
from ollama import Client
self.client = Client(host=kwargs["base_url"])
self.base_url = ensure_v1(kwargs["base_url"])
self.client = Client(host=self.base_url)
self.model_name = model_name
self.lang = lang
self.keep_alive = kwargs.get("ollama_keep_alive", int(os.environ.get("OLLAMA_KEEP_ALIVE", -1)))
@@ -1359,8 +1365,9 @@ class RAGconCV(GptV4):
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.base_url = ensure_v1(base_url)
self.client = OpenAI(api_key=key, base_url=self.base_url)
self.async_client = AsyncOpenAI(api_key=key, base_url=self.base_url)
self.model_name = model_name
self.lang = lang

View File

@@ -32,6 +32,7 @@ from common import settings
from common.exceptions import ModelException
from common.token_utils import num_tokens_from_string, truncate, total_token_count_from_response
from rag.llm.key_utils import _normalize_replicate_key
from rag.utils.url_utils import ensure_v1
import logging
import base64
@@ -259,7 +260,8 @@ class OpenAIEmbed(Base):
def __init__(self, key, model_name="text-embedding-ada-002", base_url="https://api.openai.com/v1"):
if not base_url:
base_url = "https://api.openai.com/v1"
self.client = OpenAI(api_key=key, base_url=base_url)
self.base_url = ensure_v1(base_url)
self.client = OpenAI(api_key=key, base_url=self.base_url)
self.model_name = model_name
def _call(self, batch):
@@ -281,7 +283,7 @@ class LocalAIEmbed(Base):
def __init__(self, key, model_name, base_url):
if not base_url:
raise ValueError("Local embedding model url cannot be None")
base_url = urljoin(base_url, "v1")
base_url = ensure_v1(base_url)
self.client = OpenAI(api_key="empty", base_url=base_url)
self.model_name = model_name.split("___")[0]
@@ -320,7 +322,9 @@ class AzureEmbed(OpenAIEmbed):
from openai.lib.azure import AzureOpenAI
api_key, api_version = _resolve_azure_credentials(key)
self.client = AzureOpenAI(api_key=api_key, azure_endpoint=kwargs["base_url"], api_version=api_version)
self.base_url = ensure_v1(kwargs["base_url"])
self.client = AzureOpenAI(api_key=api_key, azure_endpoint=self.base_url, api_version=api_version)
self.model_name = model_name
@@ -465,7 +469,8 @@ class OllamaEmbed(Base):
_special_tokens = ["<|endoftext|>"]
def __init__(self, key, model_name, **kwargs):
self.client = Client(host=kwargs["base_url"]) if not key or key == "x" else Client(host=kwargs["base_url"], headers={"Authorization": f"Bearer {key}"})
self.base_url = ensure_v1(kwargs["base_url"])
self.client = Client(host=self.base_url) if not key or key == "x" else Client(host=self.base_url, headers={"Authorization": f"Bearer {key}"})
self.model_name = model_name
self.keep_alive = kwargs.get("ollama_keep_alive", int(os.environ.get("OLLAMA_KEEP_ALIVE", -1)))
@@ -502,7 +507,7 @@ class XinferenceEmbed(Base):
_FACTORY_NAME = "Xinference"
def __init__(self, key, model_name="", base_url=""):
base_url = urljoin(base_url, "v1")
base_url = ensure_v1(base_url)
self.client = OpenAI(api_key=key, base_url=base_url)
self.model_name = model_name
@@ -805,7 +810,7 @@ class NvidiaEmbed(Base):
if not base_url:
base_url = "https://integrate.api.nvidia.com/v1/embeddings"
self.api_key = key
self.base_url = base_url
self.base_url = ensure_v1(base_url)
self.headers = {
"accept": "application/json",
"Content-Type": "application/json",
@@ -844,7 +849,7 @@ class LmStudioEmbed(LocalAIEmbed):
def __init__(self, key, model_name, base_url):
if not base_url:
raise ValueError("Local llm url cannot be None")
base_url = urljoin(base_url, "v1")
base_url = ensure_v1(base_url)
self.client = OpenAI(api_key="lm-studio", base_url=base_url)
self.model_name = model_name
@@ -855,7 +860,7 @@ class OpenAI_APIEmbed(OpenAIEmbed):
def __init__(self, key, model_name, base_url):
if not base_url:
raise ValueError("url cannot be None")
base_url = urljoin(base_url, "v1")
base_url = ensure_v1(base_url)
self.client = OpenAI(api_key=key, base_url=base_url)
self.model_name = model_name.split("___")[0]
@@ -1147,7 +1152,7 @@ class GPUStackEmbed(OpenAIEmbed):
def __init__(self, key, model_name, base_url):
if not base_url:
raise ValueError("url cannot be None")
base_url = urljoin(base_url, "v1")
base_url = ensure_v1(base_url)
self.client = OpenAI(api_key=key, base_url=base_url)
self.model_name = model_name

View File

@@ -27,6 +27,7 @@ from openai import OpenAI
from openai.lib.azure import AzureOpenAI
from common.token_utils import num_tokens_from_string
from rag.utils.url_utils import ensure_v1
class Base(ABC):
@@ -56,7 +57,8 @@ class GPTSeq2txt(Base):
def __init__(self, key, model_name="whisper-1", base_url="https://api.openai.com/v1", **kwargs):
if not base_url:
base_url = "https://api.openai.com/v1"
self.client = OpenAI(api_key=key, base_url=base_url)
self.base_url = ensure_v1(base_url)
self.client = OpenAI(api_key=key, base_url=self.base_url)
self.model_name = model_name
@@ -134,7 +136,8 @@ class AzureSeq2txt(Base):
_FACTORY_NAME = "Azure-OpenAI"
def __init__(self, key, model_name, lang="Chinese", **kwargs):
self.client = AzureOpenAI(api_key=key, azure_endpoint=kwargs["base_url"], api_version="2024-02-01")
self.base_url = ensure_v1(kwargs["base_url"])
self.client = AzureOpenAI(api_key=key, azure_endpoint=self.base_url, api_version="2024-02-01")
self.model_name = model_name
self.lang = lang
@@ -143,7 +146,7 @@ class XinferenceSeq2txt(Base):
_FACTORY_NAME = "Xinference"
def __init__(self, key, model_name="whisper-small", **kwargs):
self.base_url = kwargs.get("base_url", None)
self.base_url = ensure_v1(kwargs["base_url"]) if kwargs.get("base_url") else None
self.model_name = model_name
self.key = key
@@ -256,7 +259,8 @@ class GiteeSeq2txt(Base):
def __init__(self, key, model_name="whisper-1", base_url="https://ai.gitee.com/v1/", **kwargs):
if not base_url:
base_url = "https://ai.gitee.com/v1/"
self.client = OpenAI(api_key=key, base_url=base_url)
self.base_url = ensure_v1(base_url)
self.client = OpenAI(api_key=key, base_url=self.base_url)
self.model_name = model_name
@@ -266,8 +270,8 @@ class DeepInfraSeq2txt(Base):
def __init__(self, key, model_name, base_url="https://api.deepinfra.com/v1/openai", **kwargs):
if not base_url:
base_url = "https://api.deepinfra.com/v1/openai"
self.client = OpenAI(api_key=key, base_url=base_url)
self.base_url = ensure_v1(base_url)
self.client = OpenAI(api_key=key, base_url=self.base_url)
self.model_name = model_name
@@ -277,7 +281,8 @@ class CometAPISeq2txt(Base):
def __init__(self, key, model_name="whisper-1", base_url="https://api.cometapi.com/v1", **kwargs):
if not base_url:
base_url = "https://api.cometapi.com/v1"
self.client = OpenAI(api_key=key, base_url=base_url)
self.base_url = ensure_v1(base_url)
self.client = OpenAI(api_key=key, base_url=self.base_url)
self.model_name = model_name
@@ -287,7 +292,8 @@ class DeerAPISeq2txt(Base):
def __init__(self, key, model_name="whisper-1", base_url="https://api.deerapi.com/v1", **kwargs):
if not base_url:
base_url = "https://api.deerapi.com/v1"
self.client = OpenAI(api_key=key, base_url=base_url)
self.base_url = ensure_v1(base_url)
self.client = OpenAI(api_key=key, base_url=self.base_url)
self.model_name = model_name
@@ -366,7 +372,7 @@ class RAGconSeq2txt(Base):
if not base_url:
base_url = "https://connect.ragcon.com/v1"
self.base_url = base_url
self.base_url = ensure_v1(base_url)
self.model_name = model_name
self.key = key
self.lang = lang

View File

@@ -40,6 +40,7 @@ from pydantic import BaseModel, conint
from common.http_client import sync_request
from common.token_utils import num_tokens_from_string
from rag.utils.url_utils import ensure_v1
class ServeReferenceAudio(BaseModel):
@@ -88,7 +89,7 @@ class HTTPBasedTTS(Base):
def __init__(self, key, model_name, base_url, **kwargs):
self.model_name = model_name
self.base_url = base_url
self.base_url = ensure_v1(base_url)
self.api_key = key
self.headers = {"Content-Type": "application/json"}
if key and key != "x":
@@ -504,7 +505,7 @@ class RAGconTTS(Base):
if not base_url:
base_url = "https://connect.ragcon.com/v1"
self.base_url = base_url
self.base_url = ensure_v1(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}"}

50
rag/utils/url_utils.py Normal file
View File

@@ -0,0 +1,50 @@
#
# 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.
#
import re
from urllib.parse import urlparse, urlunparse
def ensure_v1(url: str) -> str:
"""Ensure the URL ends with a versioned path segment like ``/v1``.
If the path already contains a segment matching ``v{digit}`` (e.g. ``/v1``,
``/v2``, ``/v3``), the URL is returned unchanged. Otherwise the base host
is kept and ``/v1`` is appended.
Examples::
>>> ensure_v1("https://api.example.com")
'https://api.example.com/v1'
>>> ensure_v1("https://api.example.com/v1")
'https://api.example.com/v1'
>>> ensure_v1("https://api.example.com/v2/chat")
'https://api.example.com/v2/chat'
>>> ensure_v1("https://api.example.com/api/v3")
'https://api.example.com/api/v3'
"""
if not url:
return url
parsed = urlparse(url)
path = parsed.path.rstrip("/")
# Check if any path segment matches v{digit}, e.g. /v1, /v2, /v3
if re.search(r"/v\d+(?:/|$)", path + "/"):
return url
# No versioned segment found append /v1
new_path = (path + "/v1") if path else "/v1"
return urlunparse((parsed.scheme, parsed.netloc, new_path, parsed.params, parsed.query, parsed.fragment))