@@ -3,6 +3,7 @@ package ctrl
33import (
44 "bytes"
55 "encoding/json"
6+ "fmt"
67 "io"
78 "net/http"
89 "net/url"
@@ -24,6 +25,19 @@ func (c *Ctrl) PrepareHTTPRequest(ctx *gin.Context, targetURL string, reqBody []
2425 return nil , errors .Wrap (err , "ensure stream options" )
2526 }
2627 reqBody = modifiedBody
28+
29+ // Enforce configured model to prevent users from requesting more expensive models
30+ // Pass user address from context for rate limiting
31+ userAddr , _ := ctx .Get ("userAddress" )
32+ userAddrStr , _ := userAddr .(string )
33+ modifiedBody , err = c .EnforceConfiguredModel (reqBody , userAddrStr )
34+ if err != nil {
35+ // Model validation failure is a user input error (similar to invalid token, bad request body)
36+ // Mark as expected error to prevent polluting error monitoring
37+ ctx .Set ("ignoreError" , true )
38+ return nil , errors .Wrap (err , "enforce configured model" )
39+ }
40+ reqBody = modifiedBody
2741 }
2842
2943 // For text-to-image requests, ensure wait=true query parameter is set
@@ -310,3 +324,85 @@ func (c *Ctrl) EnsureStreamOptions(body []byte) ([]byte, error) {
310324
311325 return modifiedBody , nil
312326}
327+
328+ // EnforceConfiguredModel ensures that requests use the configured model from the service config.
329+ // This prevents users from requesting more expensive models while paying for cheaper ones.
330+ //
331+ // Security rationale:
332+ // - Provider advertises a specific model in the service configuration
333+ // - Pricing is based on that specific model
334+ // - Allowing users to change the model could result in:
335+ // 1. Provider paying more to backend service than they charge users
336+ // 2. Users getting access to premium models at cheaper prices
337+ //
338+ // This function forcibly overwrites any "model" field in the request body with the
339+ // configured model from c.Service.ModelType.
340+ func (c * Ctrl ) EnforceConfiguredModel (body []byte , userAddr string ) ([]byte , error ) {
341+ // Return original body if empty (e.g., GET requests)
342+ if len (body ) == 0 {
343+ return body , nil
344+ }
345+
346+ // Return original body if no model is configured
347+ if c .Service .ModelType == "" {
348+ c .logger .Warnf ("Model enforcement skipped: c.Service.ModelType is empty (Type=%s)" , c .Service .Type )
349+ return body , nil
350+ }
351+
352+ // Debug log to verify configuration
353+ c .logger .Debugf ("EnforceConfiguredModel: Service.Type=%s, Service.ModelType=%s" ,
354+ c .Service .Type , c .Service .ModelType )
355+
356+ var bodyMap map [string ]interface {}
357+
358+ err := json .Unmarshal (body , & bodyMap )
359+ if err != nil {
360+ // Return original body for non-JSON requests
361+ return body , nil
362+ }
363+
364+ // Check if request contains a model field
365+ requestModel , hasModel := bodyMap ["model" ]
366+ if ! hasModel {
367+ // No model specified, add the configured model
368+ c .logger .Infof ("No model specified in request, adding configured model: %s" , c .Service .ModelType )
369+ bodyMap ["model" ] = c .Service .ModelType
370+ } else {
371+ // Model specified in request, check if it matches configured model
372+ requestModelStr , ok := requestModel .(string )
373+ if ! ok {
374+ // Invalid model type, reject request
375+ return nil , errors .New (fmt .Sprintf ("invalid model type in request (expected string), configured model is: %s" , c .Service .ModelType ))
376+ }
377+
378+ if requestModelStr != c .Service .ModelType {
379+ // Model mismatch detected - record in rate limiter and REJECT
380+ c .logger .Warnf ("Model mismatch detected and REJECTED: user=%s, requested=%s, configured=%s" ,
381+ userAddr , requestModelStr , c .Service .ModelType )
382+
383+ // Record this attempt in rate limiter if user address is available
384+ if userAddr != "" {
385+ rateLimiter := GetRateLimiter ()
386+ shouldBlock , blockedUntil := rateLimiter .RecordModelMismatch (userAddr )
387+ if shouldBlock {
388+ c .logger .Warnf ("User will be blocked due to excessive model mismatch: user=%s, blocked_until=%s" ,
389+ userAddr , blockedUntil .Format ("2006-01-02 15:04:05" ))
390+ }
391+ }
392+
393+ return nil , errors .New (fmt .Sprintf ("model not supported: requested '%s', only '%s' is available for this service" ,
394+ requestModelStr , c .Service .ModelType ))
395+ }
396+
397+ // Model matches - log for audit
398+ c .logger .Debugf ("Model validation passed: requested=%s matches configured=%s" , requestModelStr , c .Service .ModelType )
399+ }
400+
401+ // Marshal back to JSON
402+ modifiedBody , err := json .Marshal (bodyMap )
403+ if err != nil {
404+ return body , errors .Wrap (err , "failed to marshal modified JSON body after enforcing model" )
405+ }
406+
407+ return modifiedBody , nil
408+ }
0 commit comments