feat: support direct LLM provider config with model sync
- Fix _call_provider to support custom providers (OpenAI-compatible fallback) instead of raising ValueError for unknown providers - Add POST /admin/ppt/sync-models endpoint that fetches available models from the configured provider's API - Redesign PptSettingsAdmin UI: replace Dify model dropdown with per-provider 'sync models' button + searchable model selector - Remove dead Dify-specific code (fetchDifyModels, applyDifyModel) - Update deepseek default model from deepseek-chat to deepseek-v4-pro - API Key field now always shown (not conditional on non-dify)
This commit is contained in:
parent
8e897276e7
commit
dbe2f9202d
@ -244,3 +244,14 @@ def get_settings():
|
||||
def update_settings():
|
||||
updated_by = getattr(request, "user_id", "")
|
||||
return jsonify(ppt_admin_service.update_settings(request.get_json(force=True), updated_by))
|
||||
|
||||
|
||||
@ppt_admin_bp.route("/sync-models", methods=["POST"])
|
||||
@permission_required("config_manage")
|
||||
def sync_models():
|
||||
data = request.get_json(force=True)
|
||||
return jsonify(ppt_admin_service.sync_models(
|
||||
provider=data.get("provider", ""),
|
||||
api_key=data.get("api_key", ""),
|
||||
base_url=data.get("base_url", ""),
|
||||
))
|
||||
|
||||
@ -566,3 +566,58 @@ class PptAdminService:
|
||||
db.session.add(SystemSetting(key=key, value=str(value), updated_by=updated_by))
|
||||
db.session.commit()
|
||||
return {"code": 0, "data": None}
|
||||
|
||||
def sync_models(self, provider: str, api_key: str, base_url: str = "") -> dict:
|
||||
"""从供应商 API 拉取可用模型列表。"""
|
||||
if not api_key:
|
||||
return {"code": 1001, "message": "请填写 API Key", "data": None}
|
||||
|
||||
try:
|
||||
models = self._fetch_provider_models(provider, api_key, base_url)
|
||||
except Exception as e:
|
||||
logger.warning(f"同步模型失败: {e}")
|
||||
return {"code": 5001, "message": f"同步失败: {e}", "data": None}
|
||||
|
||||
if not models:
|
||||
return {"code": 0, "data": {"models": [], "message": "未获取到模型"}}
|
||||
|
||||
return {"code": 0, "data": {"models": models}}
|
||||
|
||||
def _fetch_provider_models(self, provider: str, api_key: str, base_url: str) -> list:
|
||||
"""根据供应商类型拉取模型列表。"""
|
||||
import httpx
|
||||
|
||||
if provider == "deepseek":
|
||||
url = "https://api.deepseek.com/v1/models"
|
||||
headers = {"Authorization": f"Bearer {api_key}"}
|
||||
resp = httpx.get(url, headers=headers, timeout=10)
|
||||
resp.raise_for_status()
|
||||
return [m["id"] for m in resp.json().get("data", [])]
|
||||
|
||||
elif provider == "gemini":
|
||||
url = f"https://generativelanguage.googleapis.com/v1/models?key={api_key}"
|
||||
resp = httpx.get(url, timeout=10)
|
||||
resp.raise_for_status()
|
||||
return [m["name"].split("/")[-1] for m in resp.json().get("models", [])]
|
||||
|
||||
elif provider == "minimax":
|
||||
url = "https://api.minimax.chat/v1/models"
|
||||
headers = {"Authorization": f"Bearer {api_key}"}
|
||||
resp = httpx.get(url, headers=headers, timeout=10)
|
||||
resp.raise_for_status()
|
||||
data = resp.json()
|
||||
models = data.get("models") or data.get("data", [])
|
||||
return [m.get("id") or m.get("model", "") for m in models if m]
|
||||
|
||||
elif provider == "dify":
|
||||
return [m["model"] for m in (self._fetch_dify_models() or [])]
|
||||
|
||||
else:
|
||||
# 自定义供应商:OpenAI 兼容 /models 端点
|
||||
if not base_url:
|
||||
return []
|
||||
url = f"{base_url.rstrip('/')}/models"
|
||||
headers = {"Authorization": f"Bearer {api_key}"}
|
||||
resp = httpx.get(url, headers=headers, timeout=10)
|
||||
resp.raise_for_status()
|
||||
return [m["id"] for m in resp.json().get("data", [])]
|
||||
|
||||
@ -35,7 +35,7 @@ PROVIDERS = {
|
||||
"deepseek": LLMProviderConfig(
|
||||
name="deepseek",
|
||||
base_url="https://api.deepseek.com/v1",
|
||||
model="deepseek-chat",
|
||||
model="deepseek-v4-pro",
|
||||
max_retries=2,
|
||||
rate_limit=0,
|
||||
),
|
||||
@ -149,7 +149,16 @@ async def _call_provider(
|
||||
"generationConfig": {"temperature": 0.3, "maxOutputTokens": 4096},
|
||||
}
|
||||
else:
|
||||
raise ValueError(f"Unknown provider: {config.name}")
|
||||
# 自定义供应商:OpenAI 兼容格式
|
||||
headers["Authorization"] = f"Bearer {api_key}"
|
||||
base = config.base_url.rstrip("/")
|
||||
url = f"{base}/chat/completions"
|
||||
body = {
|
||||
"model": config.model,
|
||||
"messages": messages,
|
||||
"temperature": 0.3,
|
||||
"max_tokens": 4096,
|
||||
}
|
||||
|
||||
async with httpx.AsyncClient(timeout=timeout_s) as client:
|
||||
resp = await client.post(url, json=body, headers=headers)
|
||||
@ -159,20 +168,7 @@ async def _call_provider(
|
||||
latency_ms = (time.monotonic() - start) * 1000
|
||||
|
||||
# 解析响应
|
||||
if config.name in ("deepseek", "minimax"):
|
||||
content = ""
|
||||
choices = data.get("choices", [])
|
||||
if choices:
|
||||
content = choices[0].get("message", {}).get("content", "")
|
||||
usage = data.get("usage")
|
||||
tokens = None
|
||||
if usage:
|
||||
tokens = {
|
||||
"input": usage.get("prompt_tokens", 0),
|
||||
"output": usage.get("completion_tokens", 0),
|
||||
}
|
||||
else:
|
||||
# Gemini
|
||||
if config.name == "gemini":
|
||||
content = ""
|
||||
candidates = data.get("candidates", [])
|
||||
if candidates:
|
||||
@ -186,6 +182,19 @@ async def _call_provider(
|
||||
"input": usage_meta.get("promptTokenCount", 0),
|
||||
"output": usage_meta.get("candidatesTokenCount", 0),
|
||||
}
|
||||
else:
|
||||
# OpenAI 兼容格式(deepseek / minimax / 自定义供应商)
|
||||
content = ""
|
||||
choices = data.get("choices", [])
|
||||
if choices:
|
||||
content = choices[0].get("message", {}).get("content", "")
|
||||
usage = data.get("usage")
|
||||
tokens = None
|
||||
if usage:
|
||||
tokens = {
|
||||
"input": usage.get("prompt_tokens", 0),
|
||||
"output": usage.get("completion_tokens", 0),
|
||||
}
|
||||
|
||||
return LLMResponse(
|
||||
content=content,
|
||||
|
||||
@ -20,34 +20,28 @@
|
||||
|
||||
<el-form-item label="模型名称">
|
||||
<div style="display: flex; gap: 8px; width: 100%">
|
||||
<el-input v-model="form.ppt_llm_model" placeholder="如 deepseek-chat" />
|
||||
<el-button v-if="!difyModels.length" @click="fetchDifyModels" :loading="fetchingModels">从 Dify 获取</el-button>
|
||||
<el-dropdown v-else trigger="click" @command="(cmd: string) => applyDifyModel('ppt', cmd)">
|
||||
<el-button>从 Dify 获取 <el-icon><ArrowDown /></el-icon></el-button>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item v-for="m in difyModels" :key="m.provider + '/' + m.model"
|
||||
:command="m.provider + '|' + m.model">
|
||||
{{ m.label }} ({{ m.provider }}/{{ m.model }})
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
<el-select
|
||||
v-if="pptAvailableModels.length"
|
||||
v-model="form.ppt_llm_model"
|
||||
filterable allow-create
|
||||
placeholder="选择或输入模型名"
|
||||
style="flex: 1"
|
||||
>
|
||||
<el-option v-for="m in pptAvailableModels" :key="m" :label="m" :value="m" />
|
||||
</el-select>
|
||||
<el-input v-else v-model="form.ppt_llm_model" placeholder="如 deepseek-v4-pro" style="flex: 1" />
|
||||
<el-button @click="handlePptSyncModels" :loading="syncingPptModels">同步模型</el-button>
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item v-if="form.ppt_llm_provider !== 'dify'" label="API Key">
|
||||
<el-input v-model="form.ppt_llm_api_key" type="password" show-password placeholder="留空则使用环境变量配置" />
|
||||
<el-form-item label="API Key">
|
||||
<el-input v-model="form.ppt_llm_api_key" type="password" show-password placeholder="该供应商的 API Key" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item v-if="form.ppt_llm_provider === 'custom'" label="Base URL">
|
||||
<el-input v-model="form.ppt_llm_base_url" placeholder="如 https://api.openai.com/v1" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item v-if="form.ppt_llm_provider === 'dify'" label="">
|
||||
<el-tag type="success">通过 Dify 调用,无需配置 API Key</el-tag>
|
||||
</el-form-item>
|
||||
|
||||
<!-- 海报文案模型 -->
|
||||
<el-divider content-position="left">海报文案模型</el-divider>
|
||||
|
||||
@ -63,34 +57,30 @@
|
||||
|
||||
<el-form-item label="模型名称">
|
||||
<div style="display: flex; gap: 8px; width: 100%">
|
||||
<el-input v-model="form.poster_llm_model" placeholder="如 deepseek-chat" />
|
||||
<el-button v-if="!difyModels.length" @click="fetchDifyModels" :loading="fetchingModels">从 Dify 获取</el-button>
|
||||
<el-dropdown v-else trigger="click" @command="(cmd: string) => applyDifyModel('poster', cmd)">
|
||||
<el-button>从 Dify 获取 <el-icon><ArrowDown /></el-icon></el-button>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item v-for="m in difyModels" :key="m.provider + '/' + m.model"
|
||||
:command="m.provider + '|' + m.model">
|
||||
{{ m.label }} ({{ m.provider }}/{{ m.model }})
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
<el-select
|
||||
v-if="availableModels.length"
|
||||
v-model="form.poster_llm_model"
|
||||
filterable allow-create
|
||||
placeholder="选择或输入模型名"
|
||||
style="flex: 1"
|
||||
>
|
||||
<el-option v-for="m in availableModels" :key="m" :label="m" :value="m" />
|
||||
</el-select>
|
||||
<el-input v-else v-model="form.poster_llm_model" placeholder="如 deepseek-v4-pro" style="flex: 1" />
|
||||
<el-button @click="handleSyncModels" :loading="syncingModels">
|
||||
同步模型
|
||||
</el-button>
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item v-if="form.poster_llm_provider !== 'dify'" label="API Key">
|
||||
<el-input v-model="form.poster_llm_api_key" type="password" show-password placeholder="留空则使用环境变量配置" />
|
||||
<el-form-item label="API Key">
|
||||
<el-input v-model="form.poster_llm_api_key" type="password" show-password placeholder="该供应商的 API Key" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item v-if="form.poster_llm_provider === 'custom'" label="Base URL">
|
||||
<el-input v-model="form.poster_llm_base_url" placeholder="如 https://api.openai.com/v1" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item v-if="form.poster_llm_provider === 'dify'" label="">
|
||||
<el-tag type="success">通过 Dify 调用,无需配置 API Key</el-tag>
|
||||
</el-form-item>
|
||||
|
||||
<!-- 海报图片模型 -->
|
||||
<el-divider content-position="left">海报图片模型</el-divider>
|
||||
|
||||
@ -133,13 +123,14 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { ArrowDown } from '@element-plus/icons-vue'
|
||||
import { pptAdminApi } from '@/utils/ppt-admin-api'
|
||||
|
||||
const loading = ref(false)
|
||||
const saving = ref(false)
|
||||
const fetchingModels = ref(false)
|
||||
const difyModels = ref<Array<{ provider: string; model: string; label: string }>>([])
|
||||
const syncingModels = ref(false)
|
||||
const availableModels = ref<string[]>([])
|
||||
const syncingPptModels = ref(false)
|
||||
const pptAvailableModels = ref<string[]>([])
|
||||
|
||||
const form = reactive({
|
||||
ppt_llm_provider: 'deepseek',
|
||||
@ -158,7 +149,7 @@ const form = reactive({
|
||||
})
|
||||
|
||||
const DEFAULT_MODELS: Record<string, string> = {
|
||||
deepseek: 'deepseek-chat',
|
||||
deepseek: 'deepseek-v4-pro',
|
||||
minimax: 'MiniMax-2.7-Flash',
|
||||
gemini: 'gemini-2.5-flash',
|
||||
openai: 'gpt-4o',
|
||||
@ -213,33 +204,47 @@ function inferBaseUrl(model: string): string {
|
||||
return ''
|
||||
}
|
||||
|
||||
function applyDifyModel(target: 'ppt' | 'poster', command: string) {
|
||||
const [, model] = command.split('|')
|
||||
if (target === 'ppt') {
|
||||
form.ppt_llm_provider = 'dify'
|
||||
form.ppt_llm_model = model
|
||||
form.ppt_llm_api_key = ''
|
||||
form.ppt_llm_base_url = ''
|
||||
async function handleSyncModels() {
|
||||
syncingModels.value = true
|
||||
try {
|
||||
const res: any = await pptAdminApi.syncModels({
|
||||
provider: form.poster_llm_provider,
|
||||
api_key: form.poster_llm_api_key,
|
||||
base_url: form.poster_llm_base_url,
|
||||
})
|
||||
const models = res?.data?.models || []
|
||||
availableModels.value = models
|
||||
if (!models.length) {
|
||||
ElMessage.warning('未获取到模型,请检查 API Key 和供应商配置')
|
||||
} else {
|
||||
form.poster_llm_provider = 'dify'
|
||||
form.poster_llm_model = model
|
||||
form.poster_llm_api_key = ''
|
||||
form.poster_llm_base_url = ''
|
||||
ElMessage.success(`同步成功,共 ${models.length} 个模型`)
|
||||
}
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.message || '同步失败')
|
||||
} finally {
|
||||
syncingModels.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchDifyModels() {
|
||||
fetchingModels.value = true
|
||||
async function handlePptSyncModels() {
|
||||
syncingPptModels.value = true
|
||||
try {
|
||||
const res: any = await pptAdminApi.getAvailableModels()
|
||||
difyModels.value = res?.data?.models || []
|
||||
if (!difyModels.value.length) {
|
||||
ElMessage.warning('未获取到可用模型,请检查 Dify Workspace API Key 配置')
|
||||
const res: any = await pptAdminApi.syncModels({
|
||||
provider: form.ppt_llm_provider,
|
||||
api_key: form.ppt_llm_api_key,
|
||||
base_url: form.ppt_llm_base_url,
|
||||
})
|
||||
const models = res?.data?.models || []
|
||||
pptAvailableModels.value = models
|
||||
if (!models.length) {
|
||||
ElMessage.warning('未获取到模型,请检查 API Key 和供应商配置')
|
||||
} else {
|
||||
ElMessage.success(`同步成功,共 ${models.length} 个模型`)
|
||||
}
|
||||
} catch {
|
||||
ElMessage.error('获取模型列表失败')
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.message || '同步失败')
|
||||
} finally {
|
||||
fetchingModels.value = false
|
||||
syncingPptModels.value = false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -105,4 +105,7 @@ export const pptAdminApi = {
|
||||
getAvailableModels() {
|
||||
return api.get('/admin/ppt/available-models')
|
||||
},
|
||||
syncModels(data: { provider: string; api_key: string; base_url?: string }) {
|
||||
return api.post('/admin/ppt/sync-models', data)
|
||||
},
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user