feat: add sub2api source compatibility

This commit is contained in:
chick
2026-06-01 20:14:27 +08:00
parent 5f59b4bed3
commit 00962c3653
12 changed files with 527 additions and 25 deletions
+92
View File
@@ -9,8 +9,10 @@ import (
"mime"
"mime/multipart"
"net/http"
"net/textproto"
"strings"
"github.com/basketikun/infinite-canvas/model"
"github.com/basketikun/infinite-canvas/service"
)
@@ -84,6 +86,12 @@ func proxyAIRequest(w http.ResponseWriter, r *http.Request, path string) {
Fail(w, "AI 接口请求失败")
return
}
body, contentType, err = adaptAIRequestForChannel(body, contentType, path, channel)
if err != nil {
log.Printf("AI proxy adapt request failed: model=%s path=%s err=%v", modelName, path, err)
Fail(w, "AI 接口请求失败")
return
}
path = resolveAIProxyPath(channel.BaseURL, modelName, path)
request, err := http.NewRequest(http.MethodPost, service.BuildModelChannelURL(channel, path), bytes.NewReader(body))
if err != nil {
@@ -140,6 +148,90 @@ func copyAIResponse(w http.ResponseWriter, request *http.Request, onFailure func
_, _ = io.Copy(w, response.Body)
}
func adaptAIRequestForChannel(body []byte, contentType string, path string, channel model.ModelChannel) ([]byte, string, error) {
if path != "/images/generations" && path != "/images/edits" {
return body, contentType, nil
}
format := strings.ToLower(strings.TrimSpace(channel.RequestOptions.ImageResponseFormat))
if format == "" {
if strings.EqualFold(strings.TrimSpace(channel.Compatibility), "sub2api") {
format = "url"
} else {
format = "b64_json"
}
}
if format != "url" && format != "b64_json" {
return body, contentType, nil
}
if strings.HasPrefix(contentType, "multipart/form-data") {
return setMultipartField(body, contentType, "response_format", format)
}
var payload map[string]any
if err := json.Unmarshal(body, &payload); err != nil {
return nil, "", err
}
payload["response_format"] = format
nextBody, err := json.Marshal(payload)
if err != nil {
return nil, "", err
}
return nextBody, contentType, nil
}
func setMultipartField(body []byte, contentType string, fieldName string, value string) ([]byte, string, error) {
_, params, err := mime.ParseMediaType(contentType)
if err != nil {
return nil, "", err
}
reader := multipart.NewReader(bytes.NewReader(body), params["boundary"])
var buffer bytes.Buffer
writer := multipart.NewWriter(&buffer)
written := false
for {
part, err := reader.NextPart()
if err == io.EOF {
break
}
if err != nil {
return nil, "", err
}
if part.FormName() == fieldName && part.FileName() == "" {
if err := writer.WriteField(fieldName, value); err != nil {
_ = part.Close()
return nil, "", err
}
written = true
_ = part.Close()
continue
}
header := make(textproto.MIMEHeader, len(part.Header))
for key, values := range part.Header {
copied := make([]string, len(values))
copy(copied, values)
header[key] = copied
}
target, err := writer.CreatePart(header)
if err != nil {
_ = part.Close()
return nil, "", err
}
if _, err := io.Copy(target, part); err != nil {
_ = part.Close()
return nil, "", err
}
_ = part.Close()
}
if !written {
if err := writer.WriteField(fieldName, value); err != nil {
return nil, "", err
}
}
if err := writer.Close(); err != nil {
return nil, "", err
}
return buffer.Bytes(), writer.FormDataContentType(), nil
}
func readAIRequest(r *http.Request) ([]byte, string, string, error) {
contentType := r.Header.Get("Content-Type")
body, err := io.ReadAll(r.Body)