openclaw/src/agents/codex-native-web-search.ts

308 lines
8.5 KiB
TypeScript

import type { OpenClawConfig } from "../config/config.js";
import { ensureAuthProfileStore, listProfilesForProvider } from "./auth-profiles.js";
import { resolveDefaultModelForAgent } from "./model-selection.js";
export type CodexNativeSearchMode = "cached" | "live";
export type CodexNativeSearchContextSize = "low" | "medium" | "high";
export type CodexNativeSearchUserLocation = {
country?: string;
region?: string;
city?: string;
timezone?: string;
};
export type ResolvedCodexNativeWebSearchConfig = {
enabled: boolean;
mode: CodexNativeSearchMode;
allowedDomains?: string[];
contextSize?: CodexNativeSearchContextSize;
userLocation?: CodexNativeSearchUserLocation;
};
export type CodexNativeSearchActivation = {
globalWebSearchEnabled: boolean;
codexNativeEnabled: boolean;
codexMode: CodexNativeSearchMode;
nativeEligible: boolean;
hasRequiredAuth: boolean;
state: "managed_only" | "native_active";
inactiveReason?:
| "globally_disabled"
| "codex_not_enabled"
| "model_not_eligible"
| "codex_auth_missing";
};
export type CodexNativeSearchPayloadPatchResult = {
status: "payload_not_object" | "native_tool_already_present" | "injected";
};
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
function trimToUndefined(value: unknown): string | undefined {
if (typeof value !== "string") {
return undefined;
}
const trimmed = value.trim();
return trimmed.length > 0 ? trimmed : undefined;
}
function normalizeAllowedDomains(value: unknown): string[] | undefined {
if (!Array.isArray(value)) {
return undefined;
}
const deduped = [
...new Set(
value
.map((entry) => trimToUndefined(entry))
.filter((entry): entry is string => typeof entry === "string"),
),
];
return deduped.length > 0 ? deduped : undefined;
}
function normalizeContextSize(value: unknown): CodexNativeSearchContextSize | undefined {
if (value === "low" || value === "medium" || value === "high") {
return value;
}
return undefined;
}
function normalizeMode(value: unknown): CodexNativeSearchMode {
return value === "live" ? "live" : "cached";
}
function normalizeUserLocation(value: unknown): CodexNativeSearchUserLocation | undefined {
if (!isRecord(value)) {
return undefined;
}
const location = {
country: trimToUndefined(value.country),
region: trimToUndefined(value.region),
city: trimToUndefined(value.city),
timezone: trimToUndefined(value.timezone),
};
return location.country || location.region || location.city || location.timezone
? location
: undefined;
}
export function resolveCodexNativeWebSearchConfig(
config: OpenClawConfig | undefined,
): ResolvedCodexNativeWebSearchConfig {
const nativeConfig = config?.tools?.web?.search?.openaiCodex;
return {
enabled: nativeConfig?.enabled === true,
mode: normalizeMode(nativeConfig?.mode),
allowedDomains: normalizeAllowedDomains(nativeConfig?.allowedDomains),
contextSize: normalizeContextSize(nativeConfig?.contextSize),
userLocation: normalizeUserLocation(nativeConfig?.userLocation),
};
}
export function isCodexNativeSearchEligibleModel(params: {
modelProvider?: string;
modelApi?: string;
}): boolean {
return params.modelProvider === "openai-codex" || params.modelApi === "openai-codex-responses";
}
export function hasCodexNativeWebSearchTool(tools: unknown): boolean {
if (!Array.isArray(tools)) {
return false;
}
return tools.some(
(tool) => isRecord(tool) && typeof tool.type === "string" && tool.type === "web_search",
);
}
export function hasAvailableCodexAuth(params: {
config?: OpenClawConfig;
agentDir?: string;
}): boolean {
if (params.agentDir) {
try {
if (
listProfilesForProvider(ensureAuthProfileStore(params.agentDir), "openai-codex").length > 0
) {
return true;
}
} catch {
// Fall back to config-based detection below.
}
}
return Object.values(params.config?.auth?.profiles ?? {}).some(
(profile) => isRecord(profile) && profile.provider === "openai-codex",
);
}
export function resolveCodexNativeSearchActivation(params: {
config?: OpenClawConfig;
modelProvider?: string;
modelApi?: string;
agentDir?: string;
}): CodexNativeSearchActivation {
const globalWebSearchEnabled = params.config?.tools?.web?.search?.enabled !== false;
const codexConfig = resolveCodexNativeWebSearchConfig(params.config);
const nativeEligible = isCodexNativeSearchEligibleModel(params);
const hasRequiredAuth = params.modelProvider !== "openai-codex" || hasAvailableCodexAuth(params);
if (!globalWebSearchEnabled) {
return {
globalWebSearchEnabled,
codexNativeEnabled: codexConfig.enabled,
codexMode: codexConfig.mode,
nativeEligible,
hasRequiredAuth,
state: "managed_only",
inactiveReason: "globally_disabled",
};
}
if (!codexConfig.enabled) {
return {
globalWebSearchEnabled,
codexNativeEnabled: false,
codexMode: codexConfig.mode,
nativeEligible,
hasRequiredAuth,
state: "managed_only",
inactiveReason: "codex_not_enabled",
};
}
if (!nativeEligible) {
return {
globalWebSearchEnabled,
codexNativeEnabled: true,
codexMode: codexConfig.mode,
nativeEligible: false,
hasRequiredAuth,
state: "managed_only",
inactiveReason: "model_not_eligible",
};
}
if (!hasRequiredAuth) {
return {
globalWebSearchEnabled,
codexNativeEnabled: true,
codexMode: codexConfig.mode,
nativeEligible: true,
hasRequiredAuth: false,
state: "managed_only",
inactiveReason: "codex_auth_missing",
};
}
return {
globalWebSearchEnabled,
codexNativeEnabled: true,
codexMode: codexConfig.mode,
nativeEligible: true,
hasRequiredAuth: true,
state: "native_active",
};
}
export function buildCodexNativeWebSearchTool(
config: OpenClawConfig | undefined,
): Record<string, unknown> {
const nativeConfig = resolveCodexNativeWebSearchConfig(config);
const tool: Record<string, unknown> = {
type: "web_search",
external_web_access: nativeConfig.mode === "live",
};
if (nativeConfig.allowedDomains) {
tool.filters = {
allowed_domains: nativeConfig.allowedDomains,
};
}
if (nativeConfig.contextSize) {
tool.search_context_size = nativeConfig.contextSize;
}
if (nativeConfig.userLocation) {
tool.user_location = {
type: "approximate",
...nativeConfig.userLocation,
};
}
return tool;
}
export function patchCodexNativeWebSearchPayload(params: {
payload: unknown;
config?: OpenClawConfig;
}): CodexNativeSearchPayloadPatchResult {
if (!isRecord(params.payload)) {
return { status: "payload_not_object" };
}
const payload = params.payload;
if (hasCodexNativeWebSearchTool(payload.tools)) {
return { status: "native_tool_already_present" };
}
const tools = Array.isArray(payload.tools) ? [...payload.tools] : [];
tools.push(buildCodexNativeWebSearchTool(params.config));
payload.tools = tools;
return { status: "injected" };
}
export function shouldSuppressManagedWebSearchTool(params: {
config?: OpenClawConfig;
modelProvider?: string;
modelApi?: string;
agentDir?: string;
}): boolean {
return resolveCodexNativeSearchActivation(params).state === "native_active";
}
export function isCodexNativeWebSearchRelevant(params: {
config: OpenClawConfig;
agentId?: string;
agentDir?: string;
}): boolean {
if (resolveCodexNativeWebSearchConfig(params.config).enabled) {
return true;
}
if (hasAvailableCodexAuth(params)) {
return true;
}
const defaultModel = resolveDefaultModelForAgent({
cfg: params.config,
agentId: params.agentId,
});
const configuredProvider = params.config.models?.providers?.[defaultModel.provider];
const configuredModelApi = configuredProvider?.models?.find(
(candidate) => candidate.id === defaultModel.model,
)?.api;
return isCodexNativeSearchEligibleModel({
modelProvider: defaultModel.provider,
modelApi: configuredModelApi ?? configuredProvider?.api,
});
}
export function describeCodexNativeWebSearch(
config: OpenClawConfig | undefined,
): string | undefined {
if (config?.tools?.web?.search?.enabled === false) {
return undefined;
}
const nativeConfig = resolveCodexNativeWebSearchConfig(config);
if (!nativeConfig.enabled) {
return undefined;
}
return `Codex native search: ${nativeConfig.mode} for Codex-capable models`;
}