渠道支持多 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:
Sakurasan
2026-08-15 16:47:03 +08:00
co-authored by Claude
parent 6adadebaf0
commit 5eff32afd8
9 changed files with 200 additions and 57 deletions
+50 -10
View File
@@ -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"