Files
CLIProxyAPI/internal/config/disable_image_generation_mode.go
Hao Wang 8d4a7f1f2e feat(config): add "passthrough" mode for disable-image-generation
Adds a fourth value for the disable-image-generation setting:

- false:       inject image_generation (unchanged)
- true:        strip everywhere + 404 on /v1/images/* (unchanged)
- chat:        strip on non-images endpoints, keep /v1/images/* (unchanged)
- passthrough: never inject and never strip on non-images endpoints
               (the client payload is forwarded unchanged); behaves like
               "chat" on /v1/images/* endpoints.

image_generation injection (codex executors) is already gated on the Off
mode, and the /v1/images/* 404 gate is already gated on the All mode, so
passthrough only required a change to the payload strip logic in
payload_helpers.go, now expressed via shouldStripImageGeneration().

Closes #3831

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 22:41:15 +08:00

148 lines
3.9 KiB
Go

package config
import (
"bytes"
"encoding/json"
"fmt"
"strings"
"gopkg.in/yaml.v3"
)
// 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 {
switch m {
case DisableImageGenerationOff:
return "false"
case DisableImageGenerationAll:
return "true"
case DisableImageGenerationChat:
return "chat"
case DisableImageGenerationPassthrough:
return "passthrough"
default:
return "false"
}
}
func (m DisableImageGenerationMode) MarshalYAML() (any, error) {
switch m {
case DisableImageGenerationAll:
return true, nil
case DisableImageGenerationChat:
return "chat", nil
case DisableImageGenerationPassthrough:
return "passthrough", nil
default:
return false, nil
}
}
func (m *DisableImageGenerationMode) UnmarshalYAML(value *yaml.Node) error {
mode, err := parseDisableImageGenerationNode(value)
if err != nil {
return err
}
*m = mode
return nil
}
func (m DisableImageGenerationMode) MarshalJSON() ([]byte, error) {
switch m {
case DisableImageGenerationAll:
return []byte("true"), nil
case DisableImageGenerationChat:
return json.Marshal("chat")
case DisableImageGenerationPassthrough:
return json.Marshal("passthrough")
default:
return []byte("false"), nil
}
}
func (m *DisableImageGenerationMode) UnmarshalJSON(data []byte) error {
mode, err := parseDisableImageGenerationJSON(data)
if err != nil {
return err
}
*m = mode
return nil
}
func parseDisableImageGenerationNode(value *yaml.Node) (DisableImageGenerationMode, error) {
if value == nil {
return DisableImageGenerationOff, nil
}
// First try a typed bool decode (covers unquoted true/false and YAML 1.1 bools).
var b bool
if err := value.Decode(&b); err == nil && value.Kind == yaml.ScalarNode && value.ShortTag() == "!!bool" {
if b {
return DisableImageGenerationAll, nil
}
return DisableImageGenerationOff, nil
}
// Fall back to string decoding (covers quoted "true"/"false" and "chat").
var s string
if err := value.Decode(&s); err != nil {
return DisableImageGenerationOff, fmt.Errorf("invalid disable-image-generation value")
}
return parseDisableImageGenerationString(s)
}
func parseDisableImageGenerationJSON(data []byte) (DisableImageGenerationMode, error) {
trimmed := bytes.TrimSpace(data)
if len(trimmed) == 0 || bytes.Equal(trimmed, []byte("null")) {
return DisableImageGenerationOff, nil
}
// bool
var b bool
if err := json.Unmarshal(trimmed, &b); err == nil {
if b {
return DisableImageGenerationAll, nil
}
return DisableImageGenerationOff, nil
}
// string
var s string
if err := json.Unmarshal(trimmed, &s); err != nil {
return DisableImageGenerationOff, fmt.Errorf("invalid disable-image-generation value")
}
return parseDisableImageGenerationString(s)
}
func parseDisableImageGenerationString(s string) (DisableImageGenerationMode, error) {
s = strings.TrimSpace(strings.ToLower(s))
switch s {
case "", "false", "0", "off", "no":
return DisableImageGenerationOff, nil
case "true", "1", "on", "yes":
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, passthrough)", s)
}
}