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
71886f4e
authored
Feb 22, 2026
by
Seefs
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
feat: add pass_headers op, grouped presets (incl. Gemini 4K), and robust JSON fallback
parent
f0886c8a
Hide whitespace changes
Inline
Side-by-side
Showing
3 changed files
with
232 additions
and
48 deletions
+232
-48
relay/common/override.go
+88
-1
web/src/components/table/channels/modals/EditChannelModal.jsx
+144
-47
web/src/components/table/channels/modals/ParamOverrideEditorModal.jsx
+0
-0
No files found.
relay/common/override.go
View file @
71886f4e
...
@@ -34,7 +34,7 @@ type ConditionOperation struct {
...
@@ -34,7 +34,7 @@ type ConditionOperation struct {
type
ParamOperation
struct
{
type
ParamOperation
struct
{
Path
string
`json:"path"`
Path
string
`json:"path"`
Mode
string
`json:"mode"`
// delete, set, move, copy, prepend, append, trim_prefix, trim_suffix, ensure_prefix, ensure_suffix, trim_space, to_lower, to_upper, replace, regex_replace, return_error, prune_objects, set_header, delete_header, copy_header, move_header, sync_fields
Mode
string
`json:"mode"`
// delete, set, move, copy, prepend, append, trim_prefix, trim_suffix, ensure_prefix, ensure_suffix, trim_space, to_lower, to_upper, replace, regex_replace, return_error, prune_objects, set_header, delete_header, copy_header, move_header,
pass_headers,
sync_fields
Value
interface
{}
`json:"value"`
Value
interface
{}
`json:"value"`
KeepOrigin
bool
`json:"keep_origin"`
KeepOrigin
bool
`json:"keep_origin"`
From
string
`json:"from,omitempty"`
From
string
`json:"from,omitempty"`
...
@@ -494,6 +494,19 @@ func applyOperations(jsonStr string, operations []ParamOperation, conditionConte
...
@@ -494,6 +494,19 @@ func applyOperations(jsonStr string, operations []ParamOperation, conditionConte
if
err
==
nil
{
if
err
==
nil
{
contextJSON
,
err
=
marshalContextJSON
(
context
)
contextJSON
,
err
=
marshalContextJSON
(
context
)
}
}
case
"pass_headers"
:
headerNames
,
parseErr
:=
parseHeaderPassThroughNames
(
op
.
Value
)
if
parseErr
!=
nil
{
return
""
,
parseErr
}
for
_
,
headerName
:=
range
headerNames
{
if
err
=
copyHeaderInContext
(
context
,
headerName
,
headerName
,
op
.
KeepOrigin
);
err
!=
nil
{
break
}
}
if
err
==
nil
{
contextJSON
,
err
=
marshalContextJSON
(
context
)
}
case
"sync_fields"
:
case
"sync_fields"
:
result
,
err
=
syncFieldsBetweenTargets
(
result
,
context
,
op
.
From
,
op
.
To
)
result
,
err
=
syncFieldsBetweenTargets
(
result
,
context
,
op
.
From
,
op
.
To
)
if
err
==
nil
{
if
err
==
nil
{
...
@@ -678,6 +691,80 @@ func deleteHeaderOverrideInContext(context map[string]interface{}, headerName st
...
@@ -678,6 +691,80 @@ func deleteHeaderOverrideInContext(context map[string]interface{}, headerName st
return
nil
return
nil
}
}
func
parseHeaderPassThroughNames
(
value
interface
{})
([]
string
,
error
)
{
normalizeNames
:=
func
(
values
[]
string
)
[]
string
{
names
:=
lo
.
FilterMap
(
values
,
func
(
item
string
,
_
int
)
(
string
,
bool
)
{
headerName
:=
strings
.
TrimSpace
(
item
)
if
headerName
==
""
{
return
""
,
false
}
return
headerName
,
true
})
return
lo
.
Uniq
(
names
)
}
switch
raw
:=
value
.
(
type
)
{
case
nil
:
return
nil
,
fmt
.
Errorf
(
"pass_headers value is required"
)
case
string
:
trimmed
:=
strings
.
TrimSpace
(
raw
)
if
trimmed
==
""
{
return
nil
,
fmt
.
Errorf
(
"pass_headers value is required"
)
}
if
strings
.
HasPrefix
(
trimmed
,
"["
)
||
strings
.
HasPrefix
(
trimmed
,
"{"
)
{
var
parsed
interface
{}
if
err
:=
common
.
UnmarshalJsonStr
(
trimmed
,
&
parsed
);
err
==
nil
{
return
parseHeaderPassThroughNames
(
parsed
)
}
}
names
:=
normalizeNames
(
strings
.
Split
(
trimmed
,
","
))
if
len
(
names
)
==
0
{
return
nil
,
fmt
.
Errorf
(
"pass_headers value is invalid"
)
}
return
names
,
nil
case
[]
interface
{}
:
names
:=
lo
.
FilterMap
(
raw
,
func
(
item
interface
{},
_
int
)
(
string
,
bool
)
{
headerName
:=
strings
.
TrimSpace
(
fmt
.
Sprintf
(
"%v"
,
item
))
if
headerName
==
""
{
return
""
,
false
}
return
headerName
,
true
})
names
=
lo
.
Uniq
(
names
)
if
len
(
names
)
==
0
{
return
nil
,
fmt
.
Errorf
(
"pass_headers value is invalid"
)
}
return
names
,
nil
case
map
[
string
]
interface
{}
:
candidates
:=
make
([]
string
,
0
,
8
)
if
headersRaw
,
ok
:=
raw
[
"headers"
];
ok
{
names
,
err
:=
parseHeaderPassThroughNames
(
headersRaw
)
if
err
==
nil
{
candidates
=
append
(
candidates
,
names
...
)
}
}
if
namesRaw
,
ok
:=
raw
[
"names"
];
ok
{
names
,
err
:=
parseHeaderPassThroughNames
(
namesRaw
)
if
err
==
nil
{
candidates
=
append
(
candidates
,
names
...
)
}
}
if
headerRaw
,
ok
:=
raw
[
"header"
];
ok
{
names
,
err
:=
parseHeaderPassThroughNames
(
headerRaw
)
if
err
==
nil
{
candidates
=
append
(
candidates
,
names
...
)
}
}
names
:=
normalizeNames
(
candidates
)
if
len
(
names
)
==
0
{
return
nil
,
fmt
.
Errorf
(
"pass_headers value is invalid"
)
}
return
names
,
nil
default
:
return
nil
,
fmt
.
Errorf
(
"pass_headers value must be string, array or object"
)
}
}
type
syncTarget
struct
{
type
syncTarget
struct
{
kind
string
kind
string
key
string
key
string
...
...
web/src/components/table/channels/modals/EditChannelModal.jsx
View file @
71886f4e
...
@@ -71,6 +71,7 @@ import {
...
@@ -71,6 +71,7 @@ import {
IconServer
,
IconServer
,
IconSetting
,
IconSetting
,
IconCode
,
IconCode
,
IconCopy
,
IconGlobe
,
IconGlobe
,
IconBolt
,
IconBolt
,
IconSearch
,
IconSearch
,
...
@@ -95,6 +96,28 @@ const REGION_EXAMPLE = {
...
@@ -95,6 +96,28 @@ const REGION_EXAMPLE = {
'claude-3-5-sonnet-20240620'
:
'europe-west1'
,
'claude-3-5-sonnet-20240620'
:
'europe-west1'
,
};
};
const
PARAM_OVERRIDE_LEGACY_TEMPLATE
=
{
temperature
:
0
,
};
const
PARAM_OVERRIDE_OPERATIONS_TEMPLATE
=
{
operations
:
[
{
path
:
'temperature'
,
mode
:
'set'
,
value
:
0.7
,
conditions
:
[
{
path
:
'model'
,
mode
:
'prefix'
,
value
:
'openai/'
,
},
],
logic
:
'AND'
,
},
],
};
// 支持并且已适配通过接口获取模型列表的渠道类型
// 支持并且已适配通过接口获取模型列表的渠道类型
const
MODEL_FETCHABLE_TYPES
=
new
Set
([
const
MODEL_FETCHABLE_TYPES
=
new
Set
([
1
,
4
,
14
,
34
,
17
,
26
,
27
,
24
,
47
,
25
,
20
,
23
,
31
,
40
,
42
,
48
,
43
,
1
,
4
,
14
,
34
,
17
,
26
,
27
,
24
,
47
,
25
,
20
,
23
,
31
,
40
,
42
,
48
,
43
,
...
@@ -270,7 +293,7 @@ const EditChannelModal = (props) => {
...
@@ -270,7 +293,7 @@ const EditChannelModal = (props) => {
};
};
}
}
return
{
return
{
tagLabel
:
'Custom JSON'
,
tagLabel
:
t
(
'自定义 JSON'
)
,
tagColor
:
'orange'
,
tagColor
:
'orange'
,
preview
:
pretty
,
preview
:
pretty
,
};
};
...
@@ -608,6 +631,100 @@ const EditChannelModal = (props) => {
...
@@ -608,6 +631,100 @@ const EditChannelModal = (props) => {
}
}
};
};
const
copyParamOverrideJson
=
async
()
=>
{
const
raw
=
typeof
inputs
.
param_override
===
'string'
?
inputs
.
param_override
.
trim
()
:
''
;
if
(
!
raw
)
{
showInfo
(
t
(
'暂无可复制 JSON'
));
return
;
}
let
content
=
raw
;
if
(
verifyJSON
(
raw
))
{
try
{
content
=
JSON
.
stringify
(
JSON
.
parse
(
raw
),
null
,
2
);
}
catch
(
error
)
{
content
=
raw
;
}
}
const
ok
=
await
copy
(
content
);
if
(
ok
)
{
showSuccess
(
t
(
'参数覆盖 JSON 已复制'
));
}
else
{
showError
(
t
(
'复制失败'
));
}
};
const
parseParamOverrideInput
=
()
=>
{
const
raw
=
typeof
inputs
.
param_override
===
'string'
?
inputs
.
param_override
.
trim
()
:
''
;
if
(
!
raw
)
return
null
;
if
(
!
verifyJSON
(
raw
))
{
throw
new
Error
(
t
(
'当前参数覆盖不是合法的 JSON'
));
}
return
JSON
.
parse
(
raw
);
};
const
applyParamOverrideTemplate
=
(
templateType
=
'operations'
,
applyMode
=
'fill'
,
)
=>
{
try
{
const
parsedCurrent
=
parseParamOverrideInput
();
if
(
templateType
===
'legacy'
)
{
if
(
applyMode
===
'fill'
)
{
handleInputChange
(
'param_override'
,
JSON
.
stringify
(
PARAM_OVERRIDE_LEGACY_TEMPLATE
,
null
,
2
),
);
return
;
}
const
currentLegacy
=
parsedCurrent
&&
typeof
parsedCurrent
===
'object'
&&
!
Array
.
isArray
(
parsedCurrent
)
&&
!
Array
.
isArray
(
parsedCurrent
.
operations
)
?
parsedCurrent
:
{};
const
merged
=
{
...
PARAM_OVERRIDE_LEGACY_TEMPLATE
,
...
currentLegacy
,
};
handleInputChange
(
'param_override'
,
JSON
.
stringify
(
merged
,
null
,
2
));
return
;
}
if
(
applyMode
===
'fill'
)
{
handleInputChange
(
'param_override'
,
JSON
.
stringify
(
PARAM_OVERRIDE_OPERATIONS_TEMPLATE
,
null
,
2
),
);
return
;
}
const
currentOperations
=
parsedCurrent
&&
typeof
parsedCurrent
===
'object'
&&
!
Array
.
isArray
(
parsedCurrent
)
&&
Array
.
isArray
(
parsedCurrent
.
operations
)
?
parsedCurrent
.
operations
:
[];
const
merged
=
{
operations
:
[
...
currentOperations
,
...
PARAM_OVERRIDE_OPERATIONS_TEMPLATE
.
operations
,
],
};
handleInputChange
(
'param_override'
,
JSON
.
stringify
(
merged
,
null
,
2
));
}
catch
(
error
)
{
showError
(
error
.
message
||
t
(
'模板应用失败'
));
}
};
const
loadChannel
=
async
()
=>
{
const
loadChannel
=
async
()
=>
{
setLoading
(
true
);
setLoading
(
true
);
let
res
=
await
API
.
get
(
`/api/channel/
${
channelId
}
`
);
let
res
=
await
API
.
get
(
`/api/channel/
${
channelId
}
`
);
...
@@ -3119,51 +3236,18 @@ const EditChannelModal = (props) => {
...
@@ -3119,51 +3236,18 @@ const EditChannelModal = (props) => {
<
Button
<
Button
size=
'small'
size=
'small'
onClick=
{
()
=>
onClick=
{
()
=>
handleInputChange
(
applyParamOverrideTemplate
(
'operations'
,
'fill'
)
'param_override'
,
JSON
.
stringify
({
temperature
:
0
},
null
,
2
),
)
}
}
>
>
{
t
(
'
旧格式
模板'
)
}
{
t
(
'
填充新
模板'
)
}
</
Button
>
</
Button
>
<
Button
<
Button
size=
'small'
size=
'small'
onClick=
{
()
=>
onClick=
{
()
=>
handleInputChange
(
applyParamOverrideTemplate
(
'legacy'
,
'fill'
)
'param_override'
,
JSON
.
stringify
(
{
operations
:
[
{
path
:
'temperature'
,
mode
:
'set'
,
value
:
0.7
,
conditions
:
[
{
path
:
'model'
,
mode
:
'prefix'
,
value
:
'gpt'
,
},
],
logic
:
'AND'
,
},
],
},
null
,
2
,
),
)
}
}
>
>
{
t
(
'新格式模板'
)
}
{
t
(
'填充旧模板'
)
}
</
Button
>
<
Button
size=
'small'
type=
'tertiary'
onClick=
{
()
=>
handleInputChange
(
'param_override'
,
''
)
}
>
{
t
(
'不更改'
)
}
</
Button
>
</
Button
>
</
Space
>
</
Space
>
</
div
>
</
div
>
...
@@ -3171,20 +3255,33 @@ const EditChannelModal = (props) => {
...
@@ -3171,20 +3255,33 @@ const EditChannelModal = (props) => {
{
t
(
'此项可选,用于覆盖请求参数。不支持覆盖 stream 参数'
)
}
{
t
(
'此项可选,用于覆盖请求参数。不支持覆盖 stream 参数'
)
}
</
Text
>
</
Text
>
<
div
<
div
className=
'mt-2 rounded-lg border p-3'
className=
'mt-2 rounded-xl p-3'
style=
{
{
backgroundColor
:
'var(--semi-color-fill-0)'
}
}
style=
{
{
backgroundColor
:
'var(--semi-color-fill-0)'
,
border
:
'1px solid var(--semi-color-fill-2)'
,
}
}
>
>
<
div
className=
'flex items-center justify-between mb-2'
>
<
div
className=
'flex items-center justify-between mb-2'
>
<
Tag
color=
{
paramOverrideMeta
.
tagColor
}
>
<
Tag
color=
{
paramOverrideMeta
.
tagColor
}
>
{
paramOverrideMeta
.
tagLabel
}
{
paramOverrideMeta
.
tagLabel
}
</
Tag
>
</
Tag
>
<
Button
<
Space
spacing=
{
8
}
>
size=
'small'
<
Button
type=
'tertiary'
size=
'small'
onClick=
{
()
=>
setParamOverrideEditorVisible
(
true
)
}
icon=
{
<
IconCopy
/>
}
>
type=
'tertiary'
{
t
(
'编辑'
)
}
onClick=
{
copyParamOverrideJson
}
</
Button
>
>
{
t
(
'复制'
)
}
</
Button
>
<
Button
size=
'small'
type=
'tertiary'
onClick=
{
()
=>
setParamOverrideEditorVisible
(
true
)
}
>
{
t
(
'编辑'
)
}
</
Button
>
</
Space
>
</
div
>
</
div
>
<
pre
className=
'mb-0 text-xs leading-5 whitespace-pre-wrap break-all max-h-56 overflow-auto'
>
<
pre
className=
'mb-0 text-xs leading-5 whitespace-pre-wrap break-all max-h-56 overflow-auto'
>
{
paramOverrideMeta
.
preview
}
{
paramOverrideMeta
.
preview
}
...
...
web/src/components/table/channels/modals/ParamOverrideEditorModal.jsx
View file @
71886f4e
This source diff could not be displayed because it is too large. You can
view the blob
instead.
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