渠道: 支持手工录入模型(无 /models) + 模型映射自定义名称
- AdminChannelAddModel 改为 upstream_model + custom_name: 无 /models 接口的渠道直接填上游模型名, 可选自定义名作为客户端调用名, 全局模型不存在时自动创建 - "支持的模型"弹窗: 手工添加 + 建议下拉(自建,紧贴输入框)/水平排列/交换位置 - 网关仍按绑定映射改写请求 model 字段 Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -31,7 +31,8 @@ func (h *Handler) AdminChannelModels(c *gin.Context) {
|
||||
resp.OK(c, gin.H{"items": out})
|
||||
}
|
||||
|
||||
// AdminChannelAddModel POST /api/v1/admin/channels/:id/models — 绑定模型到渠道。
|
||||
// AdminChannelAddModel POST /api/v1/admin/channels/:id/models — 手工添加渠道支持的模型。
|
||||
// 无需渠道具备 /v1/models 接口:直接填上游模型名,可选自定义名称作为客户端调用名。
|
||||
func (h *Handler) AdminChannelAddModel(c *gin.Context) {
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||
if err != nil {
|
||||
@@ -39,31 +40,35 @@ func (h *Handler) AdminChannelAddModel(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
var req struct {
|
||||
ModelName string `json:"model_name" binding:"required"`
|
||||
UpstreamModel string `json:"upstream_model"`
|
||||
UpstreamModel string `json:"upstream_model" binding:"required"` // 渠道侧真实模型名
|
||||
CustomName string `json:"custom_name"` // 客户端调用名,空=用上游名
|
||||
Weight *int `json:"weight"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
resp.Fail(c, http.StatusBadRequest, "invalid input")
|
||||
return
|
||||
}
|
||||
var m store.Model
|
||||
if err := h.a.DB.Where("name = ?", req.ModelName).First(&m).Error; err != nil {
|
||||
resp.Fail(c, http.StatusNotFound, "model not found")
|
||||
return
|
||||
globalName := req.CustomName
|
||||
if globalName == "" {
|
||||
globalName = req.UpstreamModel
|
||||
}
|
||||
upstream := req.UpstreamModel
|
||||
if upstream == "" {
|
||||
upstream = m.Name
|
||||
// 解析或创建全局模型(客户端名)
|
||||
var m store.Model
|
||||
if err := h.a.DB.Where("name = ?", globalName).First(&m).Error; err != nil {
|
||||
m = store.Model{Name: globalName, DisplayName: globalName, Enabled: true}
|
||||
if err := h.a.DB.Create(&m).Error; err != nil {
|
||||
resp.Fail(c, http.StatusInternalServerError, "failed to create model")
|
||||
return
|
||||
}
|
||||
}
|
||||
b := store.ChannelModelBinding{
|
||||
ChannelID: id, ModelID: m.ID, UpstreamModel: upstream, Weight: intOr(req.Weight, 1),
|
||||
ChannelID: id, ModelID: m.ID, UpstreamModel: req.UpstreamModel, Weight: intOr(req.Weight, 1),
|
||||
}
|
||||
if err := h.a.DB.Create(&b).Error; err != nil {
|
||||
resp.Fail(c, http.StatusConflict, "binding may already exist")
|
||||
return
|
||||
}
|
||||
resp.Created(c, gin.H{"id": b.ID, "model_id": m.ID, "model_name": m.Name, "upstream_model": upstream, "weight": b.Weight})
|
||||
resp.Created(c, gin.H{"id": b.ID, "model_id": m.ID, "model_name": m.Name, "upstream_model": req.UpstreamModel, "weight": b.Weight})
|
||||
}
|
||||
|
||||
// AdminChannelUpdateModel PATCH /api/v1/admin/channels/:id/models/:bid — 改映射名/权重。
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, reactive, ref } from 'vue'
|
||||
import { computed, onMounted, reactive, ref } from 'vue'
|
||||
import { http, errMsg } from '@/api/client'
|
||||
import { useToastStore } from '@/stores/toast'
|
||||
import { PROTOCOL_OPTIONS, protocolShort } from '@/lib/protocol'
|
||||
@@ -127,13 +127,24 @@ async function importModels(ch: Channel) {
|
||||
const modelsOpen = ref(false)
|
||||
const mappingChan = ref<Channel | null>(null)
|
||||
const mappings = ref<ChannelModelMapping[]>([])
|
||||
const availableModels = ref<string[]>([])
|
||||
const mappingForm = reactive({ model_name: '', upstream_model: '', weight: 1 })
|
||||
const availableModels = ref<string[]>([]) // 已存在模型名,供上游模型输入联想
|
||||
const mappingForm = reactive({ upstream_model: '', custom_name: '', weight: 1 })
|
||||
const showModelSuggestions = ref(false)
|
||||
const filteredModels = computed(() => {
|
||||
const q = mappingForm.upstream_model.trim().toLowerCase()
|
||||
if (!q) return availableModels.value
|
||||
return availableModels.value.filter((m) => m.toLowerCase().includes(q))
|
||||
})
|
||||
|
||||
function pickSuggestion(m: string) {
|
||||
mappingForm.upstream_model = m
|
||||
showModelSuggestions.value = false
|
||||
}
|
||||
|
||||
async function openModels(ch: Channel) {
|
||||
mappingChan.value = ch
|
||||
mappingForm.model_name = ''
|
||||
mappingForm.upstream_model = ''
|
||||
mappingForm.custom_name = ''
|
||||
mappingForm.weight = 1
|
||||
modelsOpen.value = true
|
||||
await Promise.all([loadMappings(), loadAvailableModels()])
|
||||
@@ -159,15 +170,17 @@ async function loadAvailableModels() {
|
||||
}
|
||||
|
||||
async function addMapping() {
|
||||
if (!mappingChan.value || !mappingForm.model_name) return
|
||||
if (!mappingChan.value || !mappingForm.upstream_model) return
|
||||
try {
|
||||
await http.post(`/admin/channels/${mappingChan.value.id}/models`, {
|
||||
model_name: mappingForm.model_name,
|
||||
upstream_model: mappingForm.upstream_model,
|
||||
custom_name: mappingForm.custom_name,
|
||||
weight: Number(mappingForm.weight) || 1,
|
||||
})
|
||||
toast.ok('已绑定')
|
||||
toast.ok('已添加')
|
||||
mappingForm.upstream_model = ''
|
||||
mappingForm.custom_name = ''
|
||||
showModelSuggestions.value = false
|
||||
await loadMappings()
|
||||
} catch (e) {
|
||||
toast.err(errMsg(e))
|
||||
@@ -314,13 +327,15 @@ onMounted(load)
|
||||
</Modal>
|
||||
|
||||
<!-- 模型名称映射 -->
|
||||
<Modal :open="modelsOpen" :title="`模型映射 · ${mappingChan?.name}`" @close="modelsOpen = false">
|
||||
<div class="mb-3 text-xs text-muted">客户端调用「模型名」时,网关转发为右侧「上游名称」。</div>
|
||||
<Modal :open="modelsOpen" :title="`支持的模型 · ${mappingChan?.name}`" @close="modelsOpen = false">
|
||||
<div class="mb-3 text-xs text-muted">
|
||||
客户端调用「客户端名称」,网关转发为「上游模型」。无 /models 接口的渠道可手工添加。
|
||||
</div>
|
||||
<table class="w-full text-sm">
|
||||
<thead>
|
||||
<tr class="border-b border-edge text-left text-xs text-muted">
|
||||
<th scope="col" class="px-3 py-2 font-medium">模型</th>
|
||||
<th scope="col" class="px-3 py-2 font-medium">上游名称</th>
|
||||
<th scope="col" class="px-3 py-2 font-medium">客户端名称</th>
|
||||
<th scope="col" class="px-3 py-2 font-medium">上游模型</th>
|
||||
<th scope="col" class="px-3 py-2 font-medium">权重</th>
|
||||
<th scope="col" class="px-3 py-2" />
|
||||
</tr>
|
||||
@@ -343,25 +358,42 @@ onMounted(load)
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="mappings.length === 0">
|
||||
<td colspan="4" class="px-3 py-6 text-center text-xs text-muted">尚未绑定模型,可在下方添加</td>
|
||||
<td colspan="4" class="px-3 py-6 text-center text-xs text-muted">尚未添加模型,可在下方手工填写</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="mt-3 flex items-center gap-2 border-t border-edge pt-3">
|
||||
<select
|
||||
v-model="mappingForm.model_name"
|
||||
class="h-9 flex-1 rounded-md border border-edge2 bg-surface px-2 text-xs text-ink outline-none focus:border-accent"
|
||||
>
|
||||
<option value="" disabled>选择全局模型</option>
|
||||
<option v-for="m in availableModels" :key="m" :value="m">{{ m }}</option>
|
||||
</select>
|
||||
<input
|
||||
v-model="mappingForm.upstream_model"
|
||||
placeholder="上游名称(默认同名)"
|
||||
class="h-9 w-44 rounded-md border border-edge2 bg-surface px-2 font-mono text-xs outline-none focus:border-accent"
|
||||
v-model="mappingForm.custom_name"
|
||||
placeholder="自定义名称(可选)"
|
||||
class="h-9 w-36 shrink-0 rounded-md border border-edge2 bg-surface px-2 font-mono text-xs outline-none focus:border-accent"
|
||||
@keyup.enter="addMapping"
|
||||
/>
|
||||
<Button size="sm" @click="addMapping">绑定</Button>
|
||||
<div class="relative min-w-0 flex-1">
|
||||
<input
|
||||
v-model="mappingForm.upstream_model"
|
||||
placeholder="上游模型名"
|
||||
class="h-9 w-full rounded-md border border-edge2 bg-surface px-2 font-mono text-xs outline-none focus:border-accent"
|
||||
@focus="showModelSuggestions = true"
|
||||
@input="showModelSuggestions = true"
|
||||
@keydown.enter="addMapping"
|
||||
@keydown.esc="showModelSuggestions = false"
|
||||
/>
|
||||
<div
|
||||
v-if="showModelSuggestions && filteredModels.length"
|
||||
class="absolute left-0 right-0 top-full z-10 mt-1 max-h-40 overflow-y-auto rounded-md border border-edge bg-surface py-1 shadow-lg"
|
||||
>
|
||||
<button
|
||||
v-for="m in filteredModels"
|
||||
:key="m"
|
||||
class="block w-full truncate px-2.5 py-1.5 text-left font-mono text-xs text-ink hover:bg-surface2"
|
||||
@mousedown.prevent="pickSuggestion(m)"
|
||||
>
|
||||
{{ m }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<Button size="sm" class="shrink-0" @click="addMapping">添加</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user