Add quick LLM provider setup
This commit is contained in:
@@ -43,7 +43,7 @@ const knowledgeForm = reactive({
|
||||
});
|
||||
|
||||
const modelForm = reactive({
|
||||
provider: "openai",
|
||||
provider: "openai-compatible",
|
||||
displayName: "",
|
||||
apiType: "openai_compatible",
|
||||
modelName: "",
|
||||
@@ -66,58 +66,65 @@ const modelForm = reactive({
|
||||
timeoutSecond: 30,
|
||||
});
|
||||
|
||||
const modelPresets = [
|
||||
const quickModelForm = reactive({
|
||||
provider: "deepseek",
|
||||
modelName: "deepseek-v4-pro",
|
||||
apiKey: "",
|
||||
});
|
||||
|
||||
const quickModelProviders = [
|
||||
{
|
||||
label: "OpenAI",
|
||||
provider: "openai",
|
||||
label: "DeepSeek",
|
||||
provider: "deepseek",
|
||||
apiType: "openai_compatible",
|
||||
baseUrl: "https://api.deepseek.com",
|
||||
authType: "bearer",
|
||||
apiVersion: "",
|
||||
temperature: 0.2,
|
||||
topP: null,
|
||||
maxToken: 1024,
|
||||
contextWindow: 128000,
|
||||
models: [
|
||||
{ label: "deepseek-v4-pro", value: "deepseek-v4-pro" },
|
||||
{ label: "deepseek-v4-flash", value: "deepseek-v4-flash" },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "MiniMax",
|
||||
provider: "minimax",
|
||||
apiType: "anthropic_messages",
|
||||
baseUrl: "https://api.minimax.io/anthropic",
|
||||
authType: "bearer",
|
||||
apiVersion: "",
|
||||
temperature: null,
|
||||
topP: null,
|
||||
maxToken: 1024,
|
||||
contextWindow: 1000000,
|
||||
models: [
|
||||
{ label: "MiniMax-M3", value: "MiniMax-M3" },
|
||||
{ label: "MiniMax-M2.7", value: "MiniMax-M2.7" },
|
||||
{ label: "MiniMax-M2.7-highspeed", value: "MiniMax-M2.7-highspeed" },
|
||||
{ label: "MiniMax-M2.5", value: "MiniMax-M2.5" },
|
||||
{ label: "MiniMax-M2.5-highspeed", value: "MiniMax-M2.5-highspeed" },
|
||||
{ label: "MiniMax-M2.1", value: "MiniMax-M2.1" },
|
||||
{ label: "MiniMax-M2.1-highspeed", value: "MiniMax-M2.1-highspeed" },
|
||||
{ label: "MiniMax-M2", value: "MiniMax-M2" },
|
||||
],
|
||||
},
|
||||
] as const;
|
||||
|
||||
const protocolPresets = [
|
||||
{
|
||||
label: "OpenAI 兼容协议",
|
||||
provider: "openai-compatible",
|
||||
apiType: "openai_compatible",
|
||||
baseUrl: "https://api.openai.com/v1",
|
||||
apiUrl: "",
|
||||
authType: "bearer",
|
||||
},
|
||||
{
|
||||
label: "DeepSeek",
|
||||
provider: "deepseek",
|
||||
apiType: "openai_compatible",
|
||||
baseUrl: "https://api.deepseek.com/v1",
|
||||
apiUrl: "",
|
||||
authType: "bearer",
|
||||
},
|
||||
{
|
||||
label: "通义千问 DashScope",
|
||||
provider: "qwen",
|
||||
apiType: "openai_compatible",
|
||||
baseUrl: "https://dashscope.aliyuncs.com/compatible-mode/v1",
|
||||
apiUrl: "",
|
||||
authType: "bearer",
|
||||
},
|
||||
{
|
||||
label: "Kimi / Moonshot",
|
||||
provider: "moonshot",
|
||||
apiType: "openai_compatible",
|
||||
baseUrl: "https://api.moonshot.cn/v1",
|
||||
apiUrl: "",
|
||||
authType: "bearer",
|
||||
},
|
||||
{
|
||||
label: "硅基流动 SiliconFlow",
|
||||
provider: "siliconflow",
|
||||
apiType: "openai_compatible",
|
||||
baseUrl: "https://api.siliconflow.cn/v1",
|
||||
apiUrl: "",
|
||||
authType: "bearer",
|
||||
},
|
||||
{
|
||||
label: "火山方舟 Ark",
|
||||
provider: "volcengine-ark",
|
||||
apiType: "openai_compatible",
|
||||
baseUrl: "https://ark.cn-beijing.volces.com/api/v3",
|
||||
apiUrl: "",
|
||||
authType: "bearer",
|
||||
},
|
||||
{
|
||||
label: "Anthropic Claude",
|
||||
provider: "anthropic",
|
||||
label: "Anthropic Messages 协议",
|
||||
provider: "anthropic-compatible",
|
||||
apiType: "anthropic_messages",
|
||||
baseUrl: "https://api.anthropic.com",
|
||||
apiUrl: "",
|
||||
@@ -127,17 +134,6 @@ const modelPresets = [
|
||||
topP: null,
|
||||
topK: null,
|
||||
},
|
||||
{
|
||||
label: "Google Gemini",
|
||||
provider: "gemini",
|
||||
apiType: "gemini_generate_content",
|
||||
baseUrl: "https://generativelanguage.googleapis.com/v1beta",
|
||||
apiUrl: "",
|
||||
authType: "api_key",
|
||||
temperature: null,
|
||||
topP: null,
|
||||
topK: null,
|
||||
},
|
||||
];
|
||||
|
||||
const nullableModelFields = [
|
||||
@@ -267,6 +263,52 @@ function buildModelPayload() {
|
||||
return payload;
|
||||
}
|
||||
|
||||
async function quickAddModel() {
|
||||
const provider = selectedQuickProvider();
|
||||
if (!provider) return;
|
||||
if (!quickModelForm.apiKey) {
|
||||
ElMessage.error("请填写 API Key");
|
||||
return;
|
||||
}
|
||||
|
||||
await api.createModel({
|
||||
provider: provider.provider,
|
||||
displayName: `${provider.label} ${quickModelForm.modelName}`,
|
||||
apiType: provider.apiType,
|
||||
modelName: quickModelForm.modelName,
|
||||
baseUrl: provider.baseUrl,
|
||||
apiUrl: "",
|
||||
apiKey: quickModelForm.apiKey,
|
||||
authType: provider.authType,
|
||||
apiVersion: provider.apiVersion,
|
||||
temperature: provider.temperature,
|
||||
topP: provider.topP,
|
||||
topK: null,
|
||||
presencePenalty: null,
|
||||
frequencyPenalty: null,
|
||||
maxToken: provider.maxToken,
|
||||
contextWindow: provider.contextWindow,
|
||||
streamEnabled: 1,
|
||||
responseFormat: null,
|
||||
extraParams: null,
|
||||
remark: "快速添加",
|
||||
timeoutSecond: 30,
|
||||
});
|
||||
ElMessage.success(`${provider.label} 模型已新增`);
|
||||
quickModelForm.apiKey = "";
|
||||
await loadCurrentMenu();
|
||||
}
|
||||
|
||||
function selectedQuickProvider() {
|
||||
return quickModelProviders.find((item) => item.provider === quickModelForm.provider) ?? quickModelProviders[0];
|
||||
}
|
||||
|
||||
function applyQuickProvider(provider: string) {
|
||||
const target = quickModelProviders.find((item) => item.provider === provider);
|
||||
if (!target) return;
|
||||
quickModelForm.modelName = target.models[0]?.value ?? "";
|
||||
}
|
||||
|
||||
async function enableModel(id: number) {
|
||||
await api.enableModel(id);
|
||||
ElMessage.success("模型已启用");
|
||||
@@ -283,7 +325,7 @@ async function testModel(id: number) {
|
||||
}
|
||||
|
||||
function applyModelPreset(label: string) {
|
||||
const preset = modelPresets.find((item) => item.label === label);
|
||||
const preset = protocolPresets.find((item) => item.label === label);
|
||||
if (!preset) return;
|
||||
Object.assign(
|
||||
modelForm,
|
||||
@@ -332,7 +374,7 @@ function editModel(row: ModelItem) {
|
||||
function resetModelForm() {
|
||||
editingModelId.value = null;
|
||||
Object.assign(modelForm, {
|
||||
provider: "openai",
|
||||
provider: "openai-compatible",
|
||||
displayName: "",
|
||||
apiType: "openai_compatible",
|
||||
modelName: "",
|
||||
@@ -516,27 +558,64 @@ function buildChatQuery() {
|
||||
|
||||
<template v-if="activeMenu === 'models'">
|
||||
<div class="page-head inline">
|
||||
<div><h2>模型管理</h2><p>支持 OpenAI 兼容、Anthropic Messages、Gemini generateContent 和常见第三方 LLM。</p></div>
|
||||
<div><h2>模型管理</h2><p>DeepSeek、MiniMax 可快速添加;其他供应商按 OpenAI 兼容或 Anthropic Messages 协议添加。</p></div>
|
||||
<el-button @click="resetModelForm">清空表单</el-button>
|
||||
</div>
|
||||
|
||||
<section class="quick-model-panel">
|
||||
<div class="quick-model-head">
|
||||
<div>
|
||||
<h3>快速添加</h3>
|
||||
<p>适用于 DeepSeek 和 MiniMax,只需要填写 API Key 并选择模型。</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="quick-model-grid">
|
||||
<el-form-item label="厂商">
|
||||
<el-select v-model="quickModelForm.provider" @change="applyQuickProvider">
|
||||
<el-option
|
||||
v-for="provider in quickModelProviders"
|
||||
:key="provider.provider"
|
||||
:label="provider.label"
|
||||
:value="provider.provider"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="模型">
|
||||
<el-select v-model="quickModelForm.modelName">
|
||||
<el-option
|
||||
v-for="model in selectedQuickProvider()?.models"
|
||||
:key="model.value"
|
||||
:label="model.label"
|
||||
:value="model.value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="API Key">
|
||||
<el-input v-model="quickModelForm.apiKey" placeholder="填写后保存,列表中只展示掩码" show-password />
|
||||
</el-form-item>
|
||||
<el-form-item label=" ">
|
||||
<el-button type="primary" class="full-btn" @click="quickAddModel">快速新增</el-button>
|
||||
</el-form-item>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<el-form class="model-config-panel" label-position="top" :model="modelForm">
|
||||
<div class="model-config-grid">
|
||||
<el-form-item label="供应商预设">
|
||||
<el-select placeholder="选择预设" clearable @change="applyModelPreset">
|
||||
<el-option v-for="preset in modelPresets" :key="preset.label" :label="preset.label" :value="preset.label" />
|
||||
<el-form-item label="协议模板">
|
||||
<el-select placeholder="选择协议模板" clearable @change="applyModelPreset">
|
||||
<el-option v-for="preset in protocolPresets" :key="preset.label" :label="preset.label" :value="preset.label" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="供应商标识">
|
||||
<el-input v-model="modelForm.provider" placeholder="openai / deepseek / qwen" />
|
||||
<el-input v-model="modelForm.provider" placeholder="custom-openai / custom-anthropic" />
|
||||
</el-form-item>
|
||||
<el-form-item label="显示名称">
|
||||
<el-input v-model="modelForm.displayName" placeholder="例如:DeepSeek V3 生产" />
|
||||
<el-input v-model="modelForm.displayName" placeholder="例如:自建网关生产模型" />
|
||||
</el-form-item>
|
||||
<el-form-item label="协议类型">
|
||||
<el-select v-model="modelForm.apiType">
|
||||
<el-option label="OpenAI 兼容" value="openai_compatible" />
|
||||
<el-option label="Anthropic Messages" value="anthropic_messages" />
|
||||
<el-option label="Gemini generateContent" value="gemini_generate_content" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="模型名">
|
||||
|
||||
@@ -193,16 +193,48 @@ textarea {
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.quick-model-panel {
|
||||
margin-bottom: 18px;
|
||||
padding: 18px;
|
||||
border: 1px solid #cde7df;
|
||||
border-radius: 8px;
|
||||
background: #f8fcfb;
|
||||
}
|
||||
|
||||
.quick-model-head {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.quick-model-head h3 {
|
||||
margin: 0;
|
||||
font-size: 17px;
|
||||
}
|
||||
|
||||
.quick-model-head p {
|
||||
margin: 6px 0 0;
|
||||
color: #667a73;
|
||||
}
|
||||
|
||||
.quick-model-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 180px 240px minmax(280px, 1fr) 130px;
|
||||
gap: 12px;
|
||||
align-items: end;
|
||||
}
|
||||
|
||||
.model-config-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(180px, 1fr));
|
||||
gap: 12px 14px;
|
||||
}
|
||||
|
||||
.quick-model-panel .el-form-item,
|
||||
.model-config-panel .el-form-item {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.quick-model-panel .el-select,
|
||||
.quick-model-panel .el-input,
|
||||
.model-config-panel .el-select,
|
||||
.model-config-panel .el-input-number,
|
||||
.model-config-panel .el-input {
|
||||
|
||||
@@ -138,11 +138,7 @@ def _call_anthropic_messages(model: ModelConfig, rag_result: RagResult) -> str:
|
||||
_put_if_not_none(payload, "top_k", model.top_k)
|
||||
payload.update(_load_extra_params(model.extra_params))
|
||||
|
||||
headers = {
|
||||
"x-api-key": model.api_key,
|
||||
"anthropic-version": model.api_version or "2023-06-01",
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
headers = _anthropic_headers(model)
|
||||
try:
|
||||
response = httpx.post(
|
||||
_resolve_anthropic_endpoint(model),
|
||||
@@ -263,6 +259,19 @@ def _auth_headers(model: ModelConfig) -> dict[str, str]:
|
||||
return headers
|
||||
|
||||
|
||||
def _anthropic_headers(model: ModelConfig) -> dict[str, str]:
|
||||
headers = {"Content-Type": "application/json"}
|
||||
if model.auth_type == "bearer":
|
||||
headers["Authorization"] = f"Bearer {model.api_key}"
|
||||
else:
|
||||
headers["x-api-key"] = model.api_key
|
||||
if model.api_version:
|
||||
headers["anthropic-version"] = model.api_version
|
||||
elif model.provider == "anthropic":
|
||||
headers["anthropic-version"] = "2023-06-01"
|
||||
return headers
|
||||
|
||||
|
||||
def _decimal_to_float(value: Any) -> float | None:
|
||||
return float(value) if value is not None else None
|
||||
|
||||
|
||||
Reference in New Issue
Block a user