渠道支持多 API 格式 + 协议全名展示
- Channel 新增 formats(jsonb: chat|responses|messages), 一个渠道可同时声明 支持 OpenAI Chat Completions / OpenAI Responses API / Anthropic Messages - 路由改为按渠道声明的 formats 决定直通/转换: 客户端协议在 formats 内直通, 否则转换为其首选支持格式(chat > messages > responses) - 兼容旧数据: formats 为空时按 provider 推断(openai→chat+responses, anthropic→messages, compatible→chat) - 前端: 渠道表/表单展示 API 格式(支持多选), usage 明细显示协议全名 (OpenAI Chat Completions / OpenAI Responses API / Anthropic Messages) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
import { onMounted, reactive, ref } from 'vue'
|
||||
import { http, errMsg } from '@/api/client'
|
||||
import { useToastStore } from '@/stores/toast'
|
||||
import { PROTOCOL_OPTIONS, protocolName } from '@/lib/protocol'
|
||||
import Button from '@/components/ui/Button.vue'
|
||||
import Input from '@/components/ui/Input.vue'
|
||||
import Modal from '@/components/ui/Modal.vue'
|
||||
@@ -18,6 +19,7 @@ const busyId = ref<number | null>(null)
|
||||
const form = reactive({
|
||||
name: '',
|
||||
provider: 'openai' as 'openai' | 'anthropic' | 'compatible',
|
||||
formats: [] as string[],
|
||||
base_url: '',
|
||||
api_key: '',
|
||||
weight: 1,
|
||||
@@ -27,10 +29,15 @@ const form = reactive({
|
||||
enabled: true,
|
||||
})
|
||||
|
||||
const providerMap: Record<string, string> = {
|
||||
openai: 'OpenAI',
|
||||
anthropic: 'Anthropic',
|
||||
compatible: '兼容',
|
||||
// 按供应商推断默认支持的协议格式
|
||||
function defaultFormats(p: string): string[] {
|
||||
if (p === 'anthropic') return ['messages']
|
||||
if (p === 'openai') return ['chat', 'responses']
|
||||
return ['chat']
|
||||
}
|
||||
|
||||
function onProviderChange() {
|
||||
form.formats = defaultFormats(form.provider)
|
||||
}
|
||||
|
||||
async function load() {
|
||||
@@ -45,7 +52,7 @@ async function load() {
|
||||
function openCreate() {
|
||||
editing.value = null
|
||||
Object.assign(form, {
|
||||
name: '', provider: 'openai', base_url: '', api_key: '',
|
||||
name: '', provider: 'openai', formats: defaultFormats('openai'), base_url: '', api_key: '',
|
||||
weight: 1, priority: 0, timeout_ms: 120000, max_concurrency: 16, enabled: true,
|
||||
})
|
||||
editOpen.value = true
|
||||
@@ -54,7 +61,8 @@ function openCreate() {
|
||||
function openEdit(ch: Channel) {
|
||||
editing.value = ch
|
||||
Object.assign(form, {
|
||||
name: ch.name, provider: ch.provider, base_url: ch.base_url, api_key: '',
|
||||
name: ch.name, provider: ch.provider, formats: [...(ch.formats || defaultFormats(ch.provider))],
|
||||
base_url: ch.base_url, api_key: '',
|
||||
weight: ch.weight, priority: ch.priority, timeout_ms: ch.timeout_ms,
|
||||
max_concurrency: ch.max_concurrency, enabled: ch.enabled,
|
||||
})
|
||||
@@ -142,7 +150,7 @@ onMounted(load)
|
||||
<thead>
|
||||
<tr class="border-b border-edge text-left text-xs text-muted">
|
||||
<th scope="col" class="px-4 py-2.5 font-medium">名称</th>
|
||||
<th scope="col" class="px-4 py-2.5 font-medium">类型</th>
|
||||
<th scope="col" class="px-4 py-2.5 font-medium">API 格式</th>
|
||||
<th scope="col" class="px-4 py-2.5 font-medium">Base URL</th>
|
||||
<th scope="col" class="px-4 py-2.5 font-medium">Key</th>
|
||||
<th scope="col" class="px-4 py-2.5 font-medium">健康</th>
|
||||
@@ -153,7 +161,15 @@ onMounted(load)
|
||||
<tbody>
|
||||
<tr v-for="ch in channels" :key="ch.id" class="table-row">
|
||||
<td class="px-4 py-2.5 text-ink">{{ ch.name }}</td>
|
||||
<td class="px-4 py-2.5 font-mono text-xs text-muted">{{ providerMap[ch.provider] }}</td>
|
||||
<td class="px-4 py-2.5">
|
||||
<div class="flex flex-wrap gap-1">
|
||||
<span
|
||||
v-for="f in ch.formats || []"
|
||||
:key="f"
|
||||
class="rounded-full bg-surface2 px-2 py-0.5 text-[10px] leading-4 text-muted"
|
||||
>{{ protocolName(f) }}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-4 py-2.5 font-mono text-xs text-muted">{{ ch.base_url }}</td>
|
||||
<td class="px-4 py-2.5 font-mono text-xs text-muted">{{ ch.api_key_masked || '****' }}</td>
|
||||
<td class="px-4 py-2.5">
|
||||
@@ -186,14 +202,38 @@ onMounted(load)
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<Input v-model="form.name" label="名称" placeholder="openai" />
|
||||
<label class="block">
|
||||
<span class="mb-1.5 block text-xs font-medium text-muted">API 类型</span>
|
||||
<select v-model="form.provider" class="h-10 w-full rounded-md border border-edge2 bg-surface px-3 text-sm text-ink outline-none focus:border-accent">
|
||||
<span class="mb-1.5 block text-xs font-medium text-muted">供应商</span>
|
||||
<select
|
||||
v-model="form.provider"
|
||||
class="h-10 w-full rounded-md border border-edge2 bg-surface px-3 text-sm text-ink outline-none focus:border-accent"
|
||||
@change="onProviderChange"
|
||||
>
|
||||
<option value="openai">OpenAI</option>
|
||||
<option value="anthropic">Anthropic</option>
|
||||
<option value="compatible">兼容</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<span class="mb-1.5 block text-xs font-medium text-muted">支持的 API 格式</span>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<label
|
||||
v-for="opt in PROTOCOL_OPTIONS"
|
||||
:key="opt.value"
|
||||
class="flex cursor-pointer items-center gap-1.5 rounded-md border px-2.5 py-1.5 text-xs transition select-none"
|
||||
:class="form.formats.includes(opt.value) ? 'border-accent bg-accent-soft text-ink' : 'border-edge2 text-muted hover:border-edge'"
|
||||
>
|
||||
<input
|
||||
v-model="form.formats"
|
||||
type="checkbox"
|
||||
:value="opt.value"
|
||||
class="size-3.5 rounded accent-[var(--color-accent)]"
|
||||
/>
|
||||
{{ opt.label }}
|
||||
</label>
|
||||
</div>
|
||||
<p class="mt-1.5 text-xs text-muted">客户端协议不在其中时,网关自动转换为其支持的格式</p>
|
||||
</div>
|
||||
<Input v-model="form.base_url" label="Base URL" placeholder="https://api.openai.com" />
|
||||
<Input
|
||||
v-model="form.api_key"
|
||||
|
||||
Reference in New Issue
Block a user