feat: add tri-state support for disable-image-generation configuration

- Introduced `DisableImageGenerationMode` with support for `false`, `true`, and `chat` values.
- Updated payload handling to preserve `image_generation` on images endpoints when `chat` mode is enabled.
- Modified OpenAI image handlers (`ImagesGenerations`, `ImagesEdits`) to respect tri-state logic.
- Added unit tests for `DisableImageGenerationMode` behavior and endpoint-specific handling.
- Enhanced configuration diff logging to support `DisableImageGenerationMode`.
This commit is contained in:
Luis Pater
2026-04-30 11:59:50 +08:00
parent 46018417ad
commit f56a19e5b8
24 changed files with 398 additions and 54 deletions
@@ -16,7 +16,8 @@ import (
// and restricts matches to the given protocol when supplied. Defaults are checked
// against the original payload when provided. requestedModel carries the client-visible
// model name before alias resolution so payload rules can target aliases precisely.
func ApplyPayloadConfigWithRoot(cfg *config.Config, model, protocol, root string, payload, original []byte, requestedModel string) []byte {
// requestPath is the inbound HTTP request path (when available) used for endpoint-scoped gates.
func ApplyPayloadConfigWithRoot(cfg *config.Config, model, protocol, root string, payload, original []byte, requestedModel string, requestPath string) []byte {
if cfg == nil || len(payload) == 0 {
return payload
}
@@ -149,13 +150,34 @@ func ApplyPayloadConfigWithRoot(cfg *config.Config, model, protocol, root string
}
}
if cfg.DisableImageGeneration {
if cfg.DisableImageGeneration != config.DisableImageGenerationOff {
if cfg.DisableImageGeneration == config.DisableImageGenerationChat && isImagesEndpointRequestPath(requestPath) {
return out
}
out = removeToolTypeFromPayloadWithRoot(out, root, "image_generation")
out = removeToolChoiceFromPayloadWithRoot(out, root, "image_generation")
}
return out
}
func isImagesEndpointRequestPath(path string) bool {
path = strings.TrimSpace(path)
if path == "" {
return false
}
if path == "/v1/images/generations" || path == "/v1/images/edits" {
return true
}
// Be tolerant of prefix routers that may report a longer matched route.
if strings.HasSuffix(path, "/v1/images/generations") || strings.HasSuffix(path, "/v1/images/edits") {
return true
}
if strings.HasSuffix(path, "/images/generations") || strings.HasSuffix(path, "/images/edits") {
return true
}
return false
}
func payloadModelRulesMatch(rules []config.PayloadModelRule, protocol string, models []string) bool {
if len(rules) == 0 || len(models) == 0 {
return false
@@ -367,6 +389,24 @@ func PayloadRequestedModel(opts cliproxyexecutor.Options, fallback string) strin
}
}
func PayloadRequestPath(opts cliproxyexecutor.Options) string {
if len(opts.Metadata) == 0 {
return ""
}
raw, ok := opts.Metadata[cliproxyexecutor.RequestPathMetadataKey]
if !ok || raw == nil {
return ""
}
switch v := raw.(type) {
case string:
return strings.TrimSpace(v)
case []byte:
return strings.TrimSpace(string(v))
default:
return ""
}
}
// matchModelPattern performs simple wildcard matching where '*' matches zero or more characters.
// Examples:
//