Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .ccs-fork-upstream.env
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
UPSTREAM_TAG=v6.9.43
UPSTREAM_COMMIT=e3e60f914ba82a6caa7a17a717f65a3b2f02285f
UPSTREAM_TAG=v6.9.45
UPSTREAM_COMMIT=6ba7c810a78c9afa88550a80b90c48b24e8b4852
5 changes: 3 additions & 2 deletions config.example.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -95,8 +95,9 @@ max-retry-interval: 30
# When true, disable auth/model cooldown scheduling globally (prevents blackout windows after failure states).
disable-cooling: false

# When true, disable the built-in image_generation tool globally.
# The server will stop injecting image_generation and will also remove it from request payload tools arrays.
# disable-image-generation supports: false (default), true, or "chat".
# - 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.
disable-image-generation: false

# Core auth auto-refresh worker pool size (OAuth/file-based auth token refresh).
Expand Down
2 changes: 1 addition & 1 deletion internal/api/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -1064,7 +1064,7 @@ func (s *Server) UpdateClients(cfg *config.Config) {
}

if oldCfg != nil && oldCfg.DisableImageGeneration != cfg.DisableImageGeneration {
log.Infof("disable-image-generation updated: %t -> %t", oldCfg.DisableImageGeneration, cfg.DisableImageGeneration)
log.Infof("disable-image-generation updated: %v -> %v", oldCfg.DisableImageGeneration, cfg.DisableImageGeneration)
}

applySignatureCacheConfig(oldCfg, cfg)
Expand Down
2 changes: 1 addition & 1 deletion internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -673,7 +673,7 @@ func LoadConfigOptional(configFile string, optional bool) (*Config, error) {
cfg.ErrorLogsMaxFiles = 10
cfg.UsageStatisticsEnabled = false
cfg.DisableCooling = false
cfg.DisableImageGeneration = false
cfg.DisableImageGeneration = DisableImageGenerationOff
cfg.Pprof.Enable = false
cfg.Pprof.Addr = DefaultPprofAddr
cfg.AmpCode.RestrictManagementToLocalhost = false // Default to false: API key auth is sufficient
Expand Down
136 changes: 136 additions & 0 deletions internal/config/disable_image_generation_mode.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
package config

import (
"bytes"
"encoding/json"
"fmt"
"strings"

"gopkg.in/yaml.v3"
)

// DisableImageGenerationMode is a tri-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
type DisableImageGenerationMode int

const (
DisableImageGenerationOff DisableImageGenerationMode = iota
DisableImageGenerationAll
DisableImageGenerationChat
)

func (m DisableImageGenerationMode) String() string {
switch m {
case DisableImageGenerationOff:
return "false"
case DisableImageGenerationAll:
return "true"
case DisableImageGenerationChat:
return "chat"
default:
return "false"
}
}

func (m DisableImageGenerationMode) MarshalYAML() (any, error) {
switch m {
case DisableImageGenerationAll:
return true, nil
case DisableImageGenerationChat:
return "chat", 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")
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
default:
return DisableImageGenerationOff, fmt.Errorf("invalid disable-image-generation value %q (allowed: true, false, chat)", s)
}
}
76 changes: 76 additions & 0 deletions internal/config/disable_image_generation_mode_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package config

import (
"encoding/json"
"testing"

"gopkg.in/yaml.v3"
)

func TestDisableImageGenerationMode_UnmarshalYAML(t *testing.T) {
type wrapper struct {
V DisableImageGenerationMode `yaml:"disable-image-generation"`
}

{
var w wrapper
if err := yaml.Unmarshal([]byte("disable-image-generation: false\n"), &w); err != nil {
t.Fatalf("unmarshal false: %v", err)
}
if w.V != DisableImageGenerationOff {
t.Fatalf("false => %v, want %v", w.V, DisableImageGenerationOff)
}
}

{
var w wrapper
if err := yaml.Unmarshal([]byte("disable-image-generation: true\n"), &w); err != nil {
t.Fatalf("unmarshal true: %v", err)
}
if w.V != DisableImageGenerationAll {
t.Fatalf("true => %v, want %v", w.V, DisableImageGenerationAll)
}
}

{
var w wrapper
if err := yaml.Unmarshal([]byte("disable-image-generation: chat\n"), &w); err != nil {
t.Fatalf("unmarshal chat: %v", err)
}
if w.V != DisableImageGenerationChat {
t.Fatalf("chat => %v, want %v", w.V, DisableImageGenerationChat)
}
}
}

func TestDisableImageGenerationMode_UnmarshalJSON(t *testing.T) {
{
var v DisableImageGenerationMode
if err := json.Unmarshal([]byte("false"), &v); err != nil {
t.Fatalf("unmarshal false: %v", err)
}
if v != DisableImageGenerationOff {
t.Fatalf("false => %v, want %v", v, DisableImageGenerationOff)
}
}

{
var v DisableImageGenerationMode
if err := json.Unmarshal([]byte("true"), &v); err != nil {
t.Fatalf("unmarshal true: %v", err)
}
if v != DisableImageGenerationAll {
t.Fatalf("true => %v, want %v", v, DisableImageGenerationAll)
}
}

{
var v DisableImageGenerationMode
if err := json.Unmarshal([]byte(`"chat"`), &v); err != nil {
t.Fatalf("unmarshal chat: %v", err)
}
if v != DisableImageGenerationChat {
t.Fatalf("chat => %v, want %v", v, DisableImageGenerationChat)
}
}
}
14 changes: 9 additions & 5 deletions internal/config/sdk_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,15 @@ type SDKConfig struct {
// ProxyURL is the URL of an optional proxy server to use for outbound requests.
ProxyURL string `yaml:"proxy-url" json:"proxy-url"`

// DisableImageGeneration disables the built-in image_generation tool when true.
// When enabled, the server will avoid injecting image_generation into request payloads,
// will remove any existing image_generation tool entries from tools arrays, and will
// return 404 for /v1/images/generations and /v1/images/edits.
DisableImageGeneration bool `yaml:"disable-image-generation" json:"disable-image-generation"`
// DisableImageGeneration controls whether the built-in image_generation tool is injected/allowed.
//
// Supported values:
// - false (default): image_generation is enabled everywhere (normal behavior).
// - true: image_generation is disabled everywhere. The server stops injecting it, removes it from request payloads,
// 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.
DisableImageGeneration DisableImageGenerationMode `yaml:"disable-image-generation" json:"disable-image-generation"`

// EnableGeminiCLIEndpoint controls whether Gemini CLI internal endpoints (/v1internal:*) are enabled.
// Default is false for safety; when false, /v1internal:* requests are rejected.
Expand Down
3 changes: 2 additions & 1 deletion internal/runtime/executor/aistudio_executor.go
Original file line number Diff line number Diff line change
Expand Up @@ -428,7 +428,8 @@ func (e *AIStudioExecutor) translateRequest(req cliproxyexecutor.Request, opts c
}
payload = fixGeminiImageAspectRatio(baseModel, payload)
requestedModel := helps.PayloadRequestedModel(opts, req.Model)
payload = helps.ApplyPayloadConfigWithRoot(e.cfg, baseModel, to.String(), "", payload, originalTranslated, requestedModel)
requestPath := helps.PayloadRequestPath(opts)
payload = helps.ApplyPayloadConfigWithRoot(e.cfg, baseModel, to.String(), "", payload, originalTranslated, requestedModel, requestPath)
payload, _ = sjson.DeleteBytes(payload, "generationConfig.maxOutputTokens")
payload, _ = sjson.DeleteBytes(payload, "generationConfig.responseMimeType")
payload, _ = sjson.DeleteBytes(payload, "generationConfig.responseJsonSchema")
Expand Down
9 changes: 6 additions & 3 deletions internal/runtime/executor/antigravity_executor.go
Original file line number Diff line number Diff line change
Expand Up @@ -522,7 +522,8 @@ func (e *AntigravityExecutor) Execute(ctx context.Context, auth *cliproxyauth.Au
}

requestedModel := helps.PayloadRequestedModel(opts, req.Model)
translated = helps.ApplyPayloadConfigWithRoot(e.cfg, baseModel, "antigravity", "request", translated, originalTranslated, requestedModel)
requestPath := helps.PayloadRequestPath(opts)
translated = helps.ApplyPayloadConfigWithRoot(e.cfg, baseModel, "antigravity", "request", translated, originalTranslated, requestedModel, requestPath)

useCredits := cliproxyauth.AntigravityCreditsRequested(ctx) && antigravityCreditsRetryEnabled(e.cfg)

Expand Down Expand Up @@ -719,7 +720,8 @@ func (e *AntigravityExecutor) executeClaudeNonStream(ctx context.Context, auth *
}

requestedModel := helps.PayloadRequestedModel(opts, req.Model)
translated = helps.ApplyPayloadConfigWithRoot(e.cfg, baseModel, "antigravity", "request", translated, originalTranslated, requestedModel)
requestPath := helps.PayloadRequestPath(opts)
translated = helps.ApplyPayloadConfigWithRoot(e.cfg, baseModel, "antigravity", "request", translated, originalTranslated, requestedModel, requestPath)

useCredits := cliproxyauth.AntigravityCreditsRequested(ctx) && antigravityCreditsRetryEnabled(e.cfg)

Expand Down Expand Up @@ -1179,7 +1181,8 @@ func (e *AntigravityExecutor) ExecuteStream(ctx context.Context, auth *cliproxya
}

requestedModel := helps.PayloadRequestedModel(opts, req.Model)
translated = helps.ApplyPayloadConfigWithRoot(e.cfg, baseModel, "antigravity", "request", translated, originalTranslated, requestedModel)
requestPath := helps.PayloadRequestPath(opts)
translated = helps.ApplyPayloadConfigWithRoot(e.cfg, baseModel, "antigravity", "request", translated, originalTranslated, requestedModel, requestPath)

useCredits := cliproxyauth.AntigravityCreditsRequested(ctx) && antigravityCreditsRetryEnabled(e.cfg)

Expand Down
6 changes: 4 additions & 2 deletions internal/runtime/executor/claude_executor.go
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,8 @@ func (e *ClaudeExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, r
body = applyCloaking(ctx, e.cfg, auth, body, baseModel, apiKey)

requestedModel := helps.PayloadRequestedModel(opts, req.Model)
body = helps.ApplyPayloadConfigWithRoot(e.cfg, baseModel, to.String(), "", body, originalTranslated, requestedModel)
requestPath := helps.PayloadRequestPath(opts)
body = helps.ApplyPayloadConfigWithRoot(e.cfg, baseModel, to.String(), "", body, originalTranslated, requestedModel, requestPath)
body = ensureModelMaxTokens(body, baseModel)

// Disable thinking if tool_choice forces tool use (Anthropic API constraint)
Expand Down Expand Up @@ -349,7 +350,8 @@ func (e *ClaudeExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.A
body = applyCloaking(ctx, e.cfg, auth, body, baseModel, apiKey)

requestedModel := helps.PayloadRequestedModel(opts, req.Model)
body = helps.ApplyPayloadConfigWithRoot(e.cfg, baseModel, to.String(), "", body, originalTranslated, requestedModel)
requestPath := helps.PayloadRequestPath(opts)
body = helps.ApplyPayloadConfigWithRoot(e.cfg, baseModel, to.String(), "", body, originalTranslated, requestedModel, requestPath)
body = ensureModelMaxTokens(body, baseModel)

// Disable thinking if tool_choice forces tool use (Anthropic API constraint)
Expand Down
15 changes: 9 additions & 6 deletions internal/runtime/executor/codex_executor.go
Original file line number Diff line number Diff line change
Expand Up @@ -173,15 +173,16 @@ func (e *CodexExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, re
}

requestedModel := helps.PayloadRequestedModel(opts, req.Model)
body = helps.ApplyPayloadConfigWithRoot(e.cfg, baseModel, to.String(), "", body, originalTranslated, requestedModel)
requestPath := helps.PayloadRequestPath(opts)
body = helps.ApplyPayloadConfigWithRoot(e.cfg, baseModel, to.String(), "", body, originalTranslated, requestedModel, requestPath)
body, _ = sjson.SetBytes(body, "model", baseModel)
body, _ = sjson.SetBytes(body, "stream", true)
body, _ = sjson.DeleteBytes(body, "previous_response_id")
body, _ = sjson.DeleteBytes(body, "prompt_cache_retention")
body, _ = sjson.DeleteBytes(body, "safety_identifier")
body, _ = sjson.DeleteBytes(body, "stream_options")
body = normalizeCodexInstructions(body)
if e.cfg == nil || !e.cfg.DisableImageGeneration {
if e.cfg == nil || e.cfg.DisableImageGeneration == config.DisableImageGenerationOff {
body = ensureImageGenerationTool(body, baseModel, auth)
}

Expand Down Expand Up @@ -327,11 +328,12 @@ func (e *CodexExecutor) executeCompact(ctx context.Context, auth *cliproxyauth.A
}

requestedModel := helps.PayloadRequestedModel(opts, req.Model)
body = helps.ApplyPayloadConfigWithRoot(e.cfg, baseModel, to.String(), "", body, originalTranslated, requestedModel)
requestPath := helps.PayloadRequestPath(opts)
body = helps.ApplyPayloadConfigWithRoot(e.cfg, baseModel, to.String(), "", body, originalTranslated, requestedModel, requestPath)
body, _ = sjson.SetBytes(body, "model", baseModel)
body, _ = sjson.DeleteBytes(body, "stream")
body = normalizeCodexInstructions(body)
if e.cfg == nil || !e.cfg.DisableImageGeneration {
if e.cfg == nil || e.cfg.DisableImageGeneration == config.DisableImageGenerationOff {
body = ensureImageGenerationTool(body, baseModel, auth)
}

Expand Down Expand Up @@ -421,14 +423,15 @@ func (e *CodexExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Au
}

requestedModel := helps.PayloadRequestedModel(opts, req.Model)
body = helps.ApplyPayloadConfigWithRoot(e.cfg, baseModel, to.String(), "", body, originalTranslated, requestedModel)
requestPath := helps.PayloadRequestPath(opts)
body = helps.ApplyPayloadConfigWithRoot(e.cfg, baseModel, to.String(), "", body, originalTranslated, requestedModel, requestPath)
body, _ = sjson.DeleteBytes(body, "previous_response_id")
body, _ = sjson.DeleteBytes(body, "prompt_cache_retention")
body, _ = sjson.DeleteBytes(body, "safety_identifier")
body, _ = sjson.DeleteBytes(body, "stream_options")
body, _ = sjson.SetBytes(body, "model", baseModel)
body = normalizeCodexInstructions(body)
if e.cfg == nil || !e.cfg.DisableImageGeneration {
if e.cfg == nil || e.cfg.DisableImageGeneration == config.DisableImageGenerationOff {
body = ensureImageGenerationTool(body, baseModel, auth)
}

Expand Down
Loading
Loading