fix: support codex web tools and non-git agents

This commit is contained in:
erdemozyol
2026-04-02 13:11:26 +03:00
parent fcb1b82d9b
commit cec3629017
4 changed files with 283 additions and 2 deletions

File diff suppressed because one or more lines are too long

View File

@@ -7,6 +7,11 @@ import type { PermissionResult } from 'src/utils/permissions/PermissionResult.js
import { z } from 'zod/v4'
import { getFeatureValue_CACHED_MAY_BE_STALE } from '../../services/analytics/growthbook.js'
import { queryModelWithStreaming } from '../../services/api/claude.js'
import { collectCodexCompletedResponse } from '../../services/api/codexShim.js'
import {
resolveCodexApiCredentials,
resolveProviderRequest,
} from '../../services/api/providerConfig.js'
import { buildTool, type ToolDef } from '../../Tool.js'
import { lazySchema } from '../../utils/lazySchema.js'
import { logError } from '../../utils/log.js'
@@ -83,6 +88,213 @@ function makeToolSchema(input: Input): BetaWebSearchTool20250305 {
}
}
function isCodexResponsesWebSearchEnabled(): boolean {
if (getAPIProvider() !== 'openai') {
return false
}
const request = resolveProviderRequest({
model: getMainLoopModel(),
baseUrl: process.env.OPENAI_BASE_URL,
})
return request.transport === 'codex_responses'
}
function makeCodexWebSearchTool(input: Input): Record<string, unknown> {
const tool: Record<string, unknown> = {
type: 'web_search',
}
if (input.allowed_domains?.length) {
tool.filters = {
allowed_domains: input.allowed_domains,
}
}
const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone
if (timezone) {
tool.user_location = {
type: 'approximate',
timezone,
}
}
return tool
}
function buildCodexWebSearchInputText(input: Input): string {
if (!input.blocked_domains?.length) {
return input.query
}
// Responses web_search supports allowed_domains filters but not blocked domains.
// Convert blocked domains into common search-engine exclusion operators so the
// constraint still affects ranking and candidate selection.
const excludedSites = input.blocked_domains.map(domain => `-site:${domain}`)
return `${input.query} ${excludedSites.join(' ')}`
}
function buildCodexWebSearchInput(input: Input): Array<Record<string, unknown>> {
return [
{
type: 'message',
role: 'user',
content: [
{
type: 'input_text',
text: buildCodexWebSearchInputText(input),
},
],
},
]
}
function buildCodexWebSearchInstructions(): string {
return [
'You are the OpenClaude web search tool.',
'Search the web for the user query and return a concise factual answer.',
'Include source URLs in the response.',
].join(' ')
}
function makeOutputFromCodexWebSearchResponse(
response: Record<string, unknown>,
query: string,
durationSeconds: number,
): Output {
const results: (SearchResult | string)[] = []
const sourceMap = new Map<string, { title: string; url: string }>()
const output = Array.isArray(response.output) ? response.output : []
for (const item of output) {
if (item?.type === 'web_search_call') {
const sources = Array.isArray(item.action?.sources)
? item.action.sources
: []
for (const source of sources) {
if (typeof source?.url !== 'string' || !source.url) continue
sourceMap.set(source.url, {
title:
typeof source.title === 'string' && source.title
? source.title
: source.url,
url: source.url,
})
}
continue
}
if (item?.type !== 'message' || !Array.isArray(item.content)) {
continue
}
for (const part of item.content) {
if (part?.type === 'output_text' && typeof part.text === 'string') {
const trimmed = part.text.trim()
if (trimmed) {
results.push(trimmed)
}
}
const annotations = Array.isArray(part?.annotations)
? part.annotations
: []
for (const annotation of annotations) {
if (annotation?.type !== 'url_citation') continue
if (typeof annotation.url !== 'string' || !annotation.url) continue
sourceMap.set(annotation.url, {
title:
typeof annotation.title === 'string' && annotation.title
? annotation.title
: annotation.url,
url: annotation.url,
})
}
}
}
if (results.length === 0 && typeof response.output_text === 'string') {
const trimmed = response.output_text.trim()
if (trimmed) {
results.push(trimmed)
}
}
if (sourceMap.size > 0) {
results.push({
tool_use_id: 'codex-web-search',
content: Array.from(sourceMap.values()),
})
}
return {
query,
results,
durationSeconds,
}
}
async function runCodexWebSearch(
input: Input,
signal: AbortSignal,
): Promise<Output> {
const startTime = performance.now()
const request = resolveProviderRequest({
model: getMainLoopModel(),
baseUrl: process.env.OPENAI_BASE_URL,
})
const credentials = resolveCodexApiCredentials()
if (!credentials.apiKey) {
throw new Error('Codex web search requires CODEX_API_KEY or a valid auth.json.')
}
if (!credentials.accountId) {
throw new Error(
'Codex web search requires CHATGPT_ACCOUNT_ID or an auth.json with chatgpt_account_id.',
)
}
const body: Record<string, unknown> = {
model: request.resolvedModel,
input: buildCodexWebSearchInput(input),
instructions: buildCodexWebSearchInstructions(),
tools: [makeCodexWebSearchTool(input)],
tool_choice: 'required',
include: ['web_search_call.action.sources'],
store: false,
stream: true,
}
if (request.reasoning) {
body.reasoning = request.reasoning
}
const response = await fetch(`${request.baseUrl}/responses`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${credentials.apiKey}`,
'chatgpt-account-id': credentials.accountId,
originator: 'openclaude',
},
body: JSON.stringify(body),
signal,
})
if (!response.ok) {
const errorBody = await response.text().catch(() => 'unknown error')
throw new Error(`Codex web search error ${response.status}: ${errorBody}`)
}
const payload = await collectCodexCompletedResponse(response)
const endTime = performance.now()
return makeOutputFromCodexWebSearchResponse(
payload,
input.query,
(endTime - startTime) / 1000,
)
}
function makeOutputFromSearchResponse(
result: BetaContentBlock[],
query: string,
@@ -169,6 +381,10 @@ export const WebSearchTool = buildTool({
const provider = getAPIProvider()
const model = getMainLoopModel()
if (isCodexResponsesWebSearchEnabled()) {
return true
}
// Enable for firstParty
if (provider === 'firstParty') {
return true
@@ -221,6 +437,12 @@ export const WebSearchTool = buildTool({
}
},
async prompt() {
if (isCodexResponsesWebSearchEnabled()) {
return getWebSearchPrompt().replace(
/\n\s*-\s*Web search is only available in the US/,
'',
)
}
return getWebSearchPrompt()
},
renderToolUseMessage,
@@ -252,6 +474,12 @@ export const WebSearchTool = buildTool({
return { result: true }
},
async call(input, context, _canUseTool, _parentMessage, onProgress) {
if (isCodexResponsesWebSearchEnabled()) {
return {
data: await runCodexWebSearch(input, context.abortController.signal),
}
}
const startTime = performance.now()
const { query } = input
const userMessage = createUserMessage({