渠道: Base URL 智能识别前缀/完整端点 + API 格式垂直堆叠

- upstreamURL 按内容智能拼接: 完整端点直接用 / 含 /v1 只拼资源路径 / 纯域名拼全路径
- resolveBaseURL 不再去 /v1, 交网关识别
- 渠道表 API 格式改垂直堆叠, 显示短标识 chat/completions / responses / messages
- 前端 Base URL 提示支持前缀或完整端点

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Sakurasan
2026-08-16 00:08:52 +08:00
co-authored by Claude
parent 512e46664c
commit 8d9940ad5a
4 changed files with 25 additions and 12 deletions
+5 -4
View File
@@ -33,7 +33,8 @@ func (h *Handler) AdminChannels(c *gin.Context) {
masked = "****"
}
out = append(out, gin.H{
"id": ch.ID, "name": ch.Name, "provider": ch.Provider, "formats": ch.FormatsEffective(), "base_url": ch.BaseURL,
"id": ch.ID, "name": ch.Name, "provider": ch.Provider, "formats": ch.FormatsEffective(),
"base_url": ch.BaseURL,
"api_key_masked": masked, "weight": ch.Weight, "priority": ch.Priority,
"timeout_ms": ch.TimeoutMS, "max_concurrency": ch.MaxConcurrency,
"health_status": ch.HealthStatus, "enabled": ch.Enabled,
@@ -47,7 +48,7 @@ type channelBody struct {
Name string `json:"name" binding:"required,min=1,max=64"`
Provider string `json:"provider"` // 可选:为空时按 formats 推断(兼容旧数据)
Formats []string `json:"formats"` // 原生支持的协议 chat|responses|messages(主配置)
BaseURL string `json:"base_url"` // 可选:为空时按供应商默认(openai/anthropic)
BaseURL string `json:"base_url"` // 可选:留空按供应商默认;支持前缀或完整端点
APIKey string `json:"api_key"`
Weight *int `json:"weight"`
Priority *int `json:"priority"`
@@ -56,7 +57,7 @@ type channelBody struct {
Enabled *bool `json:"enabled"`
}
// resolveBaseURL 渠道 base_url:留空按供应商默认;兼容用户填完整地址(含 /v1)。
// resolveBaseURL 渠道 base_url:留空按供应商默认;网关按内容智能识别前缀/完整端点。
func resolveBaseURL(provider, raw string) (string, error) {
base := strings.TrimRight(raw, "/")
if base == "" {
@@ -70,7 +71,7 @@ func resolveBaseURL(provider, raw string) (string, error) {
if base == "" {
return "", errors.New("base_url required for compatible channels")
}
return strings.TrimSuffix(base, "/v1"), nil
return base, nil
}
func validateProvider(p string) bool {
+1
View File
@@ -15,6 +15,7 @@ import (
"time"
"github.com/gin-gonic/gin"
"github.com/openteam/server/internal/channel"
"github.com/openteam/server/internal/store"
)
+11
View File
@@ -5,6 +5,17 @@ export const PROTOCOL_NAMES: Record<string, string> = {
messages: 'Anthropic Messages',
}
// 渠道表格用的短标识
export const PROTOCOL_SHORT: Record<string, string> = {
chat: 'chat/completions',
responses: 'responses',
messages: 'messages',
}
export function protocolShort(p: string): string {
return PROTOCOL_SHORT[p] ?? p
}
export const PROTOCOL_OPTIONS: { value: string; label: string }[] = [
{ value: 'chat', label: 'OpenAI Chat Completions' },
{ value: 'responses', label: 'OpenAI Responses API' },
+8 -8
View File
@@ -2,12 +2,12 @@
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 { PROTOCOL_OPTIONS, protocolShort } from '@/lib/protocol'
import Button from '@/components/ui/Button.vue'
import Input from '@/components/ui/Input.vue'
import Modal from '@/components/ui/Modal.vue'
import Badge from '@/components/ui/Badge.vue'
import type { Channel } from '@/types'
import type { Channel, ChannelModelMapping } from '@/types'
const toast = useToastStore()
const channels = ref<Channel[]>([])
@@ -154,12 +154,12 @@ onMounted(load)
<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">
<div class="flex flex-wrap gap-1">
<span
<div class="flex flex-col gap-0.5">
<code
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>
class="font-mono text-[11px] leading-4 text-muted"
>{{ protocolShort(f) }}</code>
</div>
</td>
<td class="px-4 py-2.5 font-mono text-xs text-muted">{{ ch.base_url }}</td>
@@ -215,8 +215,8 @@ onMounted(load)
<Input
v-model="form.base_url"
label="Base URL(可选)"
placeholder="https://api.openai.com"
hint="留空按供应商默认;可填完整地址,如 https://api.openai.com/v1"
placeholder="https://api.openai.com/v1"
hint="支持前缀或完整端点,如 https://api.openai.com/v1 或 https://api.openai.com/v1/chat/completions;留空按供应商默认"
/>
<Input
v-model="form.api_key"