渠道: 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 = "****"
|
masked = "****"
|
||||||
}
|
}
|
||||||
out = append(out, gin.H{
|
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,
|
"api_key_masked": masked, "weight": ch.Weight, "priority": ch.Priority,
|
||||||
"timeout_ms": ch.TimeoutMS, "max_concurrency": ch.MaxConcurrency,
|
"timeout_ms": ch.TimeoutMS, "max_concurrency": ch.MaxConcurrency,
|
||||||
"health_status": ch.HealthStatus, "enabled": ch.Enabled,
|
"health_status": ch.HealthStatus, "enabled": ch.Enabled,
|
||||||
@@ -47,7 +48,7 @@ type channelBody struct {
|
|||||||
Name string `json:"name" binding:"required,min=1,max=64"`
|
Name string `json:"name" binding:"required,min=1,max=64"`
|
||||||
Provider string `json:"provider"` // 可选:为空时按 formats 推断(兼容旧数据)
|
Provider string `json:"provider"` // 可选:为空时按 formats 推断(兼容旧数据)
|
||||||
Formats []string `json:"formats"` // 原生支持的协议 chat|responses|messages(主配置)
|
Formats []string `json:"formats"` // 原生支持的协议 chat|responses|messages(主配置)
|
||||||
BaseURL string `json:"base_url"` // 可选:为空时按供应商默认(openai/anthropic)
|
BaseURL string `json:"base_url"` // 可选:留空按供应商默认;支持前缀或完整端点
|
||||||
APIKey string `json:"api_key"`
|
APIKey string `json:"api_key"`
|
||||||
Weight *int `json:"weight"`
|
Weight *int `json:"weight"`
|
||||||
Priority *int `json:"priority"`
|
Priority *int `json:"priority"`
|
||||||
@@ -56,7 +57,7 @@ type channelBody struct {
|
|||||||
Enabled *bool `json:"enabled"`
|
Enabled *bool `json:"enabled"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// resolveBaseURL 渠道 base_url:留空按供应商默认;兼容用户填完整地址(含 /v1)。
|
// resolveBaseURL 渠道 base_url:留空按供应商默认;网关按内容智能识别前缀/完整端点。
|
||||||
func resolveBaseURL(provider, raw string) (string, error) {
|
func resolveBaseURL(provider, raw string) (string, error) {
|
||||||
base := strings.TrimRight(raw, "/")
|
base := strings.TrimRight(raw, "/")
|
||||||
if base == "" {
|
if base == "" {
|
||||||
@@ -70,7 +71,7 @@ func resolveBaseURL(provider, raw string) (string, error) {
|
|||||||
if base == "" {
|
if base == "" {
|
||||||
return "", errors.New("base_url required for compatible channels")
|
return "", errors.New("base_url required for compatible channels")
|
||||||
}
|
}
|
||||||
return strings.TrimSuffix(base, "/v1"), nil
|
return base, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func validateProvider(p string) bool {
|
func validateProvider(p string) bool {
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/openteam/server/internal/channel"
|
||||||
"github.com/openteam/server/internal/store"
|
"github.com/openteam/server/internal/store"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,17 @@ export const PROTOCOL_NAMES: Record<string, string> = {
|
|||||||
messages: 'Anthropic Messages',
|
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 }[] = [
|
export const PROTOCOL_OPTIONS: { value: string; label: string }[] = [
|
||||||
{ value: 'chat', label: 'OpenAI Chat Completions' },
|
{ value: 'chat', label: 'OpenAI Chat Completions' },
|
||||||
{ value: 'responses', label: 'OpenAI Responses API' },
|
{ value: 'responses', label: 'OpenAI Responses API' },
|
||||||
|
|||||||
@@ -2,12 +2,12 @@
|
|||||||
import { onMounted, reactive, ref } from 'vue'
|
import { onMounted, reactive, ref } from 'vue'
|
||||||
import { http, errMsg } from '@/api/client'
|
import { http, errMsg } from '@/api/client'
|
||||||
import { useToastStore } from '@/stores/toast'
|
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 Button from '@/components/ui/Button.vue'
|
||||||
import Input from '@/components/ui/Input.vue'
|
import Input from '@/components/ui/Input.vue'
|
||||||
import Modal from '@/components/ui/Modal.vue'
|
import Modal from '@/components/ui/Modal.vue'
|
||||||
import Badge from '@/components/ui/Badge.vue'
|
import Badge from '@/components/ui/Badge.vue'
|
||||||
import type { Channel } from '@/types'
|
import type { Channel, ChannelModelMapping } from '@/types'
|
||||||
|
|
||||||
const toast = useToastStore()
|
const toast = useToastStore()
|
||||||
const channels = ref<Channel[]>([])
|
const channels = ref<Channel[]>([])
|
||||||
@@ -154,12 +154,12 @@ onMounted(load)
|
|||||||
<tr v-for="ch in channels" :key="ch.id" class="table-row">
|
<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 text-ink">{{ ch.name }}</td>
|
||||||
<td class="px-4 py-2.5">
|
<td class="px-4 py-2.5">
|
||||||
<div class="flex flex-wrap gap-1">
|
<div class="flex flex-col gap-0.5">
|
||||||
<span
|
<code
|
||||||
v-for="f in ch.formats || []"
|
v-for="f in ch.formats || []"
|
||||||
:key="f"
|
:key="f"
|
||||||
class="rounded-full bg-surface2 px-2 py-0.5 text-[10px] leading-4 text-muted"
|
class="font-mono text-[11px] leading-4 text-muted"
|
||||||
>{{ protocolName(f) }}</span>
|
>{{ protocolShort(f) }}</code>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</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.base_url }}</td>
|
||||||
@@ -215,8 +215,8 @@ onMounted(load)
|
|||||||
<Input
|
<Input
|
||||||
v-model="form.base_url"
|
v-model="form.base_url"
|
||||||
label="Base URL(可选)"
|
label="Base URL(可选)"
|
||||||
placeholder="https://api.openai.com"
|
placeholder="https://api.openai.com/v1"
|
||||||
hint="留空按供应商默认;可填完整地址,如 https://api.openai.com/v1"
|
hint="支持前缀或完整端点,如 https://api.openai.com/v1 或 https://api.openai.com/v1/chat/completions;留空按供应商默认"
|
||||||
/>
|
/>
|
||||||
<Input
|
<Input
|
||||||
v-model="form.api_key"
|
v-model="form.api_key"
|
||||||
|
|||||||
Reference in New Issue
Block a user