feat(tool-web): declare web tool timeout budgets via config

fetchTimeoutMs/searchTimeoutMs (default 30000) resolve to each tool's
ToolDefinition.timeoutMs, moving the budget's declaration home onto the
owning tool plugin and preserving per-tool deployment override without a
mistypable central tool-name map.
This commit is contained in:
Dudu-0223
2026-07-08 14:40:14 +08:00
parent 534b1dc6d0
commit 7a822ee402
6 changed files with 74 additions and 17 deletions
@@ -41,9 +41,11 @@ beforeEach(async () => {
await ctx.plugin(WebService, { searchProvider: WebSearchExa.EXA_PROVIDER_ID, fetchProvider: WebFetchLocal.LOCAL_FETCH_PROVIDER_ID })
await ctx.plugin(WebFetchLocal, {})
await ctx.plugin(WebSearchExa, { apiKey: 'exa-key', baseURL: 'https://api.exa.test' })
// The shipped deployment shape: the tool-call budget is deployment policy over
// the model tools, set above the provider backstop so the policy normally wins.
await ctx.plugin(TimeoutPolicy, { tools: { web_fetch: { timeoutMs: 30_000 }, web_search: { timeoutMs: 30_000 } } })
// The shipped deployment shape: the tool-call budget is declared by tool-web
// config (default 30s, attached as ToolDefinition.timeoutMs) and enforced by
// the zero-config timeout-policy plugin, set above the provider backstop so the
// policy normally wins.
await ctx.plugin(TimeoutPolicy)
fiber = await ctx.plugin(ToolWeb)
})
@@ -135,8 +137,9 @@ describe('tool-call timeout returns TOOL_TIMEOUT (deadline wins over a slow fetc
await tctx.plugin(WebService, { fetchProvider: WebFetchLocal.LOCAL_FETCH_PROVIDER_ID })
// Provider backstop well ABOVE the tool-call budget, so the policy wins.
await tctx.plugin(WebFetchLocal, { timeoutMs: 30_000, maxTimeoutMs: 60_000 })
await tctx.plugin(TimeoutPolicy, { tools: { web_fetch: { timeoutMs: 50 } } })
tfiber = await tctx.plugin(ToolWeb)
await tctx.plugin(TimeoutPolicy)
// The tool-call budget is declared by tool-web config, enforced by the policy.
tfiber = await tctx.plugin(ToolWeb, { fetchTimeoutMs: 50 })
})
afterEach(async () => {
@@ -349,3 +349,31 @@ describe('searchMaxResults is plugin config', () => {
.rejects.toThrow(/tool-web: searchMaxResults must be a positive integer/)
})
})
describe('tool-call timeout budget is plugin config', () => {
it('attaches the default 30s budget to web_fetch and web_search', async () => {
const { fiber, ctx } = await mountTools()
expect(ctx.tools.get('web_fetch')?.timeoutMs).toBe(30_000)
expect(ctx.tools.get('web_search')?.timeoutMs).toBe(30_000)
await fiber.dispose()
})
it('honors per-tool timeout overrides from config', async () => {
const { fiber, ctx } = await mountTools({ config: { fetchTimeoutMs: 60_000, searchTimeoutMs: 10_000 } })
expect(ctx.tools.get('web_fetch')?.timeoutMs).toBe(60_000)
expect(ctx.tools.get('web_search')?.timeoutMs).toBe(10_000)
await fiber.dispose()
})
it.each([
['fetchTimeoutMs', { fetchTimeoutMs: 0 }],
['searchTimeoutMs', { searchTimeoutMs: -5 }],
])('rejects a non-positive-integer %s at load', async (key, config) => {
const ctx = new Context()
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRegistry)
await ctx.plugin(WebService, {})
await expect(ctx.plugin(ToolWeb, config))
.rejects.toThrow(new RegExp(`tool-web: ${key} must be a positive integer`))
})
})