Ctrl K
ComunidadGuías…

internal/config/sdk_config.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
// Package config provides configuration management for the CLI Proxy API server.
// It handles loading and parsing YAML configuration files, and provides structured
// access to application settings including server port, authentication directory,
// debug settings, proxy configuration, and API keys.
package config

import (
	"bytes"
	"encoding/json"
	"fmt"
	"math"
	"strconv"
	"strings"
	"time"

	"gopkg.in/yaml.v3"
)

// SDKConfig represents the application's configuration, loaded from a YAML file.
type SDKConfig struct {
	// MaxRequestBodyBytes limits the size (in bytes) of incoming HTTP request bodies.
	// Values <= 0 disable the limit (default).
	MaxRequestBodyBytes int64 `yaml:"max-request-body-bytes" json:"max-request-body-bytes"`

	// ResponseCompression controls negotiated compression of downstream HTTP responses.
	// It is configured under server.response-compression. Config.ServerResponseCompression
	// mirrors it for JSON and programmatic access; read the effective value via
	// Config.EffectiveResponseCompression.
	ResponseCompression ResponseCompressionConfig `yaml:"response-compression" json:"-"`

	// Client configures client-facing compatibility behavior.
	Client ClientConfig `yaml:"client" json:"client"`

	// OAuthOnlyFields records v8 provider settings that must wait for credential
	// selection and must not affect API-key credentials. Config YAML snapshots
	// preserve the corresponding v8 paths instead of serializing this metadata.
	OAuthOnlyFields map[string]bool `yaml:"-" json:"-"`

	// CodexResponseSteering mirrors the provider-wide runtime setting for API handlers.
	CodexResponseSteering bool `yaml:"-" json:"-"`

	// ProxyURL is the URL of an optional proxy server to use for outbound requests.
	ProxyURL string `yaml:"proxy-url" json:"proxy-url"`

	// TLSHandshakeTimeoutSeconds bounds upstream TLS handshakes made by the
	// shared uTLS client, in seconds. It applies to both the Claude Code
	// Anthropic transport and the Chrome ChatGPT/Codex transport; the
	// standard fallback transport is unaffected. It starts after TCP or
	// proxy connection establishment returns and does not bound dialing or
	// proxy CONNECT. Those uTLS paths use custom TLS dialing, for which Go
	// ignores http.Transport.TLSHandshakeTimeout, so a peer that accepts the
	// connection but never answers the ClientHello would otherwise block the
	// request indefinitely and request-retry could never engage. The bound
	// applies only during the TLS handshake and is removed once it succeeds,
	// so inference and long-running streams are unaffected. The default 0
	// disables the bound (previous behavior); negative values are treated as
	// 0, and values beyond the representable duration range saturate.
	TLSHandshakeTimeoutSeconds int `yaml:"tls-handshake-timeout-seconds" json:"tls-handshake-timeout-seconds"`

	// 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.
	//   - "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 by the legacy hosted
	// image_generation tool path when a Codex image request is not proxied directly
	// through the Image API.
	//
	// The value must start with "gpt-" (case-insensitive). If empty or invalid, the
	// default base model ("gpt-5.4-mini") is used.
	GPTImage2BaseModel string `yaml:"gpt-image-2-base-model,omitempty" json:"gpt-image-2-base-model,omitempty"`

	// VideoResultAuthCacheTTL controls how long video IDs stay pinned to the credential
	// that created them. Accepts duration strings like "30m" or "3h".
	// Empty or invalid values use the default 3h.
	VideoResultAuthCacheTTL string `yaml:"video-result-auth-cache-ttl,omitempty" json:"video-result-auth-cache-ttl,omitempty"`

	// ForceModelPrefix requires explicit model prefixes (e.g., "teamA/gemini-3-pro-preview")
	// to target prefixed credentials. When false, unprefixed model requests may use prefixed
	// credentials as well.
	ForceModelPrefix bool `yaml:"force-model-prefix" json:"force-model-prefix"`

	// RequestLog enables or disables detailed request logging functionality.
	RequestLog bool `yaml:"request-log" json:"request-log"`

	// CodexOrphanDelegationCompatibility mirrors the provider-wide runtime setting for API handlers.
	CodexOrphanDelegationCompatibility bool `yaml:"-" json:"-"`

	// ClaudeCode configures Claude Code compatibility behavior.
	ClaudeCode ClaudeCodeConfig `yaml:"claude-code" json:"claude-code"`

	// APIKeys is a list of keys for authenticating clients to this proxy server.
	APIKeys []string `yaml:"api-keys" json:"api-keys"`

	// PassthroughHeaders controls whether upstream response headers are forwarded to downstream clients.
	// Default is false (disabled).
	PassthroughHeaders bool `yaml:"passthrough-headers" json:"passthrough-headers"`

	// Streaming configures server-side streaming behavior (keep-alives and safe bootstrap retries).
	Streaming StreamingConfig `yaml:"streaming" json:"streaming"`

	// NonStreamKeepAliveInterval controls how often blank lines are emitted for non-streaming responses.
	// <= 0 disables keep-alives. Value is in seconds.
	NonStreamKeepAliveInterval int `yaml:"nonstream-keepalive-interval,omitempty" json:"nonstream-keepalive-interval,omitempty"`
}

// TLSHandshakeTimeout returns the configured upstream TLS handshake bound
// for requests using the shared uTLS client. A non-positive configured
// value disables the bound and yields 0.
func (c *SDKConfig) TLSHandshakeTimeout() time.Duration {
	if c == nil || c.TLSHandshakeTimeoutSeconds <= 0 {
		return 0
	}
	const maxTLSHandshakeTimeoutSeconds = math.MaxInt64 / int64(time.Second)
	if int64(c.TLSHandshakeTimeoutSeconds) > maxTLSHandshakeTimeoutSeconds {
		return time.Duration(math.MaxInt64)
	}
	return time.Duration(c.TLSHandshakeTimeoutSeconds) * time.Second
}

// ClientConfig configures client-facing compatibility behavior.
type ClientConfig struct {
	Codex  CodexClientConfig  `yaml:"codex" json:"codex"`
	Claude ClaudeClientConfig `yaml:"claude" json:"claude"`
}

// ClaudeClientConfig controls client-facing Claude protocol compatibility.
type ClaudeClientConfig struct {
	// ShowCodexCommentary mirrors public Codex progress into visible Claude text.
	// Default false preserves the existing hidden-commentary behavior.
	ShowCodexCommentary bool `yaml:"show-codex-commentary" json:"show-codex-commentary"`
}

// CodexClientConfig configures Codex client compatibility and the model catalog.
type CodexClientConfig struct {
	// OptimizeMultiAgentV2 optimizes official Codex multi-agent requests across providers.
	// Default false leaves the client's multi-agent behavior unchanged.
	OptimizeMultiAgentV2 bool `yaml:"optimize-multi-agent-v2" json:"optimize-multi-agent-v2"`

	// EnableApplyPatch advertises freeform apply_patch only for supported models.
	// Default false clears the capability regardless of template metadata.
	EnableApplyPatch bool `yaml:"enable-apply-patch" json:"enable-apply-patch"`

	// WebSearchResultCache configures caching of upstream web search results for
	// Codex Responses clients that drop search results from replayed history.
	WebSearchResultCache CodexWebSearchResultCacheConfig `yaml:"web-search-result-cache" json:"web-search-result-cache"`
}

const (
	// DefaultCodexWebSearchResultCacheTTL is the default lifetime of cached web search results.
	DefaultCodexWebSearchResultCacheTTL = 24 * time.Hour

	// DefaultCodexWebSearchResultCacheLimit is the default maximum number of cached web search result entries.
	DefaultCodexWebSearchResultCacheLimit = 1024

	maxCodexWebSearchResultCacheTTLSeconds = int64(math.MaxInt64 / time.Second)
)

// CodexWebSearchResultCacheConfig configures the session-scoped web search result cache.
type CodexWebSearchResultCacheConfig struct {
	// Enabled controls whether web search results are cached and restored for Codex Responses requests.
	// Default is false (disabled).
	Enabled bool `yaml:"enabled" json:"enabled"`

	// TTL specifies how long cached web search results remain valid (e.g. 1800, "30m", "24h").
	// Pure numbers are interpreted as seconds. Defaults to "24h" when unset.
	TTL string `yaml:"ttl" json:"ttl"`

	// Limit specifies the maximum number of entries retained in the web search result cache.
	// Defaults to 1024 when unset or zero.
	Limit int `yaml:"limit" json:"limit"`
}

// Validate checks that the configured TTL and entry limit are valid.
func (c CodexWebSearchResultCacheConfig) Validate() error {
	if _, err := parseCodexWebSearchResultCacheTTL(c.TTL); err != nil {
		return err
	}
	if c.Limit < 0 {
		return fmt.Errorf("client.codex.web-search-result-cache.limit must not be negative")
	}
	return nil
}

// EffectiveTTL returns the parsed cache TTL, or DefaultCodexWebSearchResultCacheTTL when unset or zero.
func (c CodexWebSearchResultCacheConfig) EffectiveTTL() time.Duration {
	d, err := parseCodexWebSearchResultCacheTTL(c.TTL)
	if err != nil || d <= 0 {
		return DefaultCodexWebSearchResultCacheTTL
	}
	return d
}

// EffectiveLimit returns the configured entry limit, or DefaultCodexWebSearchResultCacheLimit when unset or non-positive.
func (c CodexWebSearchResultCacheConfig) EffectiveLimit() int {
	if c.Limit > 0 {
		return c.Limit
	}
	return DefaultCodexWebSearchResultCacheLimit
}

func (c *CodexWebSearchResultCacheConfig) UnmarshalYAML(value *yaml.Node) error {
	if c == nil {
		return nil
	}
	if value == nil || value.Kind == 0 || value.Tag == "!!null" {
		*c = CodexWebSearchResultCacheConfig{}
		return nil
	}
	if value.Kind != yaml.MappingNode {
		return fmt.Errorf("client.codex.web-search-result-cache must be a mapping")
	}
	var next CodexWebSearchResultCacheConfig
	seen := make(map[string]bool, len(value.Content)/2)
	for i := 0; i+1 < len(value.Content); i += 2 {
		keyNode := value.Content[i]
		valNode := value.Content[i+1]
		if keyNode == nil || valNode == nil {
			continue
		}
		if seen[keyNode.Value] {
			return fmt.Errorf("client.codex.web-search-result-cache: duplicate field %q", keyNode.Value)
		}
		seen[keyNode.Value] = true
		switch keyNode.Value {
		case "enabled":
			if valNode.Tag == "!!null" {
				next.Enabled = false
				continue
			}
			if valNode.Kind != yaml.ScalarNode || valNode.Tag != "!!bool" {
				return fmt.Errorf("client.codex.web-search-result-cache.enabled must be a boolean")
			}
			var enabled bool
			if errDecode := valNode.Decode(&enabled); errDecode != nil {
				return fmt.Errorf("client.codex.web-search-result-cache.enabled: %w", errDecode)
			}
			next.Enabled = enabled
		case "ttl":
			if valNode.Tag == "!!null" {
				next.TTL = ""
				continue
			}
			if valNode.Kind != yaml.ScalarNode || (valNode.Tag != "!!str" && valNode.Tag != "!!int") {
				return fmt.Errorf("client.codex.web-search-result-cache.ttl must be a duration string or integer seconds")
			}
			next.TTL = strings.TrimSpace(valNode.Value)
		case "limit":
			if valNode.Tag == "!!null" {
				next.Limit = 0
				continue
			}
			if valNode.Kind != yaml.ScalarNode || valNode.Tag != "!!int" {
				return fmt.Errorf("client.codex.web-search-result-cache.limit must be an integer")
			}
			var limit int
			if errDecode := valNode.Decode(&limit); errDecode != nil {
				return fmt.Errorf("client.codex.web-search-result-cache.limit: %w", errDecode)
			}
			next.Limit = limit
		default:
			return fmt.Errorf("client.codex.web-search-result-cache: unknown field %q", keyNode.Value)
		}
	}
	if errValidate := next.Validate(); errValidate != nil {
		return errValidate
	}
	*c = next
	return nil
}

func (c *CodexWebSearchResultCacheConfig) UnmarshalJSON(data []byte) error {
	if c == nil {
		return nil
	}
	trimmed := bytes.TrimSpace(data)
	if len(trimmed) == 0 || bytes.Equal(trimmed, []byte("null")) {
		*c = CodexWebSearchResultCacheConfig{}
		return nil
	}
	var raw struct {
		Enabled bool            `json:"enabled"`
		TTL     json.RawMessage `json:"ttl"`
		Limit   int             `json:"limit"`
	}
	decoder := json.NewDecoder(bytes.NewReader(trimmed))
	decoder.DisallowUnknownFields()
	if errDecode := decoder.Decode(&raw); errDecode != nil {
		return errDecode
	}
	ttl, errTTL := parseCodexWebSearchResultCacheTTLJSON(raw.TTL)
	if errTTL != nil {
		return errTTL
	}
	next := CodexWebSearchResultCacheConfig{
		Enabled: raw.Enabled,
		TTL:     ttl,
		Limit:   raw.Limit,
	}
	if errValidate := next.Validate(); errValidate != nil {
		return errValidate
	}
	*c = next
	return nil
}

func parseCodexWebSearchResultCacheTTLJSON(raw json.RawMessage) (string, error) {
	trimmed := bytes.TrimSpace(raw)
	if len(trimmed) == 0 || bytes.Equal(trimmed, []byte("null")) {
		return "", nil
	}
	if trimmed[0] == '"' {
		var s string
		if errUnmarshal := json.Unmarshal(trimmed, &s); errUnmarshal != nil {
			return "", fmt.Errorf("client.codex.web-search-result-cache.ttl: %w", errUnmarshal)
		}
		return strings.TrimSpace(s), nil
	}
	var n int64
	if errUnmarshal := json.Unmarshal(trimmed, &n); errUnmarshal != nil {
		return "", fmt.Errorf("client.codex.web-search-result-cache.ttl must be a duration string or integer seconds")
	}
	return strconv.FormatInt(n, 10), nil
}

func parseCodexWebSearchResultCacheTTL(raw string) (time.Duration, error) {
	raw = strings.TrimSpace(raw)
	if raw == "" || raw == "0" {
		return DefaultCodexWebSearchResultCacheTTL, nil
	}
	if secs, errParseInt := strconv.ParseInt(raw, 10, 64); errParseInt == nil {
		if secs < 0 {
			return 0, fmt.Errorf("client.codex.web-search-result-cache.ttl must not be negative")
		}
		if secs > maxCodexWebSearchResultCacheTTLSeconds {
			return 0, fmt.Errorf("client.codex.web-search-result-cache.ttl overflows duration")
		}
		return time.Duration(secs) * time.Second, nil
	}
	d, errParseDuration := time.ParseDuration(raw)
	if errParseDuration != nil {
		return 0, fmt.Errorf("client.codex.web-search-result-cache.ttl %q is invalid: %w", raw, errParseDuration)
	}
	if d < 0 {
		return 0, fmt.Errorf("client.codex.web-search-result-cache.ttl must not be negative")
	}
	if d == 0 {
		return DefaultCodexWebSearchResultCacheTTL, nil
	}
	return d, nil
}

// ClaudeCodeConfig configures Claude Code compatibility behavior.
type ClaudeCodeConfig struct {
	// DisableCloakingModelList disables model ID cloaking in Anthropic model list responses.
	DisableCloakingModelList bool `yaml:"disable-cloaking-model-list" json:"disable-cloaking-model-list"`
}

// StreamingConfig holds server streaming behavior configuration.
type StreamingConfig struct {
	// KeepAliveSeconds controls how often the server emits SSE heartbeats (": keep-alive\n\n")
	// or WebSocket Ping control frames.
	// <= 0 disables keep-alives. Default is 0.
	KeepAliveSeconds int `yaml:"keepalive-seconds,omitempty" json:"keepalive-seconds,omitempty"`

	// BootstrapRetries controls how many times the server may retry a streaming request before any bytes are sent,
	// to allow auth rotation / transient recovery.
	// <= 0 disables bootstrap retries. Default is 0.
	BootstrapRetries int `yaml:"bootstrap-retries,omitempty" json:"bootstrap-retries,omitempty"`

	// SendKeepAliveBeforeFT controls whether SSE heartbeats are sent before the first token arrives
	// when KeepAliveSeconds > 0. Default is false.
	SendKeepAliveBeforeFT bool `yaml:"send-keepalive-before-ft,omitempty" json:"send-keepalive-before-ft,omitempty"`
}