diff --git a/conf/llm_factories.json b/conf/llm_factories.json
index 681cb406a8..18c30e3abe 100644
--- a/conf/llm_factories.json
+++ b/conf/llm_factories.json
@@ -8058,6 +8058,14 @@
"model_type": "embedding"
}
]
+ },
+ {
+ "name": "New API",
+ "logo": "",
+ "tags": "LLM,TEXT EMBEDDING,TEXT RE-RANK,IMAGE2TEXT,TTS,SPEECH2TEXT",
+ "status": "1",
+ "llm": [],
+ "rank": "885"
}
]
}
diff --git a/rag/llm/chat_model.py b/rag/llm/chat_model.py
index bc452b1bbe..dd98e48ab5 100644
--- a/rag/llm/chat_model.py
+++ b/rag/llm/chat_model.py
@@ -2100,3 +2100,13 @@ class RAGconChat(Base):
base_url = "https://connect.ragcon.com/v1"
super().__init__(key, model_name, base_url, **kwargs)
+
+
+class NewAPIChat(Base):
+ _FACTORY_NAME = "New API"
+
+ def __init__(self, key, model_name, base_url, **kwargs):
+ if not base_url:
+ raise ValueError("url cannot be None")
+ model_name = model_name.split("___")[0]
+ super().__init__(key, model_name, base_url, **kwargs)
diff --git a/rag/llm/cv_model.py b/rag/llm/cv_model.py
index a832ad6c1a..5aae7c9641 100644
--- a/rag/llm/cv_model.py
+++ b/rag/llm/cv_model.py
@@ -1417,4 +1417,17 @@ class BedrockCV(Base):
return res.choices[0].message.content.strip(), total_token_count_from_response(res)
def describe(self, image):
- return self.describe_with_prompt(image)
\ No newline at end of file
+ return self.describe_with_prompt(image)
+
+
+class NewAPICv(GptV4):
+ _FACTORY_NAME = "New API"
+
+ def __init__(self, key, model_name, lang="Chinese", base_url="", **kwargs):
+ if not base_url:
+ raise ValueError("url cannot be None")
+ 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.split("___")[0]
+ self.lang = lang
+ Base.__init__(self, **kwargs)
diff --git a/rag/llm/embedding_model.py b/rag/llm/embedding_model.py
index c8ed17b34a..13025bb2d0 100644
--- a/rag/llm/embedding_model.py
+++ b/rag/llm/embedding_model.py
@@ -1300,3 +1300,13 @@ class PerplexityEmbed(Base):
def encode_queries(self, text):
embds, cnt = self.encode([text])
return np.array(embds[0]), cnt
+
+
+class NewAPIEmbed(OpenAIEmbed):
+ _FACTORY_NAME = "New API"
+
+ def __init__(self, key, model_name, base_url):
+ if not base_url:
+ raise ValueError("url cannot be None")
+ self.client = OpenAI(api_key=key, base_url=base_url)
+ self.model_name = model_name.split("___")[0]
diff --git a/rag/llm/model_meta.py b/rag/llm/model_meta.py
index 8dbd3ba5f5..6923cc65f5 100644
--- a/rag/llm/model_meta.py
+++ b/rag/llm/model_meta.py
@@ -469,3 +469,16 @@ class VLLM(OpenAIAPICompatible):
class LMStudio(OpenAIAPICompatible):
_FACTORY_NAME = "LM-Studio"
+
+
+class NewAPI(OpenAIAPICompatible):
+ _FACTORY_NAME = "New API"
+
+ def _get_api_key(self):
+ try:
+ parsed = json.loads(self.api_key)
+ if isinstance(parsed, dict):
+ return parsed.get("api_key", self.api_key)
+ except (JSONDecodeError, TypeError):
+ pass
+ return self.api_key
diff --git a/rag/llm/rerank_model.py b/rag/llm/rerank_model.py
index 9705d5c66a..8067615141 100644
--- a/rag/llm/rerank_model.py
+++ b/rag/llm/rerank_model.py
@@ -623,3 +623,37 @@ class RAGconRerank(Base):
except Exception as _e:
log_exception(_e, res)
return rank, token_count
+
+
+class NewAPIRerank(Base):
+ _FACTORY_NAME = "New API"
+
+ def __init__(self, key, model_name, base_url):
+ normalized_base_url = (base_url or "").strip()
+ if "/rerank" in normalized_base_url:
+ self.base_url = normalized_base_url.rstrip("/")
+ else:
+ self.base_url = urljoin(f"{normalized_base_url.rstrip('/')}/", "rerank").rstrip("/")
+ self.headers = {
+ "Content-Type": "application/json",
+ "Authorization": f"Bearer {key}",
+ }
+ self.model_name = model_name.split("___")[0]
+
+ def _compute_rank(self, query: str, texts: list):
+ texts = [truncate(t, 500) for t in texts]
+ data = {
+ "model": self.model_name,
+ "query": query,
+ "documents": texts,
+ "top_n": len(texts),
+ }
+ token_count = sum(num_tokens_from_string(t) for t in texts)
+ res = requests.post(self.base_url, 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)
+ return rank, token_count
diff --git a/rag/llm/sequence2txt_model.py b/rag/llm/sequence2txt_model.py
index 4624a2911a..1642314dcb 100644
--- a/rag/llm/sequence2txt_model.py
+++ b/rag/llm/sequence2txt_model.py
@@ -433,3 +433,13 @@ class RAGconSeq2txt(Base):
# Return text and token count
text = transcription.text.strip()
return text, num_tokens_from_string(text)
+
+
+class NewAPISeq2txt(GPTSeq2txt):
+ _FACTORY_NAME = "New API"
+
+ def __init__(self, key, model_name="whisper-1", base_url="", **kwargs):
+ if not base_url:
+ raise ValueError("url cannot be None")
+ model_name = model_name.split("___")[0]
+ super().__init__(key, model_name=model_name, base_url=base_url, **kwargs)
diff --git a/rag/llm/tts_model.py b/rag/llm/tts_model.py
index f37cd89c25..4f95b2cb6b 100644
--- a/rag/llm/tts_model.py
+++ b/rag/llm/tts_model.py
@@ -543,3 +543,13 @@ class RAGconTTS(Base):
for chunk in response.iter_content(chunk_size=1024):
if chunk:
yield chunk
+
+
+class NewAPITTS(OpenAITTS):
+ _FACTORY_NAME = "New API"
+
+ def __init__(self, key, model_name, base_url="", **kwargs):
+ if not base_url:
+ raise ValueError("url cannot be None")
+ model_name = model_name.split("___")[0]
+ super().__init__(key, model_name, base_url, **kwargs)
diff --git a/web/src/assets/svg/llm/new-api.svg b/web/src/assets/svg/llm/new-api.svg
new file mode 100644
index 0000000000..5af0227fbd
--- /dev/null
+++ b/web/src/assets/svg/llm/new-api.svg
@@ -0,0 +1,3 @@
+
diff --git a/web/src/components/svg-icon.tsx b/web/src/components/svg-icon.tsx
index 3e3ee85cb3..b51cc51fac 100644
--- a/web/src/components/svg-icon.tsx
+++ b/web/src/components/svg-icon.tsx
@@ -88,6 +88,7 @@ const svgIcons = [
// LLMFactory.DeerAPI,
LLMFactory.Avian,
LLMFactory.RAGcon,
+ LLMFactory.NewAPI,
];
export const LlmIcon = ({
diff --git a/web/src/constants/llm.ts b/web/src/constants/llm.ts
index 4157de1fbe..dea7096ebf 100644
--- a/web/src/constants/llm.ts
+++ b/web/src/constants/llm.ts
@@ -72,6 +72,7 @@ export enum LLMFactory {
Avian = 'Avian',
RAGcon = 'RAGcon',
Perplexity = 'Perplexity',
+ NewAPI = 'New API',
}
// Please lowercase the file name
@@ -143,6 +144,7 @@ export const IconMap = {
[LLMFactory.Avian]: 'avian',
[LLMFactory.RAGcon]: 'ragcon',
[LLMFactory.Perplexity]: 'perplexity',
+ [LLMFactory.NewAPI]: 'new-api',
};
export const ModelTypeToField: Record = {
diff --git a/web/src/pages/user-setting/setting-model/components/used-model.tsx b/web/src/pages/user-setting/setting-model/components/used-model.tsx
index ca91d52211..fa8365c1e8 100644
--- a/web/src/pages/user-setting/setting-model/components/used-model.tsx
+++ b/web/src/pages/user-setting/setting-model/components/used-model.tsx
@@ -265,11 +265,9 @@ function InstanceModelList({
))}
- {false && (
-
- )}
+
)}
diff --git a/web/src/pages/user-setting/setting-model/modal/provider-modal/constants.ts b/web/src/pages/user-setting/setting-model/modal/provider-modal/constants.ts
index e340911419..bd77e06452 100644
--- a/web/src/pages/user-setting/setting-model/modal/provider-modal/constants.ts
+++ b/web/src/pages/user-setting/setting-model/modal/provider-modal/constants.ts
@@ -23,6 +23,7 @@ export const LIST_MODEL_PROVIDERS = new Set([
LLMFactory.Xinference,
LLMFactory.LocalAI,
LLMFactory.BaiduYiYan,
+ LLMFactory.NewAPI,
// LLMFactory.HuggingFace,
// LLMFactory.GoogleCloud,
diff --git a/web/src/pages/user-setting/setting-model/modal/provider-modal/field-config/local-llm-configs.ts b/web/src/pages/user-setting/setting-model/modal/provider-modal/field-config/local-llm-configs.ts
index 8ccbe84833..8fd8967fd9 100644
--- a/web/src/pages/user-setting/setting-model/modal/provider-modal/field-config/local-llm-configs.ts
+++ b/web/src/pages/user-setting/setting-model/modal/provider-modal/field-config/local-llm-configs.ts
@@ -134,6 +134,15 @@ export const LocalLlmConfigs: Record = {
undefined,
'https://docs.vllm.ai/en/latest/',
),
+ [LLMFactory.NewAPI]: buildLocalConfig(
+ LLMFactory.NewAPI,
+ 'New API',
+ ['chat', 'embedding', 'rerank', 'image2text', 'tts', 'speech2text'],
+ undefined,
+ false,
+ undefined,
+ 'https://github.com/QuantumNous/new-api',
+ ),
// [LLMFactory.TokenPony]: buildLocalConfig(
// LLMFactory.TokenPony,
// 'TokenPony',
diff --git a/web/src/pages/user-setting/setting-model/modal/provider-modal/hooks/use-list-models-options.tsx b/web/src/pages/user-setting/setting-model/modal/provider-modal/hooks/use-list-models-options.tsx
index 0f9973dbd5..abf52103d2 100644
--- a/web/src/pages/user-setting/setting-model/modal/provider-modal/hooks/use-list-models-options.tsx
+++ b/web/src/pages/user-setting/setting-model/modal/provider-modal/hooks/use-list-models-options.tsx
@@ -1,6 +1,7 @@
import { Checkbox } from '@/components/ui/checkbox';
import { useTranslate } from '@/hooks/common-hooks';
import { IProviderModelItem } from '@/interfaces/request/llm';
+import { Pencil } from 'lucide-react';
import { useMemo } from 'react';
interface UseListModelsOptionsParams {
@@ -9,29 +10,16 @@ interface UseListModelsOptionsParams {
allSelected: boolean;
handleSelectModel: (model: IProviderModelItem) => void;
handleToggleAll: () => void;
+ onEditModel?: (model: IProviderModelItem) => void;
}
-/**
- * Build ToggleList options from the fetched model list. The first item is
- * a sentinel "All models" row that toggles the full selection.
- *
- * Why the Checkbox uses `onClick` (not `onCheckedChange`):
- * Radix Checkbox calls `event.stopPropagation()` internally on its onClick
- * when the Checkbox lives inside a form, so the row's `onClick` (attached
- * by ToggleList) never fires when the user clicks the Checkbox itself.
- * To make the Checkbox click toggle selection, we handle it in our own
- * `onClick` and re-stop propagation to (a) prevent the row's onClick from
- * double-firing and (b) make Radix's CheckboxBubbleInput dispatch a
- * non-bubbling synthetic click on its hidden form input — without this,
- * the dispatched click would bubble back to the row and re-trigger the
- * toggle, causing "Maximum update depth exceeded".
- */
export const useListModelsOptions = ({
models,
selectedModelItems,
allSelected,
handleSelectModel,
handleToggleAll,
+ onEditModel,
}: UseListModelsOptionsParams) => {
const { t } = useTranslate('setting');
@@ -75,13 +63,29 @@ export const useListModelsOptions = ({
);
})}
- {
- e.stopPropagation();
- handleSelectModel(m);
- }}
- />
+
+ {onEditModel && (
+
+ )}
+
{
+ e.stopPropagation();
+ handleSelectModel(m);
+ }}
+ />
+
),
onClick: () => handleSelectModel(m),
@@ -98,6 +102,7 @@ export const useListModelsOptions = ({
handleSelectModel,
allSelected,
handleToggleAll,
+ onEditModel,
t,
]);
};
diff --git a/web/src/pages/user-setting/setting-model/modal/provider-modal/hooks/use-list-models-picker.ts b/web/src/pages/user-setting/setting-model/modal/provider-modal/hooks/use-list-models-picker.ts
index 004c7c16b0..d68aa3acb7 100644
--- a/web/src/pages/user-setting/setting-model/modal/provider-modal/hooks/use-list-models-picker.ts
+++ b/web/src/pages/user-setting/setting-model/modal/provider-modal/hooks/use-list-models-picker.ts
@@ -154,15 +154,16 @@ export const useListModelsPicker = ({
if (models.length > 0 || listLoading) return;
setListLoading(true);
try {
- const values = (formRef.current?.getValues() || {}) as Record<
+ const rawValues = (formRef.current?.getValues() || {}) as Record<
string,
any
>;
- // Reuse the verifyTransform to build the request payload — it
- // already knows how to flatten provider-specific auth (api_key,
- // base_url, region, model_info). Pass the current modelInfoList
- // (empty on first load, populated on re-opens) so the backend
- // sees an array shape consistent with the verify/submit payloads.
+ // In viewMode, pass instance_name so the backend looks up
+ // stored credentials server-side (avoids exposing api_key).
+ // For new-instance mode, pass api_key/base_url from the form.
+ const values = viewMode && initialValues
+ ? { ...initialValues, ...rawValues }
+ : rawValues;
const verifyArgs = config.verifyTransform
? config.verifyTransform({
...values,
@@ -171,10 +172,13 @@ export const useListModelsPicker = ({
: { apiKey: values.api_key ?? '', baseUrl: values.base_url };
const res = await listProviderModels({
provider_name: llmFactory,
- api_key: (verifyArgs as any).apiKey ?? '',
- base_url: (verifyArgs as any).baseUrl,
+ api_key: viewMode ? '' : ((verifyArgs as any).apiKey ?? ''),
+ base_url: viewMode ? '' : ((verifyArgs as any).baseUrl ?? ''),
region: (verifyArgs as any).region,
model_info: (verifyArgs as any).modelInfo ?? modelInfoList,
+ ...(viewMode && initialValues?.instance_name
+ ? { instance_name: initialValues.instance_name }
+ : {}),
});
if (res?.code === 0 && Array.isArray(res.data)) {
setModelsState(res.data);
@@ -210,6 +214,8 @@ export const useListModelsPicker = ({
llmFactory,
modelInfoList,
formRef,
+ viewMode,
+ initialValues,
],
);
@@ -287,6 +293,30 @@ export const useListModelsPicker = ({
[],
);
+ // --- Model editing ---
+ const [editingModel, setEditingModel] =
+ useState(null);
+ const [editDialogOpen, setEditDialogOpen] = useState(false);
+
+ const handleEditModel = useCallback((model: IProviderModelItem) => {
+ setEditingModel(model);
+ setEditDialogOpen(true);
+ }, []);
+
+ const handleSaveEditedModel = useCallback(
+ (updated: IProviderModelItem) => {
+ const oldName = editingModel?.name;
+ if (!oldName) return;
+ const replace = (items: IProviderModelItem[]) =>
+ items.map((m) => (m.name === oldName ? updated : m));
+ setModelsState(replace);
+ setSelectedModelItemsState(replace);
+ setEditDialogOpen(false);
+ setEditingModel(null);
+ },
+ [editingModel],
+ );
+
return {
models,
listLoading,
@@ -298,5 +328,10 @@ export const useListModelsPicker = ({
handleToggleAll,
setModels,
setSelectedModelItems,
+ editingModel,
+ editDialogOpen,
+ setEditDialogOpen,
+ handleEditModel,
+ handleSaveEditedModel,
};
};
diff --git a/web/src/pages/user-setting/setting-model/modal/provider-modal/index.tsx b/web/src/pages/user-setting/setting-model/modal/provider-modal/index.tsx
index 53f5cb3ea9..a6d15a18c5 100644
--- a/web/src/pages/user-setting/setting-model/modal/provider-modal/index.tsx
+++ b/web/src/pages/user-setting/setting-model/modal/provider-modal/index.tsx
@@ -14,6 +14,7 @@ import { FieldValues } from 'react-hook-form';
import { LLMHeader } from '../../components/llm-header';
import VerifyButton from '../verify-button';
import { AddableToggleList } from './components/addable-toggle-list';
+import { AddCustomModelDialog } from './components/add-custom-model-dialog';
import { useCustomModelFields } from './components/use-custom-model-fields';
import {
useListModelsOptions,
@@ -103,6 +104,11 @@ const ProviderModal = ({
handleSelectModel,
handleToggleAll,
setModels,
+ editingModel,
+ editDialogOpen,
+ setEditDialogOpen,
+ handleEditModel,
+ handleSaveEditedModel,
} = useListModelsPicker({
visible,
hasModelNameField,
@@ -164,6 +170,7 @@ const ProviderModal = ({
allSelected,
handleSelectModel,
handleToggleAll,
+ onEditModel: handleEditModel,
});
// Submit and verify handlers — branch on viewMode and on whether the
@@ -270,6 +277,32 @@ const ProviderModal = ({
+ {editingModel && (
+ ({
+ ...f,
+ defaultValue:
+ f.name === 'name'
+ ? editingModel.name
+ : f.name === 'model_types'
+ ? editingModel.model_types ?? []
+ : f.name === 'max_tokens'
+ ? editingModel.max_tokens ?? 0
+ : f.name === 'features'
+ ? editingModel.features ?? []
+ : f.defaultValue,
+ }))}
+ onSubmit={handleSaveEditedModel}
+ submitText={tc('ok')}
+ cancelText={tc('cancel')}
+ existingNames={existingNames.filter(
+ (n) => n !== editingModel.name,
+ )}
+ />
+ )}
);
};