Skip to content

Commit bdea5a2

Browse files
authored
fix: Fix deferred tools handling in OpenAI compatibility layer (#193)
* fix: reorder tool and user messages for OpenAI API compatibility (#168) Fixes #168 OpenAI requires that an assistant message with tool_calls be immediately followed by tool messages. Previously, convertInternalUserMessage output user content before tool results, causing 400 errors. Now tool messages are pushed first. * fix: 修复OpenAI兼容层中deferred tools处理问题 提交描述: 修复了在使用OpenAI兼容API时TaskCreate工具调用失败的问题。 问题: - 当使用OpenAI兼容API模型时,调用TaskCreate工具出现"InputValidationError: The required parameter `subject` is missing"错误 - OpenAI兼容层没有正确处理deferred tools的过滤逻辑,导致工具schema没有被正确发送给模型 修复: 1. 在OpenAI兼容层中添加了与Anthropic API路径一致的deferred tools处理逻辑 2. 导入必要的工具搜索相关函数: isToolSearchEnabled, extractDiscoveredToolNames, isDeferredTool等 3. 实现工具过滤逻辑: - 检查工具搜索是否启用 - 构建deferred tools集合 - 过滤工具列表: 只包含非deferred工具或已发现的deferred工具 - 为deferred tools设置deferLoading标志 4. 修正了extractDiscoveredToolNames函数的导入路径错误 影响: - 解决了TaskCreate工具调用时的参数验证错误 - 确保OpenAI兼容层与Anthropic API路径在处理deferred tools时行为一致 - 支持工具搜索功能在OpenAI兼容模式下正常工作 修改的文件: - src/services/api/openai/index.ts - 主要修复文件 测试建议: 1. 使用OpenAI兼容API模型时,TaskCreate工具应该可以正常调用 2. 如果工具搜索功能启用,可能需要先使用ToolSearchTool来发现TaskCreate工具 3. 验证工具调用时不再出现"InputValidationError"错误 这个修复确保了当使用OpenAI兼容API(如Ollama、DeepSeek、vLLM等)时,deferred tools(如TaskCreate)能够被正确处理,解决了工具调用失败的问题。
1 parent 3683f22 commit bdea5a2

1 file changed

Lines changed: 94 additions & 20 deletions

File tree

src/services/api/openai/index.ts

Lines changed: 94 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,26 @@
11
import type { BetaToolUnion } from '@anthropic-ai/sdk/resources/beta/messages/messages.mjs'
22
import type { SystemPrompt } from '../../../utils/systemPromptType.js'
3-
import type { Message, StreamEvent, SystemAPIErrorMessage, AssistantMessage } from '../../../types/message.js'
3+
import type {
4+
Message,
5+
StreamEvent,
6+
SystemAPIErrorMessage,
7+
AssistantMessage,
8+
} from '../../../types/message.js'
49
import type { Tools } from '../../../Tool.js'
510
import { getOpenAIClient } from './client.js'
611
import { anthropicMessagesToOpenAI } from './convertMessages.js'
7-
import { anthropicToolsToOpenAI, anthropicToolChoiceToOpenAI } from './convertTools.js'
12+
import {
13+
anthropicToolsToOpenAI,
14+
anthropicToolChoiceToOpenAI,
15+
} from './convertTools.js'
816
import { adaptOpenAIStreamToAnthropic } from './streamAdapter.js'
917
import { resolveOpenAIModel } from './modelMapping.js'
1018
import { normalizeMessagesForAPI } from '../../../utils/messages.js'
1119
import { toolToAPISchema } from '../../../utils/api.js'
12-
import { getEmptyToolPermissionContext } from '../../../Tool.js'
20+
import {
21+
getEmptyToolPermissionContext,
22+
toolMatchesName,
23+
} from '../../../Tool.js'
1324
import { logForDebugging } from '../../../utils/debug.js'
1425
import { addToTotalSessionCost } from '../../../cost-tracker.js'
1526
import { calculateUSDCost } from '../../../utils/modelCost.js'
@@ -19,6 +30,14 @@ import {
1930
createAssistantAPIErrorMessage,
2031
normalizeContentFromAPI,
2132
} from '../../../utils/messages.js'
33+
import {
34+
isToolSearchEnabled,
35+
extractDiscoveredToolNames,
36+
} from '../../../utils/toolSearch.js'
37+
import {
38+
isDeferredTool,
39+
TOOL_SEARCH_TOOL_NAME,
40+
} from '../../../tools/ToolSearchTool/prompt.js'
2241

2342
/**
2443
* OpenAI-compatible query path. Converts Anthropic-format messages/tools to
@@ -43,41 +62,97 @@ export async function* queryModelOpenAI(
4362
// 2. Normalize messages using shared preprocessing
4463
const messagesForAPI = normalizeMessagesForAPI(messages, tools)
4564

46-
// 3. Build tool schemas
65+
// 3. Check if tool search is enabled (similar to Anthropic path)
66+
const useToolSearch = await isToolSearchEnabled(
67+
options.model,
68+
tools,
69+
options.getToolPermissionContext ||
70+
(async () => getEmptyToolPermissionContext()),
71+
options.agents || [],
72+
options.querySource,
73+
)
74+
75+
// 4. Build deferred tools set (similar to Anthropic path)
76+
const deferredToolNames = new Set<string>()
77+
if (useToolSearch) {
78+
for (const t of tools) {
79+
if (isDeferredTool(t)) deferredToolNames.add(t.name)
80+
}
81+
}
82+
83+
// 5. Filter tools (similar to Anthropic path)
84+
let filteredTools = tools
85+
if (useToolSearch && deferredToolNames.size > 0) {
86+
const discoveredToolNames = extractDiscoveredToolNames(messages)
87+
88+
filteredTools = tools.filter(tool => {
89+
// Always include non-deferred tools
90+
if (!deferredToolNames.has(tool.name)) return true
91+
// Always include ToolSearchTool (so it can discover more tools)
92+
if (toolMatchesName(tool, TOOL_SEARCH_TOOL_NAME)) return true
93+
// Only include deferred tools that have been discovered
94+
return discoveredToolNames.has(tool.name)
95+
})
96+
}
97+
98+
// 6. Build tool schemas with deferLoading flag
4799
const toolSchemas = await Promise.all(
48-
tools.map(tool =>
100+
filteredTools.map(tool =>
49101
toolToAPISchema(tool, {
50102
getToolPermissionContext: options.getToolPermissionContext,
51103
tools,
52104
agents: options.agents,
53105
allowedAgentTypes: options.allowedAgentTypes,
54106
model: options.model,
107+
deferLoading: useToolSearch && deferredToolNames.has(tool.name),
55108
}),
56109
),
57110
)
58-
// Filter out non-standard tools (server tools like advisor)
111+
112+
// 7. Filter out non-standard tools (server tools like advisor)
59113
const standardTools = toolSchemas.filter(
60114
(t): t is BetaToolUnion & { type: string } => {
61115
const anyT = t as Record<string, unknown>
62-
return anyT.type !== 'advisor_20260301' && anyT.type !== 'computer_20250124'
116+
return (
117+
anyT.type !== 'advisor_20260301' && anyT.type !== 'computer_20250124'
118+
)
63119
},
64120
)
65121

66-
// 4. Convert messages and tools to OpenAI format
67-
const openaiMessages = anthropicMessagesToOpenAI(messagesForAPI, systemPrompt)
122+
// 8. Convert messages and tools to OpenAI format
123+
const openaiMessages = anthropicMessagesToOpenAI(
124+
messagesForAPI,
125+
systemPrompt,
126+
)
68127
const openaiTools = anthropicToolsToOpenAI(standardTools)
69128
const openaiToolChoice = anthropicToolChoiceToOpenAI(options.toolChoice)
70129

71-
// 5. Get client and make streaming request
130+
// 9. Log tool filtering details
131+
if (useToolSearch) {
132+
const includedDeferredTools = filteredTools.filter(t =>
133+
deferredToolNames.has(t.name),
134+
).length
135+
logForDebugging(
136+
`[OpenAI] Tool search enabled: ${includedDeferredTools}/${deferredToolNames.size} deferred tools included, total tools=${openaiTools.length}`,
137+
)
138+
} else {
139+
logForDebugging(
140+
`[OpenAI] Tool search disabled, total tools=${openaiTools.length}`,
141+
)
142+
}
143+
144+
// 10. Get client and make streaming request
72145
const client = getOpenAIClient({
73146
maxRetries: 0,
74147
fetchOverride: options.fetchOverride,
75148
source: options.querySource,
76149
})
77150

78-
logForDebugging(`[OpenAI] Calling model=${openaiModel}, messages=${openaiMessages.length}, tools=${openaiTools.length}`)
151+
logForDebugging(
152+
`[OpenAI] Calling model=${openaiModel}, messages=${openaiMessages.length}, tools=${openaiTools.length}`,
153+
)
79154

80-
// 6. Call OpenAI API with streaming
155+
// 11. Call OpenAI API with streaming
81156
const stream = await client.chat.completions.create(
82157
{
83158
model: openaiModel,
@@ -103,7 +178,7 @@ export async function* queryModelOpenAI(
103178

104179
// Accumulate content blocks and usage, same as the Anthropic path in claude.ts
105180
const contentBlocks: Record<number, any> = {}
106-
let partialMessage: any = undefined
181+
let partialMessage: any
107182
let usage = {
108183
input_tokens: 0,
109184
output_tokens: 0,
@@ -121,7 +196,7 @@ export async function* queryModelOpenAI(
121196
if ((event as any).message?.usage) {
122197
usage = {
123198
...usage,
124-
...((event as any).message.usage),
199+
...(event as any).message.usage,
125200
}
126201
}
127202
break
@@ -164,11 +239,7 @@ export async function* queryModelOpenAI(
164239
const m: AssistantMessage = {
165240
message: {
166241
...partialMessage,
167-
content: normalizeContentFromAPI(
168-
[block],
169-
tools,
170-
options.agentId,
171-
),
242+
content: normalizeContentFromAPI([block], tools, options.agentId),
172243
},
173244
requestId: undefined,
174245
type: 'assistant',
@@ -192,7 +263,10 @@ export async function* queryModelOpenAI(
192263
}
193264

194265
// Track cost and token usage (matching the Anthropic path in claude.ts)
195-
if (event.type === 'message_stop' && usage.input_tokens + usage.output_tokens > 0) {
266+
if (
267+
event.type === 'message_stop' &&
268+
usage.input_tokens + usage.output_tokens > 0
269+
) {
196270
const costUSD = calculateUSDCost(openaiModel, usage as any)
197271
addToTotalSessionCost(costUSD, usage as any, options.model)
198272
}

0 commit comments

Comments
 (0)