feat: add sub2api compatibility

This commit is contained in:
chick
2026-06-01 23:43:53 +08:00
parent 5f59b4bed3
commit f66540a273
6 changed files with 325 additions and 9 deletions
+108 -1
View File
@@ -129,6 +129,8 @@ func normalizePrivateSetting(setting model.PrivateSetting) model.PrivateSetting
if setting.Channels[i].Protocol == "" {
setting.Channels[i].Protocol = "openai"
}
setting.Channels[i].Compatibility = normalizeChannelCompatibility(setting.Channels[i].Compatibility)
setting.Channels[i].RequestOptions = normalizeModelRequestOptions(setting.Channels[i].Compatibility, setting.Channels[i].RequestOptions)
if setting.Channels[i].Models == nil {
setting.Channels[i].Models = []string{}
}
@@ -202,12 +204,54 @@ func SelectModelChannel(modelName string) (model.ModelChannel, error) {
func BuildModelChannelURL(channel model.ModelChannel, path string) string {
baseURL := normalizeModelChannelBaseURL(channel.BaseURL)
lowerBaseURL := strings.ToLower(baseURL)
if !strings.HasSuffix(lowerBaseURL, "/v1") && !strings.HasSuffix(lowerBaseURL, "/api/v3") && !strings.HasSuffix(lowerBaseURL, "/api/plan/v3") {
if !hasModelChannelVersionSuffix(lowerBaseURL) && !strings.HasSuffix(lowerBaseURL, "/api/v3") && !strings.HasSuffix(lowerBaseURL, "/api/plan/v3") {
baseURL += "/v1"
}
return baseURL + path
}
func hasModelChannelVersionSuffix(baseURL string) bool {
pathValue := baseURL
if parsed, err := url.Parse(baseURL); err == nil && parsed.Scheme != "" && parsed.Host != "" {
pathValue = parsed.Path
}
pathValue = strings.TrimRight(pathValue, "/")
if pathValue == "" {
return false
}
segment := pathValue
if slash := strings.LastIndex(pathValue, "/"); slash >= 0 {
segment = pathValue[slash+1:]
}
segment = strings.ToLower(strings.TrimSpace(segment))
if len(segment) < 2 || segment[0] != 'v' || !isASCIIDigit(segment[1]) {
return false
}
i := 1
for i < len(segment) && isASCIIDigit(segment[i]) {
i++
}
if i == len(segment) {
return true
}
if segment[i] == '.' {
i++
if i == len(segment) || !isASCIIDigit(segment[i]) {
return false
}
for i < len(segment) && isASCIIDigit(segment[i]) {
i++
}
return i == len(segment)
}
suffix := segment[i:]
return strings.HasPrefix(suffix, "alpha") || strings.HasPrefix(suffix, "beta") || strings.HasPrefix(suffix, "preview")
}
func isASCIIDigit(ch byte) bool {
return ch >= '0' && ch <= '9'
}
func normalizeModelChannelBaseURL(baseURL string) string {
baseURL = strings.TrimRight(strings.TrimSpace(baseURL), "/")
parsed, err := url.Parse(baseURL)
@@ -299,6 +343,8 @@ func normalizeModelChannel(channel model.ModelChannel) model.ModelChannel {
if channel.Protocol == "" {
channel.Protocol = "openai"
}
channel.Compatibility = normalizeChannelCompatibility(channel.Compatibility)
channel.RequestOptions = normalizeModelRequestOptions(channel.Compatibility, channel.RequestOptions)
if channel.Models == nil {
channel.Models = []string{}
}
@@ -308,6 +354,30 @@ func normalizeModelChannel(channel model.ModelChannel) model.ModelChannel {
return channel
}
func normalizeChannelCompatibility(value string) string {
switch strings.ToLower(strings.TrimSpace(value)) {
case "sub2api":
return "sub2api"
default:
return "openai"
}
}
func normalizeModelRequestOptions(compatibility string, options model.ModelRequestOptions) model.ModelRequestOptions {
format := strings.ToLower(strings.TrimSpace(options.ImageResponseFormat))
switch format {
case "url", "b64_json":
options.ImageResponseFormat = format
default:
if normalizeChannelCompatibility(compatibility) == "sub2api" {
options.ImageResponseFormat = "url"
} else {
options.ImageResponseFormat = "b64_json"
}
}
return options
}
func resolveAdminChannel(index *int, channel model.ModelChannel) (model.ModelChannel, error) {
resolved := normalizeModelChannel(channel)
if strings.TrimSpace(resolved.APIKey) == "" {
@@ -380,6 +450,9 @@ func testAdminChannelModel(channel model.ModelChannel, modelName string) (string
if strings.TrimSpace(modelName) == "" {
return "", errors.New("缺少模型名称")
}
if isImageModelName(modelName) {
return testAdminImageChannelModel(channel, modelName)
}
body, _ := json.Marshal(map[string]any{
"model": modelName,
"messages": []map[string]string{{
@@ -416,6 +489,40 @@ func testAdminChannelModel(channel model.ModelChannel, modelName string) (string
return "ok", nil
}
func testAdminImageChannelModel(channel model.ModelChannel, modelName string) (string, error) {
format := strings.ToLower(strings.TrimSpace(channel.RequestOptions.ImageResponseFormat))
if format == "" {
if strings.EqualFold(strings.TrimSpace(channel.Compatibility), "sub2api") {
format = "url"
} else {
format = "b64_json"
}
}
body, _ := json.Marshal(map[string]any{
"model": modelName,
"prompt": "test",
"n": 1,
"size": "1024x1024",
"response_format": format,
})
request, err := http.NewRequest(http.MethodPost, BuildModelChannelURL(channel, "/images/generations"), strings.NewReader(string(body)))
if err != nil {
return "", err
}
request.Header.Set("Authorization", "Bearer "+channel.APIKey)
request.Header.Set("Content-Type", "application/json")
response, err := adminModelHTTPClient.Do(request)
if err != nil {
return "", safeMessageError{message: "测试失败:上游接口无响应或网络不可达"}
}
defer response.Body.Close()
responseBody, _ := io.ReadAll(response.Body)
if response.StatusCode >= http.StatusBadRequest {
return "", readAdminChannelError(responseBody, response.StatusCode, "测试失败")
}
return "ok", nil
}
func testArkSeedanceChannelModel(channel model.ModelChannel, modelName string) (string, error) {
if strings.TrimSpace(modelName) == "" {
return "", errors.New("缺少模型名称")