feat(config): add support for raw JSON payload rules

- Introduced `default-raw` and `override-raw` rules to handle raw JSON values.
- Enhanced `PayloadConfig` to validate and sanitize raw JSON payload rules.
- Updated executor logic to apply `default-raw` and `override-raw` rules.
- Extended example YAML to include usage of raw JSON rules.
This commit is contained in:
Luis Pater
2026-01-16 08:15:28 +08:00
parent 67f8732683
commit f571b1deb0
3 changed files with 148 additions and 1 deletions

View File

@@ -275,9 +275,21 @@ oauth-model-alias:
# protocol: "gemini" # restricts the rule to a specific protocol, options: openai, gemini, claude, codex # protocol: "gemini" # restricts the rule to a specific protocol, options: openai, gemini, claude, codex
# params: # JSON path (gjson/sjson syntax) -> value # params: # JSON path (gjson/sjson syntax) -> value
# "generationConfig.thinkingConfig.thinkingBudget": 32768 # "generationConfig.thinkingConfig.thinkingBudget": 32768
# default-raw: # Default raw rules set parameters using raw JSON when missing (must be valid JSON).
# - models:
# - name: "gemini-2.5-pro" # Supports wildcards (e.g., "gemini-*")
# protocol: "gemini" # restricts the rule to a specific protocol, options: openai, gemini, claude, codex
# params: # JSON path (gjson/sjson syntax) -> raw JSON value (strings are used as-is, must be valid JSON)
# "generationConfig.responseJsonSchema": "{\"type\":\"object\",\"properties\":{\"answer\":{\"type\":\"string\"}}}"
# override: # Override rules always set parameters, overwriting any existing values. # override: # Override rules always set parameters, overwriting any existing values.
# - models: # - models:
# - name: "gpt-*" # Supports wildcards (e.g., "gpt-*") # - name: "gpt-*" # Supports wildcards (e.g., "gpt-*")
# protocol: "codex" # restricts the rule to a specific protocol, options: openai, gemini, claude, codex # protocol: "codex" # restricts the rule to a specific protocol, options: openai, gemini, claude, codex
# params: # JSON path (gjson/sjson syntax) -> value # params: # JSON path (gjson/sjson syntax) -> value
# "reasoning.effort": "high" # "reasoning.effort": "high"
# override-raw: # Override raw rules always set parameters using raw JSON (must be valid JSON).
# - models:
# - name: "gpt-*" # Supports wildcards (e.g., "gpt-*")
# protocol: "codex" # restricts the rule to a specific protocol, options: openai, gemini, claude, codex
# params: # JSON path (gjson/sjson syntax) -> raw JSON value (strings are used as-is, must be valid JSON)
# "response_format": "{\"type\":\"json_schema\",\"json_schema\":{\"name\":\"answer\",\"schema\":{\"type\":\"object\"}}}"

View File

@@ -6,12 +6,14 @@ package config
import ( import (
"bytes" "bytes"
"encoding/json"
"errors" "errors"
"fmt" "fmt"
"os" "os"
"strings" "strings"
"syscall" "syscall"
log "github.com/sirupsen/logrus"
"golang.org/x/crypto/bcrypt" "golang.org/x/crypto/bcrypt"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
) )
@@ -216,8 +218,12 @@ type AmpUpstreamAPIKeyEntry struct {
type PayloadConfig struct { type PayloadConfig struct {
// Default defines rules that only set parameters when they are missing in the payload. // Default defines rules that only set parameters when they are missing in the payload.
Default []PayloadRule `yaml:"default" json:"default"` Default []PayloadRule `yaml:"default" json:"default"`
// DefaultRaw defines rules that set raw JSON values only when they are missing.
DefaultRaw []PayloadRule `yaml:"default-raw" json:"default-raw"`
// Override defines rules that always set parameters, overwriting any existing values. // Override defines rules that always set parameters, overwriting any existing values.
Override []PayloadRule `yaml:"override" json:"override"` Override []PayloadRule `yaml:"override" json:"override"`
// OverrideRaw defines rules that always set raw JSON values, overwriting any existing values.
OverrideRaw []PayloadRule `yaml:"override-raw" json:"override-raw"`
} }
// PayloadRule describes a single rule targeting a list of models with parameter updates. // PayloadRule describes a single rule targeting a list of models with parameter updates.
@@ -225,6 +231,7 @@ type PayloadRule struct {
// Models lists model entries with name pattern and protocol constraint. // Models lists model entries with name pattern and protocol constraint.
Models []PayloadModelRule `yaml:"models" json:"models"` Models []PayloadModelRule `yaml:"models" json:"models"`
// Params maps JSON paths (gjson/sjson syntax) to values written into the payload. // Params maps JSON paths (gjson/sjson syntax) to values written into the payload.
// For *-raw rules, values are treated as raw JSON fragments (strings are used as-is).
Params map[string]any `yaml:"params" json:"params"` Params map[string]any `yaml:"params" json:"params"`
} }
@@ -540,6 +547,9 @@ func LoadConfigOptional(configFile string, optional bool) (*Config, error) {
// Normalize global OAuth model name aliases. // Normalize global OAuth model name aliases.
cfg.SanitizeOAuthModelAlias() cfg.SanitizeOAuthModelAlias()
// Validate raw payload rules and drop invalid entries.
cfg.SanitizePayloadRules()
if cfg.legacyMigrationPending { if cfg.legacyMigrationPending {
fmt.Println("Detected legacy configuration keys, attempting to persist the normalized config...") fmt.Println("Detected legacy configuration keys, attempting to persist the normalized config...")
if !optional && configFile != "" { if !optional && configFile != "" {
@@ -556,6 +566,61 @@ func LoadConfigOptional(configFile string, optional bool) (*Config, error) {
return &cfg, nil return &cfg, nil
} }
// SanitizePayloadRules validates raw JSON payload rule params and drops invalid rules.
func (cfg *Config) SanitizePayloadRules() {
if cfg == nil {
return
}
cfg.Payload.DefaultRaw = sanitizePayloadRawRules(cfg.Payload.DefaultRaw, "default-raw")
cfg.Payload.OverrideRaw = sanitizePayloadRawRules(cfg.Payload.OverrideRaw, "override-raw")
}
func sanitizePayloadRawRules(rules []PayloadRule, section string) []PayloadRule {
if len(rules) == 0 {
return rules
}
out := make([]PayloadRule, 0, len(rules))
for i := range rules {
rule := rules[i]
if len(rule.Params) == 0 {
continue
}
invalid := false
for path, value := range rule.Params {
raw, ok := payloadRawString(value)
if !ok {
continue
}
trimmed := bytes.TrimSpace(raw)
if len(trimmed) == 0 || !json.Valid(trimmed) {
log.WithFields(log.Fields{
"section": section,
"rule_index": i + 1,
"param": path,
}).Warn("payload rule dropped: invalid raw JSON")
invalid = true
break
}
}
if invalid {
continue
}
out = append(out, rule)
}
return out
}
func payloadRawString(value any) ([]byte, bool) {
switch typed := value.(type) {
case string:
return []byte(typed), true
case []byte:
return typed, true
default:
return nil, false
}
}
// SanitizeOAuthModelAlias normalizes and deduplicates global OAuth model name aliases. // SanitizeOAuthModelAlias normalizes and deduplicates global OAuth model name aliases.
// It trims whitespace, normalizes channel keys to lower-case, drops empty entries, // It trims whitespace, normalizes channel keys to lower-case, drops empty entries,
// allows multiple aliases per upstream name, and ensures aliases are unique within each channel. // allows multiple aliases per upstream name, and ensures aliases are unique within each channel.

View File

@@ -1,6 +1,7 @@
package executor package executor
import ( import (
"encoding/json"
"strings" "strings"
"github.com/router-for-me/CLIProxyAPI/v6/internal/config" "github.com/router-for-me/CLIProxyAPI/v6/internal/config"
@@ -17,7 +18,7 @@ func applyPayloadConfigWithRoot(cfg *config.Config, model, protocol, root string
return payload return payload
} }
rules := cfg.Payload rules := cfg.Payload
if len(rules.Default) == 0 && len(rules.Override) == 0 { if len(rules.Default) == 0 && len(rules.DefaultRaw) == 0 && len(rules.Override) == 0 && len(rules.OverrideRaw) == 0 {
return payload return payload
} }
model = strings.TrimSpace(model) model = strings.TrimSpace(model)
@@ -55,6 +56,35 @@ func applyPayloadConfigWithRoot(cfg *config.Config, model, protocol, root string
appliedDefaults[fullPath] = struct{}{} appliedDefaults[fullPath] = struct{}{}
} }
} }
// Apply default raw rules: first write wins per field across all matching rules.
for i := range rules.DefaultRaw {
rule := &rules.DefaultRaw[i]
if !payloadRuleMatchesModel(rule, model, protocol) {
continue
}
for path, value := range rule.Params {
fullPath := buildPayloadPath(root, path)
if fullPath == "" {
continue
}
if gjson.GetBytes(source, fullPath).Exists() {
continue
}
if _, ok := appliedDefaults[fullPath]; ok {
continue
}
rawValue, ok := payloadRawValue(value)
if !ok {
continue
}
updated, errSet := sjson.SetRawBytes(out, fullPath, rawValue)
if errSet != nil {
continue
}
out = updated
appliedDefaults[fullPath] = struct{}{}
}
}
// Apply override rules: last write wins per field across all matching rules. // Apply override rules: last write wins per field across all matching rules.
for i := range rules.Override { for i := range rules.Override {
rule := &rules.Override[i] rule := &rules.Override[i]
@@ -73,6 +103,28 @@ func applyPayloadConfigWithRoot(cfg *config.Config, model, protocol, root string
out = updated out = updated
} }
} }
// Apply override raw rules: last write wins per field across all matching rules.
for i := range rules.OverrideRaw {
rule := &rules.OverrideRaw[i]
if !payloadRuleMatchesModel(rule, model, protocol) {
continue
}
for path, value := range rule.Params {
fullPath := buildPayloadPath(root, path)
if fullPath == "" {
continue
}
rawValue, ok := payloadRawValue(value)
if !ok {
continue
}
updated, errSet := sjson.SetRawBytes(out, fullPath, rawValue)
if errSet != nil {
continue
}
out = updated
}
}
return out return out
} }
@@ -116,6 +168,24 @@ func buildPayloadPath(root, path string) string {
return r + "." + p return r + "." + p
} }
func payloadRawValue(value any) ([]byte, bool) {
if value == nil {
return nil, false
}
switch typed := value.(type) {
case string:
return []byte(typed), true
case []byte:
return typed, true
default:
raw, errMarshal := json.Marshal(typed)
if errMarshal != nil {
return nil, false
}
return raw, true
}
}
// matchModelPattern performs simple wildcard matching where '*' matches zero or more characters. // matchModelPattern performs simple wildcard matching where '*' matches zero or more characters.
// Examples: // Examples:
// //