Skip to content

Commit d62efe1

Browse files
Merge pull request #2 from Passerby1011/main
修复 tool_result 转换逻辑,解决工具调用结果无法被上游模型读取的问题
2 parents 96a4506 + 9965360 commit d62efe1

2 files changed

Lines changed: 148 additions & 5 deletions

File tree

deno-proxy/src/anthropic_to_openai.ts

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,17 +15,17 @@ const THINKING_END_TAG = "</thinking>";
1515
function normalizeBlocks(content: string | ClaudeContentBlock[], triggerSignal?: string): string {
1616
if (typeof content === "string") {
1717
// 过滤掉纯文本中的工具协议标签,防止注入攻击或模型回显协议片段
18-
// 注意:合法的工具调用 / 结果会通过 tool_use / tool_result block 转换,不应该以裸标签形式出现
18+
// 注意:合法的工具调用会通过 tool_use block 转换,tool_result 现在作为纯文本处理
1919
return content
2020
// 过滤掉 <invoke>...</invoke>
2121
.replace(/<invoke\b[^>]*>[\s\S]*?<\/invoke>/gi, "")
22-
// 过滤掉 <tool_result>...</tool_result>,包括模型自己错误输出的 tool_result 片段
22+
// 过滤掉可能残留的 <tool_result>...</tool_result> 标签(虽然我们不再生成这种格式)
2323
.replace(/<tool_result\b[^>]*>[\s\S]*?<\/tool_result>/gi, "");
2424
}
2525
return content.map((block) => {
2626
if (block.type === "text") {
2727
// 即使在 text block 中,也要过滤掉工具协议标签
28-
// 因为这些不是从 tool_use/tool_result 转换来的,可能是用户注入或 assistant 自行输出的协议片段
28+
// 因为这些不是从 tool_use 转换来的,可能是用户注入或 assistant 自行输出的协议片段
2929
return block.text
3030
.replace(/<invoke\b[^>]*>[\s\S]*?<\/invoke>/gi, "")
3131
.replace(/<tool_result\b[^>]*>[\s\S]*?<\/tool_result>/gi, "");
@@ -35,7 +35,26 @@ function normalizeBlocks(content: string | ClaudeContentBlock[], triggerSignal?:
3535
return `${THINKING_START_TAG}${block.thinking}${THINKING_END_TAG}`;
3636
}
3737
if (block.type === "tool_result") {
38-
return `<tool_result id="${block.tool_use_id}">${block.content ?? ""}</tool_result>`;
38+
// 将工具结果作为纯文本传递,让 OpenAI 模型能够直接读取
39+
// 添加工具ID标识,帮助模型理解这是工具调用的结果
40+
let toolResult = block.content ?? "";
41+
42+
// 处理工具结果的内容格式
43+
if (typeof toolResult === "string") {
44+
// 如果是字符串,直接使用
45+
toolResult = toolResult;
46+
} else if (Array.isArray(toolResult)) {
47+
// 如果是数组(如测试用例中的格式),提取文本内容
48+
toolResult = toolResult
49+
.filter((item: any) => item && item.type === "text")
50+
.map((item: any) => item.text || "")
51+
.join("\n");
52+
} else if (typeof toolResult === "object" && toolResult !== null) {
53+
// 如果是对象,尝试转换为 JSON 字符串
54+
toolResult = JSON.stringify(toolResult, null, 2);
55+
}
56+
57+
return `[工具调用结果 - ID: ${block.tool_use_id}]\n${toolResult}`;
3958
}
4059
if (block.type === "tool_use") {
4160
// 只有从 tool_use 转换的 <invoke> 标签才会带触发信号
@@ -79,7 +98,11 @@ export function mapClaudeToOpenAI(body: ClaudeRequest, config: ProxyConfig, trig
7998
let content = normalizeBlocks(message.content, triggerSignal);
8099

81100
// 如果是用户消息且思考模式已启用,在消息末尾添加思考提示符
82-
if (message.role === "user" && body.thinking && body.thinking.type === "enabled") {
101+
// 但排除包含工具结果的消息,避免干扰模型对工具结果的读取
102+
const hasToolResult = Array.isArray(message.content) &&
103+
message.content.some((block: any) => block.type === "tool_result");
104+
105+
if (message.role === "user" && body.thinking && body.thinking.type === "enabled" && !hasToolResult) {
83106
content = content + THINKING_HINT;
84107
}
85108

test_tool_result_fix.js

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
// 测试 tool_result 修复效果的脚本
2+
const testRequest = {
3+
"max_tokens": 64000,
4+
"messages": [
5+
{
6+
"content": "详细介绍一下《烽火十四》",
7+
"role": "user"
8+
},
9+
{
10+
"content": [
11+
{
12+
"text": " \n\n我来帮你搜索一下《烽火十四》的详细信息 🔍\n\n\n",
13+
"type": "text"
14+
},
15+
{
16+
"id": "toolu_ugJykWgtOTZK",
17+
"input": {
18+
"query": "烽火十四",
19+
"searchTimeRange": "anytime"
20+
},
21+
"name": "lobe-web-browsing____search____builtin",
22+
"type": "tool_use"
23+
}
24+
],
25+
"role": "assistant"
26+
},
27+
{
28+
"content": [
29+
{
30+
"content": [
31+
{
32+
"text": "<searchResults>\n <item title=\"烽火十四\" url=\"https://store.steampowered.com/app/4228810/_/\" />\n <item title=\"烽火十四\" url=\"https://store.steampowered.com/app/4228810/_/?l=schinese\" />\n <item title=\"如何评价国产抗日题材FPS 游戏《烽火十四》发布首支先导 ...\" url=\"https://www.zhihu.com/question/1980307100778325712\" />\n <item title=\"Fourteen Years of Flames 烽火十四 | New Chinese Indie ...\" url=\"https://www.youtube.com/watch?v=aXbqFhnutuQ\" />\n <item title=\"《烽火十四》公布预告\" url=\"https://www.ign.com.cn/topic/57528/video/feng-huo-shi-si-gong-bu-yu-gao\" />\n <item title=\"烽火:枕戈待旦 · 烽火十四\" url=\"https://steamdb.info/app/4228810/\" />\n <item title=\"国产抗日《烽火十四》首曝确认是实机游戏时长曝光\" url=\"https://www.gamersky.com/news/202512/2055135.shtml\" />\n <item title=\"烽火十四游戏配置要求介绍\" url=\"https://www.3dmgame.com/gl/3974864.html\" />\n <item title=\"国产抗日题材FPS游戏:烽火十四! #游戏解说\" url=\"https://www.youtube.com/shorts/qYiLZWjHyoc\" />\n <item title=\"Trailer for the Chinese WWII video game: 烽火十四 (14 ...\" url=\"https://www.reddit.com/r/Sino/comments/1pin64b/trailer_for_the_chinese_wwii_video_game_%E7%83%BD%E7%81%AB%E5%8D%81%E5%9B%9B_14/\" />\n <item title=\"Post-Han : Song-Ming : Romance of the Three Kingdoms : On Seven-Star Altar, Zhuge Liang Sacrifices To The Winds; At Three Gorges, Zhou Yu Liberates the Fire - Chinese Text Project\" url=\"https://ctext.org/text.pl?node=651470&if=en\" />\n <item title=\"烽火大秦 on Steam\" url=\"https://store.steampowered.com/app/1157330/_\" />\n <item title=\"\" url=\"https://chinesenotes.com/words/21626.html\" />\n <item title=\"封神演義 : 哪吒現蓮花化身 - Chinese Text Project\" url=\"https://ctext.org/fengshen-yanyi/14\" />\n <item title=\"Legendary Story About Lantern Festival, Story of Chinese Lantern Festival\" url=\"https://www.yourchineseastrology.com/holidays/lantern-festival/legends.htm\" />\n</searchResults>",
33+
"type": "text"
34+
}
35+
],
36+
"tool_use_id": "toolu_ugJykWgtOTZK",
37+
"type": "tool_result"
38+
}
39+
],
40+
"role": "user"
41+
}
42+
],
43+
"model": "claude-sonnet-4-5-20250929",
44+
"system": [
45+
{
46+
"text": "# Role\n你是 2025 年的 AI 全能日常助手。你既严谨可靠,又亲切有趣。你的核心目标是提供**准确**的信息和**自然**的陪伴。",
47+
"type": "text"
48+
}
49+
],
50+
"thinking": {
51+
"budget_tokens": 32768,
52+
"type": "enabled"
53+
},
54+
"tools": [
55+
{
56+
"description": "a search service. Useful for when you need to answer questions about current events. Input should be a search query. Output is a JSON array of the query results",
57+
"input_schema": {
58+
"properties": {
59+
"query": {
60+
"description": "The search query",
61+
"type": "string"
62+
},
63+
"searchCategories": {
64+
"description": "The search categories you can set:",
65+
"items": {
66+
"enum": ["general", "images", "news", "science", "videos"],
67+
"type": "string"
68+
},
69+
"type": "array"
70+
},
71+
"searchEngines": {
72+
"description": "The search engines you can use:",
73+
"items": {
74+
"enum": ["google", "bilibili", "bing", "duckduckgo", "npm", "pypi", "github", "arxiv", "google scholar", "z-library", "reddit", "imdb", "brave", "wikipedia", "pinterest", "unsplash", "vimeo", "youtube"],
75+
"type": "string"
76+
},
77+
"type": "array"
78+
},
79+
"searchTimeRange": {
80+
"description": "The time range you can set:",
81+
"enum": ["anytime", "day", "week", "month", "year"],
82+
"type": "string"
83+
}
84+
},
85+
"required": ["query"],
86+
"type": "object"
87+
},
88+
"name": "lobe-web-browsing____search____builtin"
89+
}
90+
],
91+
"stream": true
92+
};
93+
94+
console.log('测试请求:');
95+
console.log(JSON.stringify(testRequest, null, 2));
96+
97+
console.log('\n\n发送测试请求到代理服务器...');
98+
99+
// 使用 fetch 发送请求
100+
fetch('http://localhost:3456/v1/messages', {
101+
method: 'POST',
102+
headers: {
103+
'Content-Type': 'application/json',
104+
'x-api-key': 'test-key'
105+
},
106+
body: JSON.stringify(testRequest)
107+
})
108+
.then(response => {
109+
if (!response.ok) {
110+
throw new Error(`HTTP error! status: ${response.status}`);
111+
}
112+
return response.text();
113+
})
114+
.then(data => {
115+
console.log('\n\n代理服务器响应:');
116+
console.log(data);
117+
})
118+
.catch(error => {
119+
console.error('错误:', error);
120+
});

0 commit comments

Comments
 (0)