feat(runtime): enhance payload rule resolution with dynamic path support
- Introduced `resolvePayloadRulePaths` function to dynamically resolve rule paths supporting array queries and complex logic. - Updated payload processing logic (`apply defaults`, `overrides`, `filters`) to handle resolved paths for better flexibility. - Added helper functions for path parsing, query matching, and logical resolution to improve modularity and reusability. - Introduced payload condition match logic, including `match`, `not-match`, `exist`, and `not-exist` rules in `PayloadConfig`. - Enhanced `payloadModelRulesMatch` function to support conditional checks at various levels. - Added helper methods for evaluating JSON path conditions and values. - Updated tests to validate new conditional rules against different payload scenarios.
This commit is contained in:
@@ -407,6 +407,17 @@ nonstream-keepalive-interval: 0
|
|||||||
# - models:
|
# - models:
|
||||||
# - name: "gemini-2.5-pro" # Supports wildcards (e.g., "gemini-*")
|
# - name: "gemini-2.5-pro" # Supports wildcards (e.g., "gemini-*")
|
||||||
# protocol: "gemini" # restricts the rule to a specific protocol, options: openai, gemini, claude, codex, antigravity
|
# protocol: "gemini" # restricts the rule to a specific protocol, options: openai, gemini, claude, codex, antigravity
|
||||||
|
# form-protocol: "responses" # restricts the rule to the source protocol, options: openai, responses, gemini, claude
|
||||||
|
# headers: # all configured request headers must match; values support "*" wildcards
|
||||||
|
# X-Client-Tier: "tenant-*-region-*"
|
||||||
|
# match: # all payload JSON paths must equal the configured values
|
||||||
|
# - "metadata.client": "codex"
|
||||||
|
# not-match: # payload JSON paths must not equal the configured values
|
||||||
|
# - "metadata.mode": "dev"
|
||||||
|
# exist: # all payload JSON paths must exist and not be null
|
||||||
|
# - "tools.#(type==\"web_search\").type"
|
||||||
|
# not-exist: # all payload JSON paths must be missing or null
|
||||||
|
# - "metadata.disable_payload"
|
||||||
# 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).
|
# default-raw: # Default raw rules set parameters using raw JSON when missing (must be valid JSON).
|
||||||
|
|||||||
@@ -344,6 +344,18 @@ type PayloadModelRule struct {
|
|||||||
Name string `yaml:"name" json:"name"`
|
Name string `yaml:"name" json:"name"`
|
||||||
// Protocol restricts the rule to a specific translator format (e.g., "gemini", "responses").
|
// Protocol restricts the rule to a specific translator format (e.g., "gemini", "responses").
|
||||||
Protocol string `yaml:"protocol" json:"protocol"`
|
Protocol string `yaml:"protocol" json:"protocol"`
|
||||||
|
// Headers restricts the rule to requests whose headers match all configured wildcard patterns.
|
||||||
|
Headers map[string]string `yaml:"headers" json:"headers"`
|
||||||
|
// FormProtocol restricts the rule to a specific source protocol (e.g., "gemini", "responses").
|
||||||
|
FormProtocol string `yaml:"form-protocol" json:"form-protocol"`
|
||||||
|
// Match requires payload JSON paths to equal the configured values.
|
||||||
|
Match []map[string]any `yaml:"match" json:"match"`
|
||||||
|
// NotMatch requires payload JSON paths to not equal the configured values.
|
||||||
|
NotMatch []map[string]any `yaml:"not-match" json:"not-match"`
|
||||||
|
// Exist requires payload JSON paths to exist and not be null.
|
||||||
|
Exist []string `yaml:"exist" json:"exist"`
|
||||||
|
// NotExist requires payload JSON paths to be missing or null.
|
||||||
|
NotExist []string `yaml:"not-exist" json:"not-exist"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// CloakConfig configures request cloaking for non-Claude-Code clients.
|
// CloakConfig configures request cloaking for non-Claude-Code clients.
|
||||||
|
|||||||
@@ -446,7 +446,7 @@ func (e *AIStudioExecutor) translateRequest(req cliproxyexecutor.Request, opts c
|
|||||||
payload = fixGeminiImageAspectRatio(baseModel, payload)
|
payload = fixGeminiImageAspectRatio(baseModel, payload)
|
||||||
requestedModel := helps.PayloadRequestedModel(opts, req.Model)
|
requestedModel := helps.PayloadRequestedModel(opts, req.Model)
|
||||||
requestPath := helps.PayloadRequestPath(opts)
|
requestPath := helps.PayloadRequestPath(opts)
|
||||||
payload = helps.ApplyPayloadConfigWithRoot(e.cfg, baseModel, to.String(), "", payload, originalTranslated, requestedModel, requestPath)
|
payload = helps.ApplyPayloadConfigWithRequest(e.cfg, baseModel, to.String(), from.String(), "", payload, originalTranslated, requestedModel, requestPath, opts.Headers)
|
||||||
payload, _ = sjson.DeleteBytes(payload, "generationConfig.maxOutputTokens")
|
payload, _ = sjson.DeleteBytes(payload, "generationConfig.maxOutputTokens")
|
||||||
payload, _ = sjson.DeleteBytes(payload, "generationConfig.responseMimeType")
|
payload, _ = sjson.DeleteBytes(payload, "generationConfig.responseMimeType")
|
||||||
payload, _ = sjson.DeleteBytes(payload, "generationConfig.responseJsonSchema")
|
payload, _ = sjson.DeleteBytes(payload, "generationConfig.responseJsonSchema")
|
||||||
|
|||||||
@@ -522,7 +522,7 @@ func (e *AntigravityExecutor) Execute(ctx context.Context, auth *cliproxyauth.Au
|
|||||||
|
|
||||||
requestedModel := helps.PayloadRequestedModel(opts, req.Model)
|
requestedModel := helps.PayloadRequestedModel(opts, req.Model)
|
||||||
requestPath := helps.PayloadRequestPath(opts)
|
requestPath := helps.PayloadRequestPath(opts)
|
||||||
translated = helps.ApplyPayloadConfigWithRoot(e.cfg, baseModel, "antigravity", "request", translated, originalTranslated, requestedModel, requestPath)
|
translated = helps.ApplyPayloadConfigWithRequest(e.cfg, baseModel, "antigravity", from.String(), "request", translated, originalTranslated, requestedModel, requestPath, opts.Headers)
|
||||||
|
|
||||||
useCredits := cliproxyauth.AntigravityCreditsRequested(ctx) && antigravityCreditsRetryEnabled(e.cfg)
|
useCredits := cliproxyauth.AntigravityCreditsRequested(ctx) && antigravityCreditsRetryEnabled(e.cfg)
|
||||||
|
|
||||||
@@ -720,7 +720,7 @@ func (e *AntigravityExecutor) executeClaudeNonStream(ctx context.Context, auth *
|
|||||||
|
|
||||||
requestedModel := helps.PayloadRequestedModel(opts, req.Model)
|
requestedModel := helps.PayloadRequestedModel(opts, req.Model)
|
||||||
requestPath := helps.PayloadRequestPath(opts)
|
requestPath := helps.PayloadRequestPath(opts)
|
||||||
translated = helps.ApplyPayloadConfigWithRoot(e.cfg, baseModel, "antigravity", "request", translated, originalTranslated, requestedModel, requestPath)
|
translated = helps.ApplyPayloadConfigWithRequest(e.cfg, baseModel, "antigravity", from.String(), "request", translated, originalTranslated, requestedModel, requestPath, opts.Headers)
|
||||||
|
|
||||||
useCredits := cliproxyauth.AntigravityCreditsRequested(ctx) && antigravityCreditsRetryEnabled(e.cfg)
|
useCredits := cliproxyauth.AntigravityCreditsRequested(ctx) && antigravityCreditsRetryEnabled(e.cfg)
|
||||||
|
|
||||||
@@ -1181,7 +1181,7 @@ func (e *AntigravityExecutor) ExecuteStream(ctx context.Context, auth *cliproxya
|
|||||||
|
|
||||||
requestedModel := helps.PayloadRequestedModel(opts, req.Model)
|
requestedModel := helps.PayloadRequestedModel(opts, req.Model)
|
||||||
requestPath := helps.PayloadRequestPath(opts)
|
requestPath := helps.PayloadRequestPath(opts)
|
||||||
translated = helps.ApplyPayloadConfigWithRoot(e.cfg, baseModel, "antigravity", "request", translated, originalTranslated, requestedModel, requestPath)
|
translated = helps.ApplyPayloadConfigWithRequest(e.cfg, baseModel, "antigravity", from.String(), "request", translated, originalTranslated, requestedModel, requestPath, opts.Headers)
|
||||||
|
|
||||||
useCredits := cliproxyauth.AntigravityCreditsRequested(ctx) && antigravityCreditsRetryEnabled(e.cfg)
|
useCredits := cliproxyauth.AntigravityCreditsRequested(ctx) && antigravityCreditsRetryEnabled(e.cfg)
|
||||||
|
|
||||||
|
|||||||
@@ -164,7 +164,7 @@ func (e *ClaudeExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, r
|
|||||||
|
|
||||||
requestedModel := helps.PayloadRequestedModel(opts, req.Model)
|
requestedModel := helps.PayloadRequestedModel(opts, req.Model)
|
||||||
requestPath := helps.PayloadRequestPath(opts)
|
requestPath := helps.PayloadRequestPath(opts)
|
||||||
body = helps.ApplyPayloadConfigWithRoot(e.cfg, baseModel, to.String(), "", body, originalTranslated, requestedModel, requestPath)
|
body = helps.ApplyPayloadConfigWithRequest(e.cfg, baseModel, to.String(), from.String(), "", body, originalTranslated, requestedModel, requestPath, opts.Headers)
|
||||||
body = ensureModelMaxTokens(body, baseModel)
|
body = ensureModelMaxTokens(body, baseModel)
|
||||||
|
|
||||||
// Disable thinking if tool_choice forces tool use (Anthropic API constraint)
|
// Disable thinking if tool_choice forces tool use (Anthropic API constraint)
|
||||||
@@ -342,7 +342,7 @@ func (e *ClaudeExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.A
|
|||||||
|
|
||||||
requestedModel := helps.PayloadRequestedModel(opts, req.Model)
|
requestedModel := helps.PayloadRequestedModel(opts, req.Model)
|
||||||
requestPath := helps.PayloadRequestPath(opts)
|
requestPath := helps.PayloadRequestPath(opts)
|
||||||
body = helps.ApplyPayloadConfigWithRoot(e.cfg, baseModel, to.String(), "", body, originalTranslated, requestedModel, requestPath)
|
body = helps.ApplyPayloadConfigWithRequest(e.cfg, baseModel, to.String(), from.String(), "", body, originalTranslated, requestedModel, requestPath, opts.Headers)
|
||||||
body = ensureModelMaxTokens(body, baseModel)
|
body = ensureModelMaxTokens(body, baseModel)
|
||||||
|
|
||||||
// Disable thinking if tool_choice forces tool use (Anthropic API constraint)
|
// Disable thinking if tool_choice forces tool use (Anthropic API constraint)
|
||||||
|
|||||||
@@ -174,7 +174,7 @@ func (e *CodexExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, re
|
|||||||
|
|
||||||
requestedModel := helps.PayloadRequestedModel(opts, req.Model)
|
requestedModel := helps.PayloadRequestedModel(opts, req.Model)
|
||||||
requestPath := helps.PayloadRequestPath(opts)
|
requestPath := helps.PayloadRequestPath(opts)
|
||||||
body = helps.ApplyPayloadConfigWithRoot(e.cfg, baseModel, to.String(), "", body, originalTranslated, requestedModel, requestPath)
|
body = helps.ApplyPayloadConfigWithRequest(e.cfg, baseModel, to.String(), from.String(), "", body, originalTranslated, requestedModel, requestPath, opts.Headers)
|
||||||
body, _ = sjson.SetBytes(body, "model", baseModel)
|
body, _ = sjson.SetBytes(body, "model", baseModel)
|
||||||
body, _ = sjson.SetBytes(body, "stream", true)
|
body, _ = sjson.SetBytes(body, "stream", true)
|
||||||
body, _ = sjson.DeleteBytes(body, "previous_response_id")
|
body, _ = sjson.DeleteBytes(body, "previous_response_id")
|
||||||
@@ -329,7 +329,7 @@ func (e *CodexExecutor) executeCompact(ctx context.Context, auth *cliproxyauth.A
|
|||||||
|
|
||||||
requestedModel := helps.PayloadRequestedModel(opts, req.Model)
|
requestedModel := helps.PayloadRequestedModel(opts, req.Model)
|
||||||
requestPath := helps.PayloadRequestPath(opts)
|
requestPath := helps.PayloadRequestPath(opts)
|
||||||
body = helps.ApplyPayloadConfigWithRoot(e.cfg, baseModel, to.String(), "", body, originalTranslated, requestedModel, requestPath)
|
body = helps.ApplyPayloadConfigWithRequest(e.cfg, baseModel, to.String(), from.String(), "", body, originalTranslated, requestedModel, requestPath, opts.Headers)
|
||||||
body, _ = sjson.SetBytes(body, "model", baseModel)
|
body, _ = sjson.SetBytes(body, "model", baseModel)
|
||||||
body, _ = sjson.DeleteBytes(body, "stream")
|
body, _ = sjson.DeleteBytes(body, "stream")
|
||||||
body = normalizeCodexInstructions(body)
|
body = normalizeCodexInstructions(body)
|
||||||
@@ -424,7 +424,7 @@ func (e *CodexExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Au
|
|||||||
|
|
||||||
requestedModel := helps.PayloadRequestedModel(opts, req.Model)
|
requestedModel := helps.PayloadRequestedModel(opts, req.Model)
|
||||||
requestPath := helps.PayloadRequestPath(opts)
|
requestPath := helps.PayloadRequestPath(opts)
|
||||||
body = helps.ApplyPayloadConfigWithRoot(e.cfg, baseModel, to.String(), "", body, originalTranslated, requestedModel, requestPath)
|
body = helps.ApplyPayloadConfigWithRequest(e.cfg, baseModel, to.String(), from.String(), "", body, originalTranslated, requestedModel, requestPath, opts.Headers)
|
||||||
body, _ = sjson.DeleteBytes(body, "previous_response_id")
|
body, _ = sjson.DeleteBytes(body, "previous_response_id")
|
||||||
body, _ = sjson.DeleteBytes(body, "prompt_cache_retention")
|
body, _ = sjson.DeleteBytes(body, "prompt_cache_retention")
|
||||||
body, _ = sjson.DeleteBytes(body, "safety_identifier")
|
body, _ = sjson.DeleteBytes(body, "safety_identifier")
|
||||||
|
|||||||
@@ -204,7 +204,7 @@ func (e *CodexWebsocketsExecutor) Execute(ctx context.Context, auth *cliproxyaut
|
|||||||
|
|
||||||
requestedModel := helps.PayloadRequestedModel(opts, req.Model)
|
requestedModel := helps.PayloadRequestedModel(opts, req.Model)
|
||||||
requestPath := helps.PayloadRequestPath(opts)
|
requestPath := helps.PayloadRequestPath(opts)
|
||||||
body = helps.ApplyPayloadConfigWithRoot(e.cfg, baseModel, to.String(), "", body, originalTranslated, requestedModel, requestPath)
|
body = helps.ApplyPayloadConfigWithRequest(e.cfg, baseModel, to.String(), from.String(), "", body, originalTranslated, requestedModel, requestPath, opts.Headers)
|
||||||
body, _ = sjson.SetBytes(body, "model", baseModel)
|
body, _ = sjson.SetBytes(body, "model", baseModel)
|
||||||
body, _ = sjson.SetBytes(body, "stream", true)
|
body, _ = sjson.SetBytes(body, "stream", true)
|
||||||
body, _ = sjson.DeleteBytes(body, "prompt_cache_retention")
|
body, _ = sjson.DeleteBytes(body, "prompt_cache_retention")
|
||||||
@@ -408,7 +408,7 @@ func (e *CodexWebsocketsExecutor) ExecuteStream(ctx context.Context, auth *clipr
|
|||||||
|
|
||||||
requestedModel := helps.PayloadRequestedModel(opts, req.Model)
|
requestedModel := helps.PayloadRequestedModel(opts, req.Model)
|
||||||
requestPath := helps.PayloadRequestPath(opts)
|
requestPath := helps.PayloadRequestPath(opts)
|
||||||
body = helps.ApplyPayloadConfigWithRoot(e.cfg, baseModel, to.String(), "", body, body, requestedModel, requestPath)
|
body = helps.ApplyPayloadConfigWithRequest(e.cfg, baseModel, to.String(), from.String(), "", body, body, requestedModel, requestPath, opts.Headers)
|
||||||
body = normalizeCodexInstructions(body)
|
body = normalizeCodexInstructions(body)
|
||||||
if e.cfg == nil || e.cfg.DisableImageGeneration == config.DisableImageGenerationOff {
|
if e.cfg == nil || e.cfg.DisableImageGeneration == config.DisableImageGenerationOff {
|
||||||
body = ensureImageGenerationTool(body, baseModel, auth)
|
body = ensureImageGenerationTool(body, baseModel, auth)
|
||||||
|
|||||||
@@ -140,7 +140,7 @@ func (e *GeminiCLIExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth
|
|||||||
basePayload = fixGeminiCLIImageAspectRatio(baseModel, basePayload)
|
basePayload = fixGeminiCLIImageAspectRatio(baseModel, basePayload)
|
||||||
requestedModel := helps.PayloadRequestedModel(opts, req.Model)
|
requestedModel := helps.PayloadRequestedModel(opts, req.Model)
|
||||||
requestPath := helps.PayloadRequestPath(opts)
|
requestPath := helps.PayloadRequestPath(opts)
|
||||||
basePayload = helps.ApplyPayloadConfigWithRoot(e.cfg, baseModel, "gemini", "request", basePayload, originalTranslated, requestedModel, requestPath)
|
basePayload = helps.ApplyPayloadConfigWithRequest(e.cfg, baseModel, "gemini", from.String(), "request", basePayload, originalTranslated, requestedModel, requestPath, opts.Headers)
|
||||||
|
|
||||||
action := "generateContent"
|
action := "generateContent"
|
||||||
if req.Metadata != nil {
|
if req.Metadata != nil {
|
||||||
@@ -296,7 +296,7 @@ func (e *GeminiCLIExecutor) ExecuteStream(ctx context.Context, auth *cliproxyaut
|
|||||||
basePayload = fixGeminiCLIImageAspectRatio(baseModel, basePayload)
|
basePayload = fixGeminiCLIImageAspectRatio(baseModel, basePayload)
|
||||||
requestedModel := helps.PayloadRequestedModel(opts, req.Model)
|
requestedModel := helps.PayloadRequestedModel(opts, req.Model)
|
||||||
requestPath := helps.PayloadRequestPath(opts)
|
requestPath := helps.PayloadRequestPath(opts)
|
||||||
basePayload = helps.ApplyPayloadConfigWithRoot(e.cfg, baseModel, "gemini", "request", basePayload, originalTranslated, requestedModel, requestPath)
|
basePayload = helps.ApplyPayloadConfigWithRequest(e.cfg, baseModel, "gemini", from.String(), "request", basePayload, originalTranslated, requestedModel, requestPath, opts.Headers)
|
||||||
|
|
||||||
projectID := resolveGeminiProjectID(auth)
|
projectID := resolveGeminiProjectID(auth)
|
||||||
|
|
||||||
|
|||||||
@@ -133,7 +133,7 @@ func (e *GeminiExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, r
|
|||||||
body = fixGeminiImageAspectRatio(baseModel, body)
|
body = fixGeminiImageAspectRatio(baseModel, body)
|
||||||
requestedModel := helps.PayloadRequestedModel(opts, req.Model)
|
requestedModel := helps.PayloadRequestedModel(opts, req.Model)
|
||||||
requestPath := helps.PayloadRequestPath(opts)
|
requestPath := helps.PayloadRequestPath(opts)
|
||||||
body = helps.ApplyPayloadConfigWithRoot(e.cfg, baseModel, to.String(), "", body, originalTranslated, requestedModel, requestPath)
|
body = helps.ApplyPayloadConfigWithRequest(e.cfg, baseModel, to.String(), from.String(), "", body, originalTranslated, requestedModel, requestPath, opts.Headers)
|
||||||
body, _ = sjson.SetBytes(body, "model", baseModel)
|
body, _ = sjson.SetBytes(body, "model", baseModel)
|
||||||
|
|
||||||
action := "generateContent"
|
action := "generateContent"
|
||||||
@@ -241,7 +241,7 @@ func (e *GeminiExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.A
|
|||||||
body = fixGeminiImageAspectRatio(baseModel, body)
|
body = fixGeminiImageAspectRatio(baseModel, body)
|
||||||
requestedModel := helps.PayloadRequestedModel(opts, req.Model)
|
requestedModel := helps.PayloadRequestedModel(opts, req.Model)
|
||||||
requestPath := helps.PayloadRequestPath(opts)
|
requestPath := helps.PayloadRequestPath(opts)
|
||||||
body = helps.ApplyPayloadConfigWithRoot(e.cfg, baseModel, to.String(), "", body, originalTranslated, requestedModel, requestPath)
|
body = helps.ApplyPayloadConfigWithRequest(e.cfg, baseModel, to.String(), from.String(), "", body, originalTranslated, requestedModel, requestPath, opts.Headers)
|
||||||
body, _ = sjson.SetBytes(body, "model", baseModel)
|
body, _ = sjson.SetBytes(body, "model", baseModel)
|
||||||
|
|
||||||
baseURL := resolveGeminiBaseURL(auth)
|
baseURL := resolveGeminiBaseURL(auth)
|
||||||
|
|||||||
@@ -339,7 +339,7 @@ func (e *GeminiVertexExecutor) executeWithServiceAccount(ctx context.Context, au
|
|||||||
body = fixGeminiImageAspectRatio(baseModel, body)
|
body = fixGeminiImageAspectRatio(baseModel, body)
|
||||||
requestedModel := helps.PayloadRequestedModel(opts, req.Model)
|
requestedModel := helps.PayloadRequestedModel(opts, req.Model)
|
||||||
requestPath := helps.PayloadRequestPath(opts)
|
requestPath := helps.PayloadRequestPath(opts)
|
||||||
body = helps.ApplyPayloadConfigWithRoot(e.cfg, baseModel, to.String(), "", body, originalTranslated, requestedModel, requestPath)
|
body = helps.ApplyPayloadConfigWithRequest(e.cfg, baseModel, to.String(), from.String(), "", body, originalTranslated, requestedModel, requestPath, opts.Headers)
|
||||||
body, _ = sjson.SetBytes(body, "model", baseModel)
|
body, _ = sjson.SetBytes(body, "model", baseModel)
|
||||||
body = helps.StripVertexOpenAIResponsesToolCallIDs(body, from.String())
|
body = helps.StripVertexOpenAIResponsesToolCallIDs(body, from.String())
|
||||||
}
|
}
|
||||||
@@ -461,7 +461,7 @@ func (e *GeminiVertexExecutor) executeWithAPIKey(ctx context.Context, auth *clip
|
|||||||
body = fixGeminiImageAspectRatio(baseModel, body)
|
body = fixGeminiImageAspectRatio(baseModel, body)
|
||||||
requestedModel := helps.PayloadRequestedModel(opts, req.Model)
|
requestedModel := helps.PayloadRequestedModel(opts, req.Model)
|
||||||
requestPath := helps.PayloadRequestPath(opts)
|
requestPath := helps.PayloadRequestPath(opts)
|
||||||
body = helps.ApplyPayloadConfigWithRoot(e.cfg, baseModel, to.String(), "", body, originalTranslated, requestedModel, requestPath)
|
body = helps.ApplyPayloadConfigWithRequest(e.cfg, baseModel, to.String(), from.String(), "", body, originalTranslated, requestedModel, requestPath, opts.Headers)
|
||||||
body, _ = sjson.SetBytes(body, "model", baseModel)
|
body, _ = sjson.SetBytes(body, "model", baseModel)
|
||||||
body = helps.StripVertexOpenAIResponsesToolCallIDs(body, from.String())
|
body = helps.StripVertexOpenAIResponsesToolCallIDs(body, from.String())
|
||||||
|
|
||||||
@@ -573,7 +573,7 @@ func (e *GeminiVertexExecutor) executeStreamWithServiceAccount(ctx context.Conte
|
|||||||
body = fixGeminiImageAspectRatio(baseModel, body)
|
body = fixGeminiImageAspectRatio(baseModel, body)
|
||||||
requestedModel := helps.PayloadRequestedModel(opts, req.Model)
|
requestedModel := helps.PayloadRequestedModel(opts, req.Model)
|
||||||
requestPath := helps.PayloadRequestPath(opts)
|
requestPath := helps.PayloadRequestPath(opts)
|
||||||
body = helps.ApplyPayloadConfigWithRoot(e.cfg, baseModel, to.String(), "", body, originalTranslated, requestedModel, requestPath)
|
body = helps.ApplyPayloadConfigWithRequest(e.cfg, baseModel, to.String(), from.String(), "", body, originalTranslated, requestedModel, requestPath, opts.Headers)
|
||||||
body, _ = sjson.SetBytes(body, "model", baseModel)
|
body, _ = sjson.SetBytes(body, "model", baseModel)
|
||||||
body = helps.StripVertexOpenAIResponsesToolCallIDs(body, from.String())
|
body = helps.StripVertexOpenAIResponsesToolCallIDs(body, from.String())
|
||||||
|
|
||||||
@@ -715,7 +715,7 @@ func (e *GeminiVertexExecutor) executeStreamWithAPIKey(ctx context.Context, auth
|
|||||||
body = fixGeminiImageAspectRatio(baseModel, body)
|
body = fixGeminiImageAspectRatio(baseModel, body)
|
||||||
requestedModel := helps.PayloadRequestedModel(opts, req.Model)
|
requestedModel := helps.PayloadRequestedModel(opts, req.Model)
|
||||||
requestPath := helps.PayloadRequestPath(opts)
|
requestPath := helps.PayloadRequestPath(opts)
|
||||||
body = helps.ApplyPayloadConfigWithRoot(e.cfg, baseModel, to.String(), "", body, originalTranslated, requestedModel, requestPath)
|
body = helps.ApplyPayloadConfigWithRequest(e.cfg, baseModel, to.String(), from.String(), "", body, originalTranslated, requestedModel, requestPath, opts.Headers)
|
||||||
body, _ = sjson.SetBytes(body, "model", baseModel)
|
body, _ = sjson.SetBytes(body, "model", baseModel)
|
||||||
body = helps.StripVertexOpenAIResponsesToolCallIDs(body, from.String())
|
body = helps.StripVertexOpenAIResponsesToolCallIDs(body, from.String())
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ package helps
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
"reflect"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
@@ -19,6 +21,11 @@ import (
|
|||||||
// model name before alias resolution so payload rules can target aliases precisely.
|
// model name before alias resolution so payload rules can target aliases precisely.
|
||||||
// requestPath is the inbound HTTP request path (when available) used for endpoint-scoped gates.
|
// requestPath is the inbound HTTP request path (when available) used for endpoint-scoped gates.
|
||||||
func ApplyPayloadConfigWithRoot(cfg *config.Config, model, protocol, root string, payload, original []byte, requestedModel string, requestPath string) []byte {
|
func ApplyPayloadConfigWithRoot(cfg *config.Config, model, protocol, root string, payload, original []byte, requestedModel string, requestPath string) []byte {
|
||||||
|
return ApplyPayloadConfigWithRequest(cfg, model, protocol, "", root, payload, original, requestedModel, requestPath, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ApplyPayloadConfigWithRequest applies payload config using source protocol and request header gates.
|
||||||
|
func ApplyPayloadConfigWithRequest(cfg *config.Config, model, protocol, formProtocol, root string, payload, original []byte, requestedModel string, requestPath string, headers http.Header) []byte {
|
||||||
if cfg == nil || len(payload) == 0 {
|
if cfg == nil || len(payload) == 0 {
|
||||||
return payload
|
return payload
|
||||||
}
|
}
|
||||||
@@ -48,7 +55,7 @@ func ApplyPayloadConfigWithRoot(cfg *config.Config, model, protocol, root string
|
|||||||
// Apply default rules: first write wins per field across all matching rules.
|
// Apply default rules: first write wins per field across all matching rules.
|
||||||
for i := range rules.Default {
|
for i := range rules.Default {
|
||||||
rule := &rules.Default[i]
|
rule := &rules.Default[i]
|
||||||
if !payloadModelRulesMatch(rule.Models, protocol, candidates) {
|
if !payloadModelRulesMatch(rule.Models, protocol, formProtocol, headers, out, root, candidates) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
for path, value := range rule.Params {
|
for path, value := range rule.Params {
|
||||||
@@ -75,7 +82,7 @@ func ApplyPayloadConfigWithRoot(cfg *config.Config, model, protocol, root string
|
|||||||
// Apply default raw rules: first write wins per field across all matching rules.
|
// Apply default raw rules: first write wins per field across all matching rules.
|
||||||
for i := range rules.DefaultRaw {
|
for i := range rules.DefaultRaw {
|
||||||
rule := &rules.DefaultRaw[i]
|
rule := &rules.DefaultRaw[i]
|
||||||
if !payloadModelRulesMatch(rule.Models, protocol, candidates) {
|
if !payloadModelRulesMatch(rule.Models, protocol, formProtocol, headers, out, root, candidates) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
for path, value := range rule.Params {
|
for path, value := range rule.Params {
|
||||||
@@ -106,7 +113,7 @@ func ApplyPayloadConfigWithRoot(cfg *config.Config, model, protocol, root string
|
|||||||
// 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]
|
||||||
if !payloadModelRulesMatch(rule.Models, protocol, candidates) {
|
if !payloadModelRulesMatch(rule.Models, protocol, formProtocol, headers, out, root, candidates) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
for path, value := range rule.Params {
|
for path, value := range rule.Params {
|
||||||
@@ -126,7 +133,7 @@ func ApplyPayloadConfigWithRoot(cfg *config.Config, model, protocol, root string
|
|||||||
// Apply override raw rules: last write wins per field across all matching rules.
|
// Apply override raw rules: last write wins per field across all matching rules.
|
||||||
for i := range rules.OverrideRaw {
|
for i := range rules.OverrideRaw {
|
||||||
rule := &rules.OverrideRaw[i]
|
rule := &rules.OverrideRaw[i]
|
||||||
if !payloadModelRulesMatch(rule.Models, protocol, candidates) {
|
if !payloadModelRulesMatch(rule.Models, protocol, formProtocol, headers, out, root, candidates) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
for path, value := range rule.Params {
|
for path, value := range rule.Params {
|
||||||
@@ -150,7 +157,7 @@ func ApplyPayloadConfigWithRoot(cfg *config.Config, model, protocol, root string
|
|||||||
// Apply filter rules: remove matching paths from payload.
|
// Apply filter rules: remove matching paths from payload.
|
||||||
for i := range rules.Filter {
|
for i := range rules.Filter {
|
||||||
rule := &rules.Filter[i]
|
rule := &rules.Filter[i]
|
||||||
if !payloadModelRulesMatch(rule.Models, protocol, candidates) {
|
if !payloadModelRulesMatch(rule.Models, protocol, formProtocol, headers, out, root, candidates) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
for _, path := range rule.Params {
|
for _, path := range rule.Params {
|
||||||
@@ -192,7 +199,7 @@ func isImagesEndpointRequestPath(path string) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func payloadModelRulesMatch(rules []config.PayloadModelRule, protocol string, models []string) bool {
|
func payloadModelRulesMatch(rules []config.PayloadModelRule, protocol string, formProtocol string, headers http.Header, payload []byte, root string, models []string) bool {
|
||||||
if len(rules) == 0 || len(models) == 0 {
|
if len(rules) == 0 || len(models) == 0 {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@@ -205,7 +212,16 @@ func payloadModelRulesMatch(rules []config.PayloadModelRule, protocol string, mo
|
|||||||
if ep := strings.TrimSpace(entry.Protocol); ep != "" && protocol != "" && !strings.EqualFold(ep, protocol) {
|
if ep := strings.TrimSpace(entry.Protocol); ep != "" && protocol != "" && !strings.EqualFold(ep, protocol) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if matchModelPattern(name, model) {
|
if !payloadFormProtocolMatches(entry.FormProtocol, formProtocol) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !payloadHeadersMatch(headers, entry.Headers) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !matchModelPattern(name, model) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if payloadModelRuleConditionsMatch(payload, root, entry) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -213,6 +229,207 @@ func payloadModelRulesMatch(rules []config.PayloadModelRule, protocol string, mo
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func payloadModelRuleConditionsMatch(payload []byte, root string, rule config.PayloadModelRule) bool {
|
||||||
|
if !payloadMatchConditionsMatch(payload, root, rule.Match) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if !payloadNotMatchConditionsMatch(payload, root, rule.NotMatch) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if !payloadExistConditionsMatch(payload, root, rule.Exist) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if !payloadNotExistConditionsMatch(payload, root, rule.NotExist) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func payloadMatchConditionsMatch(payload []byte, root string, conditions []map[string]any) bool {
|
||||||
|
for _, condition := range conditions {
|
||||||
|
for path, value := range condition {
|
||||||
|
if strings.TrimSpace(path) == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !payloadPathMatchesValue(payload, buildPayloadPath(root, path), value) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func payloadNotMatchConditionsMatch(payload []byte, root string, conditions []map[string]any) bool {
|
||||||
|
for _, condition := range conditions {
|
||||||
|
for path, value := range condition {
|
||||||
|
if strings.TrimSpace(path) == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if payloadPathMatchesValue(payload, buildPayloadPath(root, path), value) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func payloadExistConditionsMatch(payload []byte, root string, paths []string) bool {
|
||||||
|
for _, path := range paths {
|
||||||
|
if strings.TrimSpace(path) == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !payloadPathExists(payload, buildPayloadPath(root, path)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func payloadNotExistConditionsMatch(payload []byte, root string, paths []string) bool {
|
||||||
|
for _, path := range paths {
|
||||||
|
if strings.TrimSpace(path) == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if payloadPathExists(payload, buildPayloadPath(root, path)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func payloadPathMatchesValue(payload []byte, path string, value any) bool {
|
||||||
|
for _, resolvedPath := range resolvePayloadRulePaths(payload, path) {
|
||||||
|
result := gjson.GetBytes(payload, resolvedPath)
|
||||||
|
if !result.Exists() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if payloadResultEquals(result, value) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func payloadPathExists(payload []byte, path string) bool {
|
||||||
|
for _, resolvedPath := range resolvePayloadRulePaths(payload, path) {
|
||||||
|
result := gjson.GetBytes(payload, resolvedPath)
|
||||||
|
if result.Exists() && result.Type != gjson.Null {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func payloadResultEquals(result gjson.Result, value any) bool {
|
||||||
|
actual, ok := normalizedPayloadResult(result)
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
expected, ok := normalizedPayloadValue(value)
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return reflect.DeepEqual(actual, expected)
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizedPayloadResult(result gjson.Result) (any, bool) {
|
||||||
|
if !result.Exists() {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
raw := strings.TrimSpace(result.Raw)
|
||||||
|
if raw == "" {
|
||||||
|
encoded, errMarshal := json.Marshal(result.Value())
|
||||||
|
if errMarshal != nil {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
raw = string(encoded)
|
||||||
|
}
|
||||||
|
return normalizedPayloadJSON([]byte(raw))
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizedPayloadValue(value any) (any, bool) {
|
||||||
|
encoded, errMarshal := json.Marshal(value)
|
||||||
|
if errMarshal != nil {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
return normalizedPayloadJSON(encoded)
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizedPayloadJSON(data []byte) (any, bool) {
|
||||||
|
if len(strings.TrimSpace(string(data))) == 0 {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
var out any
|
||||||
|
if errUnmarshal := json.Unmarshal(data, &out); errUnmarshal != nil {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
return out, true
|
||||||
|
}
|
||||||
|
|
||||||
|
func payloadFormProtocolMatches(pattern, formProtocol string) bool {
|
||||||
|
pattern = normalizePayloadFormProtocol(pattern)
|
||||||
|
if pattern == "" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
formProtocol = normalizePayloadFormProtocol(formProtocol)
|
||||||
|
if formProtocol == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return strings.EqualFold(pattern, formProtocol)
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizePayloadFormProtocol(protocol string) string {
|
||||||
|
protocol = strings.ToLower(strings.TrimSpace(protocol))
|
||||||
|
switch protocol {
|
||||||
|
case "openai-response", "openai-responses", "response":
|
||||||
|
return "responses"
|
||||||
|
case "gemini-cli":
|
||||||
|
return "gemini"
|
||||||
|
default:
|
||||||
|
return protocol
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func payloadHeadersMatch(headers http.Header, rules map[string]string) bool {
|
||||||
|
if len(rules) == 0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
for key, pattern := range rules {
|
||||||
|
key = strings.TrimSpace(key)
|
||||||
|
if key == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
values := payloadHeaderValues(headers, key)
|
||||||
|
if len(values) == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
matched := false
|
||||||
|
for _, value := range values {
|
||||||
|
if matchModelPattern(pattern, value) {
|
||||||
|
matched = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !matched {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func payloadHeaderValues(headers http.Header, key string) []string {
|
||||||
|
if headers == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
var values []string
|
||||||
|
for headerKey, headerValues := range headers {
|
||||||
|
if strings.EqualFold(headerKey, key) {
|
||||||
|
values = append(values, headerValues...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return values
|
||||||
|
}
|
||||||
|
|
||||||
func payloadModelCandidates(model, requestedModel string) []string {
|
func payloadModelCandidates(model, requestedModel string) []string {
|
||||||
model = strings.TrimSpace(model)
|
model = strings.TrimSpace(model)
|
||||||
requestedModel = strings.TrimSpace(requestedModel)
|
requestedModel = strings.TrimSpace(requestedModel)
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package helps
|
package helps
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"net/http"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/router-for-me/CLIProxyAPI/v7/internal/config"
|
"github.com/router-for-me/CLIProxyAPI/v7/internal/config"
|
||||||
@@ -132,3 +133,181 @@ func TestApplyPayloadConfigWithRoot_DisableImageGeneration_PayloadOverrideCanRes
|
|||||||
t.Fatalf("expected tool_choice to be restored by payload override")
|
t.Fatalf("expected tool_choice to be restored by payload override")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestApplyPayloadConfigWithRequest_HeaderGateRequiresWildcardMatch(t *testing.T) {
|
||||||
|
cfg := &config.Config{
|
||||||
|
Payload: config.PayloadConfig{
|
||||||
|
Override: []config.PayloadRule{
|
||||||
|
{
|
||||||
|
Models: []config.PayloadModelRule{
|
||||||
|
{
|
||||||
|
Name: "gpt-*",
|
||||||
|
Protocol: "openai",
|
||||||
|
Headers: map[string]string{
|
||||||
|
"X-Client-Tier": "tenant-*-region-*",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Params: map[string]any{
|
||||||
|
"metadata.enabled": true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
payload := []byte(`{"model":"gpt-5.4"}`)
|
||||||
|
headers := http.Header{}
|
||||||
|
headers.Set("X-Client-Tier", "tenant-alpha-region-us")
|
||||||
|
|
||||||
|
out := ApplyPayloadConfigWithRequest(cfg, "gpt-5.4", "openai", "responses", "", payload, nil, "", "", headers)
|
||||||
|
if !gjson.GetBytes(out, "metadata.enabled").Bool() {
|
||||||
|
t.Fatalf("expected header-matched payload rule to apply, payload=%s", string(out))
|
||||||
|
}
|
||||||
|
|
||||||
|
headers.Set("X-Client-Tier", "tenant-alpha")
|
||||||
|
out = ApplyPayloadConfigWithRequest(cfg, "gpt-5.4", "openai", "responses", "", payload, nil, "", "", headers)
|
||||||
|
if gjson.GetBytes(out, "metadata.enabled").Exists() {
|
||||||
|
t.Fatalf("expected header-mismatched payload rule to be skipped, payload=%s", string(out))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestApplyPayloadConfigWithRequest_FormProtocolGateUsesSourceProtocol(t *testing.T) {
|
||||||
|
cfg := &config.Config{
|
||||||
|
Payload: config.PayloadConfig{
|
||||||
|
Override: []config.PayloadRule{
|
||||||
|
{
|
||||||
|
Models: []config.PayloadModelRule{
|
||||||
|
{Name: "gpt-*", Protocol: "openai", FormProtocol: "responses"},
|
||||||
|
},
|
||||||
|
Params: map[string]any{
|
||||||
|
"metadata.source": "responses",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Models: []config.PayloadModelRule{
|
||||||
|
{Name: "gpt-*", Protocol: "openai", FormProtocol: "openai"},
|
||||||
|
},
|
||||||
|
Params: map[string]any{
|
||||||
|
"metadata.source": "openai",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
payload := []byte(`{"model":"gpt-5.4"}`)
|
||||||
|
|
||||||
|
out := ApplyPayloadConfigWithRequest(cfg, "gpt-5.4", "openai", "openai-response", "", payload, nil, "", "", nil)
|
||||||
|
if got := gjson.GetBytes(out, "metadata.source").String(); got != "responses" {
|
||||||
|
t.Fatalf("metadata.source = %q, want responses; payload=%s", got, string(out))
|
||||||
|
}
|
||||||
|
|
||||||
|
out = ApplyPayloadConfigWithRequest(cfg, "gpt-5.4", "openai", "openai", "", payload, nil, "", "", nil)
|
||||||
|
if got := gjson.GetBytes(out, "metadata.source").String(); got != "openai" {
|
||||||
|
t.Fatalf("metadata.source = %q, want openai; payload=%s", got, string(out))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestApplyPayloadConfigWithRequest_PayloadConditionsNarrowRule(t *testing.T) {
|
||||||
|
cfg := &config.Config{
|
||||||
|
Payload: config.PayloadConfig{
|
||||||
|
Override: []config.PayloadRule{
|
||||||
|
{
|
||||||
|
Models: []config.PayloadModelRule{
|
||||||
|
{
|
||||||
|
Name: "gpt-*",
|
||||||
|
Match: []map[string]any{
|
||||||
|
{"metadata.client": "codex"},
|
||||||
|
{"tools.#(type==\"web_search\").enabled": true},
|
||||||
|
},
|
||||||
|
NotMatch: []map[string]any{
|
||||||
|
{"metadata.mode": "dev"},
|
||||||
|
},
|
||||||
|
Exist: []string{
|
||||||
|
"tools.#(type==\"web_search\").type",
|
||||||
|
},
|
||||||
|
NotExist: []string{
|
||||||
|
"metadata.missing",
|
||||||
|
"metadata.null_value",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Params: map[string]any{
|
||||||
|
"metadata.applied": true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
payload := []byte(`{"model":"gpt-5.4","metadata":{"client":"codex","mode":"prod","null_value":null},"tools":[{"type":"function"},{"type":"web_search","enabled":true}]}`)
|
||||||
|
|
||||||
|
out := ApplyPayloadConfigWithRequest(cfg, "gpt-5.4", "openai", "responses", "", payload, nil, "", "", nil)
|
||||||
|
if !gjson.GetBytes(out, "metadata.applied").Bool() {
|
||||||
|
t.Fatalf("expected payload condition-matched rule to apply, payload=%s", string(out))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestApplyPayloadConfigWithRequest_PayloadConditionsSkipRule(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
model config.PayloadModelRule
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "match mismatch",
|
||||||
|
model: config.PayloadModelRule{
|
||||||
|
Name: "gpt-*",
|
||||||
|
Match: []map[string]any{{"metadata.client": "codex"}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "not-match matched",
|
||||||
|
model: config.PayloadModelRule{
|
||||||
|
Name: "gpt-*",
|
||||||
|
NotMatch: []map[string]any{{"metadata.mode": "dev"}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "exist missing",
|
||||||
|
model: config.PayloadModelRule{
|
||||||
|
Name: "gpt-*",
|
||||||
|
Exist: []string{"metadata.missing"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "exist null",
|
||||||
|
model: config.PayloadModelRule{
|
||||||
|
Name: "gpt-*",
|
||||||
|
Exist: []string{"metadata.null_value"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "not-exist present",
|
||||||
|
model: config.PayloadModelRule{
|
||||||
|
Name: "gpt-*",
|
||||||
|
NotExist: []string{"metadata.client"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
payload := []byte(`{"model":"gpt-5.4","metadata":{"client":"other","mode":"dev","null_value":null}}`)
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
cfg := &config.Config{
|
||||||
|
Payload: config.PayloadConfig{
|
||||||
|
Override: []config.PayloadRule{
|
||||||
|
{
|
||||||
|
Models: []config.PayloadModelRule{tc.model},
|
||||||
|
Params: map[string]any{
|
||||||
|
"metadata.applied": true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
out := ApplyPayloadConfigWithRequest(cfg, "gpt-5.4", "openai", "responses", "", payload, nil, "", "", nil)
|
||||||
|
if gjson.GetBytes(out, "metadata.applied").Exists() {
|
||||||
|
t.Fatalf("expected payload condition-mismatched rule to be skipped, payload=%s", string(out))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -109,7 +109,7 @@ func (e *KimiExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, req
|
|||||||
|
|
||||||
requestedModel := helps.PayloadRequestedModel(opts, req.Model)
|
requestedModel := helps.PayloadRequestedModel(opts, req.Model)
|
||||||
requestPath := helps.PayloadRequestPath(opts)
|
requestPath := helps.PayloadRequestPath(opts)
|
||||||
body = helps.ApplyPayloadConfigWithRoot(e.cfg, baseModel, to.String(), "", body, originalTranslated, requestedModel, requestPath)
|
body = helps.ApplyPayloadConfigWithRequest(e.cfg, baseModel, to.String(), from.String(), "", body, originalTranslated, requestedModel, requestPath, opts.Headers)
|
||||||
body, err = normalizeKimiToolMessageLinks(body)
|
body, err = normalizeKimiToolMessageLinks(body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return resp, err
|
return resp, err
|
||||||
@@ -219,7 +219,7 @@ func (e *KimiExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Aut
|
|||||||
}
|
}
|
||||||
requestedModel := helps.PayloadRequestedModel(opts, req.Model)
|
requestedModel := helps.PayloadRequestedModel(opts, req.Model)
|
||||||
requestPath := helps.PayloadRequestPath(opts)
|
requestPath := helps.PayloadRequestPath(opts)
|
||||||
body = helps.ApplyPayloadConfigWithRoot(e.cfg, baseModel, to.String(), "", body, originalTranslated, requestedModel, requestPath)
|
body = helps.ApplyPayloadConfigWithRequest(e.cfg, baseModel, to.String(), from.String(), "", body, originalTranslated, requestedModel, requestPath, opts.Headers)
|
||||||
body, err = normalizeKimiToolMessageLinks(body)
|
body, err = normalizeKimiToolMessageLinks(body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
@@ -104,7 +104,7 @@ func (e *OpenAICompatExecutor) Execute(ctx context.Context, auth *cliproxyauth.A
|
|||||||
|
|
||||||
requestedModel := helps.PayloadRequestedModel(opts, req.Model)
|
requestedModel := helps.PayloadRequestedModel(opts, req.Model)
|
||||||
requestPath := helps.PayloadRequestPath(opts)
|
requestPath := helps.PayloadRequestPath(opts)
|
||||||
translated = helps.ApplyPayloadConfigWithRoot(e.cfg, baseModel, to.String(), "", translated, originalTranslated, requestedModel, requestPath)
|
translated = helps.ApplyPayloadConfigWithRequest(e.cfg, baseModel, to.String(), from.String(), "", translated, originalTranslated, requestedModel, requestPath, opts.Headers)
|
||||||
if opts.Alt == "responses/compact" {
|
if opts.Alt == "responses/compact" {
|
||||||
if updated, errDelete := sjson.DeleteBytes(translated, "stream"); errDelete == nil {
|
if updated, errDelete := sjson.DeleteBytes(translated, "stream"); errDelete == nil {
|
||||||
translated = updated
|
translated = updated
|
||||||
@@ -208,7 +208,7 @@ func (e *OpenAICompatExecutor) ExecuteStream(ctx context.Context, auth *cliproxy
|
|||||||
|
|
||||||
requestedModel := helps.PayloadRequestedModel(opts, req.Model)
|
requestedModel := helps.PayloadRequestedModel(opts, req.Model)
|
||||||
requestPath := helps.PayloadRequestPath(opts)
|
requestPath := helps.PayloadRequestPath(opts)
|
||||||
translated = helps.ApplyPayloadConfigWithRoot(e.cfg, baseModel, to.String(), "", translated, originalTranslated, requestedModel, requestPath)
|
translated = helps.ApplyPayloadConfigWithRequest(e.cfg, baseModel, to.String(), from.String(), "", translated, originalTranslated, requestedModel, requestPath, opts.Headers)
|
||||||
|
|
||||||
// Request usage data in the final streaming chunk so that token statistics
|
// Request usage data in the final streaming chunk so that token statistics
|
||||||
// are captured even when the upstream is an OpenAI-compatible provider.
|
// are captured even when the upstream is an OpenAI-compatible provider.
|
||||||
|
|||||||
@@ -494,7 +494,7 @@ func (e *XAIExecutor) prepareResponsesRequest(ctx context.Context, req cliproxye
|
|||||||
|
|
||||||
requestedModel := helps.PayloadRequestedModel(opts, req.Model)
|
requestedModel := helps.PayloadRequestedModel(opts, req.Model)
|
||||||
requestPath := helps.PayloadRequestPath(opts)
|
requestPath := helps.PayloadRequestPath(opts)
|
||||||
body = helps.ApplyPayloadConfigWithRoot(e.cfg, baseModel, to.String(), "", body, originalTranslated, requestedModel, requestPath)
|
body = helps.ApplyPayloadConfigWithRequest(e.cfg, baseModel, to.String(), from.String(), "", body, originalTranslated, requestedModel, requestPath, opts.Headers)
|
||||||
body, _ = sjson.SetBytes(body, "model", baseModel)
|
body, _ = sjson.SetBytes(body, "model", baseModel)
|
||||||
body, _ = sjson.SetBytes(body, "stream", stream)
|
body, _ = sjson.SetBytes(body, "stream", stream)
|
||||||
body, _ = sjson.DeleteBytes(body, "previous_response_id")
|
body, _ = sjson.DeleteBytes(body, "previous_response_id")
|
||||||
|
|||||||
Reference in New Issue
Block a user