Fix composite route alias attribution

This commit is contained in:
Heatherm Huang
2026-07-23 09:20:52 +08:00
parent ff666be530
commit 3a683fff55
13 changed files with 194 additions and 32 deletions
@@ -4,6 +4,7 @@ import (
"net/http/httptest"
"testing"
middleware2 "github.com/Wei-Shaw/sub2api/internal/server/middleware"
"github.com/Wei-Shaw/sub2api/internal/service"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/require"
@@ -60,3 +61,25 @@ func TestCompositeTargetPlatformResolvedAllowsConcreteGroupWithoutResolution(t *
require.True(t, compositeTargetPlatformResolved(c, apiKey, "llama-4-maverick"))
}
func TestClientRequestedModelUsesCompositePublicModel(t *testing.T) {
gin.SetMode(gin.TestMode)
c, _ := gin.CreateTestContext(httptest.NewRecorder())
c.Request = httptest.NewRequest("POST", "/v1/chat/completions", nil)
c.Request = c.Request.WithContext(service.WithCompositeRouteDecision(c.Request.Context(), service.CompositeRouteDecision{
Matched: true,
Source: service.CompositeRouteSourceExplicit,
PublicModel: "public-alias",
TargetPlatform: service.PlatformOpenAI,
UpstreamModel: "gpt-5",
}))
input := buildContentModerationInput(c, nil, middleware2.AuthSubject{UserID: 42}, service.ContentModerationProtocolOpenAIChat, "gpt-5", nil)
require.Equal(t, "public-alias", input.Model)
require.Equal(t, service.PlatformOpenAI, input.Provider)
fields := clientRequestedUsageFields(c, service.ChannelMappingResult{MappedModel: "gpt-5"}, "gpt-5", "gpt-5")
require.Equal(t, "public-alias", fields.OriginalModel)
require.Equal(t, "public-alias", fields.ChannelMappedModel)
require.Equal(t, "public-alias\u2192gpt-5", fields.ModelMappingChain)
}
@@ -23,6 +23,21 @@ func contentModerationErrorCode(decision *service.ContentModerationDecision) str
return "content_policy_violation"
}
func clientRequestedModel(c *gin.Context, fallback string) string {
fallback = strings.TrimSpace(fallback)
if c == nil || c.Request == nil {
return fallback
}
if model, ok := service.RequestedPublicModelFromContext(c.Request.Context()); ok {
return model
}
return fallback
}
func clientRequestedUsageFields(c *gin.Context, mapping service.ChannelMappingResult, fallbackModel, upstreamModel string) service.ChannelUsageFields {
return mapping.ToUsageFields(clientRequestedModel(c, fallbackModel), upstreamModel)
}
func runContentModeration(c *gin.Context, reqLog *zap.Logger, svc *service.ContentModerationService, apiKey *service.APIKey, subject middleware2.AuthSubject, protocol string, model string, body []byte) *service.ContentModerationDecision {
if svc == nil || c == nil || c.Request == nil {
return nil
@@ -71,10 +86,13 @@ func buildContentModerationInput(c *gin.Context, apiKey *service.APIKey, subject
UserID: subject.UserID,
Endpoint: GetInboundEndpoint(c),
Provider: contentModerationProvider(apiKey),
Model: strings.TrimSpace(model),
Model: clientRequestedModel(c, model),
Protocol: protocol,
Body: body,
}
if resolvedPlatform, ok := service.ResolvedTargetPlatformFromContext(c.Request.Context()); ok {
input.Provider = resolvedPlatform
}
if forcedPlatform, ok := middleware2.GetForcePlatformFromContext(c); ok {
input.Provider = strings.TrimSpace(forcedPlatform)
}
+4 -4
View File
@@ -550,7 +550,7 @@ func (h *GatewayHandler) Messages(c *gin.Context) {
RequestPayloadHash: requestPayloadHash,
ForceCacheBilling: forceCacheBilling,
APIKeyService: h.apiKeyService,
ChannelUsageFields: channelMapping.ToUsageFields(reqModel, result.UpstreamModel),
ChannelUsageFields: clientRequestedUsageFields(c, channelMapping, reqModel, result.UpstreamModel),
}); err != nil {
logger.L().With(
zap.String("component", "handler.gateway.messages"),
@@ -985,7 +985,7 @@ func (h *GatewayHandler) Messages(c *gin.Context) {
RequestPayloadHash: requestPayloadHash,
ForceCacheBilling: forceCacheBilling,
APIKeyService: h.apiKeyService,
ChannelUsageFields: channelMapping.ToUsageFields(reqModel, result.UpstreamModel),
ChannelUsageFields: clientRequestedUsageFields(c, channelMapping, reqModel, result.UpstreamModel),
}); err != nil {
logger.L().With(
zap.String("component", "handler.gateway.messages"),
@@ -1085,7 +1085,7 @@ func (h *GatewayHandler) compositeAvailableModels(ctx context.Context, groupID *
}
seen := make(map[string]struct{})
models := make([]string, 0)
for _, platform := range []string{service.PlatformAnthropic, service.PlatformGemini, service.PlatformOpenAI, service.PlatformGrok} {
for _, platform := range []string{service.PlatformAnthropic, service.PlatformGemini, service.PlatformOpenAI, service.PlatformAntigravity, service.PlatformGrok} {
for _, model := range h.gatewayService.GetAvailableModels(ctx, groupID, platform) {
model = strings.TrimSpace(model)
if model == "" {
@@ -1304,7 +1304,7 @@ func defaultModelIDsForPlatform(platform string) []string {
case service.PlatformComposite:
ids := make([]string, 0)
seen := make(map[string]struct{})
for _, concretePlatform := range []string{service.PlatformAnthropic, service.PlatformGemini, service.PlatformOpenAI, service.PlatformGrok} {
for _, concretePlatform := range []string{service.PlatformAnthropic, service.PlatformGemini, service.PlatformOpenAI, service.PlatformAntigravity, service.PlatformGrok} {
for _, id := range defaultModelIDsForPlatform(concretePlatform) {
if _, ok := seen[id]; ok {
continue
@@ -312,7 +312,7 @@ func (h *GatewayHandler) ChatCompletions(c *gin.Context) {
IPAddress: clientIP,
RequestPayloadHash: requestPayloadHash,
APIKeyService: h.apiKeyService,
ChannelUsageFields: channelMapping.ToUsageFields(reqModel, result.UpstreamModel),
ChannelUsageFields: clientRequestedUsageFields(c, channelMapping, reqModel, result.UpstreamModel),
}); err != nil {
reqLog.Error("gateway.cc.record_usage_failed",
zap.Int64("account_id", account.ID),
@@ -290,7 +290,7 @@ func (h *GatewayHandler) Responses(c *gin.Context) {
IPAddress: clientIP,
RequestPayloadHash: requestPayloadHash,
APIKeyService: h.apiKeyService,
ChannelUsageFields: channelMapping.ToUsageFields(reqModel, result.UpstreamModel),
ChannelUsageFields: clientRequestedUsageFields(c, channelMapping, reqModel, result.UpstreamModel),
}); err != nil {
reqLog.Error("gateway.responses.record_usage_failed",
zap.Int64("account_id", account.ID),
@@ -61,6 +61,14 @@ func newGatewayModelsHandlerForTest(repo service.AccountRepository) *GatewayHand
}
}
func TestDefaultModelIDsForCompositeIncludesAntigravityDefaults(t *testing.T) {
antigravityIDs := defaultModelIDsForPlatform(service.PlatformAntigravity)
require.NotEmpty(t, antigravityIDs)
compositeIDs := defaultModelIDsForPlatform(service.PlatformComposite)
require.Contains(t, compositeIDs, antigravityIDs[0])
}
func TestGatewayModels_GeminiGroupFallsBackToGeminiModels(t *testing.T) {
gin.SetMode(gin.TestMode)
@@ -304,6 +312,15 @@ func TestGatewayModels_CompositeCustomModelsListFiltersAcrossConcretePlatforms(t
},
},
},
{
ID: 3,
Platform: service.PlatformAntigravity,
Credentials: map[string]any{
"model_mapping": map[string]any{
"ag-custom-model": "ag-custom-model",
},
},
},
},
},
},
@@ -318,7 +335,7 @@ func TestGatewayModels_CompositeCustomModelsListFiltersAcrossConcretePlatforms(t
Platform: service.PlatformComposite,
ModelsListConfig: service.GroupModelsListConfig{
Enabled: true,
Models: []string{"gemini-2.5-flash", "missing-model", "gpt-5.5"},
Models: []string{"gemini-2.5-flash", "missing-model", "ag-custom-model", "gpt-5.5"},
},
},
})
@@ -329,7 +346,7 @@ func TestGatewayModels_CompositeCustomModelsListFiltersAcrossConcretePlatforms(t
var got gatewayModelsResponseForTest
require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &got))
require.Equal(t, []string{"gemini-2.5-flash", "gpt-5.5"}, modelIDsForTest(got.Data))
require.Equal(t, []string{"gemini-2.5-flash", "ag-custom-model", "gpt-5.5"}, modelIDsForTest(got.Data))
}
func TestGatewayModels_CustomModelsListKeepsConcreteModelAllowedByWildcardMapping(t *testing.T) {
@@ -549,7 +549,7 @@ func (h *GatewayHandler) GeminiV1BetaModels(c *gin.Context) {
LongContextMultiplier: 2.0, // 超出部分双倍计费
ForceCacheBilling: forceCacheBilling,
APIKeyService: h.apiKeyService,
ChannelUsageFields: channelMapping.ToUsageFields(reqModel, result.UpstreamModel),
ChannelUsageFields: clientRequestedUsageFields(c, channelMapping, reqModel, result.UpstreamModel),
}); err != nil {
logger.L().With(
zap.String("component", "handler.gemini_v1beta.models"),
@@ -232,7 +232,7 @@ func (h *OpenAIGatewayHandler) ChatCompletions(c *gin.Context) {
if service.GetOpsCyberPolicy(c) != nil {
cyberBlockKeyChat = service.CyberSessionBlockKey(apiKey.ID, c, body)
}
h.recordCyberPolicyIfMarked(c, apiKey, account, subscription, reqModel, err != nil, cyberBlockKeyChat, channelMapping.ToUsageFields(reqModel, ""), service.HashUsageRequestPayload(body))
h.recordCyberPolicyIfMarked(c, apiKey, account, subscription, reqModel, err != nil, cyberBlockKeyChat, clientRequestedUsageFields(c, channelMapping, reqModel, ""), service.HashUsageRequestPayload(body))
forwardDurationMs := time.Since(forwardStart).Milliseconds()
upstreamLatencyMs, _ := getContextInt64(c, service.OpsUpstreamLatencyMsKey)
@@ -355,7 +355,7 @@ func (h *OpenAIGatewayHandler) ChatCompletions(c *gin.Context) {
IPAddress: clientIP,
APIKeyService: h.apiKeyService,
QuotaPlatform: quotaPlatform,
ChannelUsageFields: channelMapping.ToUsageFields(reqModel, result.UpstreamModel),
ChannelUsageFields: clientRequestedUsageFields(c, channelMapping, reqModel, result.UpstreamModel),
CyberBlocked: cyberBlocked,
}); err != nil {
logger.L().With(
@@ -257,7 +257,7 @@ func (h *OpenAIGatewayHandler) Embeddings(c *gin.Context) {
IPAddress: clientIP,
APIKeyService: h.apiKeyService,
QuotaPlatform: quotaPlatform,
ChannelUsageFields: channelMapping.ToUsageFields(reqModel, result.UpstreamModel),
ChannelUsageFields: clientRequestedUsageFields(c, channelMapping, reqModel, result.UpstreamModel),
}); err != nil {
logger.L().With(
zap.String("component", "handler.openai_gateway.embeddings"),
@@ -490,7 +490,7 @@ func (h *OpenAIGatewayHandler) Responses(c *gin.Context) {
if service.GetOpsCyberPolicy(c) != nil {
cyberBlockKeyHTTP = service.CyberSessionBlockKey(apiKey.ID, c, sessionHashBody)
}
h.recordCyberPolicyIfMarked(c, apiKey, account, subscription, reqModel, err != nil, cyberBlockKeyHTTP, channelMapping.ToUsageFields(reqModel, ""), service.HashUsageRequestPayload(body))
h.recordCyberPolicyIfMarked(c, apiKey, account, subscription, reqModel, err != nil, cyberBlockKeyHTTP, clientRequestedUsageFields(c, channelMapping, reqModel, ""), service.HashUsageRequestPayload(body))
forwardDurationMs := time.Since(forwardStart).Milliseconds()
upstreamLatencyMs, _ := getContextInt64(c, service.OpsUpstreamLatencyMsKey)
responseLatencyMs := forwardDurationMs
@@ -640,7 +640,7 @@ func (h *OpenAIGatewayHandler) Responses(c *gin.Context) {
RequestPayloadHash: requestPayloadHash,
APIKeyService: h.apiKeyService,
QuotaPlatform: quotaPlatform,
ChannelUsageFields: channelMapping.ToUsageFields(reqModel, result.UpstreamModel),
ChannelUsageFields: clientRequestedUsageFields(c, channelMapping, reqModel, result.UpstreamModel),
CyberBlocked: cyberBlocked,
}); err != nil {
logger.L().With(
@@ -1027,7 +1027,7 @@ func (h *OpenAIGatewayHandler) Messages(c *gin.Context) {
if service.GetOpsCyberPolicy(c) != nil {
cyberBlockKeyMsg = service.CyberSessionBlockKey(apiKey.ID, c, body)
}
h.recordCyberPolicyIfMarked(c, apiKey, account, subscription, reqModel, err != nil, cyberBlockKeyMsg, channelMappingMsg.ToUsageFields(reqModel, ""), service.HashUsageRequestPayload(body))
h.recordCyberPolicyIfMarked(c, apiKey, account, subscription, reqModel, err != nil, cyberBlockKeyMsg, clientRequestedUsageFields(c, channelMappingMsg, reqModel, ""), service.HashUsageRequestPayload(body))
forwardDurationMs := time.Since(forwardStart).Milliseconds()
upstreamLatencyMs, _ := getContextInt64(c, service.OpsUpstreamLatencyMsKey)
responseLatencyMs := forwardDurationMs
@@ -1150,7 +1150,7 @@ func (h *OpenAIGatewayHandler) Messages(c *gin.Context) {
RequestPayloadHash: requestPayloadHash,
APIKeyService: h.apiKeyService,
QuotaPlatform: quotaPlatform,
ChannelUsageFields: channelMappingMsg.ToUsageFields(reqModel, result.UpstreamModel),
ChannelUsageFields: clientRequestedUsageFields(c, channelMappingMsg, reqModel, result.UpstreamModel),
CyberBlocked: cyberBlocked,
}); err != nil {
logger.L().With(
@@ -1828,7 +1828,7 @@ func (h *OpenAIGatewayHandler) ResponsesWebSocket(c *gin.Context) {
// 届时 defer 已清除标记)。
defer clearCyberPolicyTurnState(c)
releaseTurnSlots()
h.recordCyberPolicyIfMarked(c, apiKey, account, subscription, reqModel, turnErr != nil, cyberBlockKey, channelMappingWS.ToUsageFields(reqModel, ""), requestPayloadHash)
h.recordCyberPolicyIfMarked(c, apiKey, account, subscription, reqModel, turnErr != nil, cyberBlockKey, clientRequestedUsageFields(c, channelMappingWS, reqModel, ""), requestPayloadHash)
if service.GetOpsCyberPolicy(c) != nil {
cyberBlockedThisConn = true
}
@@ -1873,7 +1873,7 @@ func (h *OpenAIGatewayHandler) ResponsesWebSocket(c *gin.Context) {
RequestPayloadHash: requestPayloadHash,
APIKeyService: h.apiKeyService,
QuotaPlatform: quotaPlatform,
ChannelUsageFields: channelMappingWS.ToUsageFields(reqModel, result.UpstreamModel),
ChannelUsageFields: clientRequestedUsageFields(c, channelMappingWS, reqModel, result.UpstreamModel),
CyberBlocked: cyberBlocked,
}); err != nil {
reqLog.Error("openai.websocket_record_usage_failed",
@@ -2830,6 +2830,7 @@ func (h *OpenAIGatewayHandler) recordCyberPolicyIfMarked(c *gin.Context, apiKey
return
}
c.Set(cyberPolicyRecordedKey, true)
model = clientRequestedModel(c, model)
requestID := c.Writer.Header().Get("X-Request-Id")
var userID, apiKeyID int64
+14 -12
View File
@@ -75,13 +75,19 @@ func (h *OpenAIGatewayHandler) Images(c *gin.Context) {
}
requestModel := parsed.Model
ensureCompositeTargetPlatform(c, apiKey, requestModel)
clientRequestModel := clientRequestedModel(c, requestModel)
routingModel := requestModel
if resolvedModel, ok := service.ResolvedUpstreamModelFromContext(c.Request.Context()); ok {
routingModel = resolvedModel
}
if !compositeTargetPlatformAllowed(c, apiKey, requestModel, service.PlatformOpenAI) {
h.errorResponse(c, http.StatusBadRequest, "invalid_request_error", "Model is not supported by this OpenAI-compatible endpoint for composite groups")
return
}
reqLog = reqLog.With(
zap.String("model", requestModel),
zap.String("model", clientRequestModel),
zap.String("routing_model", routingModel),
zap.Bool("stream", parsed.Stream),
zap.Bool("multipart", parsed.Multipart),
zap.String("capability", string(parsed.RequiredCapability)),
@@ -103,14 +109,10 @@ func (h *OpenAIGatewayHandler) Images(c *gin.Context) {
defer imageReleaseFunc()
}
if parsed.Multipart {
setOpsRequestContext(c, requestModel, parsed.Stream)
} else {
setOpsRequestContext(c, requestModel, parsed.Stream)
}
setOpsRequestContext(c, clientRequestModel, parsed.Stream)
setOpsEndpointContext(c, "", int16(service.RequestTypeFromLegacy(parsed.Stream, false)))
channelMapping, _ := h.gatewayService.ResolveChannelMappingAndRestrict(c.Request.Context(), apiKey.GroupID, requestModel)
channelMapping, _ := h.gatewayService.ResolveChannelMappingAndRestrict(c.Request.Context(), apiKey.GroupID, routingModel)
if h.errorPassthroughService != nil {
service.BindErrorPassthroughService(c, h.errorPassthroughService)
@@ -158,7 +160,7 @@ func (h *OpenAIGatewayHandler) Images(c *gin.Context) {
requestCtx,
apiKey.GroupID,
sessionHash,
requestModel,
routingModel,
failedAccountIDs,
parsed.RequiredCapability,
)
@@ -172,7 +174,7 @@ func (h *OpenAIGatewayHandler) Images(c *gin.Context) {
zap.Int("excluded_account_count", len(failedAccountIDs)),
)
if len(failedAccountIDs) == 0 {
cls := classifyNoAccountErrorFromGin(c, h.gatewayService, apiKey, requestModel, requestModel, service.PlatformOpenAI)
cls := classifyNoAccountErrorFromGin(c, h.gatewayService, apiKey, clientRequestModel, routingModel, service.PlatformOpenAI)
if !cls.ModelNotFound {
markOpsRoutingCapacityLimitedIfNoAvailable(c, err)
}
@@ -191,7 +193,7 @@ func (h *OpenAIGatewayHandler) Images(c *gin.Context) {
return
}
if selection == nil || selection.Account == nil {
cls := classifyNoAccountErrorFromGin(c, h.gatewayService, apiKey, requestModel, requestModel, service.PlatformOpenAI)
cls := classifyNoAccountErrorFromGin(c, h.gatewayService, apiKey, clientRequestModel, routingModel, service.PlatformOpenAI)
if !cls.ModelNotFound {
markOpsRoutingCapacityLimited(c)
}
@@ -386,14 +388,14 @@ func (h *OpenAIGatewayHandler) Images(c *gin.Context) {
RequestPayloadHash: requestPayloadHash,
APIKeyService: h.apiKeyService,
QuotaPlatform: quotaPlatform,
ChannelUsageFields: channelMapping.ToUsageFields(requestModel, upstreamModel),
ChannelUsageFields: clientRequestedUsageFields(c, channelMapping, requestModel, upstreamModel),
}); err != nil {
logger.L().With(
zap.String("component", "handler.openai_gateway.images"),
zap.Int64("user_id", subject.UserID),
zap.Int64("api_key_id", apiKey.ID),
zap.Any("group_id", apiKey.GroupID),
zap.String("model", requestModel),
zap.String("model", clientRequestModel),
zap.Int64("account_id", account.ID),
).Error("openai.images.record_usage_failed", zap.Error(err))
}
@@ -1,8 +1,10 @@
package routes
import (
"bytes"
"context"
"io"
"mime/multipart"
"net/http"
"net/http/httptest"
"strings"
@@ -131,6 +133,67 @@ func TestCompositeTargetPlatformMiddlewareUsesExplicitRouteAndRewritesBody(t *te
require.Equal(t, http.StatusNoContent, w.Code)
}
func TestCompositeTargetPlatformMiddlewareUsesExplicitRouteForMultipartImages(t *testing.T) {
gin.SetMode(gin.TestMode)
router := gin.New()
resolver := service.NewCompositeRouteResolver(compositeRouteRepoStub{
routes: []service.CompositeModelRoute{
{
ID: 1,
GroupID: 1,
PublicModel: "image-alias",
MatchType: service.CompositeRouteMatchExact,
TargetPlatform: service.PlatformOpenAI,
UpstreamModel: "gpt-image-1",
Endpoint: service.CompositeRouteEndpointImages,
Priority: 100,
Enabled: true,
},
},
})
router.Use(gin.HandlerFunc(servermiddleware.APIKeyAuthMiddleware(func(c *gin.Context) {
groupID := int64(1)
c.Set(string(servermiddleware.ContextKeyAPIKey), &service.APIKey{
GroupID: &groupID,
Group: &service.Group{ID: groupID, Platform: service.PlatformComposite},
})
c.Next()
})))
router.Use(compositeTargetPlatformMiddleware(resolver))
router.POST("/v1/images/edits", func(c *gin.Context) {
platform, ok := service.ResolvedTargetPlatformFromContext(c.Request.Context())
require.True(t, ok)
require.Equal(t, service.PlatformOpenAI, platform)
upstreamModel, ok := service.ResolvedUpstreamModelFromContext(c.Request.Context())
require.True(t, ok)
require.Equal(t, "gpt-image-1", upstreamModel)
publicModel, ok := service.RequestedPublicModelFromContext(c.Request.Context())
require.True(t, ok)
require.Equal(t, "image-alias", publicModel)
body, err := io.ReadAll(c.Request.Body)
require.NoError(t, err)
require.Contains(t, string(body), "image-alias")
c.Status(http.StatusNoContent)
})
var body bytes.Buffer
writer := multipart.NewWriter(&body)
require.NoError(t, writer.WriteField("model", "image-alias"))
require.NoError(t, writer.WriteField("prompt", "draw"))
require.NoError(t, writer.Close())
req := httptest.NewRequest(http.MethodPost, "/v1/images/edits", bytes.NewReader(body.Bytes()))
req.Header.Set("Content-Type", writer.FormDataContentType())
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
require.Equal(t, http.StatusNoContent, w.Code)
}
func TestCompositeGeminiTargetPlatformMiddlewareUsesPathRoute(t *testing.T) {
gin.SetMode(gin.TestMode)
router := gin.New()
+39 -1
View File
@@ -4,6 +4,8 @@ import (
"bytes"
"errors"
"io"
"mime"
"mime/multipart"
"net/http"
"strconv"
"strings"
@@ -387,7 +389,7 @@ func compositeTargetPlatformMiddleware(resolver *service.CompositeRouteResolver)
return
}
model := strings.TrimSpace(gjson.GetBytes(body, "model").String())
model := compositeRequestModelFromBody(c.GetHeader("Content-Type"), body)
if model != "" {
decision, err := resolver.Resolve(c.Request.Context(), apiKey.Group.ID, model, compositeRouteEndpointForPath(c.Request.URL.Path))
if err != nil {
@@ -409,6 +411,42 @@ func compositeTargetPlatformMiddleware(resolver *service.CompositeRouteResolver)
}
}
func compositeRequestModelFromBody(contentType string, body []byte) string {
if model := strings.TrimSpace(gjson.GetBytes(body, "model").String()); model != "" {
return model
}
return compositeMultipartModelFromBody(contentType, body)
}
func compositeMultipartModelFromBody(contentType string, body []byte) string {
mediaType, params, err := mime.ParseMediaType(strings.TrimSpace(contentType))
if err != nil || !strings.EqualFold(mediaType, "multipart/form-data") {
return ""
}
boundary := strings.TrimSpace(params["boundary"])
if boundary == "" {
return ""
}
reader := multipart.NewReader(bytes.NewReader(body), boundary)
for {
part, err := reader.NextPart()
if errors.Is(err, io.EOF) {
return ""
}
if err != nil {
return ""
}
if part.FormName() != "model" || part.FileName() != "" {
continue
}
data, err := io.ReadAll(part)
if err != nil {
return ""
}
return strings.TrimSpace(string(data))
}
}
func compositeGeminiTargetPlatformMiddleware(resolver *service.CompositeRouteResolver) gin.HandlerFunc {
if resolver == nil {
resolver = service.NewCompositeRouteResolver(nil)