Skip to content
Toggle navigation
P
Projects
G
Groups
S
Snippets
Help
phsl
/
new-api
This project
Loading...
Sign in
Toggle navigation
Go to a project
Project
Repository
Issues
0
Merge Requests
0
Pipelines
Wiki
Snippets
Members
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Commit
5df8b34f
authored
Mar 05, 2026
by
Calcium-Ion
Committed by
GitHub
Mar 05, 2026
Browse files
Options
Browse Files
Download
Plain Diff
Merge pull request #3129 from seefs001/feature/param-override-wildcard-path
Feature/param override wildcard path
parents
f53f3269
c6ca4c3b
Hide whitespace changes
Inline
Side-by-side
Showing
3 changed files
with
511 additions
and
31 deletions
+511
-31
relay/common/override.go
+183
-16
relay/common/override_test.go
+256
-0
web/src/components/table/channels/modals/ParamOverrideEditorModal.jsx
+72
-15
No files found.
relay/common/override.go
View file @
5df8b34f
...
@@ -5,6 +5,7 @@ import (
...
@@ -5,6 +5,7 @@ import (
"fmt"
"fmt"
"net/http"
"net/http"
"regexp"
"regexp"
"sort"
"strconv"
"strconv"
"strings"
"strings"
...
@@ -487,15 +488,35 @@ func applyOperations(jsonStr string, operations []ParamOperation, conditionConte
...
@@ -487,15 +488,35 @@ func applyOperations(jsonStr string, operations []ParamOperation, conditionConte
}
}
// 处理路径中的负数索引
// 处理路径中的负数索引
opPath
:=
processNegativeIndex
(
result
,
op
.
Path
)
opPath
:=
processNegativeIndex
(
result
,
op
.
Path
)
var
opPaths
[]
string
if
isPathBasedOperation
(
op
.
Mode
)
{
opPaths
,
err
=
resolveOperationPaths
(
result
,
opPath
)
if
err
!=
nil
{
return
""
,
err
}
if
len
(
opPaths
)
==
0
{
continue
}
}
switch
op
.
Mode
{
switch
op
.
Mode
{
case
"delete"
:
case
"delete"
:
result
,
err
=
sjson
.
Delete
(
result
,
opPath
)
for
_
,
path
:=
range
opPaths
{
result
,
err
=
deleteValue
(
result
,
path
)
if
err
!=
nil
{
break
}
}
case
"set"
:
case
"set"
:
if
op
.
KeepOrigin
&&
gjson
.
Get
(
result
,
opPath
)
.
Exists
()
{
for
_
,
path
:=
range
opPaths
{
continue
if
op
.
KeepOrigin
&&
gjson
.
Get
(
result
,
path
)
.
Exists
()
{
continue
}
result
,
err
=
sjson
.
Set
(
result
,
path
,
op
.
Value
)
if
err
!=
nil
{
break
}
}
}
result
,
err
=
sjson
.
Set
(
result
,
opPath
,
op
.
Value
)
case
"move"
:
case
"move"
:
opFrom
:=
processNegativeIndex
(
result
,
op
.
From
)
opFrom
:=
processNegativeIndex
(
result
,
op
.
From
)
opTo
:=
processNegativeIndex
(
result
,
op
.
To
)
opTo
:=
processNegativeIndex
(
result
,
op
.
To
)
...
@@ -508,27 +529,82 @@ func applyOperations(jsonStr string, operations []ParamOperation, conditionConte
...
@@ -508,27 +529,82 @@ func applyOperations(jsonStr string, operations []ParamOperation, conditionConte
opTo
:=
processNegativeIndex
(
result
,
op
.
To
)
opTo
:=
processNegativeIndex
(
result
,
op
.
To
)
result
,
err
=
copyValue
(
result
,
opFrom
,
opTo
)
result
,
err
=
copyValue
(
result
,
opFrom
,
opTo
)
case
"prepend"
:
case
"prepend"
:
result
,
err
=
modifyValue
(
result
,
opPath
,
op
.
Value
,
op
.
KeepOrigin
,
true
)
for
_
,
path
:=
range
opPaths
{
result
,
err
=
modifyValue
(
result
,
path
,
op
.
Value
,
op
.
KeepOrigin
,
true
)
if
err
!=
nil
{
break
}
}
case
"append"
:
case
"append"
:
result
,
err
=
modifyValue
(
result
,
opPath
,
op
.
Value
,
op
.
KeepOrigin
,
false
)
for
_
,
path
:=
range
opPaths
{
result
,
err
=
modifyValue
(
result
,
path
,
op
.
Value
,
op
.
KeepOrigin
,
false
)
if
err
!=
nil
{
break
}
}
case
"trim_prefix"
:
case
"trim_prefix"
:
result
,
err
=
trimStringValue
(
result
,
opPath
,
op
.
Value
,
true
)
for
_
,
path
:=
range
opPaths
{
result
,
err
=
trimStringValue
(
result
,
path
,
op
.
Value
,
true
)
if
err
!=
nil
{
break
}
}
case
"trim_suffix"
:
case
"trim_suffix"
:
result
,
err
=
trimStringValue
(
result
,
opPath
,
op
.
Value
,
false
)
for
_
,
path
:=
range
opPaths
{
result
,
err
=
trimStringValue
(
result
,
path
,
op
.
Value
,
false
)
if
err
!=
nil
{
break
}
}
case
"ensure_prefix"
:
case
"ensure_prefix"
:
result
,
err
=
ensureStringAffix
(
result
,
opPath
,
op
.
Value
,
true
)
for
_
,
path
:=
range
opPaths
{
result
,
err
=
ensureStringAffix
(
result
,
path
,
op
.
Value
,
true
)
if
err
!=
nil
{
break
}
}
case
"ensure_suffix"
:
case
"ensure_suffix"
:
result
,
err
=
ensureStringAffix
(
result
,
opPath
,
op
.
Value
,
false
)
for
_
,
path
:=
range
opPaths
{
result
,
err
=
ensureStringAffix
(
result
,
path
,
op
.
Value
,
false
)
if
err
!=
nil
{
break
}
}
case
"trim_space"
:
case
"trim_space"
:
result
,
err
=
transformStringValue
(
result
,
opPath
,
strings
.
TrimSpace
)
for
_
,
path
:=
range
opPaths
{
result
,
err
=
transformStringValue
(
result
,
path
,
strings
.
TrimSpace
)
if
err
!=
nil
{
break
}
}
case
"to_lower"
:
case
"to_lower"
:
result
,
err
=
transformStringValue
(
result
,
opPath
,
strings
.
ToLower
)
for
_
,
path
:=
range
opPaths
{
result
,
err
=
transformStringValue
(
result
,
path
,
strings
.
ToLower
)
if
err
!=
nil
{
break
}
}
case
"to_upper"
:
case
"to_upper"
:
result
,
err
=
transformStringValue
(
result
,
opPath
,
strings
.
ToUpper
)
for
_
,
path
:=
range
opPaths
{
result
,
err
=
transformStringValue
(
result
,
path
,
strings
.
ToUpper
)
if
err
!=
nil
{
break
}
}
case
"replace"
:
case
"replace"
:
result
,
err
=
replaceStringValue
(
result
,
opPath
,
op
.
From
,
op
.
To
)
for
_
,
path
:=
range
opPaths
{
result
,
err
=
replaceStringValue
(
result
,
path
,
op
.
From
,
op
.
To
)
if
err
!=
nil
{
break
}
}
case
"regex_replace"
:
case
"regex_replace"
:
result
,
err
=
regexReplaceStringValue
(
result
,
opPath
,
op
.
From
,
op
.
To
)
for
_
,
path
:=
range
opPaths
{
result
,
err
=
regexReplaceStringValue
(
result
,
path
,
op
.
From
,
op
.
To
)
if
err
!=
nil
{
break
}
}
case
"return_error"
:
case
"return_error"
:
returnErr
,
parseErr
:=
parseParamOverrideReturnError
(
op
.
Value
)
returnErr
,
parseErr
:=
parseParamOverrideReturnError
(
op
.
Value
)
if
parseErr
!=
nil
{
if
parseErr
!=
nil
{
...
@@ -536,7 +612,12 @@ func applyOperations(jsonStr string, operations []ParamOperation, conditionConte
...
@@ -536,7 +612,12 @@ func applyOperations(jsonStr string, operations []ParamOperation, conditionConte
}
}
return
""
,
returnErr
return
""
,
returnErr
case
"prune_objects"
:
case
"prune_objects"
:
result
,
err
=
pruneObjects
(
result
,
opPath
,
contextJSON
,
op
.
Value
)
for
_
,
path
:=
range
opPaths
{
result
,
err
=
pruneObjects
(
result
,
path
,
contextJSON
,
op
.
Value
)
if
err
!=
nil
{
break
}
}
case
"set_header"
:
case
"set_header"
:
err
=
setHeaderOverrideInContext
(
context
,
op
.
Path
,
op
.
Value
,
op
.
KeepOrigin
)
err
=
setHeaderOverrideInContext
(
context
,
op
.
Path
,
op
.
Value
,
op
.
KeepOrigin
)
if
err
==
nil
{
if
err
==
nil
{
...
@@ -1174,6 +1255,92 @@ func copyValue(jsonStr, fromPath, toPath string) (string, error) {
...
@@ -1174,6 +1255,92 @@ func copyValue(jsonStr, fromPath, toPath string) (string, error) {
return
sjson
.
Set
(
jsonStr
,
toPath
,
sourceValue
.
Value
())
return
sjson
.
Set
(
jsonStr
,
toPath
,
sourceValue
.
Value
())
}
}
func
isPathBasedOperation
(
mode
string
)
bool
{
switch
mode
{
case
"delete"
,
"set"
,
"prepend"
,
"append"
,
"trim_prefix"
,
"trim_suffix"
,
"ensure_prefix"
,
"ensure_suffix"
,
"trim_space"
,
"to_lower"
,
"to_upper"
,
"replace"
,
"regex_replace"
,
"prune_objects"
:
return
true
default
:
return
false
}
}
func
resolveOperationPaths
(
jsonStr
,
path
string
)
([]
string
,
error
)
{
if
!
strings
.
Contains
(
path
,
"*"
)
{
return
[]
string
{
path
},
nil
}
return
expandWildcardPaths
(
jsonStr
,
path
)
}
func
expandWildcardPaths
(
jsonStr
,
path
string
)
([]
string
,
error
)
{
var
root
interface
{}
if
err
:=
common
.
Unmarshal
([]
byte
(
jsonStr
),
&
root
);
err
!=
nil
{
return
nil
,
err
}
segments
:=
strings
.
Split
(
path
,
"."
)
paths
:=
collectWildcardPaths
(
root
,
segments
,
nil
)
return
lo
.
Uniq
(
paths
),
nil
}
func
collectWildcardPaths
(
node
interface
{},
segments
[]
string
,
prefix
[]
string
)
[]
string
{
if
len
(
segments
)
==
0
{
return
[]
string
{
strings
.
Join
(
prefix
,
"."
)}
}
segment
:=
strings
.
TrimSpace
(
segments
[
0
])
if
segment
==
""
{
return
nil
}
isLast
:=
len
(
segments
)
==
1
if
segment
==
"*"
{
switch
typed
:=
node
.
(
type
)
{
case
map
[
string
]
interface
{}
:
keys
:=
lo
.
Keys
(
typed
)
sort
.
Strings
(
keys
)
return
lo
.
FlatMap
(
keys
,
func
(
key
string
,
_
int
)
[]
string
{
return
collectWildcardPaths
(
typed
[
key
],
segments
[
1
:
],
append
(
prefix
,
key
))
})
case
[]
interface
{}
:
return
lo
.
FlatMap
(
lo
.
Range
(
len
(
typed
)),
func
(
index
int
,
_
int
)
[]
string
{
return
collectWildcardPaths
(
typed
[
index
],
segments
[
1
:
],
append
(
prefix
,
strconv
.
Itoa
(
index
)))
})
default
:
return
nil
}
}
switch
typed
:=
node
.
(
type
)
{
case
map
[
string
]
interface
{}
:
if
isLast
{
return
[]
string
{
strings
.
Join
(
append
(
prefix
,
segment
),
"."
)}
}
next
,
exists
:=
typed
[
segment
]
if
!
exists
{
return
nil
}
return
collectWildcardPaths
(
next
,
segments
[
1
:
],
append
(
prefix
,
segment
))
case
[]
interface
{}
:
index
,
err
:=
strconv
.
Atoi
(
segment
)
if
err
!=
nil
||
index
<
0
||
index
>=
len
(
typed
)
{
return
nil
}
if
isLast
{
return
[]
string
{
strings
.
Join
(
append
(
prefix
,
segment
),
"."
)}
}
return
collectWildcardPaths
(
typed
[
index
],
segments
[
1
:
],
append
(
prefix
,
segment
))
default
:
return
nil
}
}
func
deleteValue
(
jsonStr
,
path
string
)
(
string
,
error
)
{
if
strings
.
TrimSpace
(
path
)
==
""
{
return
jsonStr
,
nil
}
return
sjson
.
Delete
(
jsonStr
,
path
)
}
func
modifyValue
(
jsonStr
,
path
string
,
value
interface
{},
keepOrigin
,
isPrepend
bool
)
(
string
,
error
)
{
func
modifyValue
(
jsonStr
,
path
string
,
value
interface
{},
keepOrigin
,
isPrepend
bool
)
(
string
,
error
)
{
current
:=
gjson
.
Get
(
jsonStr
,
path
)
current
:=
gjson
.
Get
(
jsonStr
,
path
)
switch
{
switch
{
...
...
relay/common/override_test.go
View file @
5df8b34f
...
@@ -2,6 +2,7 @@ package common
...
@@ -2,6 +2,7 @@ package common
import
(
import
(
"encoding/json"
"encoding/json"
"fmt"
"reflect"
"reflect"
"testing"
"testing"
...
@@ -9,6 +10,7 @@ import (
...
@@ -9,6 +10,7 @@ import (
"github.com/QuantumNous/new-api/dto"
"github.com/QuantumNous/new-api/dto"
"github.com/QuantumNous/new-api/setting/model_setting"
"github.com/QuantumNous/new-api/setting/model_setting"
"github.com/samber/lo"
)
)
func
TestApplyParamOverrideTrimPrefix
(
t
*
testing
.
T
)
{
func
TestApplyParamOverrideTrimPrefix
(
t
*
testing
.
T
)
{
...
@@ -242,6 +244,224 @@ func TestApplyParamOverrideDelete(t *testing.T) {
...
@@ -242,6 +244,224 @@ func TestApplyParamOverrideDelete(t *testing.T) {
}
}
}
}
func
TestApplyParamOverrideDeleteWildcardPath
(
t
*
testing
.
T
)
{
input
:=
[]
byte
(
`{"tools":[{"type":"bash","custom":{"input_examples":["a"],"other":1}},{"type":"code","custom":{"input_examples":["b"]}},{"type":"noop","custom":{"other":2}}]}`
)
override
:=
map
[
string
]
interface
{}{
"operations"
:
[]
interface
{}{
map
[
string
]
interface
{}{
"path"
:
"tools.*.custom.input_examples"
,
"mode"
:
"delete"
,
},
},
}
out
,
err
:=
ApplyParamOverride
(
input
,
override
,
nil
)
if
err
!=
nil
{
t
.
Fatalf
(
"ApplyParamOverride returned error: %v"
,
err
)
}
assertJSONEqual
(
t
,
`{"tools":[{"type":"bash","custom":{"other":1}},{"type":"code","custom":{}},{"type":"noop","custom":{"other":2}}]}`
,
string
(
out
))
}
func
TestApplyParamOverrideSetWildcardPath
(
t
*
testing
.
T
)
{
input
:=
[]
byte
(
`{"tools":[{"custom":{"tag":"A"}},{"custom":{"tag":"B"}},{"custom":{"tag":"C"}}]}`
)
override
:=
map
[
string
]
interface
{}{
"operations"
:
[]
interface
{}{
map
[
string
]
interface
{}{
"path"
:
"tools.*.custom.enabled"
,
"mode"
:
"set"
,
"value"
:
true
,
},
},
}
out
,
err
:=
ApplyParamOverride
(
input
,
override
,
nil
)
if
err
!=
nil
{
t
.
Fatalf
(
"ApplyParamOverride returned error: %v"
,
err
)
}
var
got
struct
{
Tools
[]
struct
{
Custom
struct
{
Enabled
bool
`json:"enabled"`
}
`json:"custom"`
}
`json:"tools"`
}
if
err
:=
json
.
Unmarshal
(
out
,
&
got
);
err
!=
nil
{
t
.
Fatalf
(
"failed to unmarshal output JSON: %v"
,
err
)
}
if
!
lo
.
EveryBy
(
got
.
Tools
,
func
(
item
struct
{
Custom
struct
{
Enabled
bool
`json:"enabled"`
}
`json:"custom"`
})
bool
{
return
item
.
Custom
.
Enabled
})
{
t
.
Fatalf
(
"expected wildcard set to enable all tools, got: %s"
,
string
(
out
))
}
}
func
TestApplyParamOverrideTrimSpaceWildcardPath
(
t
*
testing
.
T
)
{
input
:=
[]
byte
(
`{"tools":[{"custom":{"name":" alpha "}},{"custom":{"name":" beta"}},{"custom":{"name":"gamma "}}]}`
)
override
:=
map
[
string
]
interface
{}{
"operations"
:
[]
interface
{}{
map
[
string
]
interface
{}{
"path"
:
"tools.*.custom.name"
,
"mode"
:
"trim_space"
,
},
},
}
out
,
err
:=
ApplyParamOverride
(
input
,
override
,
nil
)
if
err
!=
nil
{
t
.
Fatalf
(
"ApplyParamOverride returned error: %v"
,
err
)
}
var
got
struct
{
Tools
[]
struct
{
Custom
struct
{
Name
string
`json:"name"`
}
`json:"custom"`
}
`json:"tools"`
}
if
err
:=
json
.
Unmarshal
(
out
,
&
got
);
err
!=
nil
{
t
.
Fatalf
(
"failed to unmarshal output JSON: %v"
,
err
)
}
names
:=
lo
.
Map
(
got
.
Tools
,
func
(
item
struct
{
Custom
struct
{
Name
string
`json:"name"`
}
`json:"custom"`
},
_
int
)
string
{
return
item
.
Custom
.
Name
})
if
!
reflect
.
DeepEqual
(
names
,
[]
string
{
"alpha"
,
"beta"
,
"gamma"
})
{
t
.
Fatalf
(
"unexpected names after wildcard trim_space: %v"
,
names
)
}
}
func
TestApplyParamOverrideDeleteWildcardEqualsIndexedPaths
(
t
*
testing
.
T
)
{
input
:=
[]
byte
(
`{"tools":[{"custom":{"input_examples":["a"],"other":1}},{"custom":{"input_examples":["b"],"other":2}},{"custom":{"input_examples":["c"],"other":3}}]}`
)
wildcardOverride
:=
map
[
string
]
interface
{}{
"operations"
:
[]
interface
{}{
map
[
string
]
interface
{}{
"path"
:
"tools.*.custom.input_examples"
,
"mode"
:
"delete"
,
},
},
}
indexedOverride
:=
map
[
string
]
interface
{}{
"operations"
:
lo
.
Map
(
lo
.
Range
(
3
),
func
(
index
int
,
_
int
)
interface
{}
{
return
map
[
string
]
interface
{}{
"path"
:
fmt
.
Sprintf
(
"tools.%d.custom.input_examples"
,
index
),
"mode"
:
"delete"
,
}
}),
}
wildcardOut
,
err
:=
ApplyParamOverride
(
input
,
wildcardOverride
,
nil
)
if
err
!=
nil
{
t
.
Fatalf
(
"wildcard ApplyParamOverride returned error: %v"
,
err
)
}
indexedOut
,
err
:=
ApplyParamOverride
(
input
,
indexedOverride
,
nil
)
if
err
!=
nil
{
t
.
Fatalf
(
"indexed ApplyParamOverride returned error: %v"
,
err
)
}
assertJSONEqual
(
t
,
string
(
indexedOut
),
string
(
wildcardOut
))
}
func
TestApplyParamOverrideSetWildcardKeepOrigin
(
t
*
testing
.
T
)
{
input
:=
[]
byte
(
`{"tools":[{"custom":{"tag":"A"}},{"custom":{"tag":"B","enabled":false}},{"custom":{"tag":"C"}}]}`
)
override
:=
map
[
string
]
interface
{}{
"operations"
:
[]
interface
{}{
map
[
string
]
interface
{}{
"path"
:
"tools.*.custom.enabled"
,
"mode"
:
"set"
,
"value"
:
true
,
"keep_origin"
:
true
,
},
},
}
out
,
err
:=
ApplyParamOverride
(
input
,
override
,
nil
)
if
err
!=
nil
{
t
.
Fatalf
(
"ApplyParamOverride returned error: %v"
,
err
)
}
var
got
struct
{
Tools
[]
struct
{
Custom
struct
{
Enabled
bool
`json:"enabled"`
}
`json:"custom"`
}
`json:"tools"`
}
if
err
:=
json
.
Unmarshal
(
out
,
&
got
);
err
!=
nil
{
t
.
Fatalf
(
"failed to unmarshal output JSON: %v"
,
err
)
}
enabledValues
:=
lo
.
Map
(
got
.
Tools
,
func
(
item
struct
{
Custom
struct
{
Enabled
bool
`json:"enabled"`
}
`json:"custom"`
},
_
int
)
bool
{
return
item
.
Custom
.
Enabled
})
if
!
reflect
.
DeepEqual
(
enabledValues
,
[]
bool
{
true
,
false
,
true
})
{
t
.
Fatalf
(
"unexpected enabled values after wildcard keep_origin set: %v"
,
enabledValues
)
}
}
func
TestApplyParamOverrideTrimSpaceMultiWildcardPath
(
t
*
testing
.
T
)
{
input
:=
[]
byte
(
`{"tools":[{"custom":{"items":[{"name":" alpha "},{"name":" beta "}]}},{"custom":{"items":[{"name":" gamma"}]}}]}`
)
override
:=
map
[
string
]
interface
{}{
"operations"
:
[]
interface
{}{
map
[
string
]
interface
{}{
"path"
:
"tools.*.custom.items.*.name"
,
"mode"
:
"trim_space"
,
},
},
}
out
,
err
:=
ApplyParamOverride
(
input
,
override
,
nil
)
if
err
!=
nil
{
t
.
Fatalf
(
"ApplyParamOverride returned error: %v"
,
err
)
}
var
got
struct
{
Tools
[]
struct
{
Custom
struct
{
Items
[]
struct
{
Name
string
`json:"name"`
}
`json:"items"`
}
`json:"custom"`
}
`json:"tools"`
}
if
err
:=
json
.
Unmarshal
(
out
,
&
got
);
err
!=
nil
{
t
.
Fatalf
(
"failed to unmarshal output JSON: %v"
,
err
)
}
names
:=
lo
.
FlatMap
(
got
.
Tools
,
func
(
tool
struct
{
Custom
struct
{
Items
[]
struct
{
Name
string
`json:"name"`
}
`json:"items"`
}
`json:"custom"`
},
_
int
)
[]
string
{
return
lo
.
Map
(
tool
.
Custom
.
Items
,
func
(
item
struct
{
Name
string
`json:"name"`
},
_
int
)
string
{
return
item
.
Name
})
})
if
!
reflect
.
DeepEqual
(
names
,
[]
string
{
"alpha"
,
"beta"
,
"gamma"
})
{
t
.
Fatalf
(
"unexpected names after multi wildcard trim_space: %v"
,
names
)
}
}
func
TestApplyParamOverrideSet
(
t
*
testing
.
T
)
{
func
TestApplyParamOverrideSet
(
t
*
testing
.
T
)
{
input
:=
[]
byte
(
`{"model":"gpt-4","temperature":0.7}`
)
input
:=
[]
byte
(
`{"model":"gpt-4","temperature":0.7}`
)
override
:=
map
[
string
]
interface
{}{
override
:=
map
[
string
]
interface
{}{
...
@@ -261,6 +481,42 @@ func TestApplyParamOverrideSet(t *testing.T) {
...
@@ -261,6 +481,42 @@ func TestApplyParamOverrideSet(t *testing.T) {
assertJSONEqual
(
t
,
`{"model":"gpt-4","temperature":0.1}`
,
string
(
out
))
assertJSONEqual
(
t
,
`{"model":"gpt-4","temperature":0.1}`
,
string
(
out
))
}
}
func
TestApplyParamOverrideSetWithDescriptionKeepsCompatibility
(
t
*
testing
.
T
)
{
input
:=
[]
byte
(
`{"model":"gpt-4","temperature":0.7}`
)
overrideWithoutDesc
:=
map
[
string
]
interface
{}{
"operations"
:
[]
interface
{}{
map
[
string
]
interface
{}{
"path"
:
"temperature"
,
"mode"
:
"set"
,
"value"
:
0.1
,
},
},
}
overrideWithDesc
:=
map
[
string
]
interface
{}{
"operations"
:
[]
interface
{}{
map
[
string
]
interface
{}{
"description"
:
"set temperature for deterministic output"
,
"path"
:
"temperature"
,
"mode"
:
"set"
,
"value"
:
0.1
,
},
},
}
outWithoutDesc
,
err
:=
ApplyParamOverride
(
input
,
overrideWithoutDesc
,
nil
)
if
err
!=
nil
{
t
.
Fatalf
(
"ApplyParamOverride without description returned error: %v"
,
err
)
}
outWithDesc
,
err
:=
ApplyParamOverride
(
input
,
overrideWithDesc
,
nil
)
if
err
!=
nil
{
t
.
Fatalf
(
"ApplyParamOverride with description returned error: %v"
,
err
)
}
assertJSONEqual
(
t
,
string
(
outWithoutDesc
),
string
(
outWithDesc
))
assertJSONEqual
(
t
,
`{"model":"gpt-4","temperature":0.1}`
,
string
(
outWithDesc
))
}
func
TestApplyParamOverrideSetKeepOrigin
(
t
*
testing
.
T
)
{
func
TestApplyParamOverrideSetKeepOrigin
(
t
*
testing
.
T
)
{
input
:=
[]
byte
(
`{"model":"gpt-4","temperature":0.7}`
)
input
:=
[]
byte
(
`{"model":"gpt-4","temperature":0.7}`
)
override
:=
map
[
string
]
interface
{}{
override
:=
map
[
string
]
interface
{}{
...
...
web/src/components/table/channels/modals/ParamOverrideEditorModal.jsx
View file @
5df8b34f
...
@@ -276,6 +276,7 @@ const LEGACY_TEMPLATE = {
...
@@ -276,6 +276,7 @@ const LEGACY_TEMPLATE = {
const
OPERATION_TEMPLATE
=
{
const
OPERATION_TEMPLATE
=
{
operations
:
[
operations
:
[
{
{
description
:
'Set default temperature for openai/* models.'
,
path
:
'temperature'
,
path
:
'temperature'
,
mode
:
'set'
,
mode
:
'set'
,
value
:
0.7
,
value
:
0.7
,
...
@@ -294,8 +295,9 @@ const OPERATION_TEMPLATE = {
...
@@ -294,8 +295,9 @@ const OPERATION_TEMPLATE = {
const
HEADER_PASSTHROUGH_TEMPLATE
=
{
const
HEADER_PASSTHROUGH_TEMPLATE
=
{
operations
:
[
operations
:
[
{
{
description
:
'Pass through X-Request-Id header to upstream.'
,
mode
:
'pass_headers'
,
mode
:
'pass_headers'
,
value
:
[
'
Authorization
'
],
value
:
[
'
X-Request-Id
'
],
keep_origin
:
true
,
keep_origin
:
true
,
},
},
],
],
...
@@ -304,6 +306,8 @@ const HEADER_PASSTHROUGH_TEMPLATE = {
...
@@ -304,6 +306,8 @@ const HEADER_PASSTHROUGH_TEMPLATE = {
const
GEMINI_IMAGE_4K_TEMPLATE
=
{
const
GEMINI_IMAGE_4K_TEMPLATE
=
{
operations
:
[
operations
:
[
{
{
description
:
'Set imageSize to 4K when model contains gemini/image and ends with 4k.'
,
mode
:
'set'
,
mode
:
'set'
,
path
:
'generationConfig.imageConfig.imageSize'
,
path
:
'generationConfig.imageConfig.imageSize'
,
value
:
'4K'
,
value
:
'4K'
,
...
@@ -311,7 +315,17 @@ const GEMINI_IMAGE_4K_TEMPLATE = {
...
@@ -311,7 +315,17 @@ const GEMINI_IMAGE_4K_TEMPLATE = {
{
{
path
:
'original_model'
,
path
:
'original_model'
,
mode
:
'contains'
,
mode
:
'contains'
,
value
:
'gemini-3-pro-image-preview'
,
value
:
'gemini'
,
},
{
path
:
'original_model'
,
mode
:
'contains'
,
value
:
'image'
,
},
{
path
:
'original_model'
,
mode
:
'suffix'
,
value
:
'4k'
,
},
},
],
],
logic
:
'AND'
,
logic
:
'AND'
,
...
@@ -319,11 +333,13 @@ const GEMINI_IMAGE_4K_TEMPLATE = {
...
@@ -319,11 +333,13 @@ const GEMINI_IMAGE_4K_TEMPLATE = {
],
],
};
};
const
AWS_BEDROCK_ANTHROPIC_
BETA_OVERRIDE
_TEMPLATE
=
{
const
AWS_BEDROCK_ANTHROPIC_
COMPAT
_TEMPLATE
=
{
operations
:
[
operations
:
[
{
{
description
:
'Normalize anthropic-beta header tokens for Bedrock compatibility.'
,
mode
:
'set_header'
,
mode
:
'set_header'
,
path
:
'anthropic-beta'
,
path
:
'anthropic-beta'
,
// https://github.com/BerriAI/litellm/blob/main/litellm/anthropic_beta_headers_config.json
value
:
{
value
:
{
'advanced-tool-use-2025-11-20'
:
'tool-search-tool-2025-10-19'
,
'advanced-tool-use-2025-11-20'
:
'tool-search-tool-2025-10-19'
,
bash_20241022
:
null
,
bash_20241022
:
null
,
...
@@ -355,6 +371,11 @@ const AWS_BEDROCK_ANTHROPIC_BETA_OVERRIDE_TEMPLATE = {
...
@@ -355,6 +371,11 @@ const AWS_BEDROCK_ANTHROPIC_BETA_OVERRIDE_TEMPLATE = {
'web-search-2025-03-05'
:
null
,
'web-search-2025-03-05'
:
null
,
},
},
},
},
{
description
:
'Remove all tools[*].custom.input_examples before upstream relay.'
,
mode
:
'delete'
,
path
:
'tools.*.custom.input_examples'
,
},
],
],
};
};
...
@@ -378,7 +399,7 @@ const TEMPLATE_PRESET_CONFIG = {
...
@@ -378,7 +399,7 @@ const TEMPLATE_PRESET_CONFIG = {
},
},
pass_headers_auth
:
{
pass_headers_auth
:
{
group
:
'scenario'
,
group
:
'scenario'
,
label
:
'请求头透传(
Authorization
)'
,
label
:
'请求头透传(
X-Request-Id
)'
,
kind
:
'operations'
,
kind
:
'operations'
,
payload
:
HEADER_PASSTHROUGH_TEMPLATE
,
payload
:
HEADER_PASSTHROUGH_TEMPLATE
,
},
},
...
@@ -402,9 +423,9 @@ const TEMPLATE_PRESET_CONFIG = {
...
@@ -402,9 +423,9 @@ const TEMPLATE_PRESET_CONFIG = {
},
},
aws_bedrock_anthropic_beta_override
:
{
aws_bedrock_anthropic_beta_override
:
{
group
:
'scenario'
,
group
:
'scenario'
,
label
:
'AWS Bedrock
anthropic-beta覆盖
'
,
label
:
'AWS Bedrock
Claude 兼容模板
'
,
kind
:
'operations'
,
kind
:
'operations'
,
payload
:
AWS_BEDROCK_ANTHROPIC_
BETA_OVERRIDE
_TEMPLATE
,
payload
:
AWS_BEDROCK_ANTHROPIC_
COMPAT
_TEMPLATE
,
},
},
};
};
...
@@ -764,6 +785,7 @@ const createDefaultCondition = () => normalizeCondition({});
...
@@ -764,6 +785,7 @@ const createDefaultCondition = () => normalizeCondition({});
const
normalizeOperation
=
(
operation
=
{})
=>
({
const
normalizeOperation
=
(
operation
=
{})
=>
({
id
:
nextLocalId
(),
id
:
nextLocalId
(),
description
:
typeof
operation
.
description
===
'string'
?
operation
.
description
:
''
,
path
:
typeof
operation
.
path
===
'string'
?
operation
.
path
:
''
,
path
:
typeof
operation
.
path
===
'string'
?
operation
.
path
:
''
,
mode
:
OPERATION_MODE_VALUES
.
has
(
operation
.
mode
)
?
operation
.
mode
:
'set'
,
mode
:
OPERATION_MODE_VALUES
.
has
(
operation
.
mode
)
?
operation
.
mode
:
'set'
,
value_text
:
toValueText
(
operation
.
value
),
value_text
:
toValueText
(
operation
.
value
),
...
@@ -1086,6 +1108,7 @@ const ParamOverrideEditorModal = ({ visible, value, onSave, onCancel }) => {
...
@@ -1086,6 +1108,7 @@ const ParamOverrideEditorModal = ({ visible, value, onSave, onCancel }) => {
if
(
!
keyword
)
return
operations
;
if
(
!
keyword
)
return
operations
;
return
operations
.
filter
((
operation
)
=>
{
return
operations
.
filter
((
operation
)
=>
{
const
searchableText
=
[
const
searchableText
=
[
operation
.
description
,
operation
.
mode
,
operation
.
mode
,
operation
.
path
,
operation
.
path
,
operation
.
from
,
operation
.
from
,
...
@@ -1151,10 +1174,14 @@ const ParamOverrideEditorModal = ({ visible, value, onSave, onCancel }) => {
...
@@ -1151,10 +1174,14 @@ const ParamOverrideEditorModal = ({ visible, value, onSave, onCancel }) => {
const
payloadOps
=
filteredOps
.
map
((
operation
)
=>
{
const
payloadOps
=
filteredOps
.
map
((
operation
)
=>
{
const
mode
=
operation
.
mode
||
'set'
;
const
mode
=
operation
.
mode
||
'set'
;
const
meta
=
MODE_META
[
mode
]
||
MODE_META
.
set
;
const
meta
=
MODE_META
[
mode
]
||
MODE_META
.
set
;
const
descriptionValue
=
String
(
operation
.
description
||
''
).
trim
();
const
pathValue
=
operation
.
path
.
trim
();
const
pathValue
=
operation
.
path
.
trim
();
const
fromValue
=
operation
.
from
.
trim
();
const
fromValue
=
operation
.
from
.
trim
();
const
toValue
=
operation
.
to
.
trim
();
const
toValue
=
operation
.
to
.
trim
();
const
payload
=
{
mode
};
const
payload
=
{
mode
};
if
(
descriptionValue
)
{
payload
.
description
=
descriptionValue
;
}
if
(
meta
.
path
)
{
if
(
meta
.
path
)
{
payload
.
path
=
pathValue
;
payload
.
path
=
pathValue
;
}
}
...
@@ -1563,6 +1590,7 @@ const ParamOverrideEditorModal = ({ visible, value, onSave, onCancel }) => {
...
@@ -1563,6 +1590,7 @@ const ParamOverrideEditorModal = ({ visible, value, onSave, onCancel }) => {
if
(
index
<
0
)
return
prev
;
if
(
index
<
0
)
return
prev
;
const
source
=
prev
[
index
];
const
source
=
prev
[
index
];
const
cloned
=
normalizeOperation
({
const
cloned
=
normalizeOperation
({
description
:
source
.
description
,
path
:
source
.
path
,
path
:
source
.
path
,
mode
:
source
.
mode
,
mode
:
source
.
mode
,
value
:
parseLooseValue
(
source
.
value_text
),
value
:
parseLooseValue
(
source
.
value_text
),
...
@@ -1812,14 +1840,6 @@ const ParamOverrideEditorModal = ({ visible, value, onSave, onCancel }) => {
...
@@ -1812,14 +1840,6 @@ const ParamOverrideEditorModal = ({ visible, value, onSave, onCancel }) => {
{
t
(
'重置'
)
}
{
t
(
'重置'
)
}
</
Button
>
</
Button
>
</
Space
>
</
Space
>
<
Text
type=
'tertiary'
size=
'small'
className=
'cursor-pointer select-none mt-1 whitespace-nowrap'
onClick=
{
()
=>
openFieldGuide
(
'path'
)
}
>
{
t
(
'字段速查'
)
}
</
Text
>
</
div
>
</
div
>
</
Card
>
</
Card
>
...
@@ -1891,7 +1911,7 @@ const ParamOverrideEditorModal = ({ visible, value, onSave, onCancel }) => {
...
@@ -1891,7 +1911,7 @@ const ParamOverrideEditorModal = ({ visible, value, onSave, onCancel }) => {
<
Input
<
Input
value=
{
operationSearch
}
value=
{
operationSearch
}
placeholder=
{
t
(
'搜索规则(类型 / 路径 / 来源 / 目标)'
)
}
placeholder=
{
t
(
'搜索规则(
描述 /
类型 / 路径 / 来源 / 目标)'
)
}
onChange=
{
(
nextValue
)
=>
onChange=
{
(
nextValue
)
=>
setOperationSearch
(
nextValue
||
''
)
setOperationSearch
(
nextValue
||
''
)
}
}
...
@@ -1958,6 +1978,23 @@ const ParamOverrideEditorModal = ({ visible, value, onSave, onCancel }) => {
...
@@ -1958,6 +1978,23 @@ const ParamOverrideEditorModal = ({ visible, value, onSave, onCancel }) => {
>
>
{
getOperationSummary
(
operation
,
index
)
}
{
getOperationSummary
(
operation
,
index
)
}
</
Text
>
</
Text
>
{
String
(
operation
.
description
||
''
).
trim
()
?
(
<
Text
type=
'tertiary'
size=
'small'
className=
'block mt-1'
style=
{
{
lineHeight
:
1.5
,
wordBreak
:
'break-word'
,
overflow
:
'hidden'
,
display
:
'-webkit-box'
,
WebkitLineClamp
:
2
,
WebkitBoxOrient
:
'vertical'
,
}
}
>
{
operation
.
description
}
</
Text
>
)
:
null
}
</
div
>
</
div
>
<
Tag
size=
'small'
color=
'grey'
>
<
Tag
size=
'small'
color=
'grey'
>
{
(
operation
.
conditions
||
[]).
length
}
{
(
operation
.
conditions
||
[]).
length
}
...
@@ -2035,6 +2072,7 @@ const ParamOverrideEditorModal = ({ visible, value, onSave, onCancel }) => {
...
@@ -2035,6 +2072,7 @@ const ParamOverrideEditorModal = ({ visible, value, onSave, onCancel }) => {
type=
'danger'
type=
'danger'
theme=
'borderless'
theme=
'borderless'
icon=
{
<
IconDelete
/>
}
icon=
{
<
IconDelete
/>
}
aria
-
label=
{
t
(
'删除规则'
)
}
onClick=
{
()
=>
onClick=
{
()
=>
removeOperation
(
selectedOperation
.
id
)
removeOperation
(
selectedOperation
.
id
)
}
}
...
@@ -2085,6 +2123,25 @@ const ParamOverrideEditorModal = ({ visible, value, onSave, onCancel }) => {
...
@@ -2085,6 +2123,25 @@ const ParamOverrideEditorModal = ({ visible, value, onSave, onCancel }) => {
>
>
{
MODE_DESCRIPTIONS
[
mode
]
||
''
}
{
MODE_DESCRIPTIONS
[
mode
]
||
''
}
</
Text
>
</
Text
>
<
div
className=
'mt-2'
>
<
Text
type=
'tertiary'
size=
'small'
>
{
t
(
'规则描述(可选)'
)
}
</
Text
>
<
Input
value=
{
selectedOperation
.
description
||
''
}
placeholder=
{
t
(
'例如:清理工具参数,避免上游校验错误'
)
}
onChange=
{
(
nextValue
)
=>
updateOperation
(
selectedOperation
.
id
,
{
description
:
nextValue
||
''
,
})
}
maxLength=
{
180
}
showClear
/>
<
Text
type=
'tertiary'
size=
'small'
className=
'mt-1 block'
>
{
`${String(selectedOperation.description || '').length}/180`
}
</
Text
>
</
div
>
{
meta
.
value
?
(
{
meta
.
value
?
(
mode
===
'return_error'
&&
returnErrorDraft
?
(
mode
===
'return_error'
&&
returnErrorDraft
?
(
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment