Merge pull request #3832 from haowang02/feat/disable-image-generation-passthrough

feat(config): add "passthrough" mode for disable-image-generation
This commit is contained in:
Luis Pater
2026-06-14 05:21:44 +08:00
committed by GitHub
6 changed files with 84 additions and 8 deletions

View File

@@ -116,9 +116,10 @@ max-retry-interval: 30
# When true, disable auth/model cooldown scheduling globally (prevents blackout windows after failure states).
disable-cooling: false
# disable-image-generation supports: false (default), true, or "chat".
# disable-image-generation supports: false (default), true, "chat", or "passthrough".
# - true: disable image_generation everywhere (also returns 404 for /v1/images/generations and /v1/images/edits).
# - "chat": disable image_generation injection on non-images endpoints, but keep /v1/images/generations and /v1/images/edits enabled.
# - "passthrough": never inject or strip image_generation on non-images endpoints (forward the client payload unchanged); behaves like "chat" on /v1/images/* endpoints.
disable-image-generation: false
# Base model used when proxying gpt-image-2 via the hosted image_generation tool (Responses API).

View File

@@ -9,18 +9,21 @@ import (
"gopkg.in/yaml.v3"
)
// DisableImageGenerationMode is a tri-state config value for disable-image-generation.
// DisableImageGenerationMode is a four-state config value for disable-image-generation.
//
// It supports:
// - false: enabled
// - true: disabled everywhere (including /v1/images/* endpoints)
// - "chat": disabled for all non-images endpoints, but enabled for /v1/images/generations and /v1/images/edits
// - "passthrough": never inject and never strip image_generation on non-images endpoints
// (the client payload is forwarded unchanged); on /v1/images/* endpoints behave like "chat"
type DisableImageGenerationMode int
const (
DisableImageGenerationOff DisableImageGenerationMode = iota
DisableImageGenerationAll
DisableImageGenerationChat
DisableImageGenerationPassthrough
)
func (m DisableImageGenerationMode) String() string {
@@ -31,6 +34,8 @@ func (m DisableImageGenerationMode) String() string {
return "true"
case DisableImageGenerationChat:
return "chat"
case DisableImageGenerationPassthrough:
return "passthrough"
default:
return "false"
}
@@ -42,6 +47,8 @@ func (m DisableImageGenerationMode) MarshalYAML() (any, error) {
return true, nil
case DisableImageGenerationChat:
return "chat", nil
case DisableImageGenerationPassthrough:
return "passthrough", nil
default:
return false, nil
}
@@ -62,6 +69,8 @@ func (m DisableImageGenerationMode) MarshalJSON() ([]byte, error) {
return []byte("true"), nil
case DisableImageGenerationChat:
return json.Marshal("chat")
case DisableImageGenerationPassthrough:
return json.Marshal("passthrough")
default:
return []byte("false"), nil
}
@@ -130,7 +139,9 @@ func parseDisableImageGenerationString(s string) (DisableImageGenerationMode, er
return DisableImageGenerationAll, nil
case "chat":
return DisableImageGenerationChat, nil
case "passthrough":
return DisableImageGenerationPassthrough, nil
default:
return DisableImageGenerationOff, fmt.Errorf("invalid disable-image-generation value %q (allowed: true, false, chat)", s)
return DisableImageGenerationOff, fmt.Errorf("invalid disable-image-generation value %q (allowed: true, false, chat, passthrough)", s)
}
}

View File

@@ -41,6 +41,16 @@ func TestDisableImageGenerationMode_UnmarshalYAML(t *testing.T) {
t.Fatalf("chat => %v, want %v", w.V, DisableImageGenerationChat)
}
}
{
var w wrapper
if err := yaml.Unmarshal([]byte("disable-image-generation: passthrough\n"), &w); err != nil {
t.Fatalf("unmarshal passthrough: %v", err)
}
if w.V != DisableImageGenerationPassthrough {
t.Fatalf("passthrough => %v, want %v", w.V, DisableImageGenerationPassthrough)
}
}
}
func TestDisableImageGenerationMode_UnmarshalJSON(t *testing.T) {
@@ -73,4 +83,14 @@ func TestDisableImageGenerationMode_UnmarshalJSON(t *testing.T) {
t.Fatalf("chat => %v, want %v", v, DisableImageGenerationChat)
}
}
{
var v DisableImageGenerationMode
if err := json.Unmarshal([]byte(`"passthrough"`), &v); err != nil {
t.Fatalf("unmarshal passthrough: %v", err)
}
if v != DisableImageGenerationPassthrough {
t.Fatalf("passthrough => %v, want %v", v, DisableImageGenerationPassthrough)
}
}
}

View File

@@ -17,6 +17,8 @@ type SDKConfig struct {
// and returns 404 for /v1/images/generations and /v1/images/edits.
// - "chat": disable image_generation injection for all non-images endpoints (e.g. /v1/responses, /v1/chat/completions),
// while keeping /v1/images/generations and /v1/images/edits enabled and preserving image_generation there.
// - "passthrough": do not modify the tool list on non-images endpoints — keep image_generation if the client
// sent it and do not inject it otherwise; on /v1/images/generations and /v1/images/edits behave like "chat".
DisableImageGeneration DisableImageGenerationMode `yaml:"disable-image-generation" json:"disable-image-generation"`
// GPTImage2BaseModel sets the base (mainline) model used when proxying GPT Image 2

View File

@@ -33,11 +33,9 @@ func ApplyPayloadConfigWithRequest(cfg *config.Config, model, protocol, fromProt
// Apply disable-image-generation filtering before payload rules so config payload
// overrides can explicitly re-enable image_generation when desired.
if cfg.DisableImageGeneration != config.DisableImageGenerationOff {
if cfg.DisableImageGeneration != config.DisableImageGenerationChat || !isImagesEndpointRequestPath(requestPath) {
out = removeToolTypeFromPayloadWithRoot(out, root, "image_generation")
out = removeToolChoiceFromPayloadWithRoot(out, root, "image_generation")
}
if shouldStripImageGeneration(cfg.DisableImageGeneration, requestPath) {
out = removeToolTypeFromPayloadWithRoot(out, root, "image_generation")
out = removeToolChoiceFromPayloadWithRoot(out, root, "image_generation")
}
rules := cfg.Payload
@@ -199,6 +197,23 @@ func isImagesEndpointRequestPath(path string) bool {
return false
}
// shouldStripImageGeneration reports whether the built-in image_generation tool must be
// removed from the outbound payload for the given mode and request path.
// - All: strip on every endpoint.
// - Chat: strip only on non-images endpoints; keep it on /v1/images/* endpoints.
// - Off / Passthrough: never strip. Off injects the tool elsewhere; Passthrough forwards
// the client payload untouched.
func shouldStripImageGeneration(mode config.DisableImageGenerationMode, requestPath string) bool {
switch mode {
case config.DisableImageGenerationAll:
return true
case config.DisableImageGenerationChat:
return !isImagesEndpointRequestPath(requestPath)
default:
return false
}
}
func payloadModelRulesMatch(rules []config.PayloadModelRule, protocol string, fromProtocol string, headers http.Header, payload []byte, root string, models []string) bool {
if len(rules) == 0 || len(models) == 0 {
return false

View File

@@ -97,6 +97,33 @@ func TestApplyPayloadConfigWithRoot_DisableImageGenerationChat_KeepsImageGenerat
}
}
func TestApplyPayloadConfigWithRoot_DisableImageGenerationPassthrough_KeepsPayloadUnchanged(t *testing.T) {
cfg := &config.Config{
SDKConfig: config.SDKConfig{DisableImageGeneration: config.DisableImageGenerationPassthrough},
}
payload := []byte(`{"tools":[{"type":"image_generation"},{"type":"function","name":"f1"}],"tool_choice":{"type":"image_generation"}}`)
// Passthrough must never inject or strip image_generation. The payload is forwarded as-is on
// non-images endpoints, and /v1/images/* endpoints behave like "chat" (also no removal).
for _, requestPath := range []string{"", "/v1/responses", "/v1/images/generations"} {
out := ApplyPayloadConfigWithRoot(cfg, "gpt-5.4", "openai-response", "", payload, nil, "", requestPath)
tools := gjson.GetBytes(out, "tools")
if !tools.Exists() || !tools.IsArray() {
t.Fatalf("path %q: expected tools array, got %v", requestPath, tools.Type)
}
if got := len(tools.Array()); got != 2 {
t.Fatalf("path %q: expected 2 tools (no removal), got %d", requestPath, got)
}
if got := tools.Array()[0].Get("type").String(); got != "image_generation" {
t.Fatalf("path %q: expected image_generation tool to be kept, got %q", requestPath, got)
}
if !gjson.GetBytes(out, "tool_choice").Exists() {
t.Fatalf("path %q: expected tool_choice to be kept", requestPath)
}
}
}
func TestApplyPayloadConfigWithRoot_DisableImageGeneration_PayloadOverrideCanRestoreImageGeneration(t *testing.T) {
cfg := &config.Config{
SDKConfig: config.SDKConfig{DisableImageGeneration: config.DisableImageGenerationAll},