feat(tools): resilient web search and fetch across all providers (#836)

- Add exponential backoff retry to DuckDuckGo adapter (3 attempts with
  jitter) to handle transient rate-limiting and connection errors.
- Add native fetch() fallback in WebFetch when axios hangs with custom
  DNS lookup in bundled contexts.
- Prevent broken native-path fallback for web search on OpenAI shim
  providers (minimax, moonshot, nvidia-nim, etc.) that do not support
  Anthropic's web_search_20250305 tool.
- Cherry-pick existing fixes:
  - a48bd56: cover codex/minimax/nvidia-nim in getSmallFastModel()
  - 31f0b68: 45s budget + raw-markdown fallback for secondary model
  - 446c1e8: sparse Codex /responses payload parsing
  - ae3f0b2: echo reasoning_content on assistant tool-call messages
- Fix domainCheck.test.ts mock modules to include isFirstPartyAnthropicBaseUrl
  and isGithubNativeAnthropicMode exports.

Co-authored-by: OpenClaude <openclaude@gitlawb.com>
This commit is contained in:
Kevin Codex
2026-04-23 01:14:00 +08:00
committed by GitHub
parent 3c4d8435c4
commit 531e3f1059
10 changed files with 703 additions and 81 deletions

View File

@@ -8,6 +8,7 @@ import {
convertCodexResponseToAnthropicMessage,
convertToolsToResponsesTools,
} from './codexShim.js'
import { __test as webSearchToolTest } from '../../tools/WebSearchTool/WebSearchTool.js'
const tempDirs: string[] = []
const originalEnv = {
@@ -609,6 +610,164 @@ describe('Codex request translation', () => {
])
})
test('recovers Codex web search text and sources from sparse completed response', () => {
const output = webSearchToolTest.makeOutputFromCodexWebSearchResponse(
{
output: [
{
type: 'web_search_call',
sources: [
{
title: 'OpenClaude repo',
url: 'https://github.com/example/openclaude',
},
],
},
{
type: 'message',
role: 'assistant',
content: [
{
type: 'text',
text: 'OpenClaude is available on GitHub.',
sources: [
{
title: 'Docs',
url: 'https://docs.example.com/openclaude',
},
],
},
],
},
],
},
'OpenClaude GitHub 2026',
0.42,
)
expect(output.results).toEqual([
'OpenClaude is available on GitHub.',
{
tool_use_id: 'codex-web-search',
content: [
{
title: 'OpenClaude repo',
url: 'https://github.com/example/openclaude',
},
{
title: 'Docs',
url: 'https://docs.example.com/openclaude',
},
],
},
])
})
test('falls back to a non-empty Codex web search result message', () => {
const output = webSearchToolTest.makeOutputFromCodexWebSearchResponse(
{ output: [] },
'OpenClaude GitHub 2026',
0.11,
)
expect(output.results).toEqual(['No results found.'])
})
test('surfaces Codex web search failure reason with a message', () => {
const output = webSearchToolTest.makeOutputFromCodexWebSearchResponse(
{
output: [
{
type: 'web_search_call',
status: 'failed',
error: { message: 'upstream search provider rate-limited' },
},
],
},
'OpenClaude GitHub 2026',
0.05,
)
expect(output.results).toEqual([
'Web search failed: upstream search provider rate-limited',
])
})
test('surfaces Codex web search failure reason nested under action.error', () => {
const output = webSearchToolTest.makeOutputFromCodexWebSearchResponse(
{
output: [
{
type: 'web_search_call',
status: 'failed',
action: { error: { message: 'query blocked' } },
},
],
},
'OpenClaude GitHub 2026',
0.05,
)
expect(output.results).toEqual(['Web search failed: query blocked'])
})
test('handles Codex web search failure with no reason attached', () => {
const output = webSearchToolTest.makeOutputFromCodexWebSearchResponse(
{
output: [
{
type: 'web_search_call',
status: 'failed',
},
],
},
'OpenClaude GitHub 2026',
0.05,
)
expect(output.results).toEqual(['Web search failed.'])
})
test('a failure item does not suppress sources from a later message item', () => {
const output = webSearchToolTest.makeOutputFromCodexWebSearchResponse(
{
output: [
{
type: 'web_search_call',
status: 'failed',
error: { message: 'partial outage' },
},
{
type: 'message',
role: 'assistant',
content: [
{
type: 'output_text',
text: 'Partial results below.',
sources: [
{ title: 'Docs', url: 'https://docs.example.com/openclaude' },
],
},
],
},
],
},
'OpenClaude GitHub 2026',
0.05,
)
expect(output.results).toEqual([
'Web search failed: partial outage',
'Partial results below.',
{
tool_use_id: 'codex-web-search',
content: [
{ title: 'Docs', url: 'https://docs.example.com/openclaude' },
],
},
])
})
test('translates Codex SSE text stream into Anthropic events', async () => {
const responseText = [
'event: response.output_item.added',