渠道: 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:
@@ -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 {
|
||||
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/openteam/server/internal/channel"
|
||||
"github.com/openteam/server/internal/store"
|
||||
)
|
||||
|
||||
|
||||
@@ -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' },
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user