fix(*): 模型请求协议 - 支持模型独立选协议并修正视频多参考请求

This commit is contained in:
ddCat
2026-07-28 23:05:15 +08:00
parent 7093b4e161
commit e3c501620d
20 changed files with 691 additions and 190 deletions

View File

@@ -663,7 +663,9 @@ func proxySystemRequest(c *gin.Context, svc *service.Service, user *model.User,
switch channel.InterfaceType {
case model.ChannelInterfaceOpenAIImage:
capability = "image"
case model.ChannelInterfaceNewAPIVideo, model.ChannelInterfaceNewAPIChannel1, model.ChannelInterfaceNewAPIChannel2, model.ChannelInterfaceXAIVideo:
case model.ChannelInterfaceOpenAIAudio:
capability = "audio"
case model.ChannelInterfaceNewAPIVideo, model.ChannelInterfaceNewAPIChannel1, model.ChannelInterfaceNewAPIChannel2, model.ChannelInterfaceXAIVideo, model.ChannelInterfaceGeminiVeo:
capability = "video"
}
order, err := svc.ReserveProxyBilling(user.ID, channel.ID, strings.TrimPrefix(modelName, "models/"), capability, c.GetHeader("X-Canvas-Scene"), c.GetHeader("X-Idempotency-Key"), proxyRequestVideoSeconds(c.GetHeader("Content-Type"), body))
@@ -752,7 +754,9 @@ func apiCallLog(user *model.User, channel *model.ModelChannel, billingOrderID st
switch channel.InterfaceType {
case model.ChannelInterfaceOpenAIImage:
capability = "image"
case model.ChannelInterfaceNewAPIVideo, model.ChannelInterfaceNewAPIChannel1, model.ChannelInterfaceNewAPIChannel2, model.ChannelInterfaceXAIVideo:
case model.ChannelInterfaceOpenAIAudio:
capability = "audio"
case model.ChannelInterfaceNewAPIVideo, model.ChannelInterfaceNewAPIChannel1, model.ChannelInterfaceNewAPIChannel2, model.ChannelInterfaceXAIVideo, model.ChannelInterfaceGeminiVeo:
capability = "video"
}
requestKind := "create"
@@ -763,24 +767,25 @@ func apiCallLog(user *model.User, channel *model.ModelChannel, billingOrderID st
}
}
return model.ApiCallLog{
UserID: user.ID,
ChannelID: channel.ID,
BillingOrderID: billingOrderID,
Source: "system-channel",
Capability: capability,
RequestKind: requestKind,
Billable: method == http.MethodPost,
APIFormat: channel.APIFormat,
Method: method,
Path: path,
Model: readPayloadModel(body),
Status: status,
StatusCode: statusCode,
DurationMs: duration.Milliseconds(),
Error: errorText,
ConcurrencyLimit: concurrencyLimit,
UpstreamURL: target,
RequestBody: service.SanitizeAPICallPayload(body, contentType),
UserID: user.ID,
ChannelID: channel.ID,
BillingOrderID: billingOrderID,
Source: "system-channel",
Capability: capability,
RequestKind: requestKind,
Billable: method == http.MethodPost,
APIFormat: channel.APIFormat,
Method: method,
Path: path,
Model: readPayloadModel(body),
Status: status,
StatusCode: statusCode,
DurationMs: duration.Milliseconds(),
Error: errorText,
ConcurrencyLimit: concurrencyLimit,
UpstreamURL: target,
RequestContentType: contentType,
RequestBody: service.SanitizeAPICallPayload(body, contentType),
}
}

View File

@@ -23,10 +23,13 @@ import (
)
var (
runtimeService *service.Service
geminiGeneratePath = regexp.MustCompile(`^/models/([^/:]+):(generateContent|streamGenerateContent)$`)
customGeminiRelayPath = regexp.MustCompile(`(?:^|/)models/[^/:]+:(generateContent|streamGenerateContent)$`)
openAIPostEndpoints = map[string]bool{
runtimeService *service.Service
geminiGeneratePath = regexp.MustCompile(`^/models/([^/:]+):(generateContent|streamGenerateContent)$`)
customGeminiRelayPath = regexp.MustCompile(`(?:^|/)models/[^/:]+:(generateContent|streamGenerateContent|predictLongRunning)$`)
customVideoTaskPath = regexp.MustCompile(`(?:^|/)video/generations/[^/]+$`)
customXAIVideoTaskPath = regexp.MustCompile(`(?:^|/)videos/[^/]+$`)
customGeminiOperationPath = regexp.MustCompile(`(?:^|/)(?:models/[^/]+/)?operations/[^/]+$`)
openAIPostEndpoints = map[string]bool{
"/responses": true, "/chat/completions": true, "/images/generations": true, "/images/edits": true,
"/audio/speech": true,
}
@@ -57,7 +60,13 @@ func authorizeCustomRelay(method string, target *url.URL, apiFormat string, cont
return errors.New("自定义渠道调用格式无效")
}
if method == http.MethodGet {
if len(query) != 0 || (requestPath != "/models" && !strings.HasSuffix(requestPath, "/models")) {
allowed := requestPath == "/models" || strings.HasSuffix(requestPath, "/models")
if apiFormat == "openai" {
allowed = allowed || customVideoTaskPath.MatchString(requestPath) || customXAIVideoTaskPath.MatchString(requestPath)
} else {
allowed = allowed || customGeminiOperationPath.MatchString(requestPath)
}
if len(query) != 0 || !allowed {
return errors.New("自定义渠道不允许访问该上游接口")
}
return nil
@@ -70,7 +79,7 @@ func authorizeCustomRelay(method string, target *url.URL, apiFormat string, cont
return errors.New("自定义渠道不允许使用该请求方法")
}
if apiFormat == "openai" {
if len(query) != 0 || (!strings.HasSuffix(requestPath, "/responses") && !strings.HasSuffix(requestPath, "/chat/completions")) {
if len(query) != 0 || (!strings.HasSuffix(requestPath, "/responses") && !strings.HasSuffix(requestPath, "/chat/completions") && !strings.HasSuffix(requestPath, "/video/generations") && !strings.HasSuffix(requestPath, "/videos/generations")) {
return errors.New("自定义渠道不允许访问该上游接口")
}
return nil
@@ -169,7 +178,9 @@ func interfaceAllowsProxyPath(interfaceType model.ChannelInterfaceType, requestP
return requestPath == "/responses"
case model.ChannelInterfaceOpenAIImage:
return requestPath == "/images/generations" || requestPath == "/images/edits"
case model.ChannelInterfaceNewAPIVideo, model.ChannelInterfaceNewAPIChannel1, model.ChannelInterfaceNewAPIChannel2, model.ChannelInterfaceXAIVideo:
case model.ChannelInterfaceOpenAIAudio:
return requestPath == "/audio/speech"
case model.ChannelInterfaceNewAPIVideo, model.ChannelInterfaceNewAPIChannel1, model.ChannelInterfaceNewAPIChannel2, model.ChannelInterfaceXAIVideo, model.ChannelInterfaceGeminiVeo:
return false
default:
return true

View File

@@ -28,7 +28,13 @@ func TestAuthorizeCustomRelayAllowsModelsAndAgentEndpoints(t *testing.T) {
{method: http.MethodGet, target: "https://api.example.com/v1/models", apiFormat: "openai"},
{method: http.MethodPost, target: "https://api.example.com/v1/responses", apiFormat: "openai", contentType: "application/json"},
{method: http.MethodPost, target: "https://api.example.com/v1/chat/completions", apiFormat: "openai", contentType: "application/json; charset=utf-8"},
{method: http.MethodPost, target: "https://api.example.com/v1/video/generations", apiFormat: "openai", contentType: "application/json"},
{method: http.MethodGet, target: "https://api.example.com/v1/video/generations/task-1", apiFormat: "openai"},
{method: http.MethodPost, target: "https://api.x.ai/v1/videos/generations", apiFormat: "openai", contentType: "application/json"},
{method: http.MethodGet, target: "https://api.x.ai/v1/videos/request-1", apiFormat: "openai"},
{method: http.MethodPost, target: "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-pro:streamGenerateContent?alt=sse", apiFormat: "gemini", contentType: "application/json"},
{method: http.MethodPost, target: "https://generativelanguage.googleapis.com/v1beta/models/veo-3.0-generate-preview:predictLongRunning", apiFormat: "gemini", contentType: "application/json"},
{method: http.MethodGet, target: "https://generativelanguage.googleapis.com/v1beta/operations/operation-1", apiFormat: "gemini"},
}
for _, test := range tests {
target, err := url.Parse(test.target)