diff --git a/src/agents/codex-native-web-search.shared.ts b/src/agents/codex-native-web-search.shared.ts new file mode 100644 index 00000000000..c65edf86c62 --- /dev/null +++ b/src/agents/codex-native-web-search.shared.ts @@ -0,0 +1,87 @@ +import type { OpenClawConfig } from "../config/types.openclaw.js"; +import { isRecord } from "../utils.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; +}; + +function normalizeAllowedDomains(value: unknown): string[] | undefined { + if (!Array.isArray(value)) { + return undefined; + } + const deduped = [ + ...new Set( + value + .map((entry) => (typeof entry === "string" ? entry.trim() : null)) + .filter((entry): entry is string => Boolean(entry)), + ), + ]; + 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: typeof value.country === "string" ? value.country.trim() || undefined : undefined, + region: typeof value.region === "string" ? value.region.trim() || undefined : undefined, + city: typeof value.city === "string" ? value.city.trim() || undefined : undefined, + timezone: typeof value.timezone === "string" ? value.timezone.trim() || undefined : undefined, + }; + 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 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`; +} diff --git a/src/agents/codex-native-web-search.ts b/src/agents/codex-native-web-search.ts index 174f90d419d..6453359c592 100644 --- a/src/agents/codex-native-web-search.ts +++ b/src/agents/codex-native-web-search.ts @@ -1,8 +1,12 @@ import type { OpenClawConfig } from "../config/types.openclaw.js"; -import { normalizeOptionalString } from "../shared/string-coerce.js"; import { isRecord } from "../utils.js"; import { ensureAuthProfileStore, listProfilesForProvider } from "./auth-profiles.js"; +import { resolveCodexNativeWebSearchConfig } from "./codex-native-web-search.shared.js"; import { resolveDefaultModelForAgent } from "./model-selection.js"; +export { + describeCodexNativeWebSearch, + resolveCodexNativeWebSearchConfig, +} from "./codex-native-web-search.shared.js"; export type CodexNativeSearchMode = "cached" | "live"; export type CodexNativeSearchContextSize = "low" | "medium" | "high"; @@ -40,59 +44,6 @@ export type CodexNativeSearchPayloadPatchResult = { status: "payload_not_object" | "native_tool_already_present" | "injected"; }; -function normalizeAllowedDomains(value: unknown): string[] | undefined { - if (!Array.isArray(value)) { - return undefined; - } - const deduped = [ - ...new Set( - value - .map((entry) => normalizeOptionalString(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: normalizeOptionalString(value.country), - region: normalizeOptionalString(value.region), - city: normalizeOptionalString(value.city), - timezone: normalizeOptionalString(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; @@ -286,17 +237,3 @@ export function isCodexNativeWebSearchRelevant(params: { 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`; -} diff --git a/src/agents/openai-reasoning-compat.ts b/src/agents/openai-reasoning-compat.ts index 3d994b773fe..aca56204e8d 100644 --- a/src/agents/openai-reasoning-compat.ts +++ b/src/agents/openai-reasoning-compat.ts @@ -30,7 +30,7 @@ export function resolveOpenAIReasoningEffortMap( ): Record { const provider = normalizeLowercaseStringOrEmpty(model.provider ?? ""); const id = normalizeLowercaseStringOrEmpty(model.id ?? ""); - const builtinMap = + const builtinMap: Record = (provider === "openai" || provider === "openai-codex") && OPENAI_MEDIUM_ONLY_REASONING_MODEL_IDS.has(id) ? { minimal: "medium", low: "medium" } diff --git a/src/agents/openai-transport-stream.ts b/src/agents/openai-transport-stream.ts index c52d820f515..e2c67533194 100644 --- a/src/agents/openai-transport-stream.ts +++ b/src/agents/openai-transport-stream.ts @@ -754,6 +754,20 @@ function resolveOpenAIReasoningEffort( ) as Exclude; } +function coerceOpenAIApiReasoningEffort(effort: string): OpenAIApiReasoningEffort { + const normalized = normalizeOpenAIReasoningEffort(effort); + switch (normalized) { + case "none": + case "low": + case "medium": + case "high": + case "xhigh": + return normalized; + default: + return "high"; + } +} + export function buildOpenAIResponsesParams( model: Model, context: Context, @@ -799,12 +813,17 @@ export function buildOpenAIResponsesParams( } if (model.reasoning) { if (options?.reasoningEffort || options?.reasoning || options?.reasoningSummary) { - const reasoningEffort = mapOpenAIReasoningEffortForModel({ - model, - effort: resolveOpenAIReasoningEffort(options), - }); + const requestedReasoningEffort = resolveOpenAIReasoningEffort(options); + const reasoningEffort = coerceOpenAIApiReasoningEffort( + mapOpenAIReasoningEffortForModel({ + model, + effort: requestedReasoningEffort, + }) ?? requestedReasoningEffort, + ); + const normalizedReasoningEffort: Exclude = + reasoningEffort === "none" ? "high" : reasoningEffort; params.reasoning = { - effort: reasoningEffort ?? "high", + effort: normalizedReasoningEffort, summary: options?.reasoningSummary || "auto", }; params.include = ["reasoning.encrypted_content"]; diff --git a/src/commands/configure.wizard.ts b/src/commands/configure.wizard.ts index 7396501309b..21fbb37bad3 100644 --- a/src/commands/configure.wizard.ts +++ b/src/commands/configure.wizard.ts @@ -1,5 +1,6 @@ import fsPromises from "node:fs/promises"; import nodePath from "node:path"; +import { describeCodexNativeWebSearch } from "../agents/codex-native-web-search.shared.js"; import { formatCliCommand } from "../cli/command-format.js"; import { readConfigFileSnapshot, replaceConfigFile, resolveGatewayPort } from "../config/config.js"; import { logConfigUpdated } from "../config/logging.js"; @@ -165,8 +166,7 @@ async function promptWebToolsConfig( const existingSearch = nextConfig.tools?.web?.search; const existingFetch = nextConfig.tools?.web?.fetch; const { resolveSearchProviderOptions, setupSearch } = await import("./onboard-search.js"); - const { describeCodexNativeWebSearch, isCodexNativeWebSearchRelevant } = - await import("../agents/codex-native-web-search.js"); + const { isCodexNativeWebSearchRelevant } = await import("../agents/codex-native-web-search.js"); const searchProviderOptions = resolveSearchProviderOptions(nextConfig); note( diff --git a/src/infra/restart-stale-pids.test.ts b/src/infra/restart-stale-pids.test.ts index cb3e0c794f6..ed8195c9142 100644 --- a/src/infra/restart-stale-pids.test.ts +++ b/src/infra/restart-stale-pids.test.ts @@ -776,13 +776,20 @@ describe.skipIf(isWindows)("restart-stale-pids", () => { const stalePid = process.pid + 911; Object.defineProperty(process, "platform", { value: "win32", configurable: true }); try { + let fakeNow = 0; + __testing.setDateNowOverride(() => fakeNow); mockReadWindowsListeningPids.mockReturnValue([stalePid]); mockReadWindowsProcessArgs.mockReturnValue(["openclaw", "gateway"]); mockReadWindowsProcessArgsResult.mockReturnValue({ ok: true, args: ["openclaw", "gateway"], }); - mockReadWindowsListeningPidsResult.mockReturnValue({ ok: true, pids: [stalePid] }); + mockReadWindowsListeningPidsResult.mockImplementation((_port, timeoutMs) => { + if (timeoutMs === 400) { + fakeNow += 2001; + } + return { ok: true, pids: [stalePid] }; + }); mockSpawnSync.mockReturnValue({ error: null, status: 1, @@ -803,6 +810,7 @@ describe.skipIf(isWindows)("restart-stale-pids", () => { expect.objectContaining({ timeout: 5000 }), ); } finally { + __testing.setDateNowOverride(null); if (origDescriptor) { Object.defineProperty(process, "platform", origDescriptor); } diff --git a/src/secrets/target-registry.docs.test.ts b/src/secrets/target-registry.docs.test.ts index 4dea7ae6e88..4bdb802ed04 100644 --- a/src/secrets/target-registry.docs.test.ts +++ b/src/secrets/target-registry.docs.test.ts @@ -1,34 +1,13 @@ -import { execFileSync } from "node:child_process"; import fs from "node:fs"; import path from "node:path"; import { describe, expect, it } from "vitest"; -import type { SecretRefCredentialMatrixDocument } from "./credential-matrix.js"; +import { + buildSecretRefCredentialMatrix, + type SecretRefCredentialMatrixDocument, +} from "./credential-matrix.js"; function buildSecretRefCredentialMatrixJson(): string { - const childEnv = { ...process.env }; - delete childEnv.NODE_OPTIONS; - delete childEnv.VITEST; - delete childEnv.VITEST_MODE; - delete childEnv.VITEST_POOL_ID; - delete childEnv.VITEST_WORKER_ID; - - return execFileSync( - process.execPath, - [ - "--import", - "tsx", - "--input-type=module", - "-e", - `import { buildSecretRefCredentialMatrix } from "./src/secrets/credential-matrix.ts"; -process.stdout.write(\`\${JSON.stringify(buildSecretRefCredentialMatrix(), null, 2)}\\n\`);`, - ], - { - cwd: process.cwd(), - encoding: "utf8", - env: childEnv, - maxBuffer: 10 * 1024 * 1024, - }, - ); + return `${JSON.stringify(buildSecretRefCredentialMatrix(), null, 2)}\n`; } describe("secret target registry docs", () => { diff --git a/src/wizard/setup.finalize.ts b/src/wizard/setup.finalize.ts index 5613652fdad..f375a389e40 100644 --- a/src/wizard/setup.finalize.ts +++ b/src/wizard/setup.finalize.ts @@ -1,5 +1,6 @@ import fs from "node:fs/promises"; import path from "node:path"; +import { describeCodexNativeWebSearch } from "../agents/codex-native-web-search.shared.js"; import { DEFAULT_BOOTSTRAP_FILENAME } from "../agents/workspace.js"; import { formatCliCommand } from "../cli/command-format.js"; import { @@ -516,7 +517,6 @@ export async function finalizeSetupWizard( ); } - const { describeCodexNativeWebSearch } = await import("../agents/codex-native-web-search.js"); const codexNativeSummary = describeCodexNativeWebSearch(nextConfig); const webSearchProvider = nextConfig.tools?.web?.search?.provider; const webSearchEnabled = nextConfig.tools?.web?.search?.enabled;