Skip to content

Commit ceae233

Browse files
committed
feat: 静默过滤不支持的web_search/websearch工具
1 parent e08d39c commit ceae233

File tree

5 files changed

+458
-25
lines changed

5 files changed

+458
-25
lines changed

.env.example

Lines changed: 30 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,18 @@
2323
# - clientId: IdC认证的客户端ID(IdC认证时必需)
2424
# - clientSecret: IdC认证的客户端密钥(IdC认证时必需)
2525
# - disabled: 是否禁用此配置(可选,默认false)
26-
26+
# ============================================================================
27+
# Token获取方式
28+
# ============================================================================
29+
#
30+
# Social tokens:
31+
# 通常位于 ~/.aws/sso/cache/ 目录下的JSON文件中
32+
# 文件名类似:kiro-auth-token.json 或包含 "refreshToken" 字段的缓存文件
33+
#
34+
# IdC tokens:
35+
# 位于 ~/.aws/sso/cache/ 目录下的JSON文件中
36+
# 需要同时提供 clientId 和 clientSecret
37+
#
2738
# 方式1:直接配置JSON字符串
2839
# 示例1:单个 Social 认证
2940
KIRO_AUTH_TOKEN='[{"auth":"Social","refreshToken":"your_social_refresh_token_here"}]'
@@ -35,24 +46,31 @@ KIRO_AUTH_TOKEN='[{"auth":"Social","refreshToken":"your_social_refresh_token_her
3546
# 示例2:单个 IdC 认证
3647
# KIRO_AUTH_TOKEN='[{"auth":"IdC","refreshToken":"your_idc_refresh_token_here","clientId":"your_idc_client_id","clientSecret":"your_idc_client_secret"}]'
3748

38-
# 示例3:多个认证混合(推荐生产环境,实现负载均衡和容错)
49+
# 示例3:多个认证混合
3950
# KIRO_AUTH_TOKEN='[
4051
# {
4152
# "auth": "Social",
42-
# "refreshToken": "arn:aws:sso:us-east-1:999999999999:token/refresh/token1"
43-
# },
44-
# {
45-
# "auth": "Social",
46-
# "refreshToken": "arn:aws:sso:us-east-1:999999999999:token/refresh/token2"
53+
# "refreshToken": "aorAAAAAGj....."
4754
# },
4855
# {
4956
# "auth": "IdC",
50-
# "refreshToken": "arn:aws:identitycenter::us-east-1:999999999999:account/instance/xxx",
51-
# "clientId": "https://oidc.us-east-1.amazonaws.com/clients/xxx",
52-
# "clientSecret": "xxx-secret-key-xxx"
57+
# "refreshToken": "aorAAAAAGj....",
58+
# "clientId": "uG-18bI....",
59+
# "clientSecret": "eyJraWQiOiJrZXktM....."
5360
# }
5461
# ]'
55-
62+
# ============================================================================
63+
# Token获取方式
64+
# ============================================================================
65+
#
66+
# Social tokens:
67+
# 通常位于 ~/.aws/sso/cache/ 目录下的JSON文件中
68+
# 文件名类似:kiro-auth-token.json 或包含 "refreshToken" 字段的缓存文件
69+
#
70+
# IdC tokens:
71+
# 位于 ~/.aws/sso/cache/ 目录下的JSON文件中
72+
# 需要同时提供 clientId 和 clientSecret
73+
#
5674
# Token选择策略:
5775
# - 系统使用"顺序选择"策略(sequential)
5876
# - 按配置顺序依次使用token,当前token耗尽后自动切换到下一个
@@ -88,18 +106,7 @@ LOG_FORMAT=json
88106
# LOG_CONSOLE=true
89107

90108

91-
# ============================================================================
92-
# Token获取方式
93-
# ============================================================================
94-
#
95-
# Social tokens:
96-
# 通常位于 ~/.aws/sso/cache/ 目录下的JSON文件中
97-
# 文件名类似:kiro-auth-token.json 或包含 "refreshToken" 字段的缓存文件
98-
#
99-
# IdC tokens:
100-
# 位于 ~/.aws/sso/cache/ 目录下的JSON文件中
101-
# 需要同时提供 clientId 和 clientSecret
102-
#
109+
103110
# ============================================================================
104111
# 最佳实践
105112
# ============================================================================

converter/codewhisperer.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -307,6 +307,11 @@ func BuildCodeWhispererRequest(anthropicReq types.AnthropicRequest, ctx *gin.Con
307307
continue
308308
}
309309

310+
// 过滤不支持的工具:web_search (静默过滤,不发送到上游)
311+
if tool.Name == "web_search" || tool.Name == "websearch" {
312+
continue
313+
}
314+
310315
// logger.Debug("转换工具定义",
311316
// logger.Int("tool_index", i),
312317
// logger.String("tool_name", tool.Name),
@@ -482,6 +487,11 @@ func extractToolUsesFromMessage(content any) []types.ToolUseEntry {
482487
toolUse.Name = name
483488
}
484489

490+
// 过滤不支持的工具:web_search (静默过滤)
491+
if toolUse.Name == "web_search" || toolUse.Name == "websearch" {
492+
continue
493+
}
494+
485495
// 提取 input
486496
if input, ok := block["input"].(map[string]any); ok {
487497
toolUse.Input = input
@@ -510,6 +520,11 @@ func extractToolUsesFromMessage(content any) []types.ToolUseEntry {
510520
toolUse.Name = *block.Name
511521
}
512522

523+
// 过滤不支持的工具:web_search (静默过滤)
524+
if toolUse.Name == "web_search" || toolUse.Name == "websearch" {
525+
continue
526+
}
527+
513528
if block.Input != nil {
514529
switch inp := (*block.Input).(type) {
515530
case map[string]any:

converter/tools.go

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,11 @@ func validateAndProcessTools(tools []types.OpenAITool) ([]types.AnthropicTool, e
3232
continue
3333
}
3434

35+
// 过滤不支持的工具:web_search (静默过滤,不发送到上游)
36+
if tool.Function.Name == "web_search" || tool.Function.Name == "websearch" {
37+
continue
38+
}
39+
3540
// 验证参数schema
3641
if tool.Function.Parameters == nil {
3742
validationErrors = append(validationErrors, fmt.Sprintf("tool[%d]: 参数schema不能为空", i))
@@ -236,6 +241,10 @@ func convertOpenAIContentToAnthropic(content any) (any, error) {
236241
// 如果转换失败,跳过该块但继续处理其他块
237242
continue
238243
}
244+
// 如果convertedBlock为nil,表示该块需要被过滤(如web_search)
245+
if convertedBlock == nil {
246+
continue
247+
}
239248
convertedBlocks = append(convertedBlocks, convertedBlock)
240249
} else {
241250
// 非map类型的项目,直接保留
@@ -299,8 +308,17 @@ func convertContentBlock(block map[string]any) (map[string]any, error) {
299308
// 已经是Anthropic格式,无需转换
300309
return block, nil
301310

302-
case "tool_result", "tool_use":
303-
// 工具相关块,无需转换
311+
case "tool_use":
312+
// 过滤不支持的web_search工具调用(静默过滤,返回nil表示跳过)
313+
if name, ok := block["name"].(string); ok {
314+
if name == "web_search" || name == "websearch" {
315+
return nil, nil
316+
}
317+
}
318+
return block, nil
319+
320+
case "tool_result":
321+
// tool_result块无需转换,直接返回
304322
return block, nil
305323

306324
default:

converter/tools_test.go

Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,102 @@ func TestValidateAndProcessTools_MixedValidInvalid(t *testing.T) {
169169
}
170170
}
171171

172+
func TestValidateAndProcessTools_FilterWebSearch(t *testing.T) {
173+
tools := []types.OpenAITool{
174+
{
175+
Type: "function",
176+
Function: types.OpenAIFunction{
177+
Name: "get_weather",
178+
Description: "Get weather information",
179+
Parameters: map[string]any{
180+
"type": "object",
181+
"properties": map[string]any{
182+
"location": map[string]any{"type": "string"},
183+
},
184+
},
185+
},
186+
},
187+
{
188+
Type: "function",
189+
Function: types.OpenAIFunction{
190+
Name: "web_search",
191+
Description: "Search the web",
192+
Parameters: map[string]any{
193+
"type": "object",
194+
"properties": map[string]any{
195+
"query": map[string]any{"type": "string"},
196+
},
197+
},
198+
},
199+
},
200+
{
201+
Type: "function",
202+
Function: types.OpenAIFunction{
203+
Name: "calculator",
204+
Description: "Perform calculations",
205+
Parameters: map[string]any{
206+
"type": "object",
207+
"properties": map[string]any{
208+
"expression": map[string]any{"type": "string"},
209+
},
210+
},
211+
},
212+
},
213+
}
214+
215+
result, err := validateAndProcessTools(tools)
216+
217+
// web_search should be filtered out silently, no error
218+
assert.NoError(t, err)
219+
assert.Len(t, result, 2)
220+
assert.Equal(t, "get_weather", result[0].Name)
221+
assert.Equal(t, "calculator", result[1].Name)
222+
223+
// Ensure web_search is not in the result
224+
for _, tool := range result {
225+
assert.NotEqual(t, "web_search", tool.Name)
226+
assert.NotEqual(t, "websearch", tool.Name)
227+
}
228+
}
229+
230+
func TestValidateAndProcessTools_FilterWebSearchVariant(t *testing.T) {
231+
tools := []types.OpenAITool{
232+
{
233+
Type: "function",
234+
Function: types.OpenAIFunction{
235+
Name: "websearch",
236+
Description: "Search the web (variant name)",
237+
Parameters: map[string]any{
238+
"type": "object",
239+
"properties": map[string]any{
240+
"query": map[string]any{"type": "string"},
241+
},
242+
},
243+
},
244+
},
245+
{
246+
Type: "function",
247+
Function: types.OpenAIFunction{
248+
Name: "valid_tool",
249+
Description: "A valid tool",
250+
Parameters: map[string]any{
251+
"type": "object",
252+
"properties": map[string]any{
253+
"param": map[string]any{"type": "string"},
254+
},
255+
},
256+
},
257+
},
258+
}
259+
260+
result, err := validateAndProcessTools(tools)
261+
262+
// websearch variant should also be filtered
263+
assert.NoError(t, err)
264+
assert.Len(t, result, 1)
265+
assert.Equal(t, "valid_tool", result[0].Name)
266+
}
267+
172268
func TestConvertOpenAIToolChoiceToAnthropic_StringAuto(t *testing.T) {
173269
result := convertOpenAIToolChoiceToAnthropic("auto")
174270

@@ -317,3 +413,81 @@ func TestConvertOpenAIContentToAnthropic_Default(t *testing.T) {
317413
assert.NoError(t, err)
318414
assert.Equal(t, 12345, result)
319415
}
416+
417+
func TestConvertOpenAIContentToAnthropic_FilterWebSearchInHistory(t *testing.T) {
418+
content := []any{
419+
map[string]any{
420+
"type": "text",
421+
"text": "Let me search for that information.",
422+
},
423+
map[string]any{
424+
"type": "tool_use",
425+
"id": "call_123",
426+
"name": "web_search",
427+
"input": map[string]any{
428+
"query": "test query",
429+
},
430+
},
431+
map[string]any{
432+
"type": "tool_use",
433+
"id": "call_456",
434+
"name": "calculator",
435+
"input": map[string]any{
436+
"expression": "2+2",
437+
},
438+
},
439+
}
440+
441+
result, err := convertOpenAIContentToAnthropic(content)
442+
443+
assert.NoError(t, err)
444+
assert.NotNil(t, result)
445+
446+
// 转换后的内容应该过滤掉web_search,只保留text和calculator
447+
resultArray, ok := result.([]any)
448+
assert.True(t, ok)
449+
assert.Len(t, resultArray, 2, "应该过滤掉web_search,只保留2个块")
450+
451+
// 验证第一个是text块
452+
block1, ok := resultArray[0].(map[string]any)
453+
assert.True(t, ok)
454+
assert.Equal(t, "text", block1["type"])
455+
456+
// 验证第二个是calculator的tool_use块
457+
block2, ok := resultArray[1].(map[string]any)
458+
assert.True(t, ok)
459+
assert.Equal(t, "tool_use", block2["type"])
460+
assert.Equal(t, "calculator", block2["name"])
461+
}
462+
463+
func TestConvertOpenAIContentToAnthropic_FilterWebSearchVariantInHistory(t *testing.T) {
464+
content := []any{
465+
map[string]any{
466+
"type": "tool_use",
467+
"id": "call_789",
468+
"name": "websearch",
469+
"input": map[string]any{
470+
"query": "another query",
471+
},
472+
},
473+
map[string]any{
474+
"type": "text",
475+
"text": "Some text",
476+
},
477+
}
478+
479+
result, err := convertOpenAIContentToAnthropic(content)
480+
481+
assert.NoError(t, err)
482+
assert.NotNil(t, result)
483+
484+
// 转换后的内容应该过滤掉websearch变体,只保留text
485+
resultArray, ok := result.([]any)
486+
assert.True(t, ok)
487+
assert.Len(t, resultArray, 1, "应该过滤掉websearch,只保留1个块")
488+
489+
// 验证是text块
490+
block, ok := resultArray[0].(map[string]any)
491+
assert.True(t, ok)
492+
assert.Equal(t, "text", block["type"])
493+
}

0 commit comments

Comments
 (0)