mirror of
https://github.com/router-for-me/CLIProxyAPI.git
synced 2026-02-03 04:50:52 +08:00
Introduce `PayloadConfig` in the configuration to define default and override rules for modifying payload parameters. Implement `applyPayloadConfig` and `applyPayloadConfigWithRoot` to apply these rules across various executors, ensuring consistent parameter handling for different models and protocols. Update all relevant executors to utilize this functionality.
160 lines
3.9 KiB
Go
160 lines
3.9 KiB
Go
package executor
|
|
|
|
import (
|
|
"strings"
|
|
|
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
|
|
"github.com/tidwall/gjson"
|
|
"github.com/tidwall/sjson"
|
|
)
|
|
|
|
// applyPayloadConfig applies payload default and override rules from configuration
|
|
// to the given JSON payload for the specified model.
|
|
// Defaults only fill missing fields, while overrides always overwrite existing values.
|
|
func applyPayloadConfig(cfg *config.Config, model string, payload []byte) []byte {
|
|
return applyPayloadConfigWithRoot(cfg, model, "", "", payload)
|
|
}
|
|
|
|
// applyPayloadConfigWithRoot behaves like applyPayloadConfig but treats all parameter
|
|
// paths as relative to the provided root path (for example, "request" for Gemini CLI)
|
|
// and restricts matches to the given protocol when supplied.
|
|
func applyPayloadConfigWithRoot(cfg *config.Config, model, protocol, root string, payload []byte) []byte {
|
|
if cfg == nil || len(payload) == 0 {
|
|
return payload
|
|
}
|
|
rules := cfg.Payload
|
|
if len(rules.Default) == 0 && len(rules.Override) == 0 {
|
|
return payload
|
|
}
|
|
model = strings.TrimSpace(model)
|
|
if model == "" {
|
|
return payload
|
|
}
|
|
out := payload
|
|
// Apply default rules: first write wins per field across all matching rules.
|
|
for i := range rules.Default {
|
|
rule := &rules.Default[i]
|
|
if !payloadRuleMatchesModel(rule, model, protocol) {
|
|
continue
|
|
}
|
|
for path, value := range rule.Params {
|
|
fullPath := buildPayloadPath(root, path)
|
|
if fullPath == "" {
|
|
continue
|
|
}
|
|
if gjson.GetBytes(out, fullPath).Exists() {
|
|
continue
|
|
}
|
|
updated, errSet := sjson.SetBytes(out, fullPath, value)
|
|
if errSet != nil {
|
|
continue
|
|
}
|
|
out = updated
|
|
}
|
|
}
|
|
// Apply override rules: last write wins per field across all matching rules.
|
|
for i := range rules.Override {
|
|
rule := &rules.Override[i]
|
|
if !payloadRuleMatchesModel(rule, model, protocol) {
|
|
continue
|
|
}
|
|
for path, value := range rule.Params {
|
|
fullPath := buildPayloadPath(root, path)
|
|
if fullPath == "" {
|
|
continue
|
|
}
|
|
updated, errSet := sjson.SetBytes(out, fullPath, value)
|
|
if errSet != nil {
|
|
continue
|
|
}
|
|
out = updated
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
func payloadRuleMatchesModel(rule *config.PayloadRule, model, protocol string) bool {
|
|
if rule == nil {
|
|
return false
|
|
}
|
|
if len(rule.Models) == 0 {
|
|
return false
|
|
}
|
|
for _, entry := range rule.Models {
|
|
name := strings.TrimSpace(entry.Name)
|
|
if name == "" {
|
|
continue
|
|
}
|
|
if ep := strings.TrimSpace(entry.Protocol); ep != "" && protocol != "" && !strings.EqualFold(ep, protocol) {
|
|
continue
|
|
}
|
|
if matchModelPattern(name, model) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// buildPayloadPath combines an optional root path with a relative parameter path.
|
|
// When root is empty, the parameter path is used as-is. When root is non-empty,
|
|
// the parameter path is treated as relative to root.
|
|
func buildPayloadPath(root, path string) string {
|
|
r := strings.TrimSpace(root)
|
|
p := strings.TrimSpace(path)
|
|
if r == "" {
|
|
return p
|
|
}
|
|
if p == "" {
|
|
return r
|
|
}
|
|
if strings.HasPrefix(p, ".") {
|
|
p = p[1:]
|
|
}
|
|
return r + "." + p
|
|
}
|
|
|
|
// matchModelPattern performs simple wildcard matching where '*' matches zero or more characters.
|
|
// Examples:
|
|
//
|
|
// "*-5" matches "gpt-5"
|
|
// "gpt-*" matches "gpt-5" and "gpt-4"
|
|
// "gemini-*-pro" matches "gemini-2.5-pro" and "gemini-3-pro".
|
|
func matchModelPattern(pattern, model string) bool {
|
|
pattern = strings.TrimSpace(pattern)
|
|
model = strings.TrimSpace(model)
|
|
if pattern == "" {
|
|
return false
|
|
}
|
|
if pattern == "*" {
|
|
return true
|
|
}
|
|
// Iterative glob-style matcher supporting only '*' wildcard.
|
|
pi, si := 0, 0
|
|
starIdx := -1
|
|
matchIdx := 0
|
|
for si < len(model) {
|
|
if pi < len(pattern) && (pattern[pi] == model[si]) {
|
|
pi++
|
|
si++
|
|
continue
|
|
}
|
|
if pi < len(pattern) && pattern[pi] == '*' {
|
|
starIdx = pi
|
|
matchIdx = si
|
|
pi++
|
|
continue
|
|
}
|
|
if starIdx != -1 {
|
|
pi = starIdx + 1
|
|
matchIdx++
|
|
si = matchIdx
|
|
continue
|
|
}
|
|
return false
|
|
}
|
|
for pi < len(pattern) && pattern[pi] == '*' {
|
|
pi++
|
|
}
|
|
return pi == len(pattern)
|
|
}
|