feat: support token-map rewrite for comma-separated headers and add bedrock anthropic-beta preset
This commit is contained in:
parent
a955d4102d
commit
054370abdc
@ -690,13 +690,6 @@ func setHeaderOverrideInContext(context map[string]interface{}, headerName strin
|
|||||||
if headerName == "" {
|
if headerName == "" {
|
||||||
return fmt.Errorf("header name is required")
|
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)
|
rawHeaders := ensureMapKeyInContext(context, paramOverrideContextHeaderOverride)
|
||||||
if keepOrigin {
|
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
|
rawHeaders[headerName] = headerValue
|
||||||
return nil
|
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 {
|
func copyHeaderInContext(context map[string]interface{}, fromHeader, toHeader string, keepOrigin bool) error {
|
||||||
fromHeader = normalizeHeaderContextKey(fromHeader)
|
fromHeader = normalizeHeaderContextKey(fromHeader)
|
||||||
toHeader = normalizeHeaderContextKey(toHeader)
|
toHeader = normalizeHeaderContextKey(toHeader)
|
||||||
|
|||||||
@ -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) {
|
func TestApplyParamOverrideConditionsObjectShorthand(t *testing.T) {
|
||||||
input := []byte(`{"temperature":0.7}`)
|
input := []byte(`{"temperature":0.7}`)
|
||||||
override := map[string]interface{}{
|
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) {
|
func TestGetEffectiveHeaderOverrideUsesRuntimeOverrideAsFinalResult(t *testing.T) {
|
||||||
info := &RelayInfo{
|
info := &RelayInfo{
|
||||||
UseRuntimeHeadersOverride: true,
|
UseRuntimeHeadersOverride: true,
|
||||||
|
|||||||
@ -163,7 +163,7 @@ const MODE_DESCRIPTIONS = {
|
|||||||
prune_objects: '按条件清理对象中的子项',
|
prune_objects: '按条件清理对象中的子项',
|
||||||
pass_headers: '把指定请求头透传到上游请求',
|
pass_headers: '把指定请求头透传到上游请求',
|
||||||
sync_fields: '在一个字段有值、另一个缺失时自动补齐',
|
sync_fields: '在一个字段有值、另一个缺失时自动补齐',
|
||||||
set_header: '设置运行期请求头',
|
set_header: '设置运行期请求头(支持整值覆盖,或用 JSON 映射按逗号 token 替换/删除)',
|
||||||
delete_header: '删除运行期请求头',
|
delete_header: '删除运行期请求头',
|
||||||
copy_header: '复制请求头',
|
copy_header: '复制请求头',
|
||||||
move_header: '移动请求头',
|
move_header: '移动请求头',
|
||||||
@ -214,7 +214,7 @@ const getModeToPlaceholder = (mode) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const getModeValueLabel = (mode) => {
|
const getModeValueLabel = (mode) => {
|
||||||
if (mode === 'set_header') return '请求头值';
|
if (mode === 'set_header') return '请求头值(支持字符串或 JSON 映射)';
|
||||||
if (mode === 'pass_headers') return '透传请求头(支持逗号分隔或 JSON 数组)';
|
if (mode === 'pass_headers') return '透传请求头(支持逗号分隔或 JSON 数组)';
|
||||||
if (
|
if (
|
||||||
mode === 'trim_prefix' ||
|
mode === 'trim_prefix' ||
|
||||||
@ -231,7 +231,18 @@ const getModeValueLabel = (mode) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const getModeValuePlaceholder = (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 === 'pass_headers') return 'Authorization, X-Request-Id';
|
||||||
if (
|
if (
|
||||||
mode === 'trim_prefix' ||
|
mode === 'trim_prefix' ||
|
||||||
@ -247,6 +258,11 @@ const getModeValuePlaceholder = (mode) => {
|
|||||||
return '0.7';
|
return '0.7';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getModeValueHelp = (mode) => {
|
||||||
|
if (mode !== 'set_header') return '';
|
||||||
|
return '字符串:整条请求头直接覆盖。JSON 映射:按逗号分隔 token 逐项处理,null 表示删除,string/array 表示替换,* 表示兜底规则。';
|
||||||
|
};
|
||||||
|
|
||||||
const SYNC_TARGET_TYPE_OPTIONS = [
|
const SYNC_TARGET_TYPE_OPTIONS = [
|
||||||
{ label: '请求体字段', value: 'json' },
|
{ label: '请求体字段', value: 'json' },
|
||||||
{ label: '请求头字段', value: 'header' },
|
{ 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 = [
|
const TEMPLATE_GROUP_OPTIONS = [
|
||||||
{ label: '基础模板', value: 'basic' },
|
{ label: '基础模板', value: 'basic' },
|
||||||
{ label: '场景模板', value: 'scenario' },
|
{ label: '场景模板', value: 'scenario' },
|
||||||
@ -345,6 +400,12 @@ const TEMPLATE_PRESET_CONFIG = {
|
|||||||
kind: 'operations',
|
kind: 'operations',
|
||||||
payload: CODEX_CLI_HEADER_PASSTHROUGH_TEMPLATE,
|
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 = [
|
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>
|
</div>
|
||||||
)
|
)
|
||||||
) : null}
|
) : null}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user