fix(channel):修复ali百炼渠道路径拼接问题
This commit is contained in:
1 parent
a9f7fe5311
commit
bc2f202f4f
6 files changed
+112
-12
No files matched your search
@@ -664,10 +664,13 @@ func (s *sAdmin) GetProviderDefaultURLs(ctx context.Context, _ *v1.ProviderDefau
|
||||
|
||||
// defaultProviderURLs 供应商默认 API 地址
|
||||
var defaultProviderURLs = map[int]string{
|
||||
1: "https://api.openai.com",
|
||||
2: "https://api.anthropic.com",
|
||||
3: "https://generativelanguage.googleapis.com",
|
||||
4: "https://dashscope.aliyuncs.com/compatible-mode",
|
||||
1: "https://api.openai.com",
|
||||
2: "https://api.anthropic.com",
|
||||
3: "https://generativelanguage.googleapis.com",
|
||||
// 阿里云百炼填裸域名:adaptor 会按 relay 模式自行拼 /compatible-mode/v1/...(OpenAI 兼容)、
|
||||
// /apps/anthropic/v1/messages(Claude)、/api/v1/services/aigc/...(图像),
|
||||
// 这里若带上 /compatible-mode 会导致路径重复而 404。
|
||||
4: "https://dashscope.aliyuncs.com",
|
||||
5: "https://aip.baidubce.com",
|
||||
6: "https://hunyuan.tencentcloudapi.com",
|
||||
7: "https://open.bigmodel.cn",
|
||||
|
||||
@@ -28,7 +28,7 @@ func (a *Adaptor) Init(info *common.RelayInfo) {
|
||||
// GetRequestURL 构建上游请求 URL。
|
||||
// Claude 协议走 Anthropic 兼容端点,OpenAI 走 compatible-mode 端点,图像生成使用独立端点。
|
||||
func (a *Adaptor) GetRequestURL(info *common.RelayInfo) (string, error) {
|
||||
baseURL := strings.TrimSuffix(info.ChannelMeta.BaseURL, "/")
|
||||
baseURL := normalizeBaseURL(info.ChannelMeta.BaseURL)
|
||||
|
||||
switch constant.RelayMode(info.RelayMode) {
|
||||
case constant.RelayModeClaudeMessages:
|
||||
@@ -161,6 +161,30 @@ func (a *Adaptor) GetChannelName() string {
|
||||
|
||||
var _ common.Adaptor = (*Adaptor)(nil)
|
||||
|
||||
// normalizeBaseURL 归一化渠道 base URL 为 DashScope 裸域名。
|
||||
//
|
||||
// adaptor 各端点自行拼接完整路径(compatible-mode / apps/anthropic / api/v1/services),
|
||||
// 因此 base 必须是裸域名。但用户手填时有三种常见写法都很自然:
|
||||
//
|
||||
// https://dashscope.aliyuncs.com
|
||||
// https://dashscope.aliyuncs.com/compatible-mode (旧版默认值、控制台文档)
|
||||
// https://dashscope.aliyuncs.com/compatible-mode/v1 (阿里 OpenAI SDK 文档的 base_url)
|
||||
//
|
||||
// 后两种若直接拼接会产生 /compatible-mode/compatible-mode/v1/... 这类重复路径,
|
||||
// DashScope 网关在路由层直接返回 404 且响应体为空,极难定位。
|
||||
// 这里统一剥离已知后缀,让三种写法都能正常工作。
|
||||
func normalizeBaseURL(raw string) string {
|
||||
base := strings.TrimSuffix(strings.TrimSpace(raw), "/")
|
||||
// 从长到短依次剥离,避免先匹配短后缀导致残留
|
||||
for _, suffix := range []string{"/compatible-mode/v1", "/compatible-mode"} {
|
||||
if strings.HasSuffix(base, suffix) {
|
||||
base = strings.TrimSuffix(base, suffix)
|
||||
break
|
||||
}
|
||||
}
|
||||
return strings.TrimSuffix(base, "/")
|
||||
}
|
||||
|
||||
// convertClaudeRequest 处理 Claude 入站请求的模型映射。
|
||||
func convertClaudeRequest(requestBody []byte, info *common.RelayInfo) (io.Reader, error) {
|
||||
if !info.ChannelMeta.IsModelMapped {
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
package ali
|
||||
|
||||
import "testing"
|
||||
|
||||
// TestNormalizeBaseURL 验证三种常见 base URL 写法都归一化为同一个裸域名,
|
||||
// 防止再次出现 /compatible-mode/compatible-mode/v1/... 重复路径导致上游 404。
|
||||
func TestNormalizeBaseURL(t *testing.T) {
|
||||
const want = "https://dashscope.aliyuncs.com"
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
in string
|
||||
want string
|
||||
}{
|
||||
{"裸域名", "https://dashscope.aliyuncs.com", want},
|
||||
{"裸域名带尾斜杠", "https://dashscope.aliyuncs.com/", want},
|
||||
{"带 compatible-mode", "https://dashscope.aliyuncs.com/compatible-mode", want},
|
||||
{"带 compatible-mode 尾斜杠", "https://dashscope.aliyuncs.com/compatible-mode/", want},
|
||||
{"带 compatible-mode/v1(阿里 SDK 文档写法)", "https://dashscope.aliyuncs.com/compatible-mode/v1", want},
|
||||
{"带 compatible-mode/v1 尾斜杠", "https://dashscope.aliyuncs.com/compatible-mode/v1/", want},
|
||||
{"首尾空格", " https://dashscope.aliyuncs.com/compatible-mode/v1 ", want},
|
||||
{"自定义代理域名保持不变", "https://proxy.example.com/dashscope", "https://proxy.example.com/dashscope"},
|
||||
{"自定义代理域名剥离 compatible-mode", "https://proxy.example.com/compatible-mode/v1", "https://proxy.example.com"},
|
||||
{"空串", "", ""},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
if got := normalizeBaseURL(c.in); got != c.want {
|
||||
t.Errorf("normalizeBaseURL(%q) = %q, want %q", c.in, got, c.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestNormalizeBaseURLIdempotent 归一化必须幂等,重复调用不改变结果。
|
||||
func TestNormalizeBaseURLIdempotent(t *testing.T) {
|
||||
inputs := []string{
|
||||
"https://dashscope.aliyuncs.com/compatible-mode/v1",
|
||||
"https://dashscope.aliyuncs.com/compatible-mode",
|
||||
"https://dashscope.aliyuncs.com",
|
||||
}
|
||||
for _, in := range inputs {
|
||||
once := normalizeBaseURL(in)
|
||||
if twice := normalizeBaseURL(once); twice != once {
|
||||
t.Errorf("非幂等: %q -> %q -> %q", in, once, twice)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -308,7 +308,8 @@ func (a *Adaptor) handleChatNonStreamResponse(ctx context.Context, resp *http.Re
|
||||
upstreamErr.ResponseWritten = true
|
||||
return &common.Usage{}, upstreamErr
|
||||
}
|
||||
return nil, constant.NewUpstreamError(resp.StatusCode, string(body), nil).WithRetryAfter(constant.RetryAfterFromHeader(resp.Header))
|
||||
// 响应体不是标准 OpenAI 错误格式(含空响应体):附带上游 URL 供日志定位
|
||||
return nil, constant.NewUpstreamErrorFromResponse(resp, body)
|
||||
}
|
||||
|
||||
if info.ChannelMeta.IsModelMapped {
|
||||
@@ -347,7 +348,8 @@ func (a *Adaptor) handleChatStreamResponse(ctx context.Context, resp *http.Respo
|
||||
upstreamErr.ResponseWritten = true
|
||||
return &common.Usage{}, upstreamErr
|
||||
}
|
||||
return nil, constant.NewUpstreamError(resp.StatusCode, string(body), nil).WithRetryAfter(constant.RetryAfterFromHeader(resp.Header))
|
||||
// 响应体不是标准 OpenAI 错误格式(含空响应体):附带上游 URL 供日志定位
|
||||
return nil, constant.NewUpstreamErrorFromResponse(resp, body)
|
||||
}
|
||||
|
||||
return StreamHandler(ctx, resp, info, writer)
|
||||
|
||||
@@ -30,21 +30,32 @@ type RelayError struct {
|
||||
// RetryAfter 上游 Retry-After 头解析结果(0 = 未携带)。
|
||||
// 调度重试 FSM 据此决定 429 时「原地等待重试」还是立即 failover。
|
||||
RetryAfter time.Duration
|
||||
// Detail 服务端诊断上下文(如上游请求 URL),仅出现在 Error() 输出即日志中。
|
||||
// 客户端响应只取 Message 字段(见 WriteRelayError),故此字段不会泄露上游地址。
|
||||
Detail string
|
||||
}
|
||||
|
||||
func (e *RelayError) Error() string {
|
||||
if e.Cause != nil {
|
||||
return e.Message + ": " + e.Cause.Error()
|
||||
msg := e.Message
|
||||
if e.Detail != "" {
|
||||
msg += " [" + e.Detail + "]"
|
||||
}
|
||||
return e.Message
|
||||
if e.Cause != nil {
|
||||
return msg + ": " + e.Cause.Error()
|
||||
}
|
||||
return msg
|
||||
}
|
||||
|
||||
func (e *RelayError) Unwrap() error {
|
||||
return e.Cause
|
||||
}
|
||||
|
||||
// NewUpstreamError 创建上游错误
|
||||
// NewUpstreamError 创建上游错误。
|
||||
// message 为空时(上游返回空响应体)用状态码兜底,避免日志里出现 error=[] 这类无信息量的记录。
|
||||
func NewUpstreamError(statusCode int, message string, cause error) *RelayError {
|
||||
if strings.TrimSpace(message) == "" && cause == nil {
|
||||
message = "upstream returned HTTP " + strconv.Itoa(statusCode) + " with empty response body"
|
||||
}
|
||||
return &RelayError{
|
||||
StatusCode: statusCode,
|
||||
Message: message,
|
||||
@@ -53,6 +64,17 @@ func NewUpstreamError(statusCode int, message string, cause error) *RelayError {
|
||||
}
|
||||
}
|
||||
|
||||
// NewUpstreamErrorFromResponse 由上游响应构造错误,并把请求 URL 记入 Detail 供日志定位。
|
||||
// 空响应体的 4xx 通常是 URL 路径拼错(被上游网关在路由层拒绝),此时 URL 是唯一有效线索。
|
||||
func NewUpstreamErrorFromResponse(resp *http.Response, body []byte) *RelayError {
|
||||
e := NewUpstreamError(resp.StatusCode, string(body), nil).
|
||||
WithRetryAfter(RetryAfterFromHeader(resp.Header))
|
||||
if resp.Request != nil && resp.Request.URL != nil {
|
||||
e.Detail = "upstream_url=" + resp.Request.URL.String()
|
||||
}
|
||||
return e
|
||||
}
|
||||
|
||||
// WithRetryAfter 附着上游 Retry-After 解析结果(链式调用,d<=0 时不生效)。
|
||||
func (e *RelayError) WithRetryAfter(d time.Duration) *RelayError {
|
||||
if e != nil && d > 0 {
|
||||
|
||||
@@ -194,7 +194,7 @@ const providerDefaultURLs: Record<number, string> = {
|
||||
1: 'https://api.openai.com',
|
||||
2: 'https://api.anthropic.com',
|
||||
3: 'https://generativelanguage.googleapis.com',
|
||||
4: 'https://dashscope.aliyuncs.com/compatible-mode',
|
||||
4: 'https://dashscope.aliyuncs.com',
|
||||
6: 'https://hunyuan.tencentcloudapi.com',
|
||||
7: 'https://open.bigmodel.cn/api/paas',
|
||||
8: 'https://api.deepseek.com',
|
||||
|
||||
Reference in new issue
Block a user