mirror of
https://github.com/router-for-me/CLIProxyAPI.git
synced 2026-06-17 23:33:49 +08:00
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:
@@ -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).
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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},
|
||||
|
||||
Reference in New Issue
Block a user