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
054370ab
authored
Feb 27, 2026
by
Seefs
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
feat: support token-map rewrite for comma-separated headers and add bedrock anthropic-beta preset
parent
a955d410
Hide whitespace changes
Inline
Side-by-side
Showing
3 changed files
with
288 additions
and
10 deletions
+288
-10
relay/common/override.go
+117
-7
relay/common/override_test.go
+102
-0
web/src/components/table/channels/modals/ParamOverrideEditorModal.jsx
+69
-3
No files found.
relay/common/override.go
View file @
054370ab
...
...
@@ -690,13 +690,6 @@ func setHeaderOverrideInContext(context map[string]interface{}, headerName strin
if
headerName
==
""
{
return
fmt
.
Errorf
(
"header name is required"
)
}
if
value
==
nil
{
return
fmt
.
Errorf
(
"header value is required"
)
}
headerValue
:=
strings
.
TrimSpace
(
fmt
.
Sprintf
(
"%v"
,
value
))
if
headerValue
==
""
{
return
fmt
.
Errorf
(
"header value is required"
)
}
rawHeaders
:=
ensureMapKeyInContext
(
context
,
paramOverrideContextHeaderOverride
)
if
keepOrigin
{
...
...
@@ -707,10 +700,127 @@ func setHeaderOverrideInContext(context map[string]interface{}, headerName strin
}
}
}
headerValue
,
hasValue
,
err
:=
resolveHeaderOverrideValue
(
context
,
headerName
,
value
)
if
err
!=
nil
{
return
err
}
if
!
hasValue
{
delete
(
rawHeaders
,
headerName
)
return
nil
}
rawHeaders
[
headerName
]
=
headerValue
return
nil
}
func
resolveHeaderOverrideValue
(
context
map
[
string
]
interface
{},
headerName
string
,
value
interface
{})
(
string
,
bool
,
error
)
{
if
value
==
nil
{
return
""
,
false
,
fmt
.
Errorf
(
"header value is required"
)
}
if
mapping
,
ok
:=
value
.
(
map
[
string
]
interface
{});
ok
{
return
resolveHeaderOverrideValueByMapping
(
context
,
headerName
,
mapping
)
}
if
mapping
,
ok
:=
value
.
(
map
[
string
]
string
);
ok
{
converted
:=
make
(
map
[
string
]
interface
{},
len
(
mapping
))
for
key
,
item
:=
range
mapping
{
converted
[
key
]
=
item
}
return
resolveHeaderOverrideValueByMapping
(
context
,
headerName
,
converted
)
}
headerValue
:=
strings
.
TrimSpace
(
fmt
.
Sprintf
(
"%v"
,
value
))
if
headerValue
==
""
{
return
""
,
false
,
nil
}
return
headerValue
,
true
,
nil
}
func
resolveHeaderOverrideValueByMapping
(
context
map
[
string
]
interface
{},
headerName
string
,
mapping
map
[
string
]
interface
{})
(
string
,
bool
,
error
)
{
if
len
(
mapping
)
==
0
{
return
""
,
false
,
fmt
.
Errorf
(
"header value mapping cannot be empty"
)
}
sourceValue
,
exists
:=
getHeaderValueFromContext
(
context
,
headerName
)
if
!
exists
{
return
""
,
false
,
nil
}
sourceTokens
:=
splitHeaderListValue
(
sourceValue
)
if
len
(
sourceTokens
)
==
0
{
return
""
,
false
,
nil
}
wildcardValue
,
hasWildcard
:=
mapping
[
"*"
]
resultTokens
:=
make
([]
string
,
0
,
len
(
sourceTokens
))
for
_
,
token
:=
range
sourceTokens
{
replacementRaw
,
hasReplacement
:=
mapping
[
token
]
if
!
hasReplacement
&&
hasWildcard
{
replacementRaw
=
wildcardValue
hasReplacement
=
true
}
if
!
hasReplacement
{
resultTokens
=
append
(
resultTokens
,
token
)
continue
}
replacementTokens
,
err
:=
parseHeaderReplacementTokens
(
replacementRaw
)
if
err
!=
nil
{
return
""
,
false
,
err
}
resultTokens
=
append
(
resultTokens
,
replacementTokens
...
)
}
resultTokens
=
lo
.
Uniq
(
resultTokens
)
if
len
(
resultTokens
)
==
0
{
return
""
,
false
,
nil
}
return
strings
.
Join
(
resultTokens
,
","
),
true
,
nil
}
func
parseHeaderReplacementTokens
(
value
interface
{})
([]
string
,
error
)
{
switch
raw
:=
value
.
(
type
)
{
case
nil
:
return
nil
,
nil
case
string
:
return
splitHeaderListValue
(
raw
),
nil
case
[]
string
:
tokens
:=
make
([]
string
,
0
,
len
(
raw
))
for
_
,
item
:=
range
raw
{
tokens
=
append
(
tokens
,
splitHeaderListValue
(
item
)
...
)
}
return
lo
.
Uniq
(
tokens
),
nil
case
[]
interface
{}
:
tokens
:=
make
([]
string
,
0
,
len
(
raw
))
for
_
,
item
:=
range
raw
{
itemTokens
,
err
:=
parseHeaderReplacementTokens
(
item
)
if
err
!=
nil
{
return
nil
,
err
}
tokens
=
append
(
tokens
,
itemTokens
...
)
}
return
lo
.
Uniq
(
tokens
),
nil
case
map
[
string
]
interface
{},
map
[
string
]
string
:
return
nil
,
fmt
.
Errorf
(
"header replacement value must be string, array or null"
)
default
:
token
:=
strings
.
TrimSpace
(
fmt
.
Sprintf
(
"%v"
,
raw
))
if
token
==
""
{
return
nil
,
nil
}
return
[]
string
{
token
},
nil
}
}
func
splitHeaderListValue
(
raw
string
)
[]
string
{
items
:=
strings
.
Split
(
raw
,
","
)
return
lo
.
FilterMap
(
items
,
func
(
item
string
,
_
int
)
(
string
,
bool
)
{
token
:=
strings
.
TrimSpace
(
item
)
if
token
==
""
{
return
""
,
false
}
return
token
,
true
})
}
func
copyHeaderInContext
(
context
map
[
string
]
interface
{},
fromHeader
,
toHeader
string
,
keepOrigin
bool
)
error
{
fromHeader
=
normalizeHeaderContextKey
(
fromHeader
)
toHeader
=
normalizeHeaderContextKey
(
toHeader
)
...
...
relay/common/override_test.go
View file @
054370ab
...
...
@@ -1287,6 +1287,74 @@ func TestApplyParamOverrideSetHeaderKeepOrigin(t *testing.T) {
}
}
func
TestApplyParamOverrideSetHeaderMapRewritesCommaSeparatedHeader
(
t
*
testing
.
T
)
{
input
:=
[]
byte
(
`{"temperature":0.7}`
)
override
:=
map
[
string
]
interface
{}{
"operations"
:
[]
interface
{}{
map
[
string
]
interface
{}{
"mode"
:
"set_header"
,
"path"
:
"anthropic-beta"
,
"value"
:
map
[
string
]
interface
{}{
"advanced-tool-use-2025-11-20"
:
nil
,
"computer-use-2025-01-24"
:
"computer-use-2025-01-24"
,
},
},
},
}
ctx
:=
map
[
string
]
interface
{}{
"request_headers"
:
map
[
string
]
interface
{}{
"anthropic-beta"
:
"advanced-tool-use-2025-11-20, computer-use-2025-01-24"
,
},
}
_
,
err
:=
ApplyParamOverride
(
input
,
override
,
ctx
)
if
err
!=
nil
{
t
.
Fatalf
(
"ApplyParamOverride returned error: %v"
,
err
)
}
headers
,
ok
:=
ctx
[
"header_override"
]
.
(
map
[
string
]
interface
{})
if
!
ok
{
t
.
Fatalf
(
"expected header_override context map"
)
}
if
headers
[
"anthropic-beta"
]
!=
"computer-use-2025-01-24"
{
t
.
Fatalf
(
"expected anthropic-beta to keep only mapped value, got: %v"
,
headers
[
"anthropic-beta"
])
}
}
func
TestApplyParamOverrideSetHeaderMapDeleteWholeHeaderWhenAllTokensCleared
(
t
*
testing
.
T
)
{
input
:=
[]
byte
(
`{"temperature":0.7}`
)
override
:=
map
[
string
]
interface
{}{
"operations"
:
[]
interface
{}{
map
[
string
]
interface
{}{
"mode"
:
"set_header"
,
"path"
:
"anthropic-beta"
,
"value"
:
map
[
string
]
interface
{}{
"advanced-tool-use-2025-11-20"
:
nil
,
"computer-use-2025-01-24"
:
nil
,
},
},
},
}
ctx
:=
map
[
string
]
interface
{}{
"header_override"
:
map
[
string
]
interface
{}{
"anthropic-beta"
:
"advanced-tool-use-2025-11-20,computer-use-2025-01-24"
,
},
}
_
,
err
:=
ApplyParamOverride
(
input
,
override
,
ctx
)
if
err
!=
nil
{
t
.
Fatalf
(
"ApplyParamOverride returned error: %v"
,
err
)
}
headers
,
ok
:=
ctx
[
"header_override"
]
.
(
map
[
string
]
interface
{})
if
!
ok
{
t
.
Fatalf
(
"expected header_override context map"
)
}
if
_
,
exists
:=
headers
[
"anthropic-beta"
];
exists
{
t
.
Fatalf
(
"expected anthropic-beta to be deleted when all mapped values are null"
)
}
}
func
TestApplyParamOverrideConditionsObjectShorthand
(
t
*
testing
.
T
)
{
input
:=
[]
byte
(
`{"temperature":0.7}`
)
override
:=
map
[
string
]
interface
{}{
...
...
@@ -1400,6 +1468,40 @@ func TestApplyParamOverrideWithRelayInfoMoveAndCopyHeaders(t *testing.T) {
}
}
func
TestApplyParamOverrideWithRelayInfoSetHeaderMapRewritesAnthropicBeta
(
t
*
testing
.
T
)
{
info
:=
&
RelayInfo
{
ChannelMeta
:
&
ChannelMeta
{
ParamOverride
:
map
[
string
]
interface
{}{
"operations"
:
[]
interface
{}{
map
[
string
]
interface
{}{
"mode"
:
"set_header"
,
"path"
:
"anthropic-beta"
,
"value"
:
map
[
string
]
interface
{}{
"advanced-tool-use-2025-11-20"
:
nil
,
"computer-use-2025-01-24"
:
"computer-use-2025-01-24"
,
},
},
},
},
HeadersOverride
:
map
[
string
]
interface
{}{
"anthropic-beta"
:
"advanced-tool-use-2025-11-20, computer-use-2025-01-24"
,
},
},
}
_
,
err
:=
ApplyParamOverrideWithRelayInfo
([]
byte
(
`{"temperature":0.7}`
),
info
)
if
err
!=
nil
{
t
.
Fatalf
(
"ApplyParamOverrideWithRelayInfo returned error: %v"
,
err
)
}
if
!
info
.
UseRuntimeHeadersOverride
{
t
.
Fatalf
(
"expected runtime header override to be enabled"
)
}
if
info
.
RuntimeHeadersOverride
[
"anthropic-beta"
]
!=
"computer-use-2025-01-24"
{
t
.
Fatalf
(
"expected anthropic-beta to be rewritten, got: %v"
,
info
.
RuntimeHeadersOverride
[
"anthropic-beta"
])
}
}
func
TestGetEffectiveHeaderOverrideUsesRuntimeOverrideAsFinalResult
(
t
*
testing
.
T
)
{
info
:=
&
RelayInfo
{
UseRuntimeHeadersOverride
:
true
,
...
...
web/src/components/table/channels/modals/ParamOverrideEditorModal.jsx
View file @
054370ab
...
...
@@ -163,7 +163,7 @@ const MODE_DESCRIPTIONS = {
prune_objects
:
'按条件清理对象中的子项'
,
pass_headers
:
'把指定请求头透传到上游请求'
,
sync_fields
:
'在一个字段有值、另一个缺失时自动补齐'
,
set_header
:
'设置运行期请求头'
,
set_header
:
'设置运行期请求头
(支持整值覆盖,或用 JSON 映射按逗号 token 替换/删除)
'
,
delete_header
:
'删除运行期请求头'
,
copy_header
:
'复制请求头'
,
move_header
:
'移动请求头'
,
...
...
@@ -214,7 +214,7 @@ const getModeToPlaceholder = (mode) => {
};
const
getModeValueLabel
=
(
mode
)
=>
{
if
(
mode
===
'set_header'
)
return
'请求头值'
;
if
(
mode
===
'set_header'
)
return
'请求头值
(支持字符串或 JSON 映射)
'
;
if
(
mode
===
'pass_headers'
)
return
'透传请求头(支持逗号分隔或 JSON 数组)'
;
if
(
mode
===
'trim_prefix'
||
...
...
@@ -231,7 +231,18 @@ const getModeValueLabel = (mode) => {
};
const
getModeValuePlaceholder
=
(
mode
)
=>
{
if
(
mode
===
'set_header'
)
return
'Bearer sk-xxx'
;
if
(
mode
===
'set_header'
)
{
return
[
'String example:'
,
'Bearer sk-xxx'
,
''
,
'JSON map example:'
,
'{"advanced-tool-use-2025-11-20": null, "computer-use-2025-01-24": "computer-use-2025-01-24"}'
,
''
,
'JSON map wildcard:'
,
'{"*": null, "computer-use-2025-11-24": "computer-use-2025-11-24"}'
,
].
join
(
'\n'
);
}
if
(
mode
===
'pass_headers'
)
return
'Authorization, X-Request-Id'
;
if
(
mode
===
'trim_prefix'
||
...
...
@@ -247,6 +258,11 @@ const getModeValuePlaceholder = (mode) => {
return
'0.7'
;
};
const
getModeValueHelp
=
(
mode
)
=>
{
if
(
mode
!==
'set_header'
)
return
''
;
return
'字符串:整条请求头直接覆盖。JSON 映射:按逗号分隔 token 逐项处理,null 表示删除,string/array 表示替换,* 表示兜底规则。'
;
};
const
SYNC_TARGET_TYPE_OPTIONS
=
[
{
label
:
'请求体字段'
,
value
:
'json'
},
{
label
:
'请求头字段'
,
value
:
'header'
},
...
...
@@ -303,6 +319,45 @@ const GEMINI_IMAGE_4K_TEMPLATE = {
],
};
const
AWS_BEDROCK_ANTHROPIC_BETA_OVERRIDE_TEMPLATE
=
{
operations
:
[
{
mode
:
'set_header'
,
path
:
'anthropic-beta'
,
value
:
{
'advanced-tool-use-2025-11-20'
:
'tool-search-tool-2025-10-19'
,
bash_20241022
:
null
,
bash_20250124
:
null
,
'code-execution-2025-08-25'
:
null
,
'compact-2026-01-12'
:
'compact-2026-01-12'
,
'computer-use-2025-01-24'
:
'computer-use-2025-01-24'
,
'computer-use-2025-11-24'
:
'computer-use-2025-11-24'
,
'context-1m-2025-08-07'
:
'context-1m-2025-08-07'
,
'context-management-2025-06-27'
:
'context-management-2025-06-27'
,
'effort-2025-11-24'
:
null
,
'fast-mode-2026-02-01'
:
null
,
'files-api-2025-04-14'
:
null
,
'fine-grained-tool-streaming-2025-05-14'
:
null
,
'interleaved-thinking-2025-05-14'
:
'interleaved-thinking-2025-05-14'
,
'mcp-client-2025-11-20'
:
null
,
'mcp-client-2025-04-04'
:
null
,
'mcp-servers-2025-12-04'
:
null
,
'output-128k-2025-02-19'
:
null
,
'structured-output-2024-03-01'
:
null
,
'prompt-caching-scope-2026-01-05'
:
null
,
'skills-2025-10-02'
:
null
,
'structured-outputs-2025-11-13'
:
null
,
text_editor_20241022
:
null
,
text_editor_20250124
:
null
,
'token-efficient-tools-2025-02-19'
:
null
,
'tool-search-tool-2025-10-19'
:
'tool-search-tool-2025-10-19'
,
'web-fetch-2025-09-10'
:
null
,
'web-search-2025-03-05'
:
null
,
},
},
],
};
const
TEMPLATE_GROUP_OPTIONS
=
[
{
label
:
'基础模板'
,
value
:
'basic'
},
{
label
:
'场景模板'
,
value
:
'scenario'
},
...
...
@@ -345,6 +400,12 @@ const TEMPLATE_PRESET_CONFIG = {
kind
:
'operations'
,
payload
:
CODEX_CLI_HEADER_PASSTHROUGH_TEMPLATE
,
},
aws_bedrock_anthropic_beta_override
:
{
group
:
'scenario'
,
label
:
'AWS Bedrock anthropic-beta覆盖'
,
kind
:
'operations'
,
payload
:
AWS_BEDROCK_ANTHROPIC_BETA_OVERRIDE_TEMPLATE
,
},
};
const
FIELD_GUIDE_TARGET_OPTIONS
=
[
...
...
@@ -2560,6 +2621,11 @@ const ParamOverrideEditorModal = ({ visible, value, onSave, onCancel }) => {
})
}
/>
{
getModeValueHelp
(
mode
)
?
(
<
Text
type=
'tertiary'
size=
'small'
>
{
t
(
getModeValueHelp
(
mode
))
}
</
Text
>
)
:
null
}
</
div
>
)
)
:
null
}
...
...
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